HTML Adapter
The HTML adapter (@refrakt-md/html) renders refrakt.md content to plain HTML strings. No framework runtime, no build tooling beyond TypeScript — just static HTML files.
Installation
npm install @refrakt-md/html @refrakt-md/content @refrakt-md/runes @refrakt-md/transform @refrakt-md/highlight @refrakt-md/types @markdoc/markdoc
Or scaffold a complete project:
npx create-refrakt my-site --target html
Configuration
Set target to "html" in refrakt.config.json:
{
"contentDir": "./content",
"theme": "@refrakt-md/lumina",
"target": "html",
"routeRules": [
{ "pattern": "**", "layout": "default" }
]
}
HtmlTheme Interface
The HTML adapter uses a simpler theme interface than SvelteKit — no component registry or element overrides:
interface HtmlTheme {
manifest: ThemeManifest;
layouts: Record<string, LayoutConfig>;
}
All runes render through the identity transform + CSS. Interactive runes get their behavior from @refrakt-md/behaviors via client-side initialization.
Core API
renderPage(input)
Renders a page's content to an HTML fragment. Applies the layout transform and HTML-specific tree transforms (table wrapping), then produces an HTML string. Does not include <!DOCTYPE> or <head>.
import { renderPage } from '@refrakt-md/html';
const html = renderPage({ theme, page });
renderFullPage(input, options?)
Renders a complete HTML document with <!DOCTYPE>, <head> (title, meta, OG tags, JSON-LD, stylesheets), <body> (content + context data), and script tags.
import { renderFullPage } from '@refrakt-md/html';
const html = renderFullPage(
{ theme, page },
{
stylesheets: ['/styles.css'],
scripts: ['/behaviors.js'],
headExtra: '<link rel="icon" href="/favicon.ico">',
lang: 'en',
seo: page.seo,
}
);
PageShellOptions
| Option | Type | Description |
|---|---|---|
stylesheets | string[] | CSS stylesheet URLs for <link> tags in <head> |
scripts | string[] | JavaScript URLs for <script> tags before </body> |
headExtra | string | Extra HTML to inject into <head> (use to inline highlight + site-tokens CSS) |
lang | string | HTML lang attribute (default: "en") |
baseUrl | string | Base URL for canonical URLs and absolute OG URLs |
siteName | string | Human-readable site name for og:site_name and JSON-LD entries |
defaultImage | string | Default og:image for pages without their own image |
logo | string | Site logo for Organization JSON-LD schema |
seo | PageSeo | SEO metadata (JSON-LD schemas and Open Graph tags) |
When siteName, baseUrl, defaultImage, or logo are supplied, renderFullPage emits og:site_name, absolutizes og:url and adds a canonical <link>, falls back missing images to defaultImage, and appends WebSite + Organization JSON-LD entries — matching the SvelteKit reference adapter's output. Source these from your refrakt.config.json via resolveSite() and pass them per page.
Security and Markdoc variables
The HTML adapter's build script loads content directly via loadContent, so security (untrusted-content sanitisation) and variables (Markdoc {% $name %} interpolation) are passed as positional arguments at the call site. The scaffolded build.ts shows the order; edit it to thread your own values:
const loadedSite = await loadContent(
contentDir,
'/',
icons,
communityTags,
undefined, // sandboxExamplesDir
{ version: '1.0.0' }, // variables
'strict', // securityPolicy
);
For most static sites neither option is needed; they exist for hosted-product or per-build interpolation scenarios.
composeSiteTokensCss(site, configDir)
Composes the site-level token overrides CSS (theme.tokens, theme.modes, theme.presets, site.tints) into a single string. Re-exported from @refrakt-md/transform/node for convenience.
import { composeSiteTokensCss, renderFullPage } from '@refrakt-md/html';
import { loadRefraktConfig, resolveSite } from '@refrakt-md/transform/node';
import { dirname, resolve } from 'node:path';
const configPath = resolve('refrakt.config.json');
const config = loadRefraktConfig(configPath);
const { site } = resolveSite(config);
const siteTokensCss = await composeSiteTokensCss(site, dirname(configPath));
const html = renderFullPage({ theme, page }, {
stylesheets: ['/styles.css'],
// Inline site-tokens CSS so site-level --rf-* overrides resolve last.
headExtra: siteTokensCss ? `<style>${siteTokensCss}</style>` : '',
});
Empty string when the site uses the legacy string-theme form or declares no overrides — safe to interpolate either way.
See the design tokens contract and the scoped tint projection pages for the full token surface.
Client-Side Behaviors
Interactive runes (tabs, accordion, datatable, etc.) need client-side JavaScript. The HTML adapter provides an initPage() function that dynamically imports @refrakt-md/behaviors:
import { initPage } from '@refrakt-md/html/client';
// Initialize behaviors — reads page context from embedded JSON
const cleanup = initPage(document);
// Call cleanup when navigating away (SPA) or tearing down
cleanup();
initPage reads page context from a <script id="rf-context"> JSON block that renderFullPage embeds in the document. It registers web component elements, initializes rune behaviors, and initializes layout behaviors.
@refrakt-md/behaviors is an optional peer dependency. If not installed, the page renders correctly but interactive runes (tabs, accordion, etc.) won't have JavaScript enhancement.
Usage Example
A complete build script that loads content, applies transforms, and writes static HTML:
import { createRefraktLoader } from '@refrakt-md/content';
import { renderFullPage } from '@refrakt-md/html';
import type { HtmlTheme } from '@refrakt-md/html';
import { defaultLayout } from '@refrakt-md/transform';
import { mkdirSync, writeFileSync } from 'node:fs';
import * as path from 'node:path';
import manifest from '@refrakt-md/lumina/manifest';
import { layouts } from '@refrakt-md/lumina/layouts';
const theme: HtmlTheme = { manifest, layouts: { default: defaultLayout } };
const loader = createRefraktLoader();
async function build() {
const [site, transform, hl] = await Promise.all([
loader.getSite(),
loader.getTransform(),
loader.getHighlightTransform(),
]);
const pages = site.pages
.filter(p => !p.route.draft)
.map(p => ({ url: p.route.url, title: p.frontmatter.title ?? '', draft: false }));
for (const page of site.pages) {
if (page.route.draft) continue;
// Serialize → identity transform → highlight
const renderable = hl(transform(serialize(page.renderable)));
const regions = Object.fromEntries(
[...page.layout.regions.entries()].map(([name, r]) => [
name,
{ name: r.name, mode: r.mode, content: r.content.map(c => hl(transform(serialize(c)))) },
])
);
const html = renderFullPage(
{ theme, page: { renderable, regions, title: page.frontmatter.title ?? '', url: page.route.url, pages } },
{ stylesheets: ['/styles.css'], seo: page.seo }
);
const filePath = page.route.url === '/'
? 'build/index.html'
: path.join('build', page.route.url.slice(1), 'index.html');
mkdirSync(path.dirname(filePath), { recursive: true });
writeFileSync(filePath, html);
}
}
function serialize(node: any): any {
if (node == null || typeof node !== 'object') return node;
if (Array.isArray(node)) return node.map(serialize);
if (node.$$mdtype === 'Tag') {
return { $$mdtype: 'Tag', name: node.name, attributes: node.attributes, children: (node.children ?? []).map(serialize) };
}
return node;
}
build();
Dependencies
The HTML adapter has minimal dependencies:
| Package | Required | Purpose |
|---|---|---|
@refrakt-md/transform | Yes | Identity transform engine, layout transform, renderToHtml |
@refrakt-md/types | Yes | Shared TypeScript interfaces |
@refrakt-md/behaviors | Optional | Client-side progressive enhancement for interactive runes |
Theme Integration
Themes work with the HTML adapter through their config and CSS exports. No special ./html export is needed — the ./transform export (theme config) and . export (CSS) are sufficient:
import { themeConfig } from 'my-theme/transform';
import { createTransform } from '@refrakt-md/transform';
const transform = createTransform(themeConfig);
For CSS, reference the theme's stylesheet in renderFullPage:
renderFullPage(input, { stylesheets: ['/path/to/theme.css'] });