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. Scriptor includes 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 the scriptor-plugin type, see Concepts → Plugin Discovery). A theme is just a directory; nothing scans for it by Composer type.
  • The classmap line points at a lib/ 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 run composer install for the theme yet.
  • This chapter's template.php and basic.php only use classes already loaded by Scriptor's root autoloader ($site, $site->sanitizer, …); they need no theme-local vendor/.

Gotcha: theme-local vendor. When a theme eventually does require Composer (chapter 4 onwards), it gets its own composer.json and vendor/, independent of Scriptor's root vendor/. The two never mix. If a theme needs a third-party library, require it inside the theme, not in Scriptor's root composer.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:

  1. $site->sanitizer->templateName(...): strips anything but [a-z0-9_-] from the template name so a bad page record can't make include traverse the filesystem.
  2. $site->currentTemplate(): returns the active page's template field ('home', 'basic', 'contact', …). The value comes straight from the page row in SQLite.
  3. ob_start() / $site->cache(): captures everything the included template prints. The Site class buffers the output so subclasses can layer caching, headers, or post-processing on top.
  4. 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 for template without 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.php directly to flip theme_path. It works, but the next git pull or 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

  1. Scriptor's front controller (public/index.php) resolved the request URL against the page tree and instantiated a default Site with $site->page set.
  2. It looked for themes/atelier/_ext.php, did not find one, and so used that default Site unchanged.
  3. It included themes/atelier/template.php.
  4. template.php read $site->currentTemplate() ("home" on /), could not find home.php, and fell back to basic.php.
  5. basic.php printed 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) and public/themes/atelier/ (empty, ready for chapter 4's assets).
  • themes/atelier/composer.json written (no composer install yet — chapter 4 runs it when the first class arrives).
  • themes/atelier/template.php matches the snippet above.
  • themes/atelier/basic.php exists with the two-line <h1>.
  • 'theme_path' => 'atelier/' saved in data/settings/custom.scriptor-config.php (created from the _custom.scriptor-config.php stub).
  • 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.