Templates
How a request becomes a page, which template file renders each kind of page, how the template chain resolves names, and the commands that show what a template receives.
Meridian renders every public page on the server from HTML templates in your project’s templates/ directory. A template is plain HTML with Pongo2 template syntax (Django and Jinja style) for printing values, looping and inheriting layouts. The engine decides which template to render from the URL, builds the template’s context (the variables it can read), renders it, and sends the result.
This page explains that path end to end. The rest of the section is the reference:
Everything here applies to both bundles: engine-cms and engine-dmo render templates the same way. The commands below use engine-dmo; replace it with engine-cms on a CMS project.
A minimal page
A new project has a layout, one type template and two error pages:
<!doctype html>
<html lang="{{ locale.code }}" dir="{{ locale.direction }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{ head() }}
</head>
<body>
<main id="main">{% block body %}{% endblock %}</main>
</body>
</html> {% extends "base.html" %}
{% block body %}
<h1>{{ entry.title }}</h1>
{% endblock %} A request for /about finds the published page document whose slug is about, renders page/entry.html with that document as entry, and base.html supplies the surrounding layout. head() writes the title, description, canonical link and the rest of the head metadata (see SEO and structured data).
The starter layout is a placeholder
The base.html that engine-dmo init writes is smaller than the one above: it prints its own <title> and canonical link and has no head(). engine-dmo check therefore prints warning: this head has no head() and its own tags miss a meta description on a brand-new project, and still exits 0 with 0 errors, 0 warnings, because template warnings are not counted (Testing). Replace the starter’s <title> and <link rel="canonical"> with {{ head() }}, as above, before you build on it. The core page type has only title and slug, so head() has no description for a page until you add a summary or seo field to it (SEO).
How a request becomes a page
The engine answers a request in this order. The first step that claims the path answers it.
flowchart TD
R["Request"] --> SL["Pick the site (host)<br/>and the locale (prefix or host)"]
SL --> EP{"An engine path?<br/>endpoint, feed, search, sitemap ..."}
EP -- yes --> EPA["The engine or module answers"]
EP -- no --> C{"A route matches?"}
C -- "a document" --> DOC["type/entry.html"]
C -- "an index" --> IDX["type/index.html"]
C -- "a landing page" --> LND["type/landing.html"]
C -- no --> F{"A file under public/?"}
F -- yes --> FILE["The file"]
F -- no --> RD{"A redirect rule?"}
RD -- yes --> RED["The redirect, or 410"]
RD -- no --> NF["errors/404.html, 404"]Site and locale. The request’s host picks the site (a site lists its domains in site.yaml), and the path’s locale prefix, or the host under the domain strategy, picks the locale. The rest of the path is what the site resolves.
Engine paths. /robots.txt and the sitemap, the embed widgets, a staff member’s preview, declared endpoints (form posts and fragments), a route’s feeds and calendars, the search route, tracked outbound redirects (/r/v1/...) and any path a module serves are handled before content. Only GET and HEAD go further.
Content. The path is resolved against the site’s routes:
- a document’s address renders the document with its type’s entry template;
- a route’s
index:address renders the type’s index page; - with
engine-dmo, a landing page (a category in a place) renders the type’s landing template.
A miss. A path no document or index answers is tried as a file under public/ (along the site’s public chain), then against the redirect rules, and otherwise renders the not-found template with status 404.
Every page is rendered completely in memory first. If the render fails, nothing of it is sent: the visitor gets the error template with status 500, and the error is logged with the file, line and column.
Which template renders what
| Page | Template | Status | What the template adds to the context |
|---|---|---|---|
| A document | <type>/entry.html, or the file the type’s template: names | 200 | entry, the same value under the type’s key, page |
A document in an entry variant with its own template: | that file, by convention <type>/<variant>.html | 200 | as above, with entry.variant |
| The home page | the entry template of the home document’s type (home: in routes.yaml) | 200 | as a document |
A type’s index (index: on its route) | <type>/index.html | 200 | items, pagination, facets, page with page.index true |
A landing page (engine-dmo) | <type>/landing.html, else <type>/index.html | 200 | as an index, plus landing |
The search route (special.search) | search.html | 200 | results, pagination, page |
| Not found | errors/404.html (template.not_found_template) | 404 | page with type error |
| A render error | errors/500.html (template.error_template) | 500 | page with type error |
| An item of a blocks field | blocks/<block key>.html | block, blockIndex, blockCount | |
| A rich text object block | ptext/<set>/<block key>.html | node, index |
When the not-found or error template is missing from the chain the engine answers with a plain-text response instead. The search route answers 404 when the chain has no search.html. The full context of each kind of page is on The page context.
There is no fallback to a flat <type>.html: a project that still has one gets a missing-type-template finding from engine-dmo check, naming the file it expects.
The template chain
Each site has an ordered list of template roots, its chain. A name such as page/entry.html is looked up in each root in turn and the first file that exists wins. The default chain is the site’s key, then _shared:
templates:
roots: [main, _shared] # the default; list your own to change it So templates/main/page/entry.html overrides templates/_shared/page/entry.html for the site main alone.
Three resolution rules matter:
- Extending your own name reaches the next root. A file that extends or includes the name it is itself resolved as skips its own root and continues along the chain.
templates/main/page/entry.htmlcan write{% extends "page/entry.html" %}and replace one block of the shared file, with no special keyword. - Relative first. An include or extends name is tried relative to the including file’s directory first, then from the top of each root.
- Never outside the directory. Names are relative to the templates directory.
..and absolute paths are refused.
A root-qualified name, such as _shared/page/entry.html, resolves to exactly that file. Multisite projects use the chain heavily; see Multisite templates.
Template configuration
The template: block of config/engine.yaml:
| Key | Default | Meaning |
|---|---|---|
dir | templates | The templates directory, relative to the project |
contract | 1 | The template contract version the project is written against |
cache | true | Keep parsed templates in memory. false rereads every template on each render, as serve --dev does |
strict | false | Refuse to boot when a template reads a variable or field that does not exist. Without it such a read is a warning and renders empty |
not_found_template | errors/404.html | The template for a 404 |
error_template | errors/500.html | The template for a 500 |
And per site, in site.yaml:
| Key | Default | Meaning |
|---|---|---|
templates.roots | [<site key>, _shared] | The site’s template chain |
templates.allow_unescaped | false | Allow {% autoescape off %} and the safe filter in this site’s templates. See Autoescaping |
At boot the engine parses every template on every site’s chain and runs the template check. A template that does not parse stops the boot. Other findings refuse a production boot and are warnings in development; an unknown variable or field refuses a boot only with template.strict: true.
Seeing what a template receives
You never have to guess what a page’s context holds. These commands read the project and, where they say so, the database:
| Command | What it tells you | Needs a database |
|---|---|---|
engine-dmo describe route /places/the-red-lion --json | Which route and template answer the path, and the whole context with real values | Yes |
engine-dmo render /places/the-red-lion | The rendered page through the real pipeline, in process, with no server. --json adds errors, queries, assets and accessibility findings | Yes |
engine-dmo describe template listing/entry.html | The file the name resolves to on the site’s chain, what it extends and includes, and the type it renders | No, but every ${VAR} the configuration uses must be set |
engine-dmo template api --json | Every global, filter, tag and field type the site’s templates can use, with your own types and fields | No |
engine-dmo check | Every template parsed and checked against the context, the schema and the security rules | Use --no-db to skip database checks |
render and describe route take --site, --locale and --host to pick what the request would carry. A render made by these commands is not counted as a visit. Without a database there is no way to render a page: render, describe route and engine-dmo a11y audit all stop with MRD-3001 when they cannot connect. Create a local database for them (Install).
In development, engine-dmo serve --dev rereads a template and everything it extends or includes on every request, and a template that fails shows a development error page with the file, line, column and the source line marked, in place of the site’s error template. That page is never shown without --dev.
Add a template for a type
Scaffold it: engine-dmo new template listing writes listing/entry.html in the shared root (--site main writes into the site’s own root). For an existing template, engine-dmo new template listing/entry.html --from-shared --site main copies the shared one into the site’s root so you can change it there.
Look at what the page will receive: engine-dmo describe route <a listing's path> --json.
Write the template, then run engine-dmo check. Fix every error it reports at its file, line and column.
Render it: engine-dmo render <the path>, or open it under engine-dmo serve --dev.
Seeding and importing
Getting content into a project: seed sets (seed.yaml, documents per type and locale, assets, history and publishing states, references by seed key, verify, apply and reset), generated content, content create and update, and JSON export and import between environments.
The template language
The Pongo2 syntax Meridian templates use (printing, expressions, filters, keyword arguments, inheritance, includes, macros, loops, whitespace) and the escaping and security rules the engine enforces on every template.