How-to library · Developer docs

How-to library

Task-shaped pages: a title that starts with a verb, the situation in a few lines, the steps, the working code in both engines, and why this way rather than another. Found from a search, done, and left.

Model a simple directory as a custom entity

Sixty stores with an address, hours and a photo are a definition with a listing path, a record path, a handful of areas, tags for facets and a positioned or alphabetical listing. No PHP, two templates.

Context

Context. The three-part test says a store directory is a custom entity: each store has its own fields, the directory needs a listing with filters, and a store does not belong in the page tree. The whole thing is a definition, a layout with areas, and two templates.

Steps

  1. In the admin, under Customization, add a custom entity definition: name Store, identifier store, path stores, record path store, taggable on (for facets such as "open Sunday"), positioned off (alphabetical), publishable on, listing template stores/index.html.
  2. Open the layout the definition created ("Custom entity: store"), point it at stores/show.html, and add areas: suburb (Text), address (Textarea), phone (Text), openingHours (Richtext), photo (Image), mapUrl (Text).
  3. Deploy the two templates; the routes /stores and /store/<slug> exist as soon as the definition is saved.
  4. Enter three stores and check both URLs signed out.

Code

Template code
{* templates/stores/index.html: receives $stores and $_pagination *}
{extends 'includes/frontend/base-template.html'}
{block name='content'}
<h1>Our stores</h1>
{foreach $stores as $store}
  <article class="store">
    <h2><a href="{$store->getFullPath()}">{$store->getName()|escape}</a></h2>
    <p>{$store->suburb} · {$store->phone}</p>
  </article>
{foreachelse}
  <p>No stores yet.</p>
{/foreach}
{/block}
{# templates/stores/index.html #}
{% extends 'includes/frontend/base-template.html' %}
{% block content %}
<h1>Our stores</h1>
{% for store in stores %}
  <article class="store">
    <h2><a href="{{ store.getFullPath() }}">{{ store.getName()|e }}</a></h2>
    <p>{{ store.suburb }} · {{ store.phone }}</p>
  </article>
{% else %}
  <p>No stores yet.</p>
{% endfor %}
{% endblock %}

The detail template receives the record under the identifier and as record:

Template code
{* templates/stores/show.html *}
{extends 'includes/frontend/base-template.html'}
{block name='content'}
<h1>{$store->getName()|escape}</h1>
{$photo = $store->getAreaImage('photo')}
{if !$photo->isBlank()}{$photo->getResized(1200, 600, 'fit')->getHtml(['alt' => $store->getName()|escape])}{/if}
<address>{$store->address|nl2br}</address>
{$store->openingHours}
{/block}
{# templates/stores/show.html #}
{% extends 'includes/frontend/base-template.html' %}
{% block content %}
<h1>{{ store.getName()|e }}</h1>
{% set photo = store.getAreaImage('photo') %}
{% if not photo.isBlank() %}{{ photo.getResized(1200, 600, 'fit').getHtml({'alt': store.getName()|e}) }}{% endif %}
<address>{{ store.address|nl2br }}</address>
{{ store.openingHours }}
{% endblock %}

Why this and not that

Not pages under /stores: no listing route, no facets, and sixty pages an editor has to keep in a tree. Not blog posts: dates that mean nothing and a "blogs" segment in every URL. Choose the identifier as carefully as a table name; it cannot change once records exist, and it names the template variables, the query methods and the API paths.

Go deeper

Was this page helpful?

Your answer goes to the docs team.