Pages

Every page is a folder on disk. _data.json holds content fields; _index.json holds structural metadata.

On this page

Structure

Pages live in site/pages/. The folder name is the URL slug, and nesting nests the URL:

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

Content fields. Any key is valid; the view decides which to use.

{
    "title": "My Post",
    "body": "Hello world."
}

_index.json

Structural metadata. Every page folder needs one with at least a uuid, which is what marks the folder as a page. The admin and Page::new() create it for you.

{
    "uuid": "abc123",
    "view": "post",
    "published": true,
    "access": "",
    "hidden": false,
    "locked": false,
    "created": "2026-01-01T00:00:00+00:00",
    "updated": "2026-01-01T00:00:00+00:00"
}

published (on unless set to false) and access decide who can open the page, hidden keeps it out of listings, and locked blocks changes. They are independent. See Publishing and access.

In templates

$page is the current page. The structural keys (uuid, view, published, access, hidden, locked, created, updated) are typed readonly properties. Everything else, title included, is a content field from _data.json that returns a FieldValue:

{$page->url()}
{$page->title}          {* decoded string *}
{$page->body|render}    {* via the field type's render template *}

{if !$page->body->empty()}
    <div class="prose">{$page->body|render}</div>
{/if}

Fetching pages

$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()       // direct children; hidden pages and any the visitor may not open are excluded
$page->child('name')    // named child, doesn't check it exists
$page->parent()         // parent Page or null
$page->parents()        // all ancestors, root first

children() is ordered by the view's sort key in its _index.json unless the query overrides it, and takes a query to filter and sort:

$recent  = $page->children('sort=-created');
$tagged  = $page->children('tag*=php');
$limited = $recent->limit(5);

Large sections

A view's children.path decides where a new child page goes, so a section of any size never becomes one flat folder. The tokens are {slug}, {uuid}, {uuid:N} (its first N characters), {year}, {month} and {day} (the creation date). The last segment names the page and must be {slug} or {uuid}.

// site/views/blog/_index.json
{
    "children": {
        "path": "{year}/{month}/{slug}"
    }
}

Page::new() places the page once, when it is created. A later edit never moves it. Each missing folder on the way is made as a hidden page of the section's view, so access, publishing and the router chain inherit through it like any other page. The folder path is the URL. For a friendlier address, add a route in the section's router that finds the page and redirects to it.

List such a section with find() at the depth of its pages, not with children(), which returns the folders. The registry's (view, created) and (depth, created) indexes keep the list fast at any size:

$page->find('depth=' . $page->childDepth() . ',sort=-created,limit=20')

Files

Files in a page folder are PageFile objects. See Files.

$files = $page->files();
$url   = $files[0]->url();

When you need more

To find pages anywhere in the tree, not just a page's children, see the registry.