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

When to use

You touch Site from three angles:

  • Theme subclass. You write class AtelierTheme extends Site and override init(), render(), or individual render*() hooks. The bundled basic theme is the reference.
  • Inside a template. A template.php receives $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