Skip to content
bflo.sh

ADR-0014: Site identity lives in the root content metadata

ADR-0014 — Accepted 2026-07-07. Part of the platform's decision record.

Adapted from the repo's docs/adr/0014-site-identity-in-root-content-metadata.md — Part of Internals: the platform's design set, published as content on the platform it describes.

  • Status: Accepted
  • Date: 2026-07-07
  • Deciders: contract-owner (with the lead)
  • Tiers affected: contract | backend | frontend | docs
  • Related: ADR-0005 (filesystem is the content source of truth), ADR-0006 (read-only unauthenticated public API), ADR-0009 (content sync and content root), docs/master-blueprint.md §11 (Track 4d — revalidation, the staleness caveat below), contract v0.6.0

Context

The site's display identity — the masthead name, the homepage hero lines (thesis + tagline/note), the RSS channel title/description, and the default meta description — is today configured as frontend environment variables (SITE_NAME, SITE_TAGLINE, SITE_THESIS in frontend/.env.local). That contradicts the product's core stance:

  • The filesystem content tree is the single source of truth for everything the reader sees (ADR-0005). Rebranding the site should be a content edit, not an ops change to a frontend env file followed by a redeploy.
  • The env vars are invisible to the backend, so the feed (served by the backend) and the frontend masthead can drift apart — two places declare "what this site is called".
  • The content root already has a natural home for this: the ROOT metadata.json (backend/storage/app/content/metadata.json), which today carries the root section's title/description.

The forces on the shape: the root metadata's existing top-level title/description are the ROOT SECTION's identity and must keep meaning that; site identity is related but not identical (a section title is not necessarily a masthead name); and the field must be optional — an existing content tree without any site block must keep working.

Decision

We will store site identity in an OPTIONAL site object in the ROOT content metadata file, resolve it server-side with a single canonical fallback rule, and publish it via a new contract endpoint GET /site (operation getSiteMeta, schema SiteMeta, contract v0.6.0). The frontend env-var display config (SITE_NAME, SITE_TAGLINE, SITE_THESIS) is replaced and will be removed from the frontend in a follow-up task.

The root metadata shape:

{
  "title": "Field Notes",
  "description": "...",
  "site": {
    "title": "Field Notes",
    "thesis": "Writing, kept like source.",
    "tagline": "Notes on building things."
  }
}

The existing top-level title/description remain the root section identity and double as fallbacks.

Canonical resolution rule (one implementation): title = site.title, else root title, else "Untitled"; tagline = site.tagline, else root description, else the empty string; thesis = site.thesis, else null. All values are trimmed; empty/whitespace-only strings are treated as absent. The API therefore ALWAYS returns a non-empty title and a tagline string (possibly empty); thesis is nullable.

Where the rule lives: in the Domain tier, as a static resolving constructor on the App\Domain\Content\ValueObject\SiteMeta value object (e.g. SiteMeta::resolve(rawTitle, rawDescription, rawSite)). The infrastructure SiteMetaProvider (data-engineer) reads the raw root metadata.json through the existing filesystem source machinery and delegates resolution to that constructor — it carries no fallback logic of its own. The Domain action App\Domain\Content\Action\GetSiteMeta fronts the SiteMetaProvider port for the HTTP tier. This keeps exactly one implementation of the rule, unit-testable without the filesystem.

Field semantics (as published in the contract):

  • title — site display name: masthead, HTML document title, feed channel title.
  • thesis — homepage headline line (hero statement).
  • tagline — homepage note line, default meta description, feed channel description.

Alternatives considered

  1. Keep frontend env vars — pros: zero new surface. Cons: rebranding is an ops change, not a content edit (against the file-first product stance); backend-served surfaces (the feed) cannot see the values, so identity is duplicated and drifts. Lost on principle (ADR-0005).
  2. A separate site.json at the content root — pros: clean separation from section metadata. Cons: a second root metadata file to sync, validate, and document for one small object; the root metadata.json already exists and its title/description are the natural fallbacks. Lost as unnecessary file surface.
  3. Client-side fallback resolution (publish the raw site block and let consumers resolve) — pros: thinner API. Cons: every consumer (frontend pages, feed builder) re-implements trim/fallback and they drift — the exact class of bug the author-slug episode (ADR-0013) taught us to avoid. Server-side single-rule resolution wins.

Consequences

  • Easier: rebranding is now a single content-file edit; the feed and the frontend masthead can never disagree (both consume the same resolved SiteMeta); a content tree without a site block keeps working via the fallbacks.
  • Harder / caveat — ISR staleness: frontend pages consume the API through ISR with revalidate = 300, so an identity edit becomes visible within the revalidate window (up to ~5 minutes), not instantly. This holds until the Track 4d on-demand revalidation loop lands, which will close the gap for content-driven changes generally; site identity rides that same loop.
  • Follow-up tasks: backend implementation per the v0.6.0 changelog "Impacted members" (domain-engineer, data-engineer, backend-api); then a SEPARATE frontend task to regenerate the client, consume getSiteMeta for masthead/hero/meta/feed, and remove SITE_NAME/SITE_TAGLINE/SITE_THESIS from frontend/.env.local and any env plumbing.

Compliance

  • The contract encodes the decision: GET /site returns the closed SiteMeta { title, tagline, thesis } with the resolution rule documented on the operation and schema; ddev contract-lint stays clean; oasdiff breaking --flatten-allof v0.5.0 → v0.6.0 reports no breaking changes (purely additive).
  • Detectable drift: any tier re-implementing the fallback rule outside the SiteMeta VO constructor, or the frontend reintroducing site-identity env vars, violates this ADR and is flagged in review.