Meridian handbook
For agentsRecipes

Add a route and its template

How an agent gives a content type addresses on a site with dev route add, writes the entry and index templates a route needs, and verifies them with describe route and render.

A route gives a type’s documents addresses on one site: an entry page per document (/walks/{slug}) and, optionally, an index page listing them (/walks). A route with no template is refused by check, so the route and its templates land together.

The recipe touches these files, and the engine reads them in this order when a request comes in:

flowchart LR
  req["GET /walks/cherwell-towpath"] --> routes["config/sites/main/routes.yaml<br/>uri /walks/{slug}"]
  routes --> doc["The published walk<br/>with that slug and locale"]
  doc --> chain{"walk/entry.html in<br/>templates/main/?"}
  chain -- yes --> site["templates/main/walk/entry.html"]
  chain -- no --> shared["templates/_shared/walk/entry.html"]
  site --> base["extends base.html"]
  shared --> base

Steps

Add the route. --scaffold writes the entry template from the type’s fields. It loads the whole configuration to do so, so export every ${VAR} first (DATABASE_URL, and SIGNING_KEY once engine.yaml names it): with one unset, the verb writes routes.yaml, exits 3 without the template, and a second run says nothing to change. Recover with engine-dmo new template walk/entry.html.

engine-dmo dev route add walk "/walks/{slug}" --index /walks --scaffold --apply
# config/sites/main/routes.yaml
routes:
  - type: page
    id: rte_ajdbyvntec
    uri: "/{slug}"
  - { type: walk, id: rte_34agwcmxkh, uri: "/walks/{slug}", index: /walks }

home: { type: page, slug: home }

Write the index template, which --scaffold does not:

engine-dmo new template walk/index.html

Until it exists, check fails with type "walk" has an index route and no walk/index.html on site main. The scaffold is an empty body block with no h1, so render /walks --json reports a page has exactly one h1, this one has 0 until you write it (Listing page).

Replace the scaffolds with real templates. The entry template receives the document as entry and under the type’s key (walk):

{# templates/_shared/walk/entry.html #}
{% extends "base.html" %}
{% block body %}
<article>
  <h1>{{ walk.title }}</h1>
  {{ walk.photo.figure("hero", priority=true) }}
  {% if walk.summary %}<p>{{ walk.summary }}</p>{% endif %}
  <dl>
    <dt>{{ t('walks.difficulty') }}</dt><dd>{{ walk.difficulty.label }}</dd>
    {% if walk.distanceKm %}<dt>{{ t('walks.length') }}</dt><dd>{{ t('walks.distance', 'km', walk.distanceKm|number) }}</dd>{% endif %}
  </dl>
  {% if walk.body %}<div class="prose">{{ walk.body|ptext }}</div>{% endif %}
</article>
{% endblock %}
# config/strings/en.yaml
strings:
  walks.difficulty: Difficulty
  walks.length: Length
  walks.distance: "{km} km"

figure("hero") needs a hero transform in config/images.yaml (Responsive images). An empty asset field prints nothing through figure(), img() and picture(), so the call needs no {% if %} of its own; wrap it only when you add markup around it. Write the index template next: Listing page.

Verify

engine-dmo check
engine-dmo describe route /walks/cherwell-towpath
engine-dmo render /walks/cherwell-towpath --json
engine-dmo render /walks --json

describe route says which route and template answered, and --json adds the whole context with real values. render --json must show "status": 200, "errors": [] and "a11y": []. A path that matches no published document answers 404 with kind: not_found.

Route options

Edit routes.yaml by hand for anything dev route add does not set; check --fix keeps the ids.

KeyUse it for
uriThe entry path. One string for every locale, or a map per locale: { en: "/walks/{slug}", cy: "/teithiau/{slug}" }
indexThe index path, one string or per locale. Needs <type>/index.html
paginateEntries per index page
facetsIndexed fields the index can be filtered by (?difficulty=easy)
nest: trueEntries nest under their parent’s path (/about/our-history)
priorityOrders two patterns of one shape, lowest first. Two routes of one shape and priority are refused as ambiguous. Each keeps answering for its own documents, so the number settles the check and hides nothing (DMO directory)
feed, calendarAn RSS, Atom and JSON feed, or an iCalendar feed, of the route

home: { type: page, slug: home } names the home page (or home: { type: homepage } for a singleton type). special: { search: /search } routes the search page. routes.yaml lists every key.

Where templates live

Templates resolve along the site’s chain: templates/<site>/ first, then templates/_shared/. Put templates every site uses in _shared, and a site’s own version of one in templates/<site>/ (engine-dmo new template walk/entry.html --site town --from-shared copies it). engine-dmo describe template walk/entry.html --site town says which file wins.

TemplateRendered for
<type>/entry.htmlA document’s page
<type>/index.htmlThe route’s index
<type>/landing.htmlA landing page (engine-dmo), falling back to index.html
search.htmlThe search route
errors/404.html, errors/500.htmlErrors
base.htmlThe layout every page extends (a convention, not a requirement)