Every Scriptor theme is at least two files: a composer.json that
declares the theme's autoload rules, and a template.php that
renders one HTTP response per request. Anything richer (multiple
templates, custom routing, a Site subclass, asset partials) is
layered on top in later chapters.
This chapter writes those two files, switches Scriptor over to Atelier, and reloads. By the end you have a working theme. It is ugly and prints exactly one line, but it is real.
Create the directories
A Scriptor theme lives in two sibling directories:
themes/atelier/— PHP source:template.php, the per-template files, partials,lib/. Sits outside the webroot; the browser never reads these directly. Scriptorincludes them from PHP.public/themes/atelier/— browser-served assets: CSS, JS, images, fonts. Sits inside the webroot at the same path the web server exposes.$site->themeAssetUrl('foo.css')resolves into this directory.
The bundled basic theme is laid out the same way — peek at
themes/basic/ and public/themes/basic/ side by side for a
real-world reference.
Create both now, starting from the Scriptor root:
mkdir themes/atelier
mkdir -p public/themes/atelier
cd themes/atelier
You should now have themes/atelier/ next to themes/basic/,
and an empty public/themes/atelier/ next to public/themes/basic/.
Scriptor's theme discovery is "look in themes/", nothing else;
the public half is purely for the web server to find static
files.
Write composer.json
{
"autoload": {
"classmap": ["lib"]
}
}
That is the entire file. Three facts worth noting:
- Themes do not declare
"type": "scriptor-theme"(only plugins use thescriptor-plugintype, see Concepts → Plugin Discovery). A theme is just a directory; nothing scans for it by Composer type. - The
classmapline points at alib/folder that does not exist yet. That is intentional — chapter 4 is the first time the theme owns a PHP class (lib/AtelierTheme.php). Until then there is nothing to autoload, so we do not even runcomposer installfor the theme yet. - This chapter's
template.phpandbasic.phponly use classes already loaded by Scriptor's root autoloader ($site,$site->sanitizer, …); they need no theme-localvendor/.
Gotcha: theme-local vendor. When a theme eventually does require Composer (chapter 4 onwards), it gets its own
composer.jsonandvendor/, independent of Scriptor's rootvendor/. The two never mix. If a theme needs a third-party library,requireit inside the theme, not in Scriptor's rootcomposer.json.
Write template.php
Create themes/atelier/template.php:
<?php
$tplName = $site->sanitizer->templateName($site->currentTemplate());
$tplFile = __DIR__ . "/$tplName.php";
ob_start();
if (file_exists($tplFile)) {
include $tplFile;
} else {
include __DIR__ . '/basic.php';
}
echo $site->cache();
Five executable lines. Every Scriptor theme has roughly this file;
the bundled themes/basic/template.php and the themes/info/template.php
that powers this very site are line-for-line identical. Walk
through it:
$site->sanitizer->templateName(...): strips anything but[a-z0-9_-]from the template name so a bad page record can't makeincludetraverse the filesystem.$site->currentTemplate(): returns the active page'stemplatefield ('home','basic','contact', …). The value comes straight from the page row in SQLite.ob_start() / $site->cache(): captures everything the included template prints. TheSiteclass buffers the output so subclasses can layer caching, headers, or post-processing on top.- Fallback to
basic.php: every template name a page asks for that the theme does not implement falls back to a generic layout. That keeps the page tree forgiving: an editor can pick any string fortemplatewithout 500-ing the front end.
This file you write once and never edit. The interesting per-page
work happens in basic.php, home.php etc., written in chapter 3.
Write a one-line basic.php
You need something the dispatcher can include, so add a stub:
<!doctype html>
<title><?= htmlspecialchars((string) $site->page->name) ?></title>
<h1>Hello from Atelier</h1>
<p>Page: <?= htmlspecialchars((string) $site->page->name) ?></p>
$site->page is the Scriptor\Boot\Frontend\Page for the request
the router resolved. You only need its name here; chapter 3 walks
the full DTO.
Switch Scriptor to the new theme
Scriptor ships a stub override file next to its defaults:
data/settings/_custom.scriptor-config.php. Rename the underscore
out of the way once, and from then on your changes survive every
git pull and Composer-based Scriptor update.
cd data/settings
cp _custom.scriptor-config.php custom.scriptor-config.php
Open custom.scriptor-config.php and set:
return [
'theme_path' => 'atelier/',
];
The trailing slash matters; Scriptor concatenates it into the
filesystem path. At boot, custom.scriptor-config.php is merged on
top of scriptor-config.php via array_replace_recursive, so you
only need to repeat the keys you actually want to override.
Quick test variant. You can edit
data/settings/scriptor-config.phpdirectly to fliptheme_path. It works, but the nextgit pullor Scriptor upgrade may overwrite the file. Keep the override pattern for anything you want to outlive the next update.
Save, reload http://localhost:<port>/, and you should see:
Hello from Atelier
Page: Home
If you get a blank page or a 500, the most likely cause is a typo
in template.php (forgotten semicolon, mismatched brace). PHP's
error log is the authority; the browser will not always show the
message.
What just happened, in five sentences
- Scriptor's front controller (
public/index.php) resolved the request URL against the page tree and instantiated a defaultSitewith$site->pageset. - It looked for
themes/atelier/_ext.php, did not find one, and so used that defaultSiteunchanged. - It
includedthemes/atelier/template.php. template.phpread$site->currentTemplate()("home" on/), could not findhome.php, and fell back tobasic.php.basic.phpprinted your two lines;Site::cache()returned the buffered output and the response went out.
The whole machinery is documented in
public/index.php,
under fifty lines, well worth a one-time read.
To-do before chapter 3
- Both directories exist:
themes/atelier/(PHP source) andpublic/themes/atelier/(empty, ready for chapter 4's assets). -
themes/atelier/composer.jsonwritten (nocomposer installyet — chapter 4 runs it when the first class arrives). -
themes/atelier/template.phpmatches the snippet above. -
themes/atelier/basic.phpexists with the two-line<h1>. -
'theme_path' => 'atelier/'saved indata/settings/custom.scriptor-config.php(created from the_custom.scriptor-config.phpstub). - Reloading
/shows Hello from Atelier.
When that loads cleanly, open chapter 3 and add real per-template layouts.
Behind the scenes. The Sanitizer::templateName() rules live
in
boot/Boot/Sanitizer.php.
The output-buffering pattern (start in template.php, drain in
Site::cache()) is in
boot/Frontend/Site.php
around the cache() method.