Copyright
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).
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 To hide them, add this to your
|
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 |
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 |
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 This doesn’t affect Browsersync, and wouldn’t happen if we used |
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 If a file contains Twig code, it may be better to use a 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…