Purpose
Per-request renderer for the public Scriptor frontend, and the
class your theme extends to add its own behaviour. Themes
subclass Site, override init() to set theme-specific config,
override individual render*() methods to inject markup, and
otherwise rely on the inherited surface.
FQCN + file path
- FQCN:
Scriptor\Boot\Frontend\Site - Source:
boot/Frontend/Site.php
When to use
You touch Site from three angles:
- Theme subclass. You write
class AtelierTheme extends Siteand overrideinit(),render(), or individualrender*()hooks. The bundledbasictheme is the reference. - Inside a template. A
template.phpreceives$site(the subclass instance) and reads$site->page,$site->siteUrl,$site->getPageUrl($p), etc. No subclassing; just consume. - Inside an action handler. A POST submission handler inside
a theme class calls
$this->sanitizer->text(...),$this->logger->info(...),$this->flashMsg(...), and$this->redirect(..., 303).
Build a Theme chapters 4 and 6 walk both shapes end to end.
Surface
Public properties (request-scoped)
| Property | Type | Purpose |
|---|---|---|
siteUrl |
string |
Scheme + host, no trailing slash. Honours X-Forwarded-Proto |
themeUrl |
string |
siteUrl . '/themes/' . theme_path |
version |
string |
Scriptor version string |
page |
?Page |
Resolved page DTO, or null before execute() |
urlSegments |
UrlSegments |
Parsed path segments for the current request |
input |
Request (iManager HTTP) |
Wrapper around $_GET / $_POST / $_FILES |
sanitizer |
Sanitizer |
Text / URL / markdown sanitisation facade |
session |
SessionStore |
iManager session-bag accessor |
logger |
Psr\Log\LoggerInterface |
PSR-3 logger, bound by FileLogger |
pages |
PageRepository |
Read access to the page tree |
templateParser |
TemplateRenderer |
iManager's {{ tag }} parser |
cache |
FilesystemCache |
iManager FS cache (theme-side caching) |
images |
ImageUrlBuilder |
Image-URL builder through the processor pipeline |
files |
FileRepository |
iManager file metadata access |
fileStorage |
FileStorage |
iManager file-storage facade |
config |
array<string,mixed> |
Scriptor config array verbatim |
msgs |
list<array{...}> |
In-page message queue (use addMsg(), drained by renderMsgs()) |
themeConfig is protected. Subclasses populate it in init();
templates read it via getTCP().
Public methods
public function execute(): void
Dispatches PageResolving, then PageResolved if something
resolved, otherwise RouteNotFound, otherwise throw404().
Bundled themes call this once from their _ext.php after
construction.
public function render(string $element): ?string
Render-dispatch by element name. Built-in elements: content,
navigation, messages. Theme-extension hooks that default to
the empty string: hero, mainNavItems, footerNav, socIcons,
archivesContent, archiveNav, pagination, articleDate,
emptyCsrfFields. Unknown elements return null.
public function redirect(string $url, int $status = 302): never
Sends a Location header and exits. Status-code cheat sheet (from
the source):
- 303 See Other after a POST handler. Switches the next request to GET so a reload of the landing page does not resubmit.
- 302 Found default.
- 301 Moved Permanently for permanent rewrites.
- 307 Temporary Redirect preserves the method.
public function addMsg(string $type, string $text, string $header = ''): void
Appends to the in-request message queue. Survives until the
next render('messages') call within the same request. Errors
that the user must see now belong here.
public function flashMsg(string $type, string $text, string $header = ''): void
Appends to the session-backed flash bag (key frontend_msgs).
Survives one redirect. Pair with redirect(..., 303) for the
POST-redirect-GET pattern. Opens the session lazily; themes that
never call flashMsg() keep the public site cookie-free.
public function renderMsgs(): string
Drains the flash bag into the in-request queue, then renders the
combined queue as a <ul class="messages"> and empties it. Empty
queue returns the empty string so templates can call
<?= $site->render('messages') ?> without conditionals. The
value is emitted raw; type and header are HTML-escaped.
public function getPageUrl(Page $page): string
Walks the parent chain and returns the canonical relative URL
path (slugs joined with trailing slashes). Empty slug collapses
to the root; cycles in the parent chain are detected and
collapsed. Always prefer this over hand-assembling
'/' . $page->slug . '/'.
public function getBasePath(): string
The URL prefix beneath which Scriptor is mounted (everything
left of the first slug segment, no query string). Always ends
with a single /. Useful for themes that need to build internal
links without assuming the deployed path.
public function getTCP(string $key): mixed
Theme-config-property accessor. Returns $themeConfig[$key] or
falls back to Site::TCP_DEFAULTS[$key] or null. The defaults
cover site_name, copyright_info, and the footer.* payload,
so the basic-theme default render path keeps working even before
a subclass attaches.
public function themeAssetUrl(string $relative): string
public function editorAssetUrl(string $relative): string
Resolve a relative asset path to its full public URL. Use the
first for theme assets (themes/<theme>/), the second for
editor assets that the frontend also embeds (e.g. shared Prism
CSS).
public function currentTemplate(): string
Template name driving template.php. Defaults to
$page->template; subclasses override to inject a different
template without mutating the page DTO (basic theme's blog
routing does this).
public function pages(): PageRepository
Convenience accessor that returns the same instance as the
pages property. Prefer the property in new code; the method is
kept for compatibility with legacy theme templates.
public function throw404(): never
Sends HTTP/1.0 404 Not Found, includes the theme's
404.php (or whatever $config['404page'] points at), exits.
Themes can render a custom 404 by shipping that file.
public function cache(): string
Captures the output buffer started by template.php and returns
it. Subclasses layer caching on top by overriding (BasicTheme
does this with its SuperCache integration).
For subclasses (protected)
protected function init(): void
Empty default. Subclasses override to populate $themeConfig and
any theme-specific state once construction has wired the standard
services.
protected function renderContent(): string
protected function renderNavigation(): string
Default implementations. renderContent() dispatches
ContentRendering (so plugins can substitute the slot) and falls
back to Sanitizer::markdown($page->content). renderNavigation()
walks findActiveByParent(0) and emits a flat top-level list.
Override either to control the markup.
Lifecycle
One instance per request, constructed in the theme's _ext.php
(or public/index.php if no _ext.php is in play). Constructor
signature:
public function __construct(
protected Container $container,
array $config,
protected string $scriptorRoot,
)
The Container is the League DI container set up in
boot/App.php. The constructor pulls a fixed set of services
out of it (sanitizer, session, logger, page repository, file
storage, image processor, etc.), wires up $input,
$urlSegments, $siteUrl, $themeUrl, then calls init(). By
the time your init() runs, every public property is populated.
After construction, the controller (theme's _ext.php) calls
execute() once. That dispatches the PSR-14 events that resolve
the URL into a Page and assigns it to $page. After execute(),
template.php runs with $site in scope.
Session state is opt-in: the constructor only opens the session
if a cookie is already in the request. flashMsg() opens it
lazily on the write side. Anonymous visitors with no cookie skip
the session entirely so the public site stays stateless by
default.
Common patterns
Subclassing for theme config and a custom hero
final class AtelierTheme extends \Scriptor\Boot\Frontend\Site
{
protected function init(): void
{
$this->themeConfig = [
'site_name' => 'Atelier',
'copyright_info' => '© 2026 Atelier',
'footer' => [
'sub_heading' => 'Get in touch',
'sub_paragraph' => 'Studio Berlin / Cologne',
],
];
}
public function render(string $element): ?string
{
if ($element === 'hero' && $this->page?->slug === '') {
return $this->renderHero();
}
return parent::render($element);
}
private function renderHero(): string
{
return '<section class="hero"><h1>'
. htmlspecialchars($this->getTCP('site_name'), \ENT_QUOTES)
. '</h1></section>';
}
}
POST-redirect-GET inside a theme method
public function contactAction(): void
{
if ($this->input->method() !== 'POST') {
return;
}
$email = $this->sanitizer->email($this->input->post('email', ''));
$message = $this->sanitizer->multiline($this->input->post('message', ''));
if ($email === '' || $message === '') {
$this->addMsg('error', 'Email and message are required.');
return;
}
$this->logger->info('contact form sent from {email}', ['email' => $email]);
$this->flashMsg('success', 'Thanks, we will be in touch.');
$this->redirect($this->siteUrl . '/contact/', 303);
}
See also
Page: the read-only DTO$site->pagepoints atPageRepository:$site->pagesfor tree queriesSanitizer: the$site->sanitizersurfaceImageUrlBuilder: the$site->imagessurfacePageResolving,PageResolved,RouteNotFound: the eventsexecute()dispatchesContentRendering: the eventrenderContent()dispatches- Build a Theme: Layouts, Assets, Navigation
walks
getPageUrl()andrender('navigation')against the Atelier theme - Build a Theme: Forms, Errors, Publishing
walks the PRG flow with
sanitizer,addMsg,flashMsg,logger, andredirect(..., 303)