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 inbc-infra/site/renderer/); serves marketing landing,/agents,/skills/*,/docs/*,/llms.txt,/llms-full.txt,/sitemap.xml,/robots.txtbecivic.be/api/*→ staging Worker (source inbc-infra/api/, per §4 (see architecture.md) and §10 (see lifecycle.md)); unchanged in URL surfacemcp.becivic.be→ MCP 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:
- Look up the catalogue row by
uid(primary). Whenuidis empty (author wrote an empty attribute pending PR-CI minting), fall back to name lookup. - 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'svaluefield formatted withunitper §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). - If the row exists but its status is
alphaorbeta(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. - 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>. Thedata-resolution-statusattribute 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 identifierbe-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/#d4d2ccfor dark-mode body text; ink#1a1a18/#2a2a26for 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-schemeplus a click-toggle in the header. - Visual reference: the canonical brand source is the renderer's stylesheet at
bc-infra/site/renderer/assets/style.cssand 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 fromsite/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 frombc-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 frombc-docs/skills/<id>/canonical.md; thestatusfrontmatter field drives the in-page banner (§6.1 (see schemas.md)) when status is notstable. There is no separate proposal route - Docs at
/docs/*— rendered frombc-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 frombc-docs/docs/privacy.mdx. During the alpha and beta programmes, the live/privacypage is a placeholder pointing at the in-plugin alpha privacy attachment (percowork-plugin.md §3.7). The page becomes the operative terms at post-alpha cutover. Lifecycle and divergence detail are incowork-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 scaffoldingbc-infra/site/sites/<site-id>/— per-site config, theme, content scope,wrangler.toml. Sitebecivicis the default and only live site at launchbc-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 atbc-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):
- The renderer's normal apex pass would write
dist/index.htmlfrom the rendered MDX. With the apex landing in place, this MDX pass is a no-op (noindex.mdxexists at the root) — the landing override happens regardless. - After the MDX pass, the build conditionally copies
site/index.html→dist/index.html(overwriting if both wrote) andsite/style.css→dist/landing.css(renamed to avoid colliding with the docs-page/style.cssshipped fromsite/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
prioritydescending (non-fallback sources first, fallback sources after a visual separator). Each source row shows: source class, audience region/commune scope, authentication method, andactor.primaryindicator. - Per-source eligibility predicates rendered in customer-readable prose. The renderer translates structured predicates (
field op valueencoding) into plain-language conditional sentences (e.g.user.commune.region eq brusselsrenders 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, andresumptionprose fields directly; no transformation beyond locale-appropriate wrapping. - Last-verified date (
last_verifiedfield), displayed as a human-readable date with a staleness indicator iflast_verifiedis more than six months ago. - Source health badges:
green(validation cohort showing recent confirms),amber(stale or rejected by cohort),red(parent path entry isstatus: deprecatedorstatus: 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 atstatus: deprecatedshow all source rows as red and surface the "no working online way" notice; path entries atstatus: quarantinedare 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>). purposeenum value rendered as a human-readable badge:submissionrenders as "Required for your dossier";preparationrenders as "Recommended to check beforehand";check-onlyrenders as "Verify your situation";informationalrenders as "For reference".roleoverride (if present on therequires_pathsentry) takes precedence over the path's ownpurposefield for badge text.timingfield rendered as a plain-English note:pre-filingrenders as "Needed before you file";months-before-filingrenders as "Address this several months before filing".- Per-source
actor.primaryindicator 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). notesfield 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,_redirectsdedup 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, andwrangler.toml. Each site is independently deployable with its own Cloudflare Worker route and analytics token. Per-sitewrangler.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
- §4 (Architecture overview / staging Worker routing) — see
architecture.md§4 - §6.1 (Skill schema / status frontmatter / alpha banner;
requires_paths:block on skill frontmatter) — seeschemas.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