Be Civic — Website Rendering

Canonical system specifications for the Be Civic project.

Be Civic — Website Rendering

This sub-spec covers the rendering substrate for becivic.be: the Cloudflare Workers Static Assets renderer (§20), its build-time and runtime mechanics, the brand decisions, the pages in scope at launch, and the multi-site monorepo layout (§22). The MCP server at mcp.becivic.be is in protocol.md §23.

For the MDX tag schema (<VV>, <Ref>, <Observations>) that the renderer resolves at build time, see schemas.md §6.10. For the agent-facing surfaces (/agents, /agents/manifest.json, per-endpoint pages) referenced throughout this doc, see architecture.md §13.1.

20. Website rendering

§13 covers the agent-facing surface — capability tiers, per-ecosystem capable-mode recommendations, the becivic.be/agents page, pre-flight validation. This section covers the rendering substrate that serves both human and agent surfaces under becivic.be: the self-rendered Cloudflare Worker site at bc-infra/site/renderer/ (S50). The full design brief (light/dark mockups, brand spec, accessibility baseline, pre-launch posture detail) lives at docs/website/requirements.md; this section is the architectural summary.

20.1 Hosting model — bare apex, Worker-rendered

Three Workers share the becivic.be namespace, path-routed at Cloudflare:

  • becivic.be/renderer Worker (Cloudflare Workers Static Assets binding, source in bc-infra/site/renderer/); serves marketing landing, /agents, /skills/*, /docs/*, /llms.txt, /llms-full.txt, /sitemap.xml, /robots.txt
  • becivic.be/api/*staging Worker (source in bc-infra/api/, per §4 (see architecture.md) and §10 (see lifecycle.md)); unchanged in URL surface
  • mcp.becivic.beMCP Worker (source per §23 (see protocol.md), createMcpHandler); independent subdomain, no apex routing

Routing rule. The router Worker (bc-infra/site/router-worker.js) at the apex path-routes between the renderer (default) and the staging Worker (/api/* namespace). Per S50, the former Mintlify proxy is removed. The MCP Worker is reached via DNS at its own subdomain; it does not transit the apex router. Bare apex only — www.becivic.be is removed entirely (S56).

Why this shape. Every URL the spec already cites (becivic.be/agents, becivic.be/skills/<id>, becivic.be/llms.txt) stays exactly as written. Agent discovery (llms.txt, content-negotiated markdown) lives on the apex where AI consumers expect it. MCP moves to its own subdomain so agents can discover it without colliding with HTTP-content paths.

20.2 Renderer architecture

The renderer is built on Cloudflare Workers Static Assets with a custom Worker entry (worker.ts) for runtime concerns. The build pipeline (Node, run at deploy time) walks the bc-docs source tree, renders markdown to HTML against a shared template, computes the navigation graph from docs.json, generates the Pagefind static search index, and emits a dist/ directory of static assets. The Worker serves dist/ via the Static Assets binding and intercepts request lifecycle for the runtime hooks below.

docs.json (in bc-docs) is the navigation source-of-truth read by bc-infra/site/renderer/src/nav.ts. Skill bodies (bc-docs/skills/<id>/canonical.md) are rendered to /skills/<id> and /skills/<id>/canonical; the status frontmatter field drives the in-page banner (§6.1 (see schemas.md)) when status is not stable. There is no separate proposal route — one canonical URL per skill.

20.3 Runtime hooks (S57)

Three runtime/build-time mechanics beyond plain markdown rendering:

HTMLRewriter beacon injection. Cloudflare Web Analytics token is held as a Worker Secret (BECIVIC_CF_ANALYTICS_TOKEN), not as a Plaintext variable in wrangler.toml [vars]. The Worker injects a <script> tag pointing at cloudflareinsights.com/beacon.min.js into every HTML response via HTMLRewriter().on("head", ...). This makes the token rotatable from the dashboard without rebuild and avoids leaking the token into the build output or Git history.

_redirects deduplication at build time. The renderer auto-generates per-skill canonical-URL redirects from the corpus index (e.g. /skills/<id>/skills/<id>/canonical); the operator hand-authors high-priority redirects in docs.json. The build script de-duplicates: hand-authored entries take precedence; auto-generated entries with the same source are skipped. Without this, Cloudflare's deploy fails with "duplicate redirect rule" errors when the auto-generated and hand-authored sets overlap.

MDX-tag resolution mechanics (S78, S79). The three MDX components defined in §6.10 (see schemas.md) (<VV>, <Ref>, <Observations>) resolve at build time. Tag format, substitution behaviour per resolution status, and the surface-behaviour matrix are described below. Build-pipeline files: site/core/src/tokens.ts (tag-resolution layer), site/core/src/build.ts, site/core/src/render.ts (markdown emission path); tests in site/core/scripts/test-tokens.ts and test-apex-tokens.ts.

Trigger. The renderer resolves all <VV> and <Ref> tags in every bc-docs/skills/<id>/canonical.md source during the build step that produces skill HTML and resolved markdown. The build fetches from /api/volatile-values and /api/references over HTTP (the primary path). Snapshot fallback: data-snapshot/volatile-values.jsonl and data-snapshot/references.jsonl (JSONL files committed to the repo under data-snapshot/) cover cases where the API is unreachable at build time.

Per-tag substitution for <VV>. For each <VV name="..." uid="...">...</VV> tag in the source:

  1. Look up the catalogue row by uid (primary). When uid is empty (author wrote an empty attribute pending PR-CI minting), fall back to name lookup.
  2. If the row exists and its status is one of draft | alpha | beta | stable, emit <VV name="X" uid="val-NN">CURRENT_FORMATTED_VALUE</VV> where the children are the row's value field formatted with unit per §6.3 (see schemas.md). The wrapping tag is preserved so consuming agents retain all four signals: current value (children), semantic name (name), catalogue uid (uid), and the "this content is volatile" marker (the tag itself).
  3. If the row exists but its status is alpha or beta (non-stable): emit the current value as children plus an inline stability indicator. If a prior stable value is available in the catalogue row, include it parenthetically per S5.
  4. If the row does not exist in the catalogue, or if status: deprecated, emit <VV name="X" uid="val-NN" data-resolution-status="unresolved">[unresolved]</VV>. The data-resolution-status attribute lets agents detect the broken link and surface the gap to the customer (S80).

Per-tag substitution for <Ref>. Same wrapper shape. Children are the inline citation label authored by the walker (e.g. art. 12bis §1, 2°). If the catalogue row exists, the renderer ensures attributes (title, url, last_verified) reflect the current row values. If the catalogue row does not exist or is deprecated, the renderer emits the data-resolution-status="unresolved" sentinel with children [unresolved].

Re-citation (bracket form). First citation of a reference in a skill body MUST use the full <Ref> wrapper with all attributes. Subsequent re-citations of the same reference within the same skill body MAY use the lighter `[ref-id]` bracket form. The renderer resolves the bracket form to an in-page anchor link targeting the first <Ref> instance (S81). The bracket form carries no attributes and is meaningful only after the full <Ref> has been introduced in the same body.

<Observations skill="..." /> stays self-closing. This component is not subject to the wrapper-tag substitution rules above. The renderer expands <Observations skill="..." /> into a "Community observations" block per S19 and S20, using the same build-time D1 fetch. Existing expansion behaviour is preserved.

Cache-friendly. The renderer produces fully resolved HTML and markdown at build time; no per-request catalogue fetch occurs during normal serving. The renderer MUST rebuild when /api/volatile-values or /api/references data changes, or on a scheduled cadence aligned with catalogue-update frequency. Fallback: if catalogue change rate ever exceeds rebuild cadence, switch to request-time substitution with a short Worker-side cache. This is not a v1 concern (S79).

Surface behaviour. The resolved-children format applies to all rendered surfaces:

Surface Tag behaviour
HTML page (/skills/<id>) <VV> and <Ref> rendered with resolved children; data-resolution-status attribute present only on unresolved tags; full HTML context
llms.txt Tag structure preserved; children carry resolved values; consumed by AI crawlers
llms-full.txt Same as llms.txt; full corpus listing
MCP read_skill response Resolved markdown; tags with current children forwarded verbatim; no per-request catalogue fetch in the MCP Worker
Content-negotiated markdown (Accept: application/markdown) Source-form tags preserved as authored, including whatever children the walker wrote; agents requesting raw markdown MUST resolve tags themselves by calling /api/volatile-values and /api/references directly

The raw .md source served via content negotiation is the machine-readable corpus form. It intentionally keeps tags in source form so agents can detect which values are volatile and resolve them on demand, or file observations when their lived experience disagrees with the catalogue value.

Schema for the tag shapes is in §6.10 (see schemas.md). Settled decisions: S78 (wrapper-tag format), S79 (build-time resolution), S80 (unresolved sentinel), S81 (re-citation bracket form).

20.4 Custom MDX / callout subset

The renderer's MDX subset is a deliberate narrow superset of CommonMark: standard markdown plus the three Be Civic tags (<VV>, <Ref>, <Observations>) and a callout convention for tip / warning / note panels. The callout syntax is Worker-rendered HTML at build time (no client-side component framework). Mintlify-era components (<Tip>, <Warning>, <CardGroup>, <Tabs>, <Steps>, <Accordion>, etc.) are not supported; skill content uses the renderer's callout convention only.

20.5 Brand decisions locked

  • Brand name: Be Civic. Domain becivic.be. Code/repo identifier be-civic. Per the project CLAUDE.md.
  • Wordmark: stacked composition. Gold "be" / cream-or-ink "civic" / red full-stop. Letter-spacing -0.025em, line-height 0.95.
  • Palette: gold #fae042, red #ed2939, black #0a0a0a, white #ffffff. Cream #f5f4ee / #d4d2cc for dark-mode body text; ink #1a1a18 / #2a2a26 for light-mode body text. Both bright tones held at full saturation in both light and dark modes (per the D4 design decision — soft gold-on-white contrast accepted in exchange for cross-mode colour consistency).
  • Brand typeface: Manrope, self-hosted woff2, latin subset only at launch (English-only corpus).
    • ExtraBold (800) for display + wordmark + favicon
    • Bold (700) for inline marks + section headings
    • System sans stack for body copy (no font load — speed + privacy)
    • Fonts loaded once by the renderer Worker and served from the renderer's asset bundle, so brand consistency holds across every page
  • Light + dark modes both ship at launch via prefers-color-scheme plus a click-toggle in the header.
  • Visual reference: the canonical brand source is the renderer's stylesheet at bc-infra/site/renderer/assets/style.css and adjacent assets. Favicon outlined to paths so it renders identically regardless of whether the visiting device has Manrope installed.

20.6 Pages in scope at launch

All pages served by the renderer Worker from the bc-docs source tree:

  • Marketing landing at / — served as a standalone hand-crafted HTML file from site/index.html (scroll-down sections: hero → fetch demo → how-it-works → get-started → skills grid → footer), bypassing the shared template. Stylesheet at /landing.css. See §20.9 for mechanics.
  • Agent overview at /agents — rendered from bc-docs/agents.mdx; ~40 lines after S52 implementation. Per-endpoint pages at /agents/submit/*. Machine-readable manifest at /agents/manifest.json
  • Skills at /skills/<id> and /skills/<id>/canonical — rendered from bc-docs/skills/<id>/canonical.md; the status frontmatter field drives the in-page banner (§6.1 (see schemas.md)) when status is not stable. There is no separate proposal route
  • Docs at /docs/* — rendered from bc-docs/docs/*.mdx; submission contract, voice / style guides, FAQ, glossary
  • Discovery surfaces at /llms.txt, /llms-full.txt, /sitemap.xml, /robots.txt — emitted by the renderer build pipeline
  • MDX tags<VV>, <Ref>, <Observations> resolved at build/fetch time per §20.3 and §6.10 (see schemas.md)
  • Themed via docs.json (brand colours, logo paths) plus the renderer's own stylesheet. The renderer ships its own CSS as the source of truth; no external CSS is loaded
  • Privacy page at /privacy — rendered from bc-docs/docs/privacy.mdx. During the alpha and beta programmes, the live /privacy page is a placeholder pointing at the in-plugin alpha privacy attachment (per cowork-plugin.md §3.7). The page becomes the operative terms at post-alpha cutover. Lifecycle and divergence detail are in cowork-plugin.md §3.7; the alpha attachment lifecycle is alpha-operational and may shift between phases.

Out of website scope at launch: the global activity dashboard (§6.5 (see schemas.md)) is internal-only; publication deferred until corpus has visible activity worth showing.

20.7 Trade-offs accepted

  • The renderer is now our responsibility. Markdown→HTML, search index, navigation, theme switching are all maintained code rather than vendor service. Acceptable: scope is bounded; agentic build kept the implementation cost <1 week.
  • MDX support is a deliberate narrow subset. No Tabs, Cards, Steps, Accordion, or other Mintlify components. If a skill or doc needs richer interaction, the convention is Be Civic-flavoured (callouts, MDX tags) rather than vendor components.
  • Build pipeline runs on every deploy. Tag-resolution build-fetch makes deploys dependent on /api/* being reachable; snapshot fallback covers the failure mode.

20.8 Naming reconciliation (resolution)

Earlier drafts of the spec used site/ for Cloudflare Pages Functions implementing the /api/* namespace, while the website work claimed site/ for the marketing landing source. Round-7 resolves further by introducing the multi-site monorepo (S55, §22):

  • bc-infra/site/core/ — shared rendering primitives, build pipeline, Worker scaffolding
  • bc-infra/site/sites/<site-id>/ — per-site config, theme, content scope, wrangler.toml. Site becivic is the default and only live site at launch
  • bc-infra/site/router-worker.js — apex router; routes /api/* to staging Worker, everything else to the active site's renderer
  • The four POST endpoints (/api/*) are served by a Cloudflare Worker whose source lives at bc-infra/api/ — the directory name matches the URL namespace
  • bc-infra/tools/staging-worker/ houses the scheduled cron commit Worker (unchanged)

20.9 Apex landing

The marketing landing at / is a standalone hand-crafted HTML file at site/index.html, not an MDX page rendered through the shared template. This is intentional: the landing's structure (hero with stacked wordmark, "In one exchange" chat-bubble demo, "How it works" three-step grid, "Get started" two-paths panel, six-skill card grid, footer) is fundamentally different from the documentation chrome (sidebar nav, breadcrumb, header search) that wraps /agents, /skills/*, /docs/*. Forcing the landing through the docs template would either bloat the template with landing-only branches or strip the landing of its sectioned design.

Build mechanics (site/core/src/build.ts):

  1. The renderer's normal apex pass would write dist/index.html from the rendered MDX. With the apex landing in place, this MDX pass is a no-op (no index.mdx exists at the root) — the landing override happens regardless.
  2. After the MDX pass, the build conditionally copies site/index.htmldist/index.html (overwriting if both wrote) and site/style.cssdist/landing.css (renamed to avoid colliding with the docs-page /style.css shipped from site/core/assets/).

The two stylesheets serve completely different purposes — style.css is the docs-page brand layer (sidebar, search, callouts, skill-page typography); landing.css is the marketing-landing brand layer (hero, chat bubbles, skill cards, multi-section grid). Both self-host the Manrope weights they need; /fonts/, /logo/, /favicon.svg are shared.

Why static HTML. The landing has zero dynamic content — no per-skill data injected, no MDX tag resolution, no nav graph traversal. The only "dynamic" element is the rotating example phrase in the get-started panel, which is pure client-side JS in the page itself. Static HTML is the right shape; passing it through markdown-to-HTML compilation adds steps and loses authored-HTML control over class names and DOM structure.

Source of truth. Copy lives in bc-operations/specs/site-copy-r1.md. Implementation in site/index.html. Brand styles in site/style.css. The skill count ("Six skills live") is currently hand-edited; v1.1 may add a build-time injection step.

20.10 Source of truth

The full design brief — visual mockups, brand spec, accessibility baseline (WCAG AA on default sizes, gold-on-white reserved to wordmark "be" only), performance baseline (no client-side JS for content rendering), pre-launch posture copy ("pre-launch · corpus opens Q3 2026"), licence and disclaimer placement (CC-BY-4.0 attribution + non-affiliation in footer of every page) — lives at docs/website/requirements.md. The brief is the implementation source of truth for the visual surface; §20 here is the architectural summary that integrates the website work with the rest of the spec. Decisions locked in the brief's §9 are reflected as bullets above; open questions in the brief's §10 are resolved at draft time and do not need restatement here.

20.11 Path rendering

The renderer publishes two path surfaces parallel to the existing skill surfaces:

  • becivic.be/paths/ — the path directory index page, listing all active paths grouped by theme with summary cards (title, purpose badge, source count, last-verified date).
  • becivic.be/paths/<path_id> — per-entry pages, one per path in the catalogue.

Per-entry page content. Each per-entry page renders the following fields from the path entry (§6.12 (see schemas.md)):

  • Title in all available languages (multilingual block).
  • Description in the page-language locale (default: French; locale-switching at per-entry level is a v1.1 concern).
  • Sources table, ordered by priority descending (non-fallback sources first, fallback sources after a visual separator). Each source row shows: source class, audience region/commune scope, authentication method, and actor.primary indicator.
  • Per-source eligibility predicates rendered in customer-readable prose. The renderer translates structured predicates (field op value encoding) into plain-language conditional sentences (e.g. user.commune.region eq brussels renders as "Available if your commune is in the Brussels-Capital Region"). The predicate translation rules are baked into the renderer build pipeline; new predicate fields require a renderer update.
  • Actor block rendered as plain-English handoff guidance: what the agent will do, what the customer does at the handoff point, and how to signal that the step is complete. The renderer uses the agent_responsibility, user_responsibility, and resumption prose fields directly; no transformation beyond locale-appropriate wrapping.
  • Last-verified date (last_verified field), displayed as a human-readable date with a staleness indicator if last_verified is more than six months ago.
  • Source health badges: green (validation cohort showing recent confirms), amber (stale or rejected by cohort), red (parent path entry is status: deprecated or status: quarantined) — one badge per source row. Source health is inferred from the parent path entry's status and the validation cohort consensus; there is no per-source status field. Path entries at status: deprecated show all source rows as red and surface the "no working online way" notice; path entries at status: quarantined are not rendered (audit-only).

Placement in the renderer build pipeline. The renderer processes bc-docs/paths/index.json at build time alongside the skill corpus. Path pages are generated in the same static-HTML pass as skill pages and share the same Worker routing (becivic.be/paths/* routed by the renderer Worker). No separate Worker or build step is required.

Agent surface. The per-entry pages are served with content negotiation: agents requesting application/markdown receive the path entry as structured markdown (title, description, sources table without the HTML rendering layer), consistent with the machine-readable corpus convention for skills.

<Observations> block on path canonicals (added 2026-05-15). Each per-entry path page emits an <Observations skill="<path_id>" /> block (using the same MDX aggregator as skill canonicals — the element name <Observations> is the umbrella for multiple feedback shapes per §6.10 (see schemas.md)). The aggregator queries D1 for concerns + pending amendments with target_type ∈ {path, path_source} whose target_id resolves to this path entry. Per-source concerns are grouped under the source's row. A <CohortStats> element is emitted at the top of the block (parity with skills, per locked OPEN-14), derived at render time from the validations table keyed on target_type ∈ {path, path_source} for the path entry's sources. Path canonicals do NOT surface rating aggregates (rating does not target paths per §6.2.7).

20.12 requires_paths display in skill rendering

When rendering a procedure skill that declares a requires_paths: block (§6.1 (see schemas.md)), the renderer surfaces the referenced paths as a distinct section separate from the "Related procedures" section (which renders the requires: skill-id list).

Section label. "Documents and tools you will need" (or locale equivalent). This label is distinct from "Related procedures" because the two sections have different customer action implications: requires: lists other procedures the customer may need to follow; requires_paths: lists specific documents or tools the customer needs to obtain.

Per-path display. Each entry in requires_paths renders as a card or list row showing:

  • Path title (linked to becivic.be/paths/<path_id>).
  • purpose enum value rendered as a human-readable badge: submission renders as "Required for your dossier"; preparation renders as "Recommended to check beforehand"; check-only renders as "Verify your situation"; informational renders as "For reference".
  • role override (if present on the requires_paths entry) takes precedence over the path's own purpose field for badge text.
  • timing field rendered as a plain-English note: pre-filing renders as "Needed before you file"; months-before-filing renders as "Address this several months before filing".
  • Per-source actor.primary indicator for the highest-priority source that matches the current user context (if the renderer has locale/region context; otherwise, a summary of actor patterns across sources).
  • notes field rendered verbatim as supplementary guidance text below the card.

Ordering. Entries with role: submission appear before entries with role: preparation, consistent with the priority of the document in the procedure. Within each role group, ordering follows the requires_paths array order in the skill frontmatter.

Genericity note. The renderer resolves path entries from the catalogue at build time, the same way <VV> and <Ref> tags are resolved (§20.3). Rendering does not depend on any specific agent platform's key-value stores [e.g., Project Memory in Anthropic platforms]; path data is always sourced from the catalogue file.

22. Multi-site monorepo

The renderer is structured as a multi-site monorepo (S55) so future experimental verticals (e.g. tailored "buying a house" deployments) can instantiate as new entries under sites/ without forking the rendering pipeline.

Path Directory in the monorepo. Paths live in the existing bc-docs repo at bc-docs/paths/index.json, alongside skills/, docs/, data/, and schemas/. The multi-site layout requires no change to accommodate paths; the renderer build pipeline reads bc-docs/paths/index.json as a first-class content source, the same way it reads bc-docs/skills/<id>/canonical.md entries.

22.1 Layout

  • site/core/ — shared rendering primitives (markdown→HTML, template engine, search-index generation, navigation graph), build pipeline, and Worker scaffolding (Static Assets binding, HTMLRewriter beacon, _redirects dedup logic, runtime hooks per §20.3). Maintained as a single shared codebase; sites consume it as a build-time dependency.
  • site/sites/<site-id>/ — per-site config, theme, content scope, and wrangler.toml. Each site is independently deployable with its own Cloudflare Worker route and analytics token. Per-site wrangler.toml (operator-confirmed: per-site rather than top-level), so each site can move through staging / production cutover independently.

Per S58, the site/ tree lives at the root of the combined repo (post bc-docs+bc-infra merge), alongside api/, mcp/, tools/, and the content directories (skills/, agents/, docs/, data/, schemas/).

Site becivic is the default and only live site at v1 launch. Its config points the build pipeline at the local content tree (skills/, docs/, agents.mdx) and applies the locked Be Civic brand.

22.2 Per-site config shape (sketch)

A sites/<site-id>/site.config.ts (or equivalent) declares: site-id, custom-domain, content-source path or repo, navigation source-of-truth path, brand stylesheet, analytics-token Secret name, MDX-tag resolution endpoint base, build-time fallback snapshot path. Schema and exact field names finalise during implementation.

22.3 Future verticals

A new vertical (e.g. sites/buying-a-house/) can:

  • subset the corpus by category or skill-id list,
  • apply a vertical-specific brand and theme,
  • ship under a different domain or subdomain (custom Cloudflare route),
  • inject vertical-specific MDX tags or surface affordances.

No experiments at v1 launch; the scaffold ships in v1 specifically because the restructure is much cheaper at zero-tenant scale than once a single site has accreted hard-coded assumptions.

22.4 Out of scope

  • Multi-tenant runtime isolation (sites share the rendering codebase; security boundary is the Worker route, not the runtime).
  • Per-site D1 database isolation — v1 has one D1 instance shared across the corpus; vertical-specific data tiers are deferred until a vertical needs them.
  • Cross-site shared user state — sites are separately deployed and do not share session.

Cross-references

Cross-doc references are inlined throughout this document in the form §X.Y (see .md). The list below was the pre-reconciliation manifest from the 2026-05-11 split, retained for audit; it can be deleted at the next split-or-merge cycle.

  • §4 (Architecture overview / staging Worker routing) — see architecture.md §4
  • §6.1 (Skill schema / status frontmatter / alpha banner; requires_paths: block on skill frontmatter) — see schemas.md §6.1
  • §6.5 (Skills index and activity dashboards) — see schemas.md §6.5
  • §6.10 (MDX-tag conventions / schema for VV, Ref, Observations) — see schemas.md §6.10
  • §6.12 (Path Directory schema — path entries, source entries, source classes, actor block, per-source predicates) — see schemas.md §6.12
  • §10 (Branch policy / CI / worker deploy actions) — see lifecycle.md §10
  • §13 (Reference consumer / agent-facing surface) — see architecture.md §13
  • §13.1 (Agent interface manifest / per-endpoint pages) — see architecture.md §13.1
  • §23 (MCP server at mcp.becivic.be; path MCP tools) — see protocol.md §23
  • §23.2.1 (Path Directory MCP tools) — see protocol.md §23.2.1