Guide 05 · Developer docs

Content types and custom entities

Which built-in type fits which purpose, how categories, tags, slugs, custom URLs and SEO settings behave across them, how revisions and publishing gate what the public sees, how to model a domain the platform has no type for, and where uploaded files live.

L1–L3
Unit 01 · The built-in content types

Pages and the page tree

Pages are the default content type: a tree under the home page, where every page's URL is built from the slugs above it. What the tree gives you for free, nested URLs, breadcrumbs and child navigation, which template renders a page, and when a flat structure is the better fit.

Who this is for. The theme developer building a site section and the content modeller deciding how deep it should go. Afterwards you can render breadcrumbs and section navigation from the tree, and know what moving a page does to its URLs.

The tree

Every site has one page with no parent and no slug, the home page, which answers at /. Every other page has a parent and a slug that is unique among its siblings, and its position orders it among them; editors change the position by dragging in the admin's page list. On this site, /developer-docs is a page whose parent is the home page, and /developer-docs/api-reference is a child of that page.

The tree gives three things without any work:

  • Nested URLs. A page's address is the slugs of its ancestors joined with slashes, below the home page.
  • Breadcrumbs. getAllParents() returns the ancestors, ordered from the root to the closest.
  • Section navigation. getChildren() returns a page's children in position order, so a landing page can list its section without a query.
Template code A breadcrumb and the children of the current page
<nav class="breadcrumb">
  {foreach $page->getAllParents() as $ancestor}
    <a href="{$ancestor->getFullPath()}">{$ancestor->getName()|escape}</a> /
  {/foreach}
  <span>{$page->getName()|escape}</span>
</nav>

{$children = $page->getChildren()}
{if count($children) > 0}
  <ul class="subnav">
    {foreach $children as $child}
      <li><a href="{$child->getFullPath()}">{$child->getName()|escape}</a></li>
    {/foreach}
  </ul>
{/if}
<nav class="breadcrumb">
  {% for ancestor in page.getAllParents() %}
    <a href="{{ ancestor.getFullPath() }}">{{ ancestor.getName()|e }}</a> /
  {% endfor %}
  <span>{{ page.getName()|e }}</span>
</nav>

{% set children = page.getChildren() %}
{% if children|length > 0 %}
  <ul class="subnav">
    {% for child in children %}
      <li><a href="{{ child.getFullPath() }}">{{ child.getName()|e }}</a></li>
    {% endfor %}
  </ul>
{% endif %}

A visitor only ever receives published children. A signed-in administrator receives the unpublished ones too, because the admin setting Show unpublished is on by default, so check navigation like this in a private window. getParent() is the page above; its getChildren() are the current page's siblings.

Two paths are available on every page. getFullPath() is the path to link to: the page's custom URL when it has one, otherwise its place in the tree. getOriginalFullPath() is always the tree path. Both return an empty string when no path can be built, so treat an empty value as "no URL" rather than printing it.

Which template renders a page

A page renders through the layout on its active revision: the layout names a template file, and that file is the entry point. The template receives the page as page and again as record. A page whose revision has no layout renders pages/pages/show.html, which the framework does not ship, so give every page a layout or give the site that file. The layout model explains the rest, and Start with a placeholder template walks through it once.

Moving a page moves every URL below it

Because the address is built from the ancestors' slugs, moving a page under another parent, or changing the slug of a page high in the tree, changes the URL of every page beneath it at once. The platform creates no redirects for the old addresses. Add them as rewrites in the same change: Slugs shows what to do.

Flat or nested

Nest when the URL should say where a page belongs (/services/consulting/pricing) and when a landing page should list its section. Keep pages directly under the home page when they are independent destinations, such as campaign or legal pages, where an ancestor in the URL would be noise.

A tree of pages is the wrong shape in two cases. If readers progress through the content in order, a book gives reading order and position numbers that a tree does not. If there are many items with the same fields, filtered and listed rather than navigated, they are custom entity records: the page module has no listing route with paging or filters.

"Related pages" means translations

getRelatedPages() and getSourcePage() link a page to its translations, not to related content. To let an author pick related content, give the layout an area of type Records and read it with getAreaRecords(). Translations covers the language pair.

Go deeper

Was this page helpful?

Your answer goes to the docs team.