engine.yaml: content, media and search
Every key of engine.yaml's content, media, events, tags, approvals, endpoints, forms, search, maps, admin and notifications sections, with type, default and meaning.
These sections of config/engine.yaml tune how content, media, search and the admin behave for the whole install. Per-site and per-type settings live in the site and schema files. Value formats (durations, retention periods, sizes) are explained on engine.yaml.
content
| Key | Type | Default | Meaning |
|---|---|---|---|
content.trash_retention | retention | 30d | How long a trashed document waits before the nightly purge deletes it. A document on hold is kept until released |
content.revisions.prune | boolean | true | Prune old revisions daily. false keeps every revision and does not schedule the task |
content.revisions.keep_all | retention | 90d | Every revision younger than this is kept. At least 1d |
content.revisions.keep_last | integer | 20 | And the newest N of each document’s revision chain. At least 1 |
content.revisions.keep_monthly | boolean | true | And the newest revision of each calendar month (UTC) of each chain |
content.health.stale_after | retention | 12m | A live page older than this fails the content health staleness signal. At least 1d |
A type’s own revisions: block in its schema file overrides content.revisions key by key. A document under legal hold keeps every revision. engine-dmo content prune-revisions --dry-run shows what the sweep would remove.
media
| Key | Type | Default | Meaning |
|---|---|---|---|
media.max_upload_bytes | size | 50MB | The largest file an upload may carry |
media.max_pixels | integer | 50000000 | The largest image, in pixels, the pipeline decodes. At least 1,000,000 |
media.accept | list of strings | [image/jpeg, image/png, image/webp, image/avif, application/pdf, application/gpx+xml] | The media types an upload may have. An asset field’s own accept narrows it further |
media.strip_exif_gps | boolean | true | Remove GPS coordinates from uploaded images |
media.require_alt_text | boolean | true | An image used on a page needs alt text before the page publishes |
media.signing_key | secret | Derived | The key derivative addresses are signed with. Unset, a key derived from server.security.signing_key is used. Changing it changes every derivative address |
media.derivatives.keep_unused | retention | 90d | A derivative nothing has served or warmed for this long is removed by the nightly prune. At least 1d |
media.rights.terms | map | The four built-in terms | The licence vocabulary staff choose from, by key. Setting it replaces the built-in terms |
media.rights.terms.<key>.label | map of strings | The term’s name per locale, such as {en-GB: "Stock, website only, 2 years"} | |
media.rights.terms.<key>.uses | list of strings | Where an asset under the term may be used: website, social, newsletter, print, editorial, press, partner, handover | |
media.rights.terms.<key>.territory | list of strings | Where it may be used: WORLD or country codes | |
media.rights.terms.<key>.duration | string | Empty | How long the licence runs from its start, such as 2y or 18m; empty is perpetual |
media.rights.terms.<key>.credit_required | boolean | false | The credit must be shown wherever the asset is |
media.rights.expiry_warnings | list of strings | [180d, 90d, 30d, 0d] | When, before a licence ends, the people who manage media rights are told |
media.rights.block_publish_when_expired | boolean | true | An asset whose licence has ended blocks publishing a page that uses it. false makes it a warning |
media.rights.warn_publish_when_expiring | boolean | true | Warn at publish when a licence ends while the page stays live |
media.rights.on_expiry | string | warn | What a live page does once an asset’s licence has ended: warn (nothing changes on the page) or placeholder (the image renders as absent) |
media.orphans.scan_schedule | string | 0 3 * * * | When the full reference scan, which finds assets nothing uses, runs. Cron, UTC |
media.duplicates.threshold | integer | 10 | How many of a perceptual hash’s 64 bits may differ for two images to be offered as near duplicates |
The built-in licence terms are commissioned-perpetual (every use, worldwide, perpetual), stock-2yr-web (website and social, two years), a member-supplied term (website, social and newsletter, credit required) and press-only (press and editorial, credit required).
events
| Key | Type | Default | Meaning |
|---|---|---|---|
events.occurrence_window.behind | retention | 90d | How far into the past a recurring event’s occurrences are kept |
events.occurrence_window.ahead | retention | 540d | How far ahead they are generated. At most five years |
events.roll_at | time of day | 03:30 | When the window rolls forward each day, in the default site’s reporting time zone |
tags
| Key | Type | Default | Meaning |
|---|---|---|---|
tags.promotion.threshold | integer | 0 (meaning 5) | How many distinct subjects a freeform tag must be on before it is offered for promotion into the vocabulary and the people who manage tags are told |
approvals
How the moderation queue treats changes members submit through the portal (engine-dmo). Which fields a member may edit, and whether a tier’s changes need review, are set in the schema.
| Key | Type | Default | Meaning |
|---|---|---|---|
approvals.review_window_poll | duration | 15m | How often review windows are closed and returned drafts expired |
approvals.returned_expiry | retention | 30d | A draft returned to its member and left untouched this long is discarded as if rejected, and the member is told |
approvals.expected_turnaround | duration | 24h | The review time members are told to expect; a change waiting longer is overdue |
approvals.bulk_approve.enabled | boolean | true | Reviewers may approve several low-risk changes at once |
approvals.bulk_approve.low_risk_types | list of strings | [opening_hours, phone, link, boolean, number, date, datetime, select, address, geo] | The field types whose changes count as low risk |
endpoints
Install-wide defaults for declared endpoints (forms, enquiries, newsletter sign-up and the rest in each site’s endpoints.yaml). A site’s endpoint may tighten these and may not loosen a security-relevant one.
| Key | Type | Default | Meaning |
|---|---|---|---|
endpoints.defaults.rate_limit.public_get | rate limit | 60/minute/ip | Public GET endpoints, per client address |
endpoints.defaults.rate_limit.public_post | rate limit | 10/hour/ip | Public POST endpoints, per client address |
endpoints.defaults.rate_limit.public_mail | rate limit | 500/day/endpoint | Mail sent by public endpoints, per endpoint |
endpoints.defaults.rate_limit.member | rate limit | 60/hour/session | Member endpoints, per session |
endpoints.defaults.rate_limit.staff | rate limit | 600/minute/session | Staff endpoints, per session |
endpoints.defaults.rate_limit.embed | rate limit | 600/minute/origin | Embeddable widgets, per embedding origin. Must be <count>/<window>/origin |
endpoints.defaults.spam.proof_of_work | boolean | true | Public forms carry a small proof-of-work challenge the browser solves. There is never a CAPTCHA |
endpoints.defaults.spam.honeypot | boolean | true | Public forms carry a hidden field a person leaves empty |
endpoints.defaults.spam.min_fill_time | duration | 2s | A submission faster than this after the form loaded is treated as automated |
endpoints.max_input_bytes | size | 256KB | The largest non-upload body an endpoint accepts |
forms
The Forms module’s retention of stored submissions.
| Key | Type | Default | Meaning |
|---|---|---|---|
forms.max_retention | retention | 2y | The longest any endpoint’s store step may keep a submission |
forms.orphan_retention | retention | 30d | How long submissions outlive their endpoint leaving the configuration |
forms.purge_schedule | string | 0 2 * * * | When the purge of expired submissions runs. Cron, UTC |
search
The search backend is always an external service. driver: none is a valid, explicit choice: the search route then serves browse mode.
search:
driver: meilisearch
meilisearch:
url: ${MEILISEARCH_URL}
api_key: ${MEILISEARCH_API_KEY} | Key | Type | Default | Meaning |
|---|---|---|---|
search.driver | string | none | meilisearch, elasticsearch, opensearch or none |
search.log_queries | boolean | true | Record queries for the zero-result and search reports. false is refused in production |
search.min_query_length | integer | 2 | Shorter queries are not sent to the backend |
search.degraded_mode | string | browse | What the search route does while the backend is failing: browse (serve browse results) or error |
search.runs_kept | integer | 20 | Finished rebuild, purge and sweep runs kept per site and kind, 1 to 1000 |
search.circuit_breaker.failures | integer | 5 | Failures within window that stop the engine asking the backend |
search.circuit_breaker.window | duration | 30s | The window failures are counted in |
search.circuit_breaker.cooldown | duration | 1m | How long the breaker stays open before trying again |
search.meilisearch.url | string | Empty | The Meilisearch base URL. Required with that driver |
search.meilisearch.api_key | secret | Empty | Its API or master key. Set the variable empty for a server with no key |
search.meilisearch.index_prefix | string | meridian | Prefix of every index name, so several installs can share one server |
search.meilisearch.timeout | duration | 2s | Each request’s timeout |
search.elasticsearch.addresses | list of strings | [] | The cluster’s URLs. Required with that driver; the first is used |
search.elasticsearch.username | string | Empty | Basic authentication user |
search.elasticsearch.password | secret | Empty | Basic authentication password |
search.elasticsearch.index_prefix | string | meridian | Index name prefix |
search.elasticsearch.timeout | duration | 3s | Each request’s timeout |
search.opensearch.* | The same keys as elasticsearch, with the same defaults. With driver: opensearch and no opensearch.addresses, the elasticsearch block is used |
Also accepted and not used in this release: search.meilisearch.addresses, .username and .password; search.elasticsearch.url and .api_key (and the same under opensearch). Their secret forms are still held to the secret rule.
maps
Static maps are drawn on the server from a tile provider and cached as an image, so a visitor’s browser never contacts the provider. With no tiles, a static map renders nothing.
maps:
tiles: https://api.maptiler.com/maps/streets-v2/{z}/{x}/{y}.png?key={key}
key: ${MAP_TILES_KEY}
attribution: "© MapTiler © OpenStreetMap contributors" | Key | Type | Default | Meaning |
|---|---|---|---|
maps.tiles | string | Empty | The tile URL template, with {z}, {x}, {y}, and {key} where the provider takes the key in the address. Any service on the standard tile scheme works, including your own |
maps.key | secret | Empty | The provider’s key, written into {key} |
maps.attribution | string | Empty | The credit the provider’s licence requires, shown beneath every map |
maps.max_zoom | integer | 0 (meaning 18) | The highest zoom a template may ask for |
maps.tile_size | integer | 0 (meaning 256) | The provider’s tile edge in pixels |
maps.timeout | duration | 0s (meaning 10s) | The time limit for fetching one map’s tiles |
admin
| Key | Type | Default | Meaning |
|---|---|---|---|
admin.rich_text_editor | string | Empty (Editor.js) | The rich text editing surface in the admin and the portal: editorjs (the default) or native |
admin.rich_text_input_rules | boolean | false | Markdown-style shortcuts in the editor: ## starts a heading, - a list, **bold** bolds, where the field allows the result |
admin.theme.brand_name | string | Empty | The name the admin shows in place of the product’s |
admin.theme.logo | string | Empty | The logo: a path under the project’s public/ starting with / (/branding/logo.svg for public/branding/logo.svg), SVG or PNG |
admin.theme.logo_dark | string | Empty | The logo for the dark colour scheme, as logo |
admin.theme.favicon | string | Empty | The admin’s favicon, as logo; ICO is also accepted |
admin.theme.css_variables | map of strings | {} | Brand tokens as CSS custom properties; engine-dmo check lists the themeable names when one is not. Every colour pair is checked against WCAG 2.2 AA contrast when the configuration loads; tokens that carry target size, focus, spacing, type or motion cannot be set |
admin.theme.custom_css | string | Empty | A .css file under public/, as logo, loaded after the admin’s own stylesheet |
admin.theme.contrast_check | string | Empty (enforce) | enforce makes a failing pair an error; warn a warning (itself a warning in production) |
notifications
When staff are emailed about what the admin already shows them.
| Key | Type | Default | Meaning |
|---|---|---|---|
notifications.mention_email_delay | duration | 15m | A mention still unread after this long is emailed |
notifications.digest_hour | time of day | 08:00 | When the daily digest of everything else goes, in each user’s own time zone |
notifications.email_includes_body | boolean | true | Include the comment or change in the email; false sends a summary and a link only |
notifications.retention | retention | 90d | Read notifications are deleted after this |
engine.yaml: authentication and the MCP
Every key of engine.yaml's auth section (local staff accounts, OpenID Connect single sign-on, staff and member sessions, member sign-in) and its mcp section (the remote content MCP endpoint, its clients, tokens and limits), with type, default and meaning.
engine.yaml: mail and newsletter
Every key of engine.yaml's mail section (the transactional mail driver for SMTP, Postmark, Amazon SES or Resend, bounce webhooks, templates, suppression and broadcast sending) and its newsletter section, with type, default and meaning.