Meridian handbook
Concepts

Sites, locales and routes

How one Meridian install serves several sites in several locales, how locales are declared once and enabled per site, how routes give types URLs, and how a request finds its site, locale and document.

One install serves one or more sites from one database. The install declares its locales once; each site enables a subset and chooses how locales appear in its URLs. Each site’s routes decide which content types have pages on it, and at which URI patterns.

A minimal example

Two locales, one site that serves both, and two routed types:

# config/locales.yaml
locales:
  available:
    - code: en
      label: English
      source: true
      timezone: Europe/London
      currency: GBP
    - code: cy
      label: Cymraeg
      fallback: [en]
# config/sites/main/site.yaml
site:
  name: { en: "Visit Wyndmouth", cy: "Ymweld â Wyndmouth" }
  domains: [www.wyndmouth.example, localhost]
  default: true

locales:
  strategy: prefix
  enabled: [en, cy]
  default: en
# config/sites/main/routes.yaml
routes:
  - type: page
    uri: "/{slug}"

  - type: article
    uri:
      en: "/news/{slug}"
      cy: "/newyddion/{slug}"
    index:
      en: "/news"
      cy: "/newyddion"
    paginate: 12

home: { type: page, slug: home }

Run engine-dmo check --fix after writing these by hand: it gives each locale, site and route its id: (see Stable ids). The engine-dmo dev locale add, dev site add and dev route add verbs write the same configuration with ids for you.

With this configuration an English article is at /news/<slug> and its Welsh translation at /cy/newyddion/<slug>. The site needs article/entry.html and article/index.html on its template chain; check refuses the routes until they exist.

Locales

config/locales.yaml lists every locale the install knows, once. A site never redeclares a locale; it enables some of them.

locales:
  available:
    - code: en-GB
      label: English
      source: true
      timezone: Europe/London
      currency: GBP
    - code: cy-GB
      label: Cymraeg
      fallback: [en-GB]
KeyDefaultMeaning
coderequiredA BCP 47 tag such as en-GB, cy or zh-Hans. Declared once
labelrequiredThe locale’s name, as the admin and language switchers show it
sourcefalseThe locale content is written in first and translated from. Exactly one locale must be the source
defaultfalseThe install’s default locale; at most one. Without one, the source locale is the default
timezonethe source locale’sAn IANA time zone. Required on the source locale
currencythe source locale’sAn ISO 4217 code. Required on the source locale
fallbacknoneLocales to read a value from, in order, when this locale has none
statutoryfalseA locale that must be published wherever the source is; publishing without it is a prompt, never a block
translatorsnoneThe staff group translation jobs into this locale are assigned to by default
machine_translationnoneassist or forbid: whether machine translation may be offered for this locale
derived_fromnoneA locale this one is derived from, such as en-US from en-GB
directionfrom the scriptltr or rtl
calendar, numbering, hour_cycle, first_day_of_weekfrom CLDR for the localeHow dates, numbers and times are formatted
collationfrom the tagThe database collation that sorts text in this locale
slugsfrom the scriptnative keeps slugs in the locale’s script; transliterate converts them to Latin
fold_diacriticstrueWhether search treats é as e

groups (beside available) declares sets of locales that share values for fields translated per group. engine-dmo check reports a missing source locale, two source locales, two defaults, an unknown time zone or currency, a fallback to an unknown locale or to itself, and a locale in two groups, each at its line. Localisation explains how fields are translated.

Sites

Each site is a directory under config/sites/, named by its key. An install with one site is no different from one with ten: the admin simply shows no site switcher.

# config/sites/town/site.yaml
site:
  name: { en: "Larkwyn" }
  domains: [www.larkwyn.example, town.localhost]

templates:
  roots: [town, _shared]

locales:
  enabled: [en]

templates/town/ must exist as a directory, since the chain names it; check refuses a root that does not.

The site: block:

KeyDefaultMeaning
namerequiredThe site’s name per locale, read in templates as site.name
short_namenoneA shorter name per locale
domainsnoneThe host names the site answers, matched exactly (with or without the port)
defaultfalseThe site that answers any host no site names. With one site it is the default; with several, exactly one must say default: true
path_prefixnoneA path the site is served under on a shared host, starting with /
timezone, default_currencythe default locale’sThe site’s own, where they differ
countrynoneThe site’s country
organisationthe site nameThe organisation behind the site, for structured data
networkdefaultThe network the site belongs to (config/networks.yaml); sites in one network share organisations, people and partner accounts
position0The order sites are listed in; sites with the same position are ordered by key

The rest of site.yaml:

BlockHolds
localesThe locales the site enables, its default locale, its URL strategy and per-site fallback chains (below)
templatesroots, the template chain (default [<key>, _shared]); allow_unescaped, which permits unescaped output on this site
seoTitle template, default social image, sitemap, robots rules and structured data
navThe site’s navigation slots; their items are edited in the admin
linksHow links to other websites behave
accessibilityThe accessibility statement’s path, conformance and contact
embedThe widget surface for embedding content in other sites (off by default)
deliveryrendered (the engine serves pages) or headless (a frontend reads the content API)
assetsThe site’s frontend build entries and size budgets
mailThe sender address the site’s mail goes out as
settingsFree-form values templates read as settings.<path>
membershipsThe membership tiers the site sells (engine-dmo)

site.yaml has every key.

What sites share and what they do not

Shared by every sitePer site
The locales declared, the content types, the media library, tags, staff users and rolesDomains, enabled locales, routes, templates (through the chain), navigation, SEO, endpoints, outbound webhooks

A content type is on every site unless sites: in its schema file limits it, and a type or field declared in config/sites/<key>/schema/ belongs to that site alone. A document has an owner site and can be shared to other sites, where it has its own URL from each site’s routes. Multisite templates covers template chains and per-site overrides.

A site’s locales and URL strategy

locales:
  strategy: prefix
  enabled: [en, cy]
  default: en
  fallback:
    cy: [en]
KeyDefaultMeaning
enabledthe source localeThe locales this site serves. Each must be declared in locales.yaml
defaultthe first enabledThe site’s default locale
strategyprefixHow a URL names its locale (below)
domainsnoneWith strategy: domain, the host for every enabled locale
fallbackthe locale’s own chainA per-site fallback chain that replaces the locale’s, using only locales this site enables
StrategyThe default localeOther locales
prefixNo prefix: /newsPrefixed: /cy/newyddion
prefix-allPrefixed too: /en/newsPrefixed: /cy/newyddion
domainIts own host: www.wyndmouth.example/newsTheir own hosts: www.wyndmouth-cy.example/newyddion

The prefix is the locale’s language subtag (cy) when no other enabled locale shares it, and otherwise the whole code in lower case (en-gb, en-ie).

# strategy: domain
locales:
  strategy: domain
  enabled: [en, cy]
  default: en
  domains:
    en: www.wyndmouth.example
    cy: www.wyndmouth-cy.example

check refuses strategy: domain without a host for every enabled locale.

Routes

A route gives a content type pages on a site. Without a route a type’s documents still exist and can be listed and related, but have no URL on that site, and check warns about a site with no routes.yaml.

routes:
  - type: article
    uri:
      en: "/news/{publishedAt|date:'Y/m'}/{slug}"
      cy: "/newyddion/{publishedAt|date:'Y/m'}/{slug}"
    index:
      en: "/news"
      cy: "/newyddion"
    paginate: 12
    feed:
      formats: [rss, atom]
KeyMeaning
typeThe content type routed. A type has at most one route per site, and only on sites the type is on
uriThe URI pattern, one for every locale or one per locale. Placeholders name fields of the type: {slug}, or a value through a filter such as `{publishedAt
indexThe type’s index page, listing its published documents in the type’s order. Renders <type>/index.html
paginateDocuments per index page, read as ?page=N
priorityA number, default 0, that orders two patterns of one shape, lowest first. check refuses two routes of the same shape and priority (routes "/{slug}" (page@en) and "/{slug}" (area@en) are ambiguous: same shape and priority; give one a priority). It does not decide who answers: a document’s address is stored when it is published and found by that, so each type answers for its own documents and one address holds one document
facetsIndexed fields that filter the index by query parameter
feedRSS, Atom and JSON Feed of the newest documents
calendariCalendar files for a type with a recurrence field
nestDocuments of the type nest under each other: /cotswolds/chipping-norton
parentDocuments hang under a related document’s address
landingsLanding pages: a category, optionally in a place, at a clean URL

The rest of routes.yaml:

KeyMeaning
homeThe document served at /: { type: page, slug: home } names a routed document by slug; { type: homepage } names a singleton type
redirectsThe site’s structural redirects: { from, to, status, locale }. from is an exact path, a prefix with a trailing slash, or a pattern with * captured as {1}, {2} in to. status is 301 (default), 302, 307, 308, or 410 with no to for content that is gone
specialEngine pages at a path per locale, such as search: { en: "/search", cy: "/chwilio" }, which renders search.html

home: { type, slug } takes one slug for every locale: a document is the home page in a locale when its slug in that locale is exactly that value. Give the home page the same slug (home) in every locale. A Welsh translation slugged hafan is not the Welsh home page: /cy/ is not found and the page is served at /cy/hafan. The home page answers / (or /cy/), and its slug path (/home) is not found.

Without home, a document of any routed type whose slug in a locale is exactly __home__ answers / in that locale, so an editor can choose the home page. A site uses one mechanism or the other, and check refuses both together.

check validates redirects at their line: a 410 with a target, a status outside the five, a loop, and a target the site does not answer:

config/sites/main/routes.yaml:29:13: redirect loop: /loop-a -> /loop-b -> /loop-a
config/sites/main/routes.yaml:31:25: redirect target /nowhere/at/all resolves to nothing on site main in cy, en

routes.yaml has every key, and Redirects and URLs covers the redirects editors manage in the admin.

Addresses are stored, not computed

When a locale of a document is published, its address on every site it is on is resolved from that site’s route and stored. Requests are answered from the stored addresses, so a page’s URL does not change because a field it does not use changed. When a published slug changes, the old address redirects (301) to the new one. When a route’s pattern or parent changes, documents whose stored address no longer matches are moved when the engine next starts, each old address redirecting to the new one.

How a request is answered

A request passes through three choices, each made from the configuration and the stored addresses:

flowchart TD
  req["Request: host and path"] --> site{"Host in a site's domains?"}
  site -- yes --> s1["That site"]
  site -- no --> s2["The default: true site"]
  s1 --> loc{"strategy: domain?"}
  s2 --> loc
  loc -- yes --> l1["The host names the locale"]
  loc -- no --> l2{"First path segment<br/>a locale prefix?"}
  l2 -- yes --> l3["That locale"]
  l2 -- no --> l4["The site's default locale"]
  l1 --> page{"Rest of the path"}
  l3 --> page
  l4 --> page
  page -- "redirect rule" --> redirect["Redirect"]
  page -- "stored document address" --> doc["Document, rendered with<br/>type/entry.html on the site's chain"]
  page -- "index, feed, landing or special page" --> other["That page's template"]
  page -- "nothing" --> notfound["errors/404.html"]

The site

The request’s host is matched against every site’s domains, with and without the port. A site with a path_prefix matches only paths under it. With strategy: domain, each locale’s host also names its site. A host no site names is answered by the default: true site.

The locale

With strategy: domain, the host names the locale. Otherwise the first path segment is compared with each enabled locale’s prefix; when none matches, the request is in the site’s default locale (under prefix, the default locale’s own prefix is not a route).

The page

The rest of the path is matched against the site’s redirects, its stored document addresses, its index, feed, calendar and landing pages, and its special pages. A document renders with its type’s template resolved along the site’s template chain; nothing matching renders errors/404.html.

engine-dmo describe route <path> shows which route answers a path, the template the chain resolved and the whole template context, and engine-dmo render <path> --host <host> --locale <code> renders it, both against a database.

Commands

CommandDoes
engine-dmo dev locale add <code> --label Cymraeg --fallback en --site mainDeclare a locale and enable it on a site. Without --label the label is the code
engine-dmo dev site add <key> --domain <host> --locales en,cy --name "..."Add a site directory with site.yaml and routes.yaml
engine-dmo dev site rename <old> <new>Rename a site: its directory moves and its id and data stay
engine-dmo dev route add <type> <uri> --index <uri> --site <key>Route a type on a site
engine-dmo describe projectEvery site, locale and route with its id
engine-dmo localeLocale coverage, what has drifted from the source, and adding a locale to existing content