Skip to main content

ADR-197: Unified Web Publishing Component System

Status: Accepted Date: 2026-02-14 Deciders: Hal Casteel (CTO) Extends: ADR-195 (Push-Button Documentation Publishing Platform)


Context

Problem

CODITECT projects generate rich documentation (markdown, JSX dashboards, Mermaid diagrams, LaTeX equations). The BIO-QMS project proved that a Vite+React viewer with unified/remark/rehype rendering works well — but the implementation required extensive manual tuning:

  1. Mermaid theming required iterating through 60+ theme variables to achieve high-contrast dark-mode rendering (commit 8cae94f)
  2. CSS styling for code blocks, tables, headings, and diagrams was crafted per-project with no shared standard
  3. Subgraph color conventions for Mermaid architecture diagrams were invented ad-hoc (Actors=blue, Core=indigo, Data=teal, etc.)
  4. SVG background stripping was needed because Mermaid injects backgrounds that conflict with container styling
  5. Pre-render pattern for Mermaid (render to SVG string before React setHtml()) was discovered through trial-and-error, not documented
  6. Forced dark background on diagram containers was the solution for light/dark mode contrast, but this pattern wasn't codified

Each new project would repeat this discovery process. One-off web publishing doesn't scale.

What ADR-195 Covers (vs. This ADR)

ADR-195 defines the platform architecture: what the viewer does (render markdown, search, present, deploy to cloud, package as npm). ADR-197 defines the component contracts: how each piece is built, themed, and composed so that any project gets identical rendering by adopting the standard components.

ConcernADR-195ADR-197 (this)
Viewer application architectureYesNo
publish.json manifestYesNo
Cloud deployment (Cloud Run + CDN)YesNo
Markdown pipeline specificationMentionedDetailed
Mermaid rendering approachNoYes
CSS design token systemNoYes
Component interface contractsNoYes
Theme configuration specificationNoYes
Subgraph color conventionsNoYes
Scaffold templatesMentioned (create-doc-site)Detailed
Quality grading criteriaNoYes

Decision

D1: Modular Component Architecture

The web publishing system is decomposed into 7 core components that compose like building blocks. Each component has a defined interface contract, CSS token dependencies, and can be used independently or as part of the full stack.

@coditect/web-publisher (meta-package)
├── MarkdownRenderer # Markdown → HTML via unified pipeline + Mermaid pre-render
├── MetadataBar # Frontmatter display (audience badges, status, word count)
├── Sidebar # Collapsible category tree with expand-all/collapse-all
├── SearchPanel # Full-text body content search (MiniSearch) with snippets
├── Breadcrumbs # Category → Document navigation trail
├── TableOfContents # Right-rail heading navigation (h2/h3)
├── CategoryLanding # Category index page with document grid
└── generate-publish-manifest.js # Build-time: markdown → publish.json with body_text

Rationale: Lego-like composition means a project can adopt just MarkdownRenderer for minimal rendering, or the full stack for a complete publishing platform. Components share CSS tokens but have no hard coupling. Full-text search indexes both metadata and stripped markdown body content — no server required.

D2: Markdown Rendering Pipeline (Canonical Stack)

The markdown rendering pipeline is standardized on the following exact dependency chain. All CODITECT web publishing projects MUST use this stack:

Source (.md) → parseFrontmatter() → unified pipeline → HTML string → pre-render Mermaid → setHtml()

Pipeline Configuration (REQUIRED):

import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkGfm from "remark-gfm";
import remarkFrontmatter from "remark-frontmatter";
import remarkMath from "remark-math";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeHighlight from "rehype-highlight";
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypeKatex from "rehype-katex";
import rehypeRaw from "rehype-raw";

const processor = unified()
.use(remarkParse)
.use(remarkFrontmatter, ["yaml"])
.use(remarkGfm)
.use(remarkMath)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeSlug)
.use(rehypeAutolinkHeadings, { behavior: "wrap" })
.use(rehypeHighlight, { ignoreMissing: true })
.use(rehypeKatex)
.use(rehypeStringify);

Plugin Order Matters: rehypeRaw must come BEFORE rehypeSlug and rehypeHighlight to ensure raw HTML blocks are processed before heading IDs and code highlighting are applied.

Frontmatter Extraction: Use browser-compatible parseFrontmatter() function (NOT gray-matter which depends on Node.js Buffer). The reference implementation in BIO-QMS handles YAML key-value pairs and list items without any Node.js dependencies.

Rationale: This exact stack was proven in production on 83+ documents in BIO-QMS. Every plugin is necessary: remarkGfm for tables/task lists, remarkMath+rehypeKatex for equations, rehypeRaw for embedded HTML, rehypeHighlight for code blocks.

D3: Mermaid Pre-Render Pattern (REQUIRED)

Mermaid diagrams MUST be pre-rendered to SVG strings and injected into the HTML before React processes it. DOM mutation after dangerouslySetInnerHTML gets overwritten by React re-renders.

Pattern:

// 1. Process markdown to HTML string
let htmlStr = String(await processor.process(body));

// 2. Find all mermaid code blocks in the HTML
const mermaidRegex = /<pre><code class="[^"]*language-mermaid[^"]*">([\s\S]*?)<\/code><\/pre>/g;

// 3. For each match: strip highlight spans, decode HTML entities, render to SVG
let match;
while ((match = mermaidRegex.exec(htmlStr)) !== null) {
const raw = match[1].replace(/<[^>]*>/g, ""); // Strip spans
const decoder = document.createElement("textarea");
decoder.innerHTML = raw;
const source = decoder.textContent; // Decode entities
const id = `mermaid-${Math.random().toString(36).slice(2, 10)}`;
let { svg } = await mermaid.render(id, source);
// Strip Mermaid's injected SVG background
svg = svg.replace(/(<svg[^>]*?)style="([^"]*)"/, (m, pre, style) => {
const cleaned = style.replace(/background[^;]*(;|$)/g, "background:transparent;");
return `${pre}style="${cleaned}"`;
});
htmlStr = htmlStr.replace(match[0], `<div class="mermaid-diagram">${svg}</div>`);
}

// 4. Set the HTML with all diagrams already rendered as inline SVG
setHtml(htmlStr);

Why Pre-Render (NOT Post-Render):

  • dangerouslySetInnerHTML replaces the entire DOM subtree on every React state update
  • Post-render DOM mutations (calling mermaid.run() after mount) get wiped on re-render
  • Pre-rendering embeds the SVG directly in the HTML string, making it part of React's virtual DOM
  • This pattern was discovered after multiple failed approaches in BIO-QMS development

SVG Background Stripping: Mermaid's renderer injects a background style into the SVG element. This must be stripped and replaced with background:transparent so the container's background controls the visual appearance.

Rationale: This is the only reliable approach for React SPA rendering. It was proven through iteration — mermaid.run(), mermaid.init(), and useEffect-based approaches all fail because React overwrites the DOM.

D4: Mermaid Theme Configuration (60+ Variables)

All CODITECT web publishing projects MUST use theme: "base" with the following themeVariables configuration. The dark theme is NOT used because it has limited override capability.

Complete Theme Specification:

CategoryVariableValuePurpose
BackgroundbackgroundtransparentContainer controls background
BackgrounddarkModetrueDark mode flag
TypographyfontFamilyInter, system-ui, sans-serifSystem font stack
TypographyfontSize13pxBase diagram font size
PrimaryprimaryColor#1c3048Muted steel blue node fill
PrimaryprimaryTextColor#d8dee6Node text
PrimaryprimaryBorderColor#5a8ab4Node border
SecondarysecondaryColor#1a2636Cool slate node fill (no purple)
SecondarysecondaryTextColor#d8dee6Alternate text
SecondarysecondaryBorderColor#6a8098Alternate border
TertiarytertiaryColor#182e2eMuted teal fill
TertiarytertiaryTextColor#d8dee6Third text
TertiarytertiaryBorderColor#508888Third border
LineslineColor#8a95a5Edge/arrow color
LinestextColor#d8dee6General text
NodesmainBkg#1c3048Main background
NodesnodeBorder#5a8ab4Node borders
NodesnodeTextColor#d8dee6Node text
ClustersclusterBkg#10151e80Subgraph fill (50% opacity)
ClustersclusterBorder#3a4454Subgraph border
LabelstitleColor#d8dee6Subgraph titles
LabelsedgeLabelBackground#10151eEdge label background
LabelslabelTextColor#d8dee6Label text
LabelslabelBackground#10151eLabel background
SequenceactorBkg#182230Actor box fill
SequenceactorBorder#5a8ab4Actor border
SequenceactorTextColor#d8dee6Actor text
SequenceactorLineColor#8a95a5Lifeline color
SequencesignalColor#8a95a5Message arrow
SequencesignalTextColor#d8dee6Message text
SequencelabelBoxBkgColor#182230Label box fill
SequencelabelBoxBorderColor#4a5568Label box border
SequenceloopTextColor#d8dee6Loop annotation text
SequenceactivationBorderColor#5a8ab4Activation bar border
SequenceactivationBkgColor#1c3048Activation bar fill
SequencesequenceNumberColor#d8dee6Sequence number text
SequencenoteBkgColor#283848Note background
SequencenoteBorderColor#4a5568Note border
SequencenoteTextColor#d8dee6Note text
ClassclassText#d8dee6Class diagram text
FlowchartfillType0#1c3048Steel blue
FlowchartfillType1#1a2636Cool slate
FlowchartfillType2#182e2eMuted teal
FlowchartfillType3#24303aWarm grey
FlowchartfillType4-7Repeat 0-38 fills cycling (no purple/red)
State/ERlabelColor#d8dee6State label text
State/ERaltBackground#182230Alternate background
State/ERcompositeBackground#10151e80Composite fill
State/ERcompositeBorder#3a4454Composite border
State/ERcompositeTitleBackground#182230Composite title fill
State/ERrelationColor#8a95a5Relation arrow
State/ERrelationLabelBackground#10151eRelation label bg
State/ERrelationLabelColor#d8dee6Relation label text

Neutral Professional Palette Principle: ALL text colors use #d8dee6 (muted off-white). ALL line/edge colors use #8a95a5 (medium grey). Node fills use muted steel blue, cool slate, and teal — no purple, no saturated colors. This creates a professional, readable appearance that works for both technical and executive audiences.

Rationale: These 60+ variables were tuned through multiple iterations across flowchart, sequence, class, and state/ER diagram types in BIO-QMS. The values are optimized for readability at 13px on dark backgrounds.

D5: Forced Dark Diagram Background

Mermaid diagram containers MUST use a forced dark background (#0f172a — slate-900) regardless of the viewer's light/dark mode setting.

.mermaid-diagram {
margin: 1.5rem 0;
padding: 1.5rem 1rem;
background: #0f172a;
border: 1px solid #334155;
border-radius: 0.5rem;
overflow-x: auto;
text-align: center;
}

Why NOT Dual-Theme:

  • Mermaid's color system is tuned for one background — supporting both light and dark backgrounds doubles the theming work
  • The dark palette was already tuned (60+ variables); creating a light-mode equivalent would require duplicating all values
  • Forced dark creates a consistent "diagram island" that looks correct in any viewer theme
  • Architecture diagrams contain colored subgraphs that conflict with light backgrounds

CSS Edge Thickness (REQUIRED):

.mermaid-diagram .edgePath .path,
.mermaid-diagram .flowchart-link,
.mermaid-diagram line,
.mermaid-diagram .relation,
.mermaid-diagram .messageLine0,
.mermaid-diagram .messageLine1 {
stroke-width: 2px !important;
}
.mermaid-diagram .actor-line {
stroke-width: 2.5px !important;
stroke: #e2e8f0 !important;
}
.mermaid-diagram .messageText {
font-size: 13px !important;
font-weight: 500 !important;
fill: #f1f5f9 !important;
}

Rationale: Default Mermaid edge widths (1px) are nearly invisible on dark backgrounds at typical screen resolutions. The 2px minimum ensures visibility without overwhelming the diagram content.

D6: Mermaid Subgraph Color Conventions

Architecture diagrams using Mermaid subgraphs MUST use the following semantic color palette for consistent visual communication across all projects:

CategoryFillBorderSemantic Meaning
Actors/Input#1e3a5f#38bdf8Users, external systems, entry points
Core/Control#2d2250#818cf8Core logic, orchestration, control flow
Data/Workers#1a3636#2dd4bfData processing, background jobs, pipelines
Persistence#1e293b#475569Databases, file systems, storage
Integration#451a03#fbbf24External APIs, third-party services
Regulated#3b1d1d#f87171Compliance, audit, security-critical

Mermaid Style Directive Format:

Rationale: Consistent colors across diagrams enable readers to instantly identify component categories without reading legends. This palette was tuned in BIO-QMS across 12 diagrams in 3 architecture documents.

D7: CSS Design Token Integration

Web publishing CSS MUST integrate with the CODITECT Design Tokens Standard (CODITECT-STANDARD-DESIGN-TOKENS.md). The following publishing-specific tokens extend the base token set:

:root {
/* Publishing Layout */
--coditect-pub-max-width: 900px;
--coditect-pub-sidebar-width: 280px;
--coditect-pub-header-height: 56px;

/* Publishing Typography */
--coditect-pub-font-body: "Inter", system-ui, -apple-system, sans-serif;
--coditect-pub-font-code: "JetBrains Mono", "Fira Code", monospace;
--coditect-pub-font-size-base: 15px;
--coditect-pub-line-height: 1.7;

/* Diagram Tokens */
--coditect-pub-diagram-bg: #0f172a;
--coditect-pub-diagram-border: #334155;
--coditect-pub-diagram-text: #f1f5f9;
--coditect-pub-diagram-line: #e2e8f0;
--coditect-pub-diagram-edge-width: 2px;

/* Code Block Tokens */
--coditect-pub-code-bg: #1e293b;
--coditect-pub-code-border: #334155;
--coditect-pub-code-text: #e2e8f0;

/* Metadata Bar */
--coditect-pub-meta-bg: var(--coditect-color-neutral-800);
--coditect-pub-meta-border: var(--coditect-color-neutral-700);
}

Rationale: Publishing-specific tokens with the --coditect-pub- prefix avoid collision with application-level tokens while maintaining the naming convention from CODITECT-STANDARD-DESIGN-TOKENS.md.

D8: Component Interface Contracts

Each component has a defined prop interface and behavioral contract:

MarkdownRenderer:

interface MarkdownRendererProps {
content: string; // Raw markdown with optional frontmatter
onHeadings?: (headings: Heading[]) => void; // TOC callback
className?: string; // Additional CSS classes
}
// Renders: frontmatter extraction → unified pipeline → Mermaid pre-render → HTML
// Exports: extractSections(content) for presentation mode

MermaidDiagram (standalone):

interface MermaidDiagramProps {
source: string; // Mermaid syntax (e.g., "graph TD\n A-->B")
id?: string; // Unique DOM ID (auto-generated if omitted)
className?: string;
}
// Renders: mermaid.render() → SVG string → div.mermaid-diagram

MetadataBar:

interface MetadataBarProps {
frontmatter: Record<string, any>; // Parsed YAML frontmatter
wordCount?: number;
}
// Renders: audience badge, category, status, word count

CodeBlock:

interface CodeBlockProps {
code: string;
language?: string;
showCopy?: boolean; // Default: true
showLineNumbers?: boolean; // Default: false
}
// Renders: syntax-highlighted code with copy button

Rationale: Defined interfaces enable the agentic system to generate, modify, and compose these components programmatically. When an agent creates a new project viewer, it knows exactly what props to pass and what behavior to expect.

D9: Project Scaffold Templates

Three scaffold templates enable rapid adoption:

Minimal Template (5 files):

project/
├── components/
│ └── MarkdownRenderer.jsx
├── styles.css # Dark-mode diagram CSS
├── vite.config.js
├── package.json # Exact dependency versions
└── index.html

Standard Template (10 files):

project/
├── components/
│ ├── MarkdownRenderer.jsx
│ ├── MetadataBar.jsx
│ ├── NavTree.jsx
│ └── SearchPanel.jsx
├── viewer.jsx # Main SPA shell
├── styles.css
├── tailwind.config.js
├── vite.config.js
├── package.json
└── index.html

Full Template (15+ files):

project/
├── components/
│ ├── MarkdownRenderer.jsx
│ ├── MetadataBar.jsx
│ ├── NavTree.jsx
│ ├── SearchPanel.jsx
│ ├── PresentationMode.jsx
│ └── DashboardLoader.jsx
├── dashboards/ # JSX dashboards
├── docs/ # Markdown content
├── scripts/
│ └── generate-publish-manifest.js
├── viewer.jsx
├── styles.css
├── tailwind.config.js
├── vite.config.js
├── package.json
├── publish.json # Content manifest (ADR-195)
└── index.html

Scaffold Command:

/component-create web-publisher my-project --template standard

Rationale: Templates eliminate the bootstrapping cost. A new project goes from zero to rendering markdown + Mermaid diagrams with correct theming in under 5 minutes.

D10: Dependency Pinning (REQUIRED)

All scaffold templates MUST pin exact dependency versions. No ^ or ~ semver ranges.

Canonical dependency set (as of February 2026):

{
"dependencies": {
"katex": "0.16.28",
"mermaid": "11.12.2",
"react": "19.2.4",
"react-dom": "19.2.4",
"rehype-autolink-headings": "7.1.0",
"rehype-highlight": "7.0.2",
"rehype-katex": "7.0.1",
"rehype-raw": "7.0.0",
"rehype-slug": "6.0.0",
"rehype-stringify": "10.0.1",
"remark-frontmatter": "5.0.0",
"remark-gfm": "4.0.1",
"remark-math": "6.0.0",
"remark-parse": "11.0.0",
"remark-rehype": "11.1.2",
"unified": "11.0.5"
},
"devDependencies": {
"@tailwindcss/vite": "4.1.18",
"@vitejs/plugin-react": "5.1.4",
"tailwindcss": "4.1.18",
"vite": "7.3.1"
}
}

Rationale: Rendering consistency requires identical dependency versions. A unified 11.0.5 → 12.0.0 upgrade could change HTML output, breaking diagram rendering. Version updates must be tested and applied across all projects simultaneously.

D11: Multi-Project Dashboard Switching (Phase 2)

A single viewer deployment supports switching between multiple projects, each with its own dashboard data and branding. This eliminates per-project viewer forks.

Project Configuration (project-config.json):

Each project defines a branding configuration file in the viewer root:

{
"id": "coditect-pilot",
"name": "CODITECT Pilot",
"title": "CODITECT Platform — Pilot Dashboard",
"description": "AI-Driven Work Order Platform for Regulated Industries",
"footer": "CODITECT Pilot | Internal"
}

Project Manifest (public/project-manifest.json):

Generated by scripts/generate-project-manifest.js from project-config.json. Lists all registered projects with their JSON data file paths:

{
"version": 1,
"generated": "2026-02-16T...",
"defaultProject": "coditect-pilot",
"projects": [
{
"id": "coditect-pilot",
"name": "CODITECT Pilot",
"title": "CODITECT Platform — Pilot Dashboard",
"description": "AI-Driven Work Order Platform",
"jsonFile": "project-dashboard-data.json",
"footer": "CODITECT Pilot | Internal"
}
]
}

ProjectSwitcher Component Interface:

interface ProjectSwitcherProps {
projects: Project[]; // From project-manifest.json
currentProject: Project; // Currently active project
onSwitch: (project: Project) => void; // Switch handler
}
// Exports: resolveInitialProject(projects, defaultId) helper
// Resolution priority: URL ?project=ID > localStorage > manifest defaultProject
// Single-project mode: component renders nothing (hidden)

Viewer Manifest Loading:

// viewer.jsx startup sequence:
// 1. Fetch /project-manifest.json (graceful fallback if missing)
// 2. resolveInitialProject() from URL/localStorage/default
// 3. Apply branding (title, description, footer) from matched project
// 4. Fetch project-specific JSON: project.jsonFile

Per-Project JSON Naming:

  • Multi-project: project-dashboard-data-{id}.json
  • Default fallback: project-dashboard-data.json (backward compatible)
  • Generator flag: --project ID produces per-project filename

Rationale: Consumer projects (e.g., BIO-QMS) should NOT maintain local copies of viewer, components, dashboards, or scripts. Everything lives in coditect-core's tools/web-publishing-platform/ as the canonical source. Per-project differentiation is achieved through project-config.json (branding) and per-project JSON data files (content), not code forks. This was validated during J.18 consolidation where BIO-QMS was confirmed to have zero local dashboard infrastructure — all components are consumed from core.


Consequences

Positive

  • Eliminate one-off publishing: Any CODITECT project adopts consistent rendering via scaffold templates
  • Change CSS once, apply everywhere: Design token updates propagate to all projects using the token system
  • Agentic reproducibility: The agentic system can create, modify, and deploy web publishing components by following the interface contracts
  • Mermaid consistency: 60+ theme variables are standardized — no more per-project tuning
  • Architecture diagram readability: Subgraph color conventions create visual consistency across all system diagrams
  • Light/dark mode solved: Forced dark diagram background eliminates the dual-theme problem

Negative

  • Opinionated theming: Projects cannot easily deviate from the dark-mode Mermaid theme without overriding the standard
  • Dependency coupling: All projects must use the same unified/remark/rehype version chain
  • React dependency: Components are React-specific (could be generalized to web components in future)

Risks

  • Mermaid version upgrades: New Mermaid versions may change themeVariable behavior (mitigate: pin versions, test before upgrading)
  • CSS specificity conflicts: !important overrides in diagram CSS may conflict with project-specific styles (mitigate: scope all diagram CSS under .mermaid-diagram)
  • Template drift: Scaffold templates may diverge from the reference implementation over time (mitigate: automated template validation in CI)

Reference Implementation

BIO-QMS Viewer is the canonical reference implementation:

FileComponentLines
components/MarkdownRenderer.jsxFull rendering pipeline + Mermaid pre-render327
styles.cssDiagram CSS + dark-mode tokens + edge thickness~430
viewer.jsxSPA shell with sidebar navigation~400
package.jsonExact pinned dependencies50
vite.config.jsVite configuration with static copy~30
docs/architecture/14-c4-architecture.mdSubgraph styled architecture diagrams~300
docs/architecture/15-mermaid-diagrams.mdFull diagram gallery with all diagram types~400

Key commits:

  • 03fa78d — feat(viewer): Add client-side Mermaid diagram rendering via mermaid.js
  • 8cae94f — fix(viewer): Unified dark-mode Mermaid diagram styling across all architecture docs

Migration Path for Existing Projects

Projects with existing markdown rendering (e.g., coditect-core component-viewer, trajectory-dashboard):

  1. Adopt MarkdownRenderer.jsx from the standard template — drop-in replacement
  2. Add Mermaid CSS to existing stylesheet (.mermaid-diagram rules)
  3. Add Mermaid dependency and initialize with standard themeVariables
  4. Update package.json to use pinned versions
  5. Validate against CODITECT-STANDARD-WEB-PUBLISHING.md quality criteria

Glossary

TermDefinition
Pre-renderConverting Mermaid source to SVG string before React processes the HTML
Theme variablesMermaid's 60+ CSS-like configuration values that control diagram appearance
SubgraphA named group of nodes in a Mermaid flowchart, rendered as a bordered container
Forced darkUsing background: #0f172a on diagram containers regardless of viewer theme
ScaffoldA project template that includes all standard components pre-configured

  • ADR-195: Push-Button Documentation Publishing Platform (platform architecture)
  • ADR-196: NDA-Gated Conditional Access Control (authentication layer)
  • ADR-085: Atomic Design Component Library (design system patterns)
  • ADR-209: Executive Briefing Generator Integration (data-driven dashboard extension)
  • CODITECT-STANDARD-DESIGN-TOKENS.md: Base design token definitions
  • CODITECT-STANDARD-UI-UX.md: UI/UX design principles
  • CODITECT-STANDARD-WEB-PUBLISHING.md: Implementation standard (companion to this ADR)

Author: Hal Casteel Project: PILOT (CODITECT Framework) Track: H.10 (Unified Web Publishing Component System)