Pages
Every page is a folder on disk. data.json holds content fields; index.json holds structural metadata.
Page structure
Pages live in site/pages/. The folder name is the URL slug. Nesting creates nested URLs:
site/pages/
data.json ← / (root page)
index.json
blog/
data.json ← /blog/
index.json
my-post/
data.json ← /blog/my-post/
index.json
data.json
Holds content fields. Any key is valid, the view template decides what to use.
{
"title": "My Post",
"body": "Hello world."
}
index.json
Holds structural metadata. Every page folder must contain one with at least a uuid, it's the canonical marker that a folder is a page. There's no auto-generation: create it explicitly (the admin UI and Page::new() both do this for you).
{
"uuid": "abc123",
"view": "post",
"status": "published",
"hidden": false,
"locked": false,
"created": "2026-01-01T00:00:00+00:00",
"updated": "2026-01-01T00:00:00+00:00"
}
hidden pages are excluded from children() by default. locked pages cannot be deleted via the admin. status is available for filtering but not enforced by the framework.
Template variables
The current page is always available as $page. Structural properties (uuid, view, status, hidden, created, updated) are typed readonly properties, hidden is a bool, the rest are strings:
{$page->title}
{$page->url()}
{$page->created}
title isn't a structural property, it's an ordinary content field like any other, read from data.json. Content fields are accessed via __get, which returns a FieldValue object:
{$page->body} {* FieldValue::__toString(), decoded string *}
{$page->body|render} {* rendered via the field's type template *}
{if !$page->body->empty()}
<div class="prose">{$page->body|render}</div>
{/if}
Fetching pages in PHP
$post = $app->page('blog/my-post'); // any page by path
$home = $app->page('/'); // root page
$active = $app->page(); // current request page
Children and parents
$page->children() // PageCollection of direct children (hidden excluded)
$page->child('name') // named child Page, doesn't check it exists
$page->parent() // parent Page or null
$page->parents() // PageCollection of all ancestors, root first
children() queries the registry (not the filesystem), ordered by the view's own sort key in its index.json unless the query overrides it. The result is cached on the page instance.
Querying children
children()'s $query argument filters and sorts by a query string (PageCollection itself has no find(), the query has to go in here):
$recent = $page->children('status=published, sort=-created');
$tagged = $page->children('tag*=php'); // field contains value
$limited = $recent->limit(5);
Operators: = != *= (contains) ^= (starts) $= (ends) < > <= >=. Sort prefix - for descending.
Page files
Files placed in a page folder are accessible as PageFile objects:
$files = $page->files();
$url = $files[0]->url();
$size = $files[0]->size;
<?php
// Fetch a page
$post = $app->page('blog/my-post');
echo $post->title; // content field, decoded to a plain string
echo $post->url(); // /blog/my-post/
// Access a content field
$field = $post->body; // FieldValue object
$raw = $field->value; // raw stored string
$html = $field->decode(); // decoded value (HTML for markdown, etc.)
$empty = $field->empty(); // true if decoded value is blank
// Query children
$posts = $app->page('blog')
->children('status=published, sort=-created')
->limit(10);