In chapter 2 you wrote a template.php that picks a per-page-template
file from disk based on $site->currentTemplate(). That dispatcher
is finished; it does not change for the rest of the tutorial. This
chapter writes the files it dispatches to.
Atelier needs four:
home.php, the showcase landing,basic.php, the fallback you stubbed in chapter 2 (now made real),projects.php, a gallery of child pages,contact.php, text plus a form (form logic lands in chapter 6).
All four follow the same shape: print HTML, pull dynamic bits off
$site. No SQL, no if ($_GET[...]), no fetch loops. That all
lives in the Site base class or a subclass you'll add in
chapter 4.
What $site and $site->page offer
Every page template runs with three things in scope:
$site: your liveSiteinstance. The default surface includessiteUrl,themeUrl,themeAssetUrl(),currentTemplate(),render(),messages,addMsg()and (via__get)pagesfor thePageRepository.$site->page: thePagevalue object for the URL the router resolved. Public fields:name,slug,template,pagetype,menu_title,content,parent. Plusid(),active(),created(),updated()accessors, and__getfor anything custom you put on the page.$config: thescriptor-config.phparray, mainly useful forsite_nameand language.
Templates touch $site->render('content') rather than
$site->page->content directly. The renderer applies cleaning,
HTML escaping decisions, and Markdown plugins. Your template just
emits the result. Concepts → Frontend Events covers what runs
inside render().
basic.php: make the stub real
Replace the chapter-2 stub with a usable single-column layout:
<!doctype html>
<html lang="<?= htmlspecialchars((string) ($config['lang'] ?? 'en')) ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars((string) $site->page->name) ?> — <?= htmlspecialchars((string) ($config['site_name'] ?? 'Atelier')) ?></title>
</head>
<body class="page page--basic">
<main>
<article>
<h1><?= htmlspecialchars((string) $site->page->name) ?></h1>
<?= $site->render('messages') ?>
<?= $site->render('content') ?>
</article>
</main>
</body>
</html>
Three things to notice:
<title>uses$site->page->name, not$site->page->menu_title. The menu title is a short label for navigation; the page title wants the long version.$site->render('messages')goes above content. That is where Scriptor flushes one-shot user-feedback ("Message sent", "Could not save") that an action set via$site->addMsg(). Even templates that never set messages should render the slot so a parent template that does, still works.- No PHP class involved. The default
Siteis enough for a minimal Atelier; you only subclass it in chapter 4.
home.php: the showcase
The home page wants a distinct layout, not just "basic with a bigger heading". Replace it with a hero block:
<!doctype html>
<html lang="<?= htmlspecialchars((string) ($config['lang'] ?? 'en')) ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars((string) ($config['site_name'] ?? 'Atelier')) ?></title>
</head>
<body class="page page--home">
<main>
<header class="hero">
<h1>Atelier</h1>
<p>Independent design + build. Selected work below.</p>
<p>
<a href="<?= htmlspecialchars($site->siteUrl) ?>/projects/">See projects</a> ·
<a href="<?= htmlspecialchars($site->siteUrl) ?>/contact/">Get in touch</a>
</p>
</header>
<section class="home-intro">
<?= $site->render('content') ?>
</section>
</main>
</body>
</html>
The <h1> is the brand, not the page name; a deliberate
home-page choice. Body content from the editor still renders, in
its own <section>.
projects.php: list the children
Atelier's Projects page is a hub: it has no body content of its
own, just a grid of links to its child pages. Themes legitimately
need to walk the page tree for this, so it is the one template
where $site->pages shows up:
<?php $children = $site->pages->findActiveByParent((int) $site->page->id()); ?>
<!doctype html>
<html lang="<?= htmlspecialchars((string) ($config['lang'] ?? 'en')) ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars((string) $site->page->name) ?> — <?= htmlspecialchars((string) ($config['site_name'] ?? 'Atelier')) ?></title>
</head>
<body class="page page--projects">
<main>
<header>
<h1><?= htmlspecialchars((string) $site->page->name) ?></h1>
<?= $site->render('content') ?>
</header>
<?php if ($children === []): ?>
<p>No projects published yet.</p>
<?php else: ?>
<ul class="project-grid">
<?php foreach ($children as $child): ?>
<li>
<a href="<?= htmlspecialchars($site->siteUrl . '/' . $site->page->slug . '/' . $child->slug . '/') ?>">
<?= htmlspecialchars($child->name) ?>
</a>
</li>
<?php endforeach ?>
</ul>
<?php endif ?>
</main>
</body>
</html>
Two notes:
findActiveByParent()returns active children only. That matches the editor's "inactive" flag, so drafts and hidden pages drop out automatically. There is also afindByParent()that includes inactive rows; use it sparingly, and only in admin-side or preview templates.- The URL is hand-built from slugs. Scriptor's default front
controller resolves
/projects/project-a/by walking from a parent's slug down; here we mirror that. Chapter 4 introduces a tidier helper.
Set the Projects page's template field to projects in the
editor and reload /projects/. You should see the children
listed, or "No projects published yet" if you haven't added any.
Gotcha: template names are user input. The
templatefield on a page is a free-text input in the editor; nothing prevents an editor from typingProjects(capital P) orproject(no s).Sanitizer::templateName()normalises whitespace and blocks traversal, but does not auto-fix casing. Stick to a small fixed set; if you find yourself withProjectsvsprojectscollisions, document the contract.
contact.php: text and a form
Contact is basic.php plus a <form>. The form submits nothing
yet; chapter 6 adds the action handler. We just want the markup
visible so chapter 4's layout work has something to style:
<!doctype html>
<html lang="<?= htmlspecialchars((string) ($config['lang'] ?? 'en')) ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars((string) $site->page->name) ?> — <?= htmlspecialchars((string) ($config['site_name'] ?? 'Atelier')) ?></title>
</head>
<body class="page page--contact">
<main>
<article>
<h1><?= htmlspecialchars((string) $site->page->name) ?></h1>
<?= $site->render('messages') ?>
<?= $site->render('content') ?>
<form class="contact-form" method="post" action="">
<input type="hidden" name="action" value="contact">
<p>
<label for="contact-name">Name</label>
<input id="contact-name" name="name" type="text" required>
</p>
<p>
<label for="contact-email">Email</label>
<input id="contact-email" name="email" type="email" required>
</p>
<p>
<label for="contact-message">Message</label>
<textarea id="contact-message" name="message" rows="6" required></textarea>
</p>
<p><button type="submit">Send</button></p>
</form>
</article>
</main>
</body>
</html>
The hidden action=contact field is the convention Scriptor's
sample themes use to route form submissions to a server-side
handler. Chapter 6 wires the handler. For now the form just
posts back to itself and reloads.
A quick smoke test
In the editor, set:
- Home →
template = home, - About →
template = basic, - Projects →
template = projects, - Project A / B / C →
template = basic, - Contact →
template = contact.
Then visit each URL. Each should land on a different layout. No styling yet, but the four pages should differ in structure (hero on home, listing on projects, form on contact). The next chapter makes them look like a single site.
To-do before chapter 4
-
home.php,basic.php,projects.php,contact.phpall exist underthemes/atelier/. - Editor's
templatefields point at the new templates. -
/,/about/,/projects/,/projects/project-a/,/contact/all 200 without warnings.
When that's clean, open chapter 4 (partials, assets, and the top navigation).
Behind the scenes. Page's public fields live at
boot/Frontend/Page.php.
The PageRepository::findActiveByParent() query is around line
117 of
boot/Frontend/PageRepository.php.
The Site::render() event surface is at line 160 of
boot/Frontend/Site.php.