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 04 · Custom entities

Listing and detail templates

How a definition's paths become a listing route and a detail route, what each route hands its template, and a worked pair of templates for a store locator.

Who this is for. Developers giving a definition its pages. After this page you can write the listing and detail templates for a definition, predict when either route answers 404, and choose paths that do not collide with the rest of the site.

Two routes per definition

Every definition can have two routes, built from its settings (Defining a custom entity):

ListingDetail
Exists whenThe definition is URL addressable and has a pathThe definition is URL addressable and has a record path
RendersThe definition's templateThe template of the definition's layout
The template receivesThe records under the plural identifier, such as $stores; $_pagination; tags or categories when filteredThe record under the identifier, such as $store, and again as $record
Answers 404 whenThe template is empty, or the page number is not a numberThe layout has no template

The routes are rebuilt from the definitions on every request, and they are registered before the routes of the platform's own modules.

Keep definition paths clear of module paths

Because custom entity routes come first, a definition whose path is a module's prefix takes that address over. A definition path of blogs takes over the blog listing.

The listing route

The listing shows the records of the current site when the definition is sited, ordered by position when it is positioned. After the definition's path, the URL can carry:

URLShows
/storesThe first page
/stores/2Page 2; a segment that is not a number answers 404
/stores/tags/open-sundayRecords with that tag
/stores/categories/metroRecords in that category

Several slugs combine with & (all of them) or | (any of them); use one operator in a single value. The matching tags or categories are passed to the template as tags or categories.

The detail route

The slug is what follows the record path: /store/richmond shows the record with the slug richmond, from the current site when the definition is sited. A signed-in backend user can add ?revision=<id> to see another revision of the record, as on a page (Revisions and the active revision).

The record reaches the template twice: under the definition's identifier and as $record. A store's template can write {$store->openingHours} or {$record->openingHours}; the identifier reads better, and $record lets one partial serve several definitions.

Where the templates go

Nothing enforces a folder layout, but one folder per definition reads well: set the definition's template to stores/index.html and point its layout at stores/show.html.

A worked pair

The listing loops over $stores and links each record to its detail page:

Template code templates/stores/index.html
{foreach $stores as $store}
  <article>
    <h2><a href="{$store->getFullPath()}">{$store->getName()|escape}</a></h2>
    <p>{$store->suburb}</p>
  </article>
{foreachelse}
  <p>No stores yet.</p>
{/foreach}
{include 'includes/pagination.html'}
{% for store in stores %}
  <article>
    <h2><a href="{{ store.getFullPath() }}">{{ store.getName()|e }}</a></h2>
    <p>{{ store.suburb }}</p>
  </article>
{% else %}
  <p>No stores yet.</p>
{% endfor %}
{% include 'includes/pagination.html' %}

The pagination include is your own partial, reading $_pagination. The detail template prints the record's layout areas with the shorthand; an image area's shorthand returns the image object, blank when the area is empty:

Template code templates/stores/show.html
<h1>{$store->getName()|escape}</h1>
{$store->openingHours}
{if !$store->photo->isBlank()}
  {$store->photo->getResized(800, 600, 'fit')->getHtml()}
{/if}
<h1>{{ store.getName()|e }}</h1>
{{ store.openingHours }}
{% if not store.photo.isBlank() %}
  {{ store.photo.getResized(800, 600, 'fit').getHtml() }}
{% endif %}

More than one detail template

A definition has one layout, so every record renders through the same detail template. When some records need another presentation, branch on a select layout area inside the template and include a partial; when they need different fields, make them a separate definition. Defining a custom entity shows both.

Go deeper

Was this page helpful?

Your answer goes to the docs team.