Skip to content

Site config

Site config is a sibling of the Manifest grammar, not part of it. The four atoms describe content; siteDefaults describes the deployment: the locales the site publishes in, its brand and title, its canonical origin, its icons, its analytics ids and its media taxonomy. The deployment declares it as a TypeScript object and passes it to the adapter; the runtime seeds it into the site_config table and every render, MCP catalog build and Admin page reads it back from there.

siteDefaults

KeyTypeRequiredRules
localesstring[]noOrdered. locales[0] is the canonical locale. Empty or omitted turns the locale subsystem off site-wide. Every entry must canonicalize; see Boot validation.
brandstringnoOperator-facing label for Admin chrome and MCP serverInfo.title. Distinct from title.
titlestringnoSite title — <title> suffix and og:site_name.
descriptionstringnoDefault <meta name="description"> and og:description for entries with none.
originstringnoCanonical absolute origin, no trailing slash, for example https://example.com. Used to build absolute URLs in /llms.txt, the .md mirrors and /sitemap.xml. An empty origin yields relative URLs.
iconsSiteIcon[]noOne site identity reused by browser favicons, Admin chrome and MCP serverInfo.icons. Declaring an empty array is an error.
ga4MeasurementIdstringnoGA4 Measurement ID such as G-XXXXXXXXXX. When present the runtime injects the standard gtag snippet into rendered public HTML.
facebookPixelIdstringnoMeta Pixel ID. When present the runtime injects the standard Pixel base snippet.
media.purposesMediaPurposePolicy[]noThe upload taxonomy. Omitting the key, or declaring an empty array, keeps first-party media uploads disabled.

Nothing in this object is validated for length or content beyond the rules above: brand, title, description and origin are free strings.

SiteIcon

ts
interface SiteIcon {
  readonly src: string;
  readonly mimeType?: "image/png" | "image/jpeg" | "image/svg+xml" | "image/webp";
  readonly sizes?: readonly string[];
  readonly theme?: "light" | "dark";
}
FieldRules
srcEither root-relative (starts with a single /, no whitespace, no backslash) or an absolute https: URL. Anything else is rejected.
mimeTypeOptional. One of the four listed types.
sizesOptional. When present, a non-empty array whose entries are any or <width>x<height> with non-zero decimal dimensions, for example 64x64.
themeOptional. light or dark.

When the deployment declares no icons, the runtime stores DEFAULT_SITE_ICONS:

ts
const DEFAULT_SITE_ICONS = [{
  src: "/_mantle/admin/favicon.svg",
  mimeType: "image/svg+xml",
  sizes: ["any"],
}];

Multiple renditions are allowed. Keep SVG as the source and add a PNG rendition when a target MCP client needs a baseline raster format. GET /favicon.ico resolves against this list; see Surface.

MediaPurposePolicy

ts
interface MediaPurposePolicy {
  readonly name: string;
  readonly required: readonly string[];
  readonly maxBytes: Readonly<Record<string, number>>;
}
FieldRules
nameThe slug callers pass as purpose. Must match ^[a-z0-9]+(-[a-z0-9]+)*$: lowercase alphanumerics, dash-separated, no leading, trailing or repeated dashes.
requiredOrdered list of acceptable mime slots, at least one. Each entry uses the HTML <input accept> grammar.
maxBytesPer-mime byte cap keyed by fully expanded mime type. Must name every mime that appears in any slot after expansion, and every value must be a positive number.

A slot entry is one of a full mime (image/jpeg), a comma-list of full mimes (image/jpg,image/png, meaning either is acceptable for that slot), or a shorthand subtype (webp expands to image/webp, jpg and image/jpg both expand to image/jpeg). Whitespace around commas is tolerated.

Slot position does not determine variant role. Per asset the uploading agent picks one mime per slot and independently declares exactly one supplied variant as primary — the format <img> falls back to — with the rest alternate, preferred through <picture><source>. Because a variant maps to a slot by its mime alone, mime sets across slots must not overlap; an overlap is rejected at boot rather than per upload.

Runtime SiteConfig

siteDefaults is the author-time declaration. SiteConfig is the read shape that templates, the MCP catalog and Admin see after the seed has run and an operator has had a chance to edit.

FieldTypeValue when the row is absent
titlestring"CMS"
descriptionstring""
originstring""
brandstring"AotterMantle"
localesreadonly string[][]
canonicalLocalestring | nulllocales[0] or null when the list is empty
iconsreadonly SiteIcon[]DEFAULT_SITE_ICONS
ga4MeasurementIdstring | undefinedundefined (an empty stored value also reads as undefined)
facebookPixelIdstring | undefinedundefined
media.purposesreadonly MediaPurposePolicy[][]

canonicalLocale is computed, never stored. Templates emit <html lang> only when it is non-null; silent omission is the correct behaviour for a zero-locale site, not a fabricated default.

site_config rows

The table is a flat key/value store. Keys fall into two ownership classes, and the seed treats them differently.

KeyOwnershipWritten by seedSource of truth
brandUI-editable, seed-onceINSERT … ON CONFLICT DO NOTHINGThe database, once the row exists
titleUI-editable, seed-onceINSERT … ON CONFLICT DO NOTHINGThe database, once the row exists
descriptionUI-editable, seed-onceINSERT … ON CONFLICT DO NOTHINGThe database, once the row exists
ga4MeasurementIdUI-editable, seed-onceINSERT … ON CONFLICT DO NOTHINGThe database, once the row exists
facebookPixelIdUI-editable, seed-onceINSERT … ON CONFLICT DO NOTHINGThe database, once the row exists
originCode-canonical, boot-syncedUpsert when the stored value differsThe declaration
faviconUrlCode-canonical, boot-syncedUpsert when the stored value differs; holds the icons array as JSONThe declaration
localesCode-canonical, boot-syncedUpsert when the stored value differs; holds the declared list as a comma-separated stringThe declaration
mediaPurposesCode-canonical, boot-syncedUpsert when the stored value differs; holds the purposes array as JSONThe declaration

Blank values are skipped in both classes: an absent, empty or empty-array field never writes and never clears an existing row, so a partial declaration cannot clobber stored values. The boot-synced keys are read-compared before writing, so an unchanged deployment issues no write.

The seed-once keys have an Admin edit path at PATCH /admin/api/site-settings (owner only); the boot-synced keys do not, which is why the declaration wins on every boot. A custom-domain change therefore becomes canonical by editing the code and redeploying, with no manual database edit.

WarningmediaPurposes is JSON. Rows written by pre-#272 deployments used a CSV form and do not round-trip. Re-run the seed, or delete the row, after upgrading.

Validated at boot

Storage preparation calls assertSiteDefaultsCanonical(siteDefaults) synchronously, before the runtime accepts traffic. It throws — it does not return diagnostics — so a typo rejects the deployment rather than corrupting the seed.

ErrorThrown when
InvalidSiteDefaultsErrorAny declared locale fails canonicalization. Carries invalidLocales; the message adds script-subtag guidance when one is present.
InvalidSiteIconsErroricons is declared as an empty array, or any icon fails the SiteIcon rules. Carries the offending icons.
InvalidMediaPurposesErrorAny declared purpose fails a policy rule. Carries one issue per purpose.

InvalidMediaPurposesError reports exactly one reason per purpose, checked in this order and stopping at the first hit:

ReasonMeaning
invalid-slugname does not match the slug pattern.
empty-requiredrequired is an empty array.
empty-required-slotA required entry parses to zero mimes.
overlapping-slot-mimesTwo slots accept the same mime, so a variant cannot be mapped to one slot.
maxBytes-missing-mimemaxBytes has no entry for a mime that some slot accepts.
maxBytes-non-positiveA maxBytes entry is not a positive number.

Locale canonicalization accepts a 2- or 3-letter ISO 639 language plus an optional 2-letter ISO 3166 region, separated by - or _, or run together in the 2+2 form. Case is irrelevant: zh-tw, ZH_TW and zhTW all canonicalize to zh-TW. The canonicalized list is deduplicated, so ["zh-tw", "zh-TW"] collapses to one entry. Script subtags are valid BCP 47 but deliberately unsupported in v0.1 — zh-Hant, zh-Hans, sr-Latn and sr-Cyrl are all rejected; use region tags such as zh-TW and zh-CN. Variants such as de-1996 are likewise rejected. The locales row stores the declared list verbatim once it validates.

Deployment readiness runs alongside the seed and collects boot-phase diagnostics, throwing BootValidationError when any is an error.

CheckDiagnostic
checkSiteLocales: the site locale list canonicalizes.INVALID_LOCALE at site_config/locales, listing the invalid entries
checkSiteLocales: no Schema declares localized: true while the site has zero valid locales.SCHEMA_LOCALIZED_REQUIRES_SITE_LOCALES
translates references resolve: the parent exists, is not itself localized, and declares the join field.TRANSLATES_PARENT_UNKNOWN, TRANSLATES_PARENT_IS_LOCALIZED, TRANSLATES_FIELD_NOT_IN_PARENT
Every handler.kind: ref key is registered in the handlers map. The registered keys are attached as candidates.HANDLER_NOT_REGISTERED
No HTTP Trigger path falls under an adapter-reserved prefix.TRIGGER_PATH_INVALID
Every sql View's dialect is supported by the bound storage adapter.VIEW_DIALECT_UNSUPPORTED

Checked per request

ReadWhen
siteConfig.load()Every public render, every /llms.txt, .md mirror and sitemap response, every MCP catalog build, the Admin site payload. Reads all rows and applies the fallbacks above.
siteConfig.readLocales()Locale resolution on public routes and the write-time locale gate. Reads only the locales row; the value is memoized per repository instance once a prepared revision proves the code-owned locale policy is current.
siteConfig.readMediaPurposes()Upload authorization. Always reads the canonical row, never a cached catalog snapshot.

The data.locale write gate runs on every authoring path — Admin, Staff MCP and builtin Procedures — after stamping, against the locales read for that request. Its Schema-side rules are in Schema.

ConditionResult
Non-localized Schema and data.locale is present.INPUT_VALIDATION_FAILED
Localized Schema and data.locale is missing or empty.INPUT_VALIDATION_FAILED, skipped for partial draft saves; publish re-checks.
Localized Schema and data.locale is not in the site locales.INPUT_VALIDATION_FAILED with the enabled locales as candidates
The site locale list is empty.Membership is not checked.

Media checks run per upload. create_media_upload and commit_media_upload are registered only when the runtime has a mediaStorage port bound and at least one purpose is declared; without both, the tools do not appear in tools/list at all.

ConditionDiagnostic
purpose is not one of the declared slugs. The declared set is returned in expected.MEDIA_PURPOSE_REJECTED
The variants manifest does not cover every required mime for the purpose.MEDIA_VARIANTS_INCOMPLETE
A variant's declared byteSize exceeds its mime's maxBytes.MEDIA_VARIANT_SIZE_EXCEEDED
A single upload exceeds its cap.MEDIA_SIZE_EXCEEDED
A modern format is larger than its fallback, which means the uploader skipped optimization.MEDIA_VARIANTS_SUSPICIOUS_SIZE

The Cloudflare recipe for binding R2 is in Media on R2.

Example

ts
import { createMantleWorker } from "@aotter/mantle/cloudflare";
import { plan } from "../.mantle/generated/mantle.js";
import { handlers } from "./handlers.js";

export default createMantleWorker({
  plan,
  handlers,
  siteDefaults: {
    locales: ["en", "zh-TW"],
    brand: "Northwind",
    title: "Northwind Supply",
    description: "Industrial parts, shipped the same day.",
    origin: "https://northwind.example.com",
    icons: [
      { src: "/site-icon.svg", mimeType: "image/svg+xml", sizes: ["any"] },
      { src: "/site-icon.png", mimeType: "image/png", sizes: ["64x64"] },
    ],
    ga4MeasurementId: "G-XXXXXXXXXX",
    media: {
      purposes: [
        {
          name: "product-cover",
          required: ["image/jpg,image/png", "webp", "avif"],
          maxBytes: {
            "image/jpeg": 5_000_000,
            "image/png": 5_000_000,
            "image/webp": 3_000_000,
            "image/avif": 2_000_000,
          },
        },
      ],
    },
  },
});

en is canonical, so / redirects to /en and zh-TW is served at /zh-tw. Both icon files live in the project's public/ directory. The one purpose declares three slots: the first accepts a JPEG or a PNG primary, the other two carry the modern alternates. See Conventional Worker for the surrounding bindings and Public web for what the locale list turns on.

Source