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
Custom entitiesPage 23 of 35Updated
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.
The definition is URL addressable and has a record path
Renders
The definition's template
The template of the definition's layout
The template receives
The records under the plural identifier, such as $stores; $_pagination; tags or categories when filtered
The record under the identifier, such as $store, and again as $record
Answers 404 when
The template is empty, or the page number is not a number
The 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:
URL
Shows
/stores
The first page
/stores/2
Page 2; a segment that is not a number answers 404
/stores/tags/open-sunday
Records with that tag
/stores/categories/metro
Records 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:
{% 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:
<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.