Plan Site via HTML Adapter
Refactor plan serve and plan build to render through @refrakt-md/html instead of a bespoke HTML pipeline, and introduce a planLayout in the transform layer.
Refactor plan serve and plan build to render through @refrakt-md/html instead of a bespoke HTML pipeline, and introduce a planLayout in the transform layer.
The plan site (plan serve / plan build) currently maintains its own HTML rendering pipeline:
<style> and hardcoded sidebar markuprenderToHtml() calls bypassing the layout transform systemgetLuminaBaseCss()escapeHtml, nav rendering, and document structureMeanwhile, @refrakt-md/html already provides a framework-agnostic rendering path that solves all of these:
renderFullPage() — complete <!DOCTYPE html> documents with SEO meta, script injection, stylesheet linkslayoutTransform() — the same layout system used by docs, blog, and default layoutsHtmlTheme — a clean abstraction over layouts + manifest, decoupled from any specific themeinitPage() — progressive enhancement for interactive runes via @refrakt-md/behaviorsThe plan site is essentially a lightweight refrakt site with plan-specific content loading. It should use the same rendering infrastructure.
@refrakt-md/plan — use @refrakt-md/html insteadplanLayout in @refrakt-md/transform alongside docsLayout and blogArticleLayoutdefault.css, minimal.css) remain as fallbacks@refrakt-md/highlight to code fences so plan content gets proper code coloring@refrakt-md/behaviors via initPage() to provide copy-to-clipboard on code blocksplan serve / plan build ├── Scanner (plan-specific content loading — unchanged) ├── Pipeline hooks: register/aggregate/postProcess (unchanged) ├── Auto-generated dashboard content (unchanged) ├── @refrakt-md/highlight │ └── createHighlightTransform() → walk tree, apply Shiki, emit CSS ├── @refrakt-md/behaviors (client-side) │ └── initPage() → copy-to-clipboard on all <pre> elements └── @refrakt-md/html ├── renderFullPage(input, options) │ ├── layoutTransform(planLayout, pageData) │ └── renderToHtml(tree) └── HtmlTheme { manifest, layouts: { plan: planLayout } }
planLayout)A new LayoutConfig in packages/transform/src/layouts.ts:
plansidebar — fixed sidebar navigation (source: region:nav)main — primary content area (source: content)The sidebar structure (group titles, entity links, active state) is built by @refrakt-md/plan and passed as the nav region in LayoutPageData.regions. The layout just places it.
Current --theme flag semantics are preserved but the implementation changes:
--theme value | Behaviour |
|---|---|
default | Built-in plan styles from runes/plan/styles/default.css — includes tokens, entity cards, sidebar, dashboard grid |
minimal | Built-in plan styles from runes/plan/styles/minimal.css — print-friendly, no sidebar |
| Path to CSS file | User-provided CSS, passed as a stylesheet to renderFullPage() |
The key change: instead of inlining CSS into a <style> tag, stylesheets are passed to renderFullPage() via the stylesheets option. For built-in themes, the CSS is written to the output directory (for build) or served from a route (for serve).
The @refrakt-md/lumina dependency is removed from @refrakt-md/plan. The plan's built-in themes include their own base reset/typography tokens (they already do via tokens.css).
The sidebar navigation is currently built by buildNavigation() in render-pipeline.ts and rendered directly into the HTML shell. Under the new design:
buildNavigation() still builds the NavGroup[] structurebuildNavRegion() function converts it into a renderable tag tree (serialized tags with BEM classes matching the existing .rf-plan-sidebar__* selectors)regions.nav in the LayoutPageData given to renderFullPage()planLayout places the nav region in the sidebar slotThis keeps the plan-specific nav logic in @refrakt-md/plan while the structural placement is handled by the layout system.
@refrakt-md/plan assembles a minimal HtmlTheme for the plan site:
const planTheme: HtmlTheme = { manifest: { name: 'plan', routeRules: [{ match: '**', layout: 'plan' }], }, layouts: { plan: planLayout, }, };
All routes use the plan layout. The theme is constructed internally — users don't need to configure it.
For each entity page and the dashboard, @refrakt-md/plan builds a LayoutPageData:
{ renderable: serializedTree, // Markdoc → serialize → identity transform regions: { nav: navTree }, // sidebar navigation as renderable tree title: page.title, url: page.url, pages: allPageUrls, frontmatter: {}, headings: [], }
The current plan site renders code fences as plain <pre><code> with no coloring. The refactored pipeline adds @refrakt-md/highlight:
createHighlightTransform() returns an async tree-walker that finds all elements with data-language attributes and applies Shiki syntax highlightingrenderToHtml().css property containing the generated highlight theme CSS — this is concatenated with the plan theme CSS and served as a single stylesheetconst highlightTransform = await createHighlightTransform(); const highlighted = await highlightTransform(transformed); // highlightTransform.css → append to theme stylesheet
The highlight transform uses CSS variables by default, so it inherits light/dark mode from the plan theme automatically. No additional configuration needed.
This adds @refrakt-md/highlight as a dependency of @refrakt-md/plan. The highlight package depends on Shiki (already a project dependency via the editor).
The copy-to-clipboard button (and any future behaviors) are provided by @refrakt-md/behaviors, loaded client-side via the initPage() entry point from @refrakt-md/html/client.
What this enables:
<pre> code blocks — automatic, no per-block opt-inHow it's wired in:
serve: The behaviors JS bundle is served at /__plan-behaviors.js and included via the scripts option on renderFullPage()build: The bundle is written to {out}/behaviors.js and referenced by all pagesThe behaviors bundle is small (~4KB gzipped) and has no framework dependencies. The copy behavior wraps each <pre> in a .rf-code-wrapper div and injects a button — the plan theme CSS needs a few selectors to style these (.rf-code-wrapper, .rf-copy-button, .rf-copy-button--copied).
renderFullPage() output (as before, but using the HTML adapter)/__plan-theme.css and referenced via stylesheets: ['/__plan-theme.css']headExtra option on renderFullPage()pageIndex map is rebuilt on file changes (unchanged){out}/theme.css../theme.css or {baseUrl}theme.cssrenderFullPage() handles the document shell (unchanged interface for callers)Before:
@refrakt-md/plan → @refrakt-md/runes, @refrakt-md/transform, @refrakt-md/types, @refrakt-md/lumina
After:
@refrakt-md/plan → @refrakt-md/runes, @refrakt-md/transform, @refrakt-md/types, @refrakt-md/html, @refrakt-md/highlight
@refrakt-md/lumina is no longer a dependency. @refrakt-md/html is added (it only depends on transform + types, so no new transitive dependencies). @refrakt-md/highlight is added for syntax highlighting (depends on Shiki). @refrakt-md/behaviors is a runtime dependency loaded client-side — not a build-time package dependency.
In scope:
planLayout in packages/transform/src/layouts.tsrender-pipeline.ts to use renderFullPage() from @refrakt-md/html@refrakt-md/highlight in the render pipeline@refrakt-md/behaviors.rf-code-wrapper, .rf-copy-button)getLuminaBaseCss() and the bespoke HTML shell@refrakt-md/plan dependenciespackages/lumina/styles/layouts/plan.css) — optional, for users who want plan content in a Lumina-themed siteOut of scope:
The refactoring is internal to @refrakt-md/plan. The CLI interface (plan serve, plan build) and all flags remain identical. Users see no change in behaviour — only the internal rendering path changes.