Meridian handbook
For agentsRecipes

Landing pages

How an agent declares place-and-category landing pages on an engine-dmo listing route, what makes a page live, the optional landing template, and how to verify the configuration and the pages.

engine-dmo

This feature is part of the DMO bundle (engine-dmo).

A landing page is a listing index filtered by a place and a category, at a clean address with its own title and description: “Places to stay in Monmouth” at /monmouth/places-to-stay instead of /places?area=...&category=stay. The engine counts the published results of every pairing and publishes each page with at least min_results, lists it in the sitemap, and leaves a redirect when a page goes away.

Start from the DMO directory: a listing route with an index:, area and category in its facets:, and a routed area type. As there, the examples use the locale code en: replace it with your source locale everywhere, because a slug under a locale the site does not enable only warns (slug for locale en-GB, which site main does not enable), and the page is then served at the raw option value (/monmouth/stay).

Declare the landings

Add landings: to the listing route. Leave the rest of the file as it is, the page route with its priority: included:

# config/sites/main/routes.yaml
routes:
  - type: page
    id: rte_rabyc471nc            # keep the ids the file already has
    uri: "/{slug}"
    priority: 10
  - type: listing
    id: rte_fggnn9hxs9
    uri: "/places/{slug}"
    index: "/places"
    paginate: 24
    facets: [area, category]
    landings:
      - key: by-place
        place: area
        category: category
        uri: "{place}/{category}"
        slugs:
          stay: { en: places-to-stay }
          eat-drink: { en: food-and-drink }
          see-do: { en: things-to-do }
        min_results: 3
        title: { en: "{category} in {place}" }
        description: { en: "{count} {category_lower} in {place}, including {examples}." }
  - type: area
    id: rte_e3nyk9a1s7
    uri: "/{slug}"
    nest: true
    index: "/areas"

home: { type: page, slug: home }
engine-dmo check --fix
engine-dmo schema plan
engine-dmo schema apply

check --fix writes the landing’s lnd_ id. Give slugs, title and description a value for every locale the site enables; each slug is one path segment. check refuses a landing whose place or category is not in the route’s facets, a route without index:, or a place type not routed on the site.

KeyMeaning
placeA facet that is a single relation to a place type (area)
categoryA facet that is a select or a relation
uri"{place}/{category}" puts the page under the place’s own address; "{category}" alone makes a category page under the index; "{month}" with month: a month page for events
min_resultsThe fewest published results for a page to be live (default 3)
title, descriptionPatterns with {place}, {category}, {category_lower}, {count}, {examples}
contentA type editors fill in for each page, adding their own copy

The template

A landing page renders listing/landing.html when the site’s chain has one, otherwise listing/index.html, with everything an index page has plus landing (title, heading, description, content, breadcrumbs, siblings, jsonld and more). page.title is the generated title and head() prints it, with the description, so an index template whose h1 allows for a landing already works ({% if page.landing %}{{ landing.heading }}{% else %}..., as the directory’s does).

A landing page’s own template, when it should differ from the index, needs its one h1, its breadcrumbs and its results:

{# templates/_shared/listing/landing.html #}
{% extends "base.html" %}
{% block body %}
<nav aria-label="{{ t('crumbs.label') }}">
  <ol>{% for c in landing.breadcrumbs %}<li>{% if c.current %}<span aria-current="page">{{ c.title }}</span>{% else %}<a href="{{ c.uri|url }}">{{ c.title }}</a>{% endif %}</li>{% endfor %}</ol>
</nav>
<h1>{{ landing.heading }}</h1>
{% if landing.description %}<p>{{ landing.description }}</p>{% endif %}
<ul class="cards">
  {% for listing in items %}
  <li data-mrd-impression="{{ listing.id }}">
    <h2><a href="{{ listing.uri|url }}">{{ listing.title }}</a></h2>
    <p>{{ listing.summary }}</p>
  </li>
  {% endfor %}
</ul>
{{ impressions() }}
{% if landing.siblings %}
<h2>{{ t('landing.more') }}</h2>
<ul>{% for s in landing.siblings %}<li><a href="{{ s.uri|url }}">{{ s.title }}</a> ({{ s.count }})</li>{% endfor %}</ul>
{% endif %}
{% endblock %}

Add crumbs.label and landing.more to every locale’s strings. There is no {% block title %} to fill: head() prints the title, and the workflow’s layout has only a body block.

To link from an area to its landing pages:

{% if entry.landings %}
<h2>{{ t('area.explore') }}</h2>
<ul>{% for l in entry.landings %}<li><a href="{{ l.url|url }}">{{ l.title }}</a></li>{% endfor %}</ul>
{% endif %}

When pages appear

Pages are counted by a background job of the running server: at every start, whenever published content on the site changes, and when routes move. engine-dmo render runs no jobs, so right after schema apply and seed apply a landing address answers 404 until a server (engine-dmo dev up or engine-dmo serve) has started once against the database. The job runs in the background just after the server starts, under a second on a small seed, so allow a moment and poll rather than trusting the first 404:

LISTEN_ADDR=127.0.0.1:8091 BASE_URL=http://localhost:8091 engine-dmo dev up &
until curl -sf -o /dev/null http://localhost:8091/monmouth/places-to-stay; do sleep 1; done

LISTEN_ADDR chooses the port on a new project, because its engine.yaml reads ${LISTEN_ADDR:-127.0.0.1:8080}; dev up --listen does the same. dev up also binds 127.0.0.1:9090 for its metrics, and a second server on one machine stops at start with MRD-3003: metrics listen on 127.0.0.1:9090: bind: address already in use. For a second project at once, set telemetry.metrics.bind in its engine.yaml to another port. Stop the server when you have your answer; the counts stay in the database, so render then sees them.

Verify

engine-dmo check
engine-dmo render /monmouth/places-to-stay --json     # after a server has run
engine-dmo a11y audit --all                            # live landing pages are in the sitemap

A live landing answers 200 with the generated title. A pairing with fewer than min_results results answers 404, which is correct. See Landing pages for month pages, editors’ copy and redirects.