Meridian handbook
Getting started

Project layout

Every directory and file a Meridian project can hold, what each is for, which bundle reads it, and which files you write and which the tooling writes.

A project is one directory. The engine reads it from the current directory, or from the directory --project names; --config changes the configuration directory’s name from config. Everything in it is text you commit to version control, except the few local directories listed under What not to commit.

The whole tree

engine.yaml
locales.yaml
images.yaml
accessibility.yaml
networks.yaml
memberships.yaml
paidlistings.yaml
package.json
vite.config.js
AGENTS.md
README.md
.gitignore

Only config/engine.yaml, config/locales.yaml, one site directory and the templates its routes need are required. Every other file is optional: leave it out and the engine uses its defaults.

What reads what

The engine reads config/ and templates/ at start and on every check; schema apply copies the schema into the database, seed apply loads seed/ into it once, and from then on content lives in the database:

flowchart LR
  subgraph project["The project directory"]
    config["config/<br/>engine, locales, sites, schema, access"]
    templates["templates/"]
    public["public/"]
    seed["seed/"]
  end
  config -->|schema apply| db[("PostgreSQL<br/>schema copy and content")]
  seed -->|seed apply, once| db
  config --> engine["The engine<br/>serve, dev up, check"]
  templates --> engine
  public -->|served as is| engine
  db --> engine
  engine --> site["Public sites, admin, APIs"]
  config -->|guide| agents["AGENTS.md and skills"]

Who writes what

You write every file in the project. The tooling writes into your files in three cases only, each one a command you run on purpose, never the running engine:

  • engine-dmo check --fix, the engine-dmo dev verbs and engine-dmo dev up write the id: of each configuration element that has none, and the verbs make the edit you asked for. They keep your comments and layout. See Stable ids.
  • engine-dmo init and engine-dmo new write new files.
  • engine-dmo config schema --write and init write the editor schemas under .meridian/schema/ and the editor settings that map them.

Content (documents, media, organisations, users) lives in the database, not in the project. The project’s seed/ holds content to load into a database, which is a one-way copy.

config/

Configuration is YAML. Every file is validated as a whole by engine-dmo check, which reports every problem at once with its file, line and column. Secrets never appear as values: a secret key must be a bare ${VAR} reference to an environment variable. Configuration reference lists every key of every file.

Install-wide files

PathHoldsBundle
engine.yamlThe install: server, database, storage, mail, search, authentication, media, cache, logging, jobs, analytics and each module’s own section. RequiredBoth
locales.yamlEvery locale the install knows, its formatting and its fallback chain; exactly one is the source locale. RequiredBoth
images.yamlImage breakpoints, defaults and named transformsBoth
accessibility.yamlThe register of known accessibility issues the accessibility statement listsBoth
networks.yamlNetworks of sites that share organisations, people and partner accounts. Without it every site is in one networkBoth, used by the DMO modules
memberships.yamlThe membership tiers, in rank orderengine-dmo (Memberships)
paidlistings.yamlPrices and payment for paid listingsengine-dmo (Paid listings)
schema/Content types. Any .yaml file at any depth is read; schema/_shared.yaml holds field groups other files include with use:; other files whose names start with _ are not read as typesBoth
blocks/_index.yamlBlocks shared by every blocks fieldBoth
strings/<locale>.yamlThe interface strings templates print with t(), one file per localeBoth
access/roles.yamlStaff roles: your own, and replacements for the built-in onesBoth
access/grants.yamlWhich staff group holds which role, under which scope. The starter’s gives the administrators group the built-in owner role, and user create puts the first user in administrators; keep a grant like it, or no one can manage the installBoth
access/users.yamlCustom fields on the staff user recordBoth
analytics/dashboards.yamlThe organisation dashboards and the metrics staff enter by handBoth
analytics/reports.yamlScheduled member and organisation reportsengine-dmo (Member report)
analytics/bots.txtUser agent substrings added to the shipped list of bots, one per lineBoth
crm/organisation.yaml, crm/person.yamlCustom fields on organisations and peopleengine-dmo (CRM)
portal/portal.yamlThe member portal’s install-wide settingsengine-dmo (Member portal)
portal/enquiries.yamlCustom fields on enquiriesengine-dmo (Member portal)
portal/roles.yamlPartner roles: what a person may do for an organisation in the portalengine-dmo (CRM)
tags/vocabulary.yamlThe starting tag vocabulary. Loaded into the database by engine-dmo tags import or a seed; the admin edits it after thatBoth
tags/smartlists.yamlThe starting smart lists, loaded the same wayBoth
search/synonyms.yamlThe starting search synonyms, loaded by engine-dmo search synonyms or a seedBoth
ai/prompts/<key>.yamlYour override of one of the engine’s AI promptsBoth (AI)
guidance/<site>.md, guidance/<site>/<type>.mdEditorial guidance in Markdown that the content MCP gives an agent writing content for that site or typeBoth

A file that belongs to a module the binary does not compile is an error at the file, naming the module. On engine-cms, config/memberships.yaml gives:

config/memberships.yaml:1:1: memberships.yaml belongs to the memberships module, which this bundle (cms) does not compile
    1 | tiers:
      | ^
      = use a bundle that compiles memberships, such as the DMO platform's bin/engine-dmo, or remove it
      = error code MRD-1006

A module’s section of engine.yaml (such as portal:) is refused the same way.

config/sites/

One directory per site; the directory name is the site’s key. An install with one site still has config/sites/<key>/.

PathHoldsBundle
site.yamlThe site’s name, domains, enabled locales and URL strategy, template roots, SEO, navigation slots, widgets, delivery mode and settings. RequiredBoth
routes.yamlWhich types the site routes and at which URI patterns, index pages, feeds, calendars, landing pages, redirects, special pages and the home page. Without it no type is routable on the siteBoth
schema/Types and fields that belong to this site aloneBoth
images.yamlImage transforms added to the install’s, or replacing one by nameBoth
endpoints.yamlDeclared endpoints: fragment, JSON, stream or redirect responses built from a fixed set of stepsBoth
sources.yamlConfigured, cached, allowlisted upstream data sources the endpoints readBoth
importers.yamlScheduled feed importsBoth
webhooks.yamlOutbound webhooks on content eventsBoth
plugins.yamlPlugin binaries the endpoints call (an experimental protocol)Both
portal.yamlThis site’s portal over the install’s: path, routes, screens and actionsengine-dmo (Member portal)
promotions.yamlPromotion slots and the label each promoted placement showsengine-dmo (Promotions)

Sites, locales and routes explains how site.yaml and routes.yaml work together.

templates/

Pongo2 templates (a Django-like syntax), autoescaped. templates/_shared/ holds what every site uses, and templates/<site>/ holds a site’s overrides. A site resolves each template along its chain of roots, most specific first: [<site>, _shared] unless templates.roots in site.yaml says otherwise. Every root a site names must exist as a directory.

Path in a rootHolds
base.html (or any layout)Layouts other templates extend
<type>/entry.htmlA document of the type (the default; a type or variant may name another template)
<type>/index.htmlThe type’s index page, for a route with index:
errors/404.html, errors/500.htmlThe not found and error pages
blocks/<block>.htmlOne block of a blocks field
components/<name>/A component: <name>.html, its contract component.yaml, and optionally co-located CSS
ptext/<set>/<type>.htmlA custom Portable Text block in a rich text field
emails/Transactional mail templates, overriding the engine’s own
partials/Fragments your templates include; the name is yours
portal/The member portal’s screens (engine-dmo)

engine-dmo check refuses a routed type with no template on a site’s chain, an index route without its index.html, and any template that does not compile, uses an unknown variable root or an unknown filter. Templates and Multisite templates cover the rest.

public/

Static files served as they are, at the same path. A request resolves along the site’s chain: public/<site>/, then public/_shared/, then public/. Built and versioned files (under public/build/) are cached for a year, anything else for an hour; the build’s manifest is never served. public.mounts in engine.yaml serves a directory outside public/ at a path prefix.

seed/

Content to load into a database with engine-dmo seed apply. It goes through the same validation and revision history as an editor’s save. engine-dmo seed verify checks it against the schema without a database, and is what CI runs. It needs every ${VAR} in the configuration set (a placeholder is enough), and it checks types, fields and locales, not values: a value a field would refuse fails only in seed apply.

PathHolds
seed.yamlThe manifest: name, description, locales, optional generated analytics, named profiles, and a media library to import photographs from. locales limits only the generated analytics; documents load every locale their files give
documents/<type>/<key>.yamlOne document: its values per locale, which locales to publish, and optionally its history, schedule, tags and site sharing
users/<key>.yamlA staff user for development: email, name, groups, password and authenticator secret. Never created in production
navigation/<slot>.yamlThe items of a navigation slot
assets/Media files the seed uploads
organisations/, people/Organisations and people (engine-dmo)
generate.yaml, generate/Rules for engine-dmo seed write, which writes generated records into the seed
<set>/Another seed set, applied with --set <set>

A starter seed document:

# seed/documents/page/home.yaml
locales:
  en:
    title: Welcome
    slug: home
publish: [en]

Frontend build files

The starter has no frontend build: no package.json, src/ or vite.config.js, and its CSS and JavaScript, if any, go in public/. A project with its own CSS and JavaScript build keeps its sources in src/ (entries in src/entries/, one per site by default: src/entries/<site>.js), with package.json and vite.config.js at the project root. The vite extension, named in engine.yaml as extensions: [vite], runs Vite’s dev server beside engine-dmo serve --dev, and engine-dmo build runs the production build and then check. Output goes to public/build/. The frontend build covers it.

tests/

tests/templates/*.test.yaml are template tests: pages to render through the real pipeline against the seeded database, each with what it must contain. engine-dmo template test runs them. Testing templates covers the format.

Agent and editor files

PathWritten byHolds
AGENTS.mdengine-dmo init, refreshed by the dev verbsWorking guidance for a coding agent, generated from this project by engine-dmo guide
.claude/skills/meridian-project/SKILL.mdThe sameThe same guidance as a skill file
.claude/skills/meridian/SKILL.mdengine-dmo init, refreshed by the dev verbsThe handbook’s For agents page as a skill, matching the engine version
.meridian/schema/*.jsonengine-dmo init, engine-dmo config schema --writeJSON Schemas of every configuration file, for your editor
.vscode/settings.json, .zed/settings.jsonengine-dmo init --editorsEditor mappings from each configuration file to its schema

Regenerate AGENTS.md by hand with engine-dmo guide --out AGENTS.md, the project skill with engine-dmo guide --skill --out .claude/skills/meridian-project/SKILL.md, and the meridian skill with engine-dmo docs skill --out .claude/skills/meridian/SKILL.md. init --agents=false writes none of them.

The starter’s README.md points at this page. Its hints, and the engine’s, name handbook pages or commands. Earlier builds, including the 0.8.0 release images, also cite internal design documents in some hints, such as (27-frontend-toolkit.md) or (ADR-0019); those are not part of the handbook and not published, so search the handbook for the topic instead.

What not to commit

The starter’s .gitignore:

/storage/
/node_modules/
/public/build/
/.meridian/
.env
PathWhy it is local
storage/Uploaded media and image derivatives with the filesystem storage driver (storage.filesystem.root, ./storage by default). Back it up with the database, never commit it
node_modules/Installed frontend packages
public/build/Frontend build output, rebuilt by engine-dmo build
.meridian/Editor schemas, and dev-status.json, the last result of engine-dmo dev up
.envLocal environment variables. The engine never reads it; your shell or process manager does