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] | Key | Default | Meaning |
|---|---|---|
code | required | A BCP 47 tag such as en-GB, cy or zh-Hans. Declared once |
label | required | The locale’s name, as the admin and language switchers show it |
source | false | The locale content is written in first and translated from. Exactly one locale must be the source |
default | false | The install’s default locale; at most one. Without one, the source locale is the default |
timezone | the source locale’s | An IANA time zone. Required on the source locale |
currency | the source locale’s | An ISO 4217 code. Required on the source locale |
fallback | none | Locales to read a value from, in order, when this locale has none |
statutory | false | A locale that must be published wherever the source is; publishing without it is a prompt, never a block |
translators | none | The staff group translation jobs into this locale are assigned to by default |
machine_translation | none | assist or forbid: whether machine translation may be offered for this locale |
derived_from | none | A locale this one is derived from, such as en-US from en-GB |
direction | from the script | ltr or rtl |
calendar, numbering, hour_cycle, first_day_of_week | from CLDR for the locale | How dates, numbers and times are formatted |
collation | from the tag | The database collation that sorts text in this locale |
slugs | from the script | native keeps slugs in the locale’s script; transliterate converts them to Latin |
fold_diacritics | true | Whether 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:
| Key | Default | Meaning |
|---|---|---|
name | required | The site’s name per locale, read in templates as site.name |
short_name | none | A shorter name per locale |
domains | none | The host names the site answers, matched exactly (with or without the port) |
default | false | The site that answers any host no site names. With one site it is the default; with several, exactly one must say default: true |
path_prefix | none | A path the site is served under on a shared host, starting with / |
timezone, default_currency | the default locale’s | The site’s own, where they differ |
country | none | The site’s country |
organisation | the site name | The organisation behind the site, for structured data |
network | default | The network the site belongs to (config/networks.yaml); sites in one network share organisations, people and partner accounts |
position | 0 | The order sites are listed in; sites with the same position are ordered by key |
The rest of site.yaml:
| Block | Holds |
|---|---|
locales | The locales the site enables, its default locale, its URL strategy and per-site fallback chains (below) |
templates | roots, the template chain (default [<key>, _shared]); allow_unescaped, which permits unescaped output on this site |
seo | Title template, default social image, sitemap, robots rules and structured data |
nav | The site’s navigation slots; their items are edited in the admin |
links | How links to other websites behave |
accessibility | The accessibility statement’s path, conformance and contact |
embed | The widget surface for embedding content in other sites (off by default) |
delivery | rendered (the engine serves pages) or headless (a frontend reads the content API) |
assets | The site’s frontend build entries and size budgets |
mail | The sender address the site’s mail goes out as |
settings | Free-form values templates read as settings.<path> |
memberships | The membership tiers the site sells (engine-dmo) |
site.yaml has every key.
What sites share and what they do not
| Shared by every site | Per site |
|---|---|
| The locales declared, the content types, the media library, tags, staff users and roles | Domains, 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] | Key | Default | Meaning |
|---|---|---|
enabled | the source locale | The locales this site serves. Each must be declared in locales.yaml |
default | the first enabled | The site’s default locale |
strategy | prefix | How a URL names its locale (below) |
domains | none | With strategy: domain, the host for every enabled locale |
fallback | the locale’s own chain | A per-site fallback chain that replaces the locale’s, using only locales this site enables |
| Strategy | The default locale | Other locales |
|---|---|---|
prefix | No prefix: /news | Prefixed: /cy/newyddion |
prefix-all | Prefixed too: /en/news | Prefixed: /cy/newyddion |
domain | Its own host: www.wyndmouth.example/news | Their 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] | Key | Meaning |
|---|---|
type | The content type routed. A type has at most one route per site, and only on sites the type is on |
uri | The 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 |
index | The type’s index page, listing its published documents in the type’s order. Renders <type>/index.html |
paginate | Documents per index page, read as ?page=N |
priority | A 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 |
facets | Indexed fields that filter the index by query parameter |
feed | RSS, Atom and JSON Feed of the newest documents |
calendar | iCalendar files for a type with a recurrence field |
nest | Documents of the type nest under each other: /cotswolds/chipping-norton |
parent | Documents hang under a related document’s address |
landings | Landing pages: a category, optionally in a place, at a clean URL |
The rest of routes.yaml:
| Key | Meaning |
|---|---|
home | The document served at /: { type: page, slug: home } names a routed document by slug; { type: homepage } names a singleton type |
redirects | The 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 |
special | Engine 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
| Command | Does |
|---|---|
engine-dmo dev locale add <code> --label Cymraeg --fallback en --site main | Declare 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 project | Every site, locale and route with its id |
engine-dmo locale | Locale coverage, what has drifted from the source, and adding a locale to existing content |
Concepts
The ideas every Meridian project rests on (sites, locales, routes, documents and publishing, modules and bundles, stable ids, users and permissions) and how they fit together.
Documents, revisions and publishing
How Meridian stores content as documents with a shared revision chain and one chain per locale, the per-locale workflow states, what publishing checks and does, scheduling, the trash, and revision retention.