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
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.
| Key | Meaning |
|---|---|
place | A facet that is a single relation to a place type (area) |
category | A 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_results | The fewest published results for a page to be live (default 3) |
title, description | Patterns with {place}, {category}, {category_lower}, {count}, {examples} |
content | A 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.
Partner portal
How an agent sets up the member portal on engine-dmo for partner businesses, declaring membership tiers, the member_editable rules that say what each tier may change, an extra portal screen, an organisation with a membership and an invitation, and how to verify each part.
Claims
How an agent lets a business claim its listing on engine-dmo, seeding an organisation for each listing, writing the claim link that has no helper, reading the verification mail, and verifying the form from a script.