Copyright © 2025 Oliver Davies (opdavies). All rights reserved.

The code portions of this book are dedicated to the public domain under the terms of the Creative Commons Zero (CC0 1.0 Universal) license (https://creativecommons.org/publicdomain/zero/1.0).

This means you are free to copy, modify, distribute the code portions only, even for commercial purposes, without asking permission, and without saying where you got them. This is so there is no fear your creations are your own after learning to code using them.

The non-code prose and images are licensed under the more restrictive Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License (CC BY-NC-ND 4.0) (https://creativecommons.org/licenses/by-nc-nd/4.0).

This means that no non-code prose or image from the book may be reproduced, distributed, or transmitted in any form or by any means, without the prior written permission of the copyright holder. This includes remaking the same content in different formats such as PDF or EPUB since these fall under the definition of derived works. This restriction is primarily in place to ensure outdated copies are not redistributed given the frequent updates expected.

"Sculpin from Scratch" is a legal trademark of Oliver Davies (opdavies) but can be used freely to refer to this book without limitation. To avoid potential confusion, intentionally using this trademark to refer to other projects - free or proprietary - is prohibited.

Prerequisites

In this book, we’ll be using Sculpin - a static site generator written in PHP.

Whilst you don’t need to write any PHP to use it, you need to have it installed.

Run php -v in a Terminal to see if you have it installed and, if so, what version.

You’ll also need Composer - PHP’s package manager - to download and install packages (including Sculpin itself).

Once we have it installed, we’ll be using HTML, Twig and Markdown to build pages.

Why a static website?

Security

In development…​

Performance

In development…​

Easy to deploy

In development…​

Rapid development

In development…​

Why Sculpin?

In development…​

Creating composer.json

In an empty directory, run composer init to generate a composer.json file.

This will start the Composer config generator, which will ask questions and generate the file accordingly.

This is what I entered, and you can see a preview of the generated file:

  Welcome to the Composer config generator

This command will guide you through creating your composer.json config.

Package name (<vendor>/<name>) [opdavies/tmp.d-tkmnc-poi7]: opdavies/sculpin-from-scratch
Description []:
Author [Oliver Davies <[email protected]>, n to skip]:
Minimum Stability []:
Package Type (e.g. library, project, metapackage, composer-plugin) []: project
License []:

Define your dependencies.

Would you like to define your dependencies (require) interactively [yes]? no
Would you like to define your dev dependencies (require-dev) interactively [yes]? no
Add PSR-4 autoload mapping? Maps namespace "Opdavies\SculpinFromScratch" to the entered relative path. [src/, n to skip]:
n

{
    "name": "opdavies/sculpin-from-scratch",
    "type": "project",
    "authors": [
        {
            "name": "Oliver Davies",
            "email": "[email protected]"
        }
    ],
    "require": {}
}

Do you confirm generation [yes]?

Installing Sculpin

If you didn’t define your dependencies interactively, you will need to add Sculpin to your project.

To do this, run composer require sculpin/sculpin.

You will start to see output like this as Composer finds Sculpin’s dependencies, determines the compatible versions, and starts to download them:

./composer.json has been updated
Running composer update sculpin/sculpin
Loading composer repositories with package information
Updating dependencies
Lock file operations: 50 installs, 0 updates, 0 removals
  - Locking dflydev/ant-path-matcher (v1.0.4)
  - Locking dflydev/apache-mime-types (v1.0.1)
  - Locking dflydev/canal (v1.0.0)

You may also get asked this question:

Do you trust "sculpin/sculpin-theme-composer-plugin" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?]

I allowed it, but don’t usually use it.

Finally, you’ll see this line that shows the Sculpin version number and confirms it has been installed.

Using version ^3.2 for sculpin/sculpin

Now, you should have a vendor directory containing all of the project dependencies, and the vendor/bin/sculpin binary you’ll need to generate the site:

 > tree vendor -L 1 vendor/bin

vendor
├── autoload.php
├── bin
├── composer
├── dflydev
├── doctrine
├── evenement
├── fig
├── michelf
├── netcarver
├── psr
├── react
├── sculpin
├── symfony
├── twig
└── webignition
vendor/bin
├── sculpin
├── sculpin.php
└── var-dump-server

Your first build

With Sculpin installed, run vendor/bin/sculpin generate to generate the website.

The "/home/opdavies/Code/code.oliverdavies.uk/opdavies/sculpin-from-scratch/source" directory does not exist.

The source directory contains your source files - e.g. your HTML, Twig and Markdown files.

Without it, Sculpin cannot generate a website.

To fix this, create an empty directory by running mkdir source and run vendor/bin/sculpin generate again.

You will see this message in your output, but you can ignore it:

Didn't find at least one of this type : posts

We will cover content types in another chapter.

Where we are

This is what your project should now look like:

.
├── composer.json
├── composer.lock
├── source
└── vendor

This will build, but as you haven’t added any source files, nothing will be generated.

Let’s create your first page in the next chapter.

Building your first page

To generate a site, you’ll need to create some source files.

In the source directory, create an index.md file with this content:

---
---

Hello, World!

This is a Markdown file that will be converted into an index.html file.

The two lines containing three dashes are the front matter section that stores the metadata of the page (we’ll look more into this later).

The text outside of the front matter will be converted from Markdown to HTML.

Run vendor/bin/sculpin generate again to generate the website.

This time, you should see output like this:

Detected new or updated files
Generating: 100% (1 sources / 0.00 seconds)
Converting: 100% (1 sources / 0.02 seconds)
Formatting: 100% (1 sources / 0.00 seconds)
Processing completed in 0.03 seconds

The build was successful!

You should have a new directory (output_dev) containing an index.html file.

Depending on your PHP version, you may see deprecation errors when running vendor/bin/sculpin generate.

To hide them, add this to your php.ini file:

error_reporting = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED

Watching for changes

To automatically re-generate index.html when index.md changes, run vendor/bin/sculpin generate --watch.

Reviewing index.html

If you look at the index.html file, you should see this:

<p>Hello, World!</p>

The Hello, World! text has been parsed as Markdown and converted to HTML.

The same would happen for any Markdown text.

If, for example, the string changed to # Hello, World!, the HTML would change to be a level 1 heading.

Where we are

This is what your project should now look like:

.
├── composer.json
├── composer.lock
├── output_dev
│   └── index.html
├── source
│   └── index.md
└── vendor

You have a source directory for your source files and an output_dev directory for the generated static files.

When you run vendor/bin/sculpin generate, any source files will be converted to HTML and placed in the output directory.

To avoid committing the generated HTML files, add output_* to your .gitignore file.

Serving generated pages

Serving pages with Sculpin

To see the generated files in a browser, you can use Sculpin’s built-in web server.

To use it, run vendor/bin/sculpin generate --server.

You should get this output, confirming a development server is running:

Detected new or updated files
Generating: 100% (1 sources / 0.00 seconds)
Converting: 100% (1 sources / 0.01 seconds)
Formatting: 100% (1 sources / 0.00 seconds)
Processing completed in 0.02 seconds
Starting Sculpin server for the dev environment with debug true
Development server is running at http://localhost:8000
Quit the server with CONTROL-C.

Go to http://localhost:8000 and see your page.

To serve the files and watch for changes, run vendor/bin/sculpin generate --server --watch.

Serving pages with Browsersync

Using --watch will re-generate the HTML files when source files are changed, but you need to manually refresh your browser to see them.

If you want this to happen automatically, you can replace Sculpin’s development server with Browsersync.

Remove --server from your build command, and use vendor/bin/sculpin generate --watch so Sculpin will continue watching for changes.

Instead, run this command in the output_dev directory to have Browsersync serve the files:

browser-sync start -ws

You should see that Browsersync is serving files and watching for changes:

[Browsersync] Access URLs:
 --------------------------------------
       Local: http://localhost:3000
    External: http://192.168.1.111:3000
 --------------------------------------
          UI: http://localhost:3001
 UI External: http://192.168.1.111:3001
 --------------------------------------
[Browsersync] Serving files from: ./
[Browsersync] Watching files...

If so, you can go to http://localhost:3000 to see your page.

Updating index.md

In order for auto-refresh to work, we need to make a small change to index.md.

Browsersync needs to find a <body> HTML tag for it to inject its auto-refresh script but there isn’t have one in our website.

For it to work, add one to index.md:

---
---

<body>

Hello, World!

In a later chapter, we’ll look at layouts and refactor this code.

Because we’re using index.md, Sculpin will parse this as Markdown and output <body> within a paragraph.

This doesn’t affect Browsersync, and wouldn’t happen if we used index.twig.

Twig, front matter and page variables

The front matter section of your index.md is currently empty, so let’s add some information to it and display it on the page.

There are some reserved keywords but, for the most part, you can add whatever you want to the front matter section.

Let’s add an author name:

---
author: Oliver Davies
---

In YAML, author is the key and the value is my name.

Sculpin creates variables from the front matter of each page and makes them available within a page variable.

You can then use Twig’s print statement to place it within the page content:

Hello, {{ page.author }}!

The variable will be replaced when vendor/bin/sculpin generate is run, so this is what would be rendered in index.html:

<p>Hello, Oliver Davies!</p>

You can also use more complex YAML structures and Twig features, such as rendering a list of tags on the page.

---
author: Oliver Davies
tags:
  - php
  - sculpin
---

Hello, {{ page.author }}!

Tags:

{% for tag in page.tags %}
- {{ tag }}
{% endfor %}

You can put Twig code in .md files, but it can cause some odd HTML to be generated.

If a file contains Twig code, it may be better to use a .twig or .html.twig file.

It will still be converted to a HTML file, but the content won’t be parsed as Markdown.

Welcome to SculpinCon!

Let’s build a website for a fictional Sculpin conference.

Let’s simplify the current page by creating and displaying an appropriate title.

---
title: Welcome to SculpinCon!
---

<h1>{{ page.title }}</h1>

The conference will have a CFP (call for papers).

Let’s add information about that in the next chapter.

Layouts

Under development…​

Output directories

Under development…​

Deploying the files

Under development…​

Content types

Under development…​

Site variables

Under development…​

Writing custom services

Under development…​

Testing with PHPUnit

Under development…​