Meridian handbook
For agentsRecipes

Search page

How an agent adds a search page to a Meridian site with the special search route and search.html, what browse mode returns without a search service, how to connect one, and how to verify the page renders and is accessible.

Search is a special route that renders search.html with a results object. With no search service configured, the page answers in browse mode over published content; connect Meilisearch, Elasticsearch or OpenSearch for ranked, matched results. The template is the same either way.

Route it

# config/sites/main/routes.yaml
special:
  search: /search

Per locale: search: { en: /search, cy: /chwilio }. check refuses a site that routes search without a search.html, and warns while no backend is configured.

Write the template

{# templates/_shared/search.html #}
{% extends "base.html" %}
{% block body %}
<h1>{{ t('search.title') }}</h1>
<form method="get" action="{{ request.path|url }}" role="search">
  <label for="q">{{ t('search.field') }}</label>
  <input id="q" type="search" name="q" value="{{ results.query }}">
  <button type="submit">{{ t('search.go') }}</button>
</form>
{% if results.degraded %}<p role="status">{{ t('search.browse_notice') }}</p>{% endif %}
{% if results.items %}
<p>{{ t('search.count', results.total) }}</p>
<ol>
  {% for item in results.items %}{% if item.title %}
  <li>
    <h2><a href="{% if item.clickUrl %}{{ item.clickUrl|url }}{% else %}{{ item.uri|url }}{% endif %}">{{ item.title }}</a></h2>
    {% if item.highlight %}<p>{{ item.highlight }}</p>{% elif item.summary %}<p>{{ item.summary }}</p>{% endif %}
  </li>
  {% endif %}{% endfor %}
</ol>
{% if results.pagination.next %}<a rel="next" href="{{ results.pagination.next|url }}">{{ t('pager.next') }}</a>{% endif %}
{% elif results.query %}
<p>{{ t('search.none') }}</p>
{% endif %}
{% endblock %}
# config/strings/en.yaml
strings:
  search.title: Search
  search.field: What are you looking for?
  search.go: Search
  search.browse_notice: Results are filtered, not ranked.
  search.count:
    one: "{count} result"
    other: "{count} results"
  search.none: Nothing matched. Try a shorter phrase.
  • The form is a plain GET, so search works without JavaScript and every result page has an address.
  • {% if item.title %} leaves out results with no title (a singleton home page, for example), which would otherwise print an empty link that fails the accessibility audit.
  • If you print the type facet (results.facets.type), label each value with f.label|default:f.value: in browse mode the labels can be empty.
  • searchForm() prints a search box for the header: a GET form to this route, and nothing on a site that routes no search.

Connect a search service

Browse mode lists published documents in the locale and does not rank or match words, so tell the person it is a fallback. For real search, run a backend and point the engine at it:

engine-dmo dev config set engine.yaml search.driver meilisearch

The connection settings and keys (always ${VAR}) are in Search. Then engine-dmo search rebuild indexes every published document and engine-dmo search test "towpath" shows what a query returns.

Verify

Search pages are not in the sitemap, so name them:

engine-dmo render '/search?q=towpath' --json
engine-dmo a11y audit --path /search --path '/search?q=towpath'

Both must answer 200 with no findings.