Markdown Plugin
Design Log #155 — Markdown Plugin
Written for AI agents. See Log Methodology Note below for details.
Background
Jay Stack needs a markdown rendering plugin for content-driven pages — blogs, documentation, changelogs, knowledge bases. The plugin should integrate with the three-phase rendering model, support code highlighting and mermaid diagrams, and work with both static directories of markdown files and dynamic markdown values.
Related
- DL#39 — Plugin package requirements
- DL#60 — Plugin system refinement, dynamic contracts
- DL#84 — Headless component props
- DL#85 — Rendering phases and agent kit
- DL#130 — Plugin routes and templates
- DL#152 — Phase-aware contract props
Problem
No built-in way to render markdown content in Jay Stack pages. Projects that need blog posts, documentation, or markdown-driven content must implement their own parsing, rendering, and routing. This is a common enough need to warrant a plugin.
Questions & Answers
Q1: How should the directory-to-pages component know which markdown directory to scan?
A1: Via props on the headless component script tag. This requires a framework change — currently <script type="application/jay-headless"> supports plugin, contract, and key attributes but no props. See "Framework Gap" section below.
Q2: Should code highlighting use a JS library (Shiki, Prism) or CSS-only?
A2: CSS-only with pre-tagged HTML. The marked renderer extension tokenizes code into <span class="token keyword"> etc. A shipped CSS file provides colors. No client JS needed, theme-able via CSS custom properties.
Q3: Should mermaid render at build time or client-side?
A3: Build-time SVG in the slow phase. No client JS, no layout shift, diagrams are part of the static HTML. Mermaid is a build-time dependency only.
Q4: What markdown parser?
A4: marked — fast, lightweight, extensible via tokenizer/renderer extensions.
Q5: Should the single-value renderer support interactive updates?
A5: Yes — two components. markdown-content renders at slow phase (static). markdown-live renders at fast+interactive (dynamic, re-parses on client when value changes).
Design
Components
1. markdown-pages — Directory to pages
A headless component that scans a directory of .md files and provides page data. The page's page.jay-html references it, and it contributes a slug param via loadParams.
Usage in page.jay-html:
<html>
<head>
<script
type="application/jay-headless"
plugin="@jay-framework/markdown"
contract="markdown-pages"
key="post"
props='{ "contentDir": "./content" }'
></script>
</head>
<body>
<article>
<h1>{post.title}</h1>
<time>{post.date}</time>
<div>{post.content}</div>
</article>
</body>
</html>
Directory structure:
src/pages/blog/[slug]/
page.jay-html ← references markdown-pages
page.jay-contract ← optional, for page-level data
content/
getting-started.md
advanced-topics.md
changelog.md
Markdown file format:
---
title: Getting Started
date: 2026-07-15
description: Learn how to set up the project
tags: [tutorial, beginner]
---
# Getting Started
Your content here...
```typescript
const hello = 'world';
```
<div class="md-mermaid"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 263.77 116.9" width="263.77" height="116.9">
<style>
svg {
/* Derived from --bg and --fg (overridable via --line, --accent, etc.) */
--_text: var(--fg);
--_text-sec: var(--muted, color-mix(in srgb, var(--fg) 60%, var(--bg)));
--_text-muted: var(--muted, color-mix(in srgb, var(--fg) 40%, var(--bg)));
--_text-faint: color-mix(in srgb, var(--fg) 25%, var(--bg));
--_line: var(--line, color-mix(in srgb, var(--fg) 50%, var(--bg)));
--_arrow: var(--accent, color-mix(in srgb, var(--fg) 85%, var(--bg)));
--_node-fill: var(--surface, color-mix(in srgb, var(--fg) 3%, var(--bg)));
--_node-stroke: var(--border, color-mix(in srgb, var(--fg) 20%, var(--bg)));
--_group-fill: var(--bg);
--_group-hdr: color-mix(in srgb, var(--fg) 5%, var(--bg));
--_inner-stroke: color-mix(in srgb, var(--fg) 12%, var(--bg));
--_key-badge: color-mix(in srgb, var(--fg) 10%, var(--bg));
}
</style>
<defs>
<marker id="arrowhead" markerWidth="8" markerHeight="5" refX="7" refY="2.5" orient="auto">
<polygon points="0 0, 8 2.5, 0 5" fill="var(--_arrow)" stroke="var(--_arrow)" stroke-width="0.75" stroke-linejoin="round" />
</marker>
<marker id="arrowhead-start" markerWidth="8" markerHeight="5" refX="1" refY="2.5" orient="auto-start-reverse">
<polygon points="8 0, 0 2.5, 8 5" fill="var(--_arrow)" stroke="var(--_arrow)" stroke-width="0.75" stroke-linejoin="round" />
</marker>
</defs>
<polyline class="edge" data-from="A" data-to="B" data-style="solid" data-arrow-start="false" data-arrow-end="true" points="110.108,58.45 158.108,58.45" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
<g class="node" data-id="A" data-label="Start" data-shape="rectangle">
<rect x="40" y="40" width="70.108" height="36.900000000000006" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
<text x="75.054" y="58.45" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)" dy="4.55">Start</text>
</g>
<g class="node" data-id="B" data-label="End" data-shape="rectangle">
<rect x="158.108" y="40" width="65.662" height="36.900000000000006" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
<text x="190.93900000000002" y="58.45" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)" dy="4.55">End</text>
</g>
</svg></div>
Component behavior:
loadParamsscanscontentDirrelative to the page, yields{ slug }for each.mdfile (filename without extension)- Slow render: reads the markdown file, extracts frontmatter, parses markdown to HTML (with code highlighting and mermaid SVG), returns ViewState
- No fast or interactive phases needed — content is static
2. markdown-content — Static single-value renderer
Takes a markdown string via props, renders to HTML at build time.
Usage:
<jay:markdown-content>
<div>{html}</div>
</jay:markdown-content>
The component receives markdown via props (from the parent page's data), renders at slow phase, outputs html-string.
3. markdown-live — Dynamic single-value renderer
Takes a markdown string that can change at request time or on the client.
Usage:
<jay:markdown-live>
<div>{html}</div>
</jay:markdown-live>
- Fast phase: server-side markdown parse with
marked - Interactive phase: client-side re-parse when the markdown prop changes
- Ships
markedto the client bundle (lightweight ~35KB)
Framework Gap: Props on Keyed Headless Components
Current state: parseHeadlessImports in compiler-jay-html/lib/jay-target/jay-html-parser.ts (line 622) reads plugin, contract, and key attributes from <script type="application/jay-headless">. No props attribute is parsed.
What's needed: The markdown-pages component needs contentDir as a prop. Instance-based headless components (<jay:markdown-content>) already support props via the contract's props section. But page-level keyed headless components don't.
Proposed fix: Add props attribute parsing to parseHeadlessImports. The attribute value is a JSON string. Parsed props are passed to the component's withSlowlyRender / withFastRender via the existing props parameter.
<script
type="application/jay-headless"
plugin="@jay-framework/markdown"
contract="markdown-pages"
key="post"
props='{ "contentDir": "./content" }'
></script>
This is a small, backward-compatible change — existing headless imports without props continue to work. The contract's props section declares the expected shape, and the validate command checks consistency (DL#124, DL#152).
Contracts
markdown-pages.jay-contract:
name: markdown-pages
description: Renders a directory of markdown files as pages
props:
- name: contentDir
kind: required
description: Path to markdown directory (relative to page)
params:
- name: slug
kind: required
tags:
- tag: title
type: data
dataType: string
phase: slow
description: Title from frontmatter
- tag: content
type: data
dataType: html-string
phase: slow
description: Rendered HTML from markdown body
- tag: description
type: data
dataType: string
phase: slow
description: Description from frontmatter
- tag: date
type: data
dataType: string
phase: slow
description: Date from frontmatter (ISO string)
- tag: tags
type: sub-contract
repeated: true
phase: slow
description: Tags from frontmatter
tags:
- tag: name
type: data
dataType: string
- tag: frontmatter
type: data
dataType: string
phase: slow
description: Full frontmatter as JSON string (for custom fields)
markdown-content.jay-contract:
name: markdown-content
description: Renders a markdown string to HTML at build time
props:
- name: markdown
kind: required
description: Markdown string to render
tags:
- tag: html
type: data
dataType: html-string
phase: slow
markdown-live.jay-contract:
name: markdown-live
description: Renders markdown with fast+interactive updates
props:
- name: markdown
kind: required
description: Markdown string to render
tags:
- tag: html
type: data
dataType: html-string
phase: fast+interactive
Markdown Processing
Parser setup
import { marked } from 'marked';
import yaml from 'js-yaml';
interface ParsedMarkdown {
frontmatter: Record<string, any>;
html: string;
}
function extractFrontmatter(content: string): { frontmatter: Record<string, any>; body: string } {
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
if (!match) return { frontmatter: {}, body: content };
return {
frontmatter: yaml.load(match[1]) as Record<string, any>,
body: match[2],
};
}
function parseMarkdown(content: string, options?: { renderMermaid?: boolean }): ParsedMarkdown {
const { frontmatter, body } = extractFrontmatter(content);
const html = marked.parse(body); // with extensions registered
return { frontmatter, html };
}
Code highlighting extension
Tokenize code blocks into CSS-class-tagged spans. Support common languages: JavaScript/TypeScript, HTML, CSS, YAML, JSON, Bash, Python.
const codeRenderer: marked.RendererExtension = {
name: 'code',
renderer(token) {
const lang = token.lang || '';
const highlighted = highlightCode(token.text, lang);
return `<pre class="md-code"><code class="language-${lang}">${highlighted}</code></pre>`;
},
};
The highlightCode function uses regex-based tokenization per language:
- Keywords →
<span class="token keyword"> - Strings →
<span class="token string"> - Comments →
<span class="token comment"> - Numbers →
<span class="token number"> - Punctuation →
<span class="token punctuation">
Markdown Theme CSS
Ship complete markdown theme CSS files — not just code highlighting, but the full rendered output: headings, paragraphs, blockquotes, lists, tables, code blocks, mermaid containers. This gives agents a working starting point to choose from or customize.
Shipped themes:
lib/themes/
markdown-default.css # Clean, neutral — works on light backgrounds
markdown-docs.css # Documentation-style (wider code blocks, tighter spacing)
markdown-blog.css # Blog-style (larger body text, generous spacing)
Each theme uses CSS custom properties for easy overrides:
.md {
--md-font-body: inherit;
--md-font-code: 'Fira Code', monospace;
--md-color-heading: inherit;
--md-color-link: #2563eb;
--md-color-code-bg: #f1f5f9;
--md-color-blockquote-border: #e2e8f0;
--md-spacing-block: 1.5rem;
}
.md h1 {
font-size: 2rem;
font-weight: 700;
color: var(--md-color-heading);
}
.md h2 {
font-size: 1.5rem;
font-weight: 600;
color: var(--md-color-heading);
}
.md p {
line-height: 1.7;
margin-bottom: var(--md-spacing-block);
}
.md blockquote {
border-left: 3px solid var(--md-color-blockquote-border);
padding-left: 1rem;
}
.md pre.md-code {
background: var(--md-color-code-bg);
border-radius: 0.5rem;
padding: 1rem;
overflow-x: auto;
}
.md .md-mermaid {
text-align: center;
margin: var(--md-spacing-block) 0;
}
/* ... */
Code highlighting tokens in each theme:
.md .token.keyword {
color: #8b5cf6;
}
.md .token.string {
color: #059669;
}
.md .token.comment {
color: #94a3b8;
font-style: italic;
}
.md .token.number {
color: #d97706;
}
.md .token.punctuation {
color: #64748b;
}
Consuming themes: The jay-html compiler resolves <link> CSS paths relative to the page directory — it does not resolve npm package paths. Instead, the page's linked CSS file uses @import (which Vite resolves from node_modules):
/* src/styles/markdown-theme.css */
@import '@jay-framework/markdown/themes/markdown-blog.css';
/* Override custom properties to match project DESIGN.md */
.md {
--md-color-link: var(--accent);
--md-color-heading: var(--text-primary);
}
<!-- page.jay-html -->
<link rel="stylesheet" href="../styles/markdown-theme.css" />
This uses Vite's built-in @import resolution for npm packages. No framework changes needed.
Mermaid extension
Detect ```mermaid fences and render to SVG at build time.
import mermaid from 'mermaid';
const mermaidRenderer: marked.RendererExtension = {
name: 'code',
renderer(token) {
if (token.lang !== 'mermaid') return false; // fall through to default
const svg = renderMermaidSync(token.text); // build-time only
return `<div class="md-mermaid">${svg}</div>`;
},
};
Mermaid initialization runs once at build time. The mermaid package renders to SVG string using its renderToSVG API (or the CLI wrapper). This is a server-only dependency — not shipped to the client.
Frontmatter and SEO
The markdown-pages component automatically maps frontmatter fields to <head> tags via the existing headTags mechanism (DL#127). The component returns headTags in its phaseOutput:
Recognized frontmatter fields:
| Frontmatter field | Maps to | Example |
|---|---|---|
title |
<title> + <meta property="og:title"> |
<title>Getting Started</title> |
description |
<meta name="description"> + <meta property="og:description"> |
SEO description |
canonical |
<link rel="canonical"> |
Canonical URL |
image |
<meta property="og:image"> |
Open Graph image |
author |
<meta name="author"> |
Author name |
date |
<meta property="article:published_time"> |
ISO 8601 date |
Unrecognized fields become <meta name="fieldName" content="value"> automatically. This lets markdown authors add arbitrary metadata without framework changes:
---
title: My Post
category: tutorials
reading-time: 5 min
---
Produces:
<title>My Post</title>
<meta property="og:title" content="My Post" />
<meta name="category" content="tutorials" />
<meta name="reading-time" content="5 min" />
Array values like tags: [tutorial, beginner] are skipped for <meta> — they're available via the frontmatter JSON string in ViewState for template rendering.
Implementation in the component:
const KNOWN_FIELDS = new Set([
'title',
'description',
'canonical',
'image',
'author',
'date',
'tags',
]);
function frontmatterToHeadTags(fm: Record<string, any>): HeadTag[] {
const tags: HeadTag[] = [];
if (fm.title) {
tags.push({ tag: 'title', children: fm.title });
tags.push({ tag: 'meta', attrs: { property: 'og:title', content: fm.title } });
}
if (fm.description) {
tags.push({ tag: 'meta', attrs: { name: 'description', content: fm.description } });
tags.push({ tag: 'meta', attrs: { property: 'og:description', content: fm.description } });
}
if (fm.canonical) {
tags.push({ tag: 'link', attrs: { rel: 'canonical', href: fm.canonical } });
}
if (fm.image) {
tags.push({ tag: 'meta', attrs: { property: 'og:image', content: fm.image } });
}
if (fm.author) {
tags.push({ tag: 'meta', attrs: { name: 'author', content: fm.author } });
}
if (fm.date) {
tags.push({
tag: 'meta',
attrs: { property: 'article:published_time', content: new Date(fm.date).toISOString() },
});
}
for (const [key, value] of Object.entries(fm)) {
if (KNOWN_FIELDS.has(key)) continue;
if (typeof value === 'string' || typeof value === 'number') {
tags.push({ tag: 'meta', attrs: { name: key, content: String(value) } });
}
}
return tags;
}
Merge behavior: The page's page.jay-html can define static <title> and <meta> in <head>. Component-injected head tags merge with (and override) the static ones — component tags win for same-name meta tags.
Plugin Structure
packages/plugins/markdown/
plugin.yaml
package.json
vite.config.ts
tsconfig.json
lib/
index.ts # Server exports
index.client.ts # Client exports (markdown-live)
parse-markdown.ts # marked setup, frontmatter, extensions
code-highlighter.ts # CSS-class tokenizer per language
mermaid-renderer.ts # Mermaid → SVG at build time
head-tags.ts # Frontmatter → headTags SEO mapping
themes/
markdown-default.css # Clean, neutral
markdown-docs.css # Documentation-style
markdown-blog.css # Blog-style
components/
markdown-pages.ts # Directory → pages
markdown-content.ts # Static renderer
markdown-live.ts # Dynamic renderer
agent-kit/
designer/
markdown-usage.md # Guide for using markdown components
test/
parse-markdown.test.ts
code-highlighter.test.ts
head-tags.test.ts
fixtures/
sample-post.md
code-post.md
mermaid-post.md
dist/
plugin.yaml
name: markdown
description: Markdown rendering — pages from directories, inline content, code highlighting, mermaid diagrams
contracts:
- name: markdown-pages
contract: markdown-pages.jay-contract
component: markdownPages
description: Renders a directory of markdown files as routable pages
- name: markdown-content
contract: markdown-content.jay-contract
component: markdownContent
description: Renders a markdown string to HTML at build time (slow phase)
- name: markdown-live
contract: markdown-live.jay-contract
component: markdownLive
description: Renders markdown with fast+interactive updates
Implementation Plan
Phase 1: Framework — Props on keyed headless components
- Update
parseHeadlessImportsto readpropsattribute (JSON string) - Pass parsed props through to component
withSlowlyRender/withFastRender - Update
checkComponentPropsAndParamsto validate keyed headless props - Tests
Phase 2: Core markdown parsing
parse-markdown.ts— marked setup, frontmatter extractioncode-highlighter.ts— CSS-class tokenizer for JS/TS/HTML/CSS/YAML/JSON/Bash/Pythonmarkdown-code.css— default theme with CSS custom properties- Tests with fixture markdown files
Phase 3: Mermaid rendering
mermaid-renderer.ts— build-time SVG rendering- Marked extension for mermaid fences
- Tests with mermaid fixtures
Phase 4: Components
markdown-pages— directory scanning, loadParams, slow rendermarkdown-content— static single-value renderermarkdown-live— dynamic renderer with client-side re-parse- Contracts, plugin.yaml, package.json, build config
Phase 5: Agent kit and docs
agent-kit/designer/markdown-usage.md- Integration test with example project
Trade-offs
| Decision | Pro | Con |
|---|---|---|
marked over remark |
Fast, lightweight, simple API | Less extensible AST, fewer plugins |
| CSS-only code highlighting | No client JS, theme-able, fast | Less accurate than Shiki/Prism, limited language support |
| Build-time mermaid SVG | No client JS, no layout shift | Requires mermaid as build dep (~50MB), can't update diagrams interactively |
| Props on headless script tag | Per-page config, familiar pattern | Framework change needed |
Verification Criteria
markdown-pagesscans a directory and generates correct slugs vialoadParams- Frontmatter extracted correctly (title, date, description, tags, custom fields)
- Code blocks highlighted with CSS classes for all supported languages
- Mermaid fences render to inline SVG
markdown-contentrenders markdown at slow phasemarkdown-liverenders at fast phase and re-renders on client when value changes- Plugin validates with
jay-stack validate-plugin - Example page in a test project renders correctly
Implementation Results
What was built
Plugin at packages/plugins/markdown/ with 29 tests, dual build (server + client), validates clean.
Core library:
parse-markdown.ts—Markedparser with configurable mermaid renderer viacreateMarkedParser(mermaidRenderer?). Without mermaid renderer, fences output<pre class="md-mermaid-source">fallback.code-highlighter.ts— regex-based CSS-class tokenizer for 8 languageshead-tags.ts— frontmatter → HeadTag mapping with unknown-field pass-throughmermaid-renderer.ts— shells out tommdc(via@mermaid-js/mermaid-cli+ Puppeteer) for build-time SVG
Three components:
markdownPages— keyed headless, reads.mdfiles by slug fromcontentDir(via DL#156 headless props), usesloadParamsto enumerate slugs, renders with mermaid SVGmarkdownContent— instance-based, static slow-phase renderer with mermaid SVGmarkdownLive— instance-based, fast+interactive renderer WITHOUT mermaid (client-side only)
Three CSS themes: default, docs, blog
Agent kit: markdown-usage.md
Smoke tests: Two test pages in examples/jay-stack/smoke-test/:
/markdown/[slug]/—markdown-pagescomponent rendering.mdfiles with mermaid diagrams/markdown-live/—markdown-livecomponent rendering markdown at request time
Framework changes (DL#156)
LoadParamstype accepts optionalpropsparameterrunLoadParamspassespart.headlessPropssoloadParamscan access component configuration (e.g.,contentDir)- Both dev server and production build pipeline pass headless props through
Key architectural decision: Server vs client mermaid rendering
Mermaid requires a DOM to render SVGs. We evaluated four approaches:
@mermaid-js/mermaid-cli(chosen for server) — shells out tommdcwhich uses Puppeteer/Chromium. Produces real SVGs at build time. Heavy dev dependency (~150MB with Chromium) but correct output.- Mermaid + JSDOM — lighter but rendering quirks with mermaid's DOM usage.
- Client-side rendering — no build dependency but ~500KB client bundle and layout shift.
- Placeholder only — no rendering, just styled source blocks.
Result: split architecture. Server components (markdown-pages, markdown-content) use mmdc for real SVG output. Client component (markdown-live) outputs <pre class="md-mermaid-source"> placeholder — client-side mermaid.js can be added by the project if needed.
The createMarkedParser(mermaidRenderer?) factory enables this split: server code passes renderMermaidBlock, client code passes nothing. No conditional imports, no Node.js APIs in the client bundle.
Production build fix: headless props vs route params
Problem: The production build skipped markdown/[slug] entirely — loadParams yielded [{slug: "hello"}] correctly, but the route materialization step filtered it out.
Root cause: The route scanner's parseHeadlessProps (DL#156) merged ALL headless YAML body values into route.inferredParams. For the markdown route, this included {contentDir: "src/pages/markdown/content"}. The materializeRouteParams function then compared loadParams output ({slug: "hello"}) against inferredParams ({contentDir: "..."}) via paramsMatchInferred — which failed because contentDir isn't a route param, causing all param combinations to be skipped.
Fix in route-scanner.ts: Filter headless props based on route type:
- Dynamic routes (with
[slug]etc.): only include props whose keys match dynamic segment names. Component props likecontentDirare excluded — they're configuration, not route params. - Static routes (no dynamic segments): include all props as
inferredParams. This preserves the static override pattern whereslug: ceramic-flower-vasetells the build which product a static page represents.
Debugging note: Required rebuilding route-scanner and production-server dist — the stack-cli imports from pre-built dist, not source. Source changes without rebuild are invisible to the build pipeline.
Mermaid: replaced mermaid-cli with beautiful-mermaid
The initial implementation used @mermaid-js/mermaid-cli which shells out to mmdc via Puppeteer/Chromium (~150MB). This imposed an unacceptable dependency on every consumer of the markdown plugin.
Replaced with beautiful-mermaid (~2MB) — a pure JS mermaid renderer with zero DOM dependencies. Uses elkjs for layout.
- Regular dependency (not devDep) — consumers get it automatically, no browser installation needed
- Async API:
renderMermaidSVGAsync(code)→ SVG string - Mermaid fences are pre-processed before
markedparsing (since marked's code renderer is sync) - Sync path (
parseMarkdown) still produces fallback<pre class="md-mermaid-source">for client-side rendering - Async path (
parseMarkdownWithMermaid) renders actual SVG viabeautiful-mermaid— used by server components
CSS @import resolution in compiler
Linked CSS files with @import statements (e.g., markdown-blog.css importing markdown-default.css) failed because the compiler inlined the CSS content without resolving nested imports.
Fix in jay-html-parser.ts: Added resolveNestedCssImports() — after reading a linked CSS file, scans for @import statements, resolves them relative to the CSS file's directory, and inlines them recursively. Handles circular imports and missing files.
Additional fix: The initial implementation used require('node:fs') inside the function, which fails in ESM context. Replaced with a module-level import fsSync from 'fs'.
Deviations from design
- Mermaid rendering is split between server (
beautiful-mermaidSVG) and client (source fallback). The design assumed a single build-time approach, but the client component can't use the server renderer. loadParamsrequired a framework change —LoadParamstype updated to accept optional props parameter. Not anticipated in the original DL#155 design (was expected to be covered by DL#156 alone, butloadParamsis a separate code path from render).- Mermaid dependency changed from
@mermaid-js/mermaid-cli(Puppeteer, ~150MB) tobeautiful-mermaid(pure JS, ~2MB). The design assumed build-time-only rendering via headless browser; the pure JS approach is simpler and lighter. markdown-pagescontracttagssub-contract neededtrackBy: name— the original contract design omitted this, caught by the validate command.- Route scanner needed to distinguish component props from route params — headless YAML body values serve two purposes (component configuration and route param declaration). The scanner now filters by route type to avoid production build failures.
- Compiler CSS
@importresolution was not in the original design but was needed for theme CSS files that use@importfor composition.
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.