AdaptersHTML Adapter

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

OptionTypeDescription
stylesheetsstring[]CSS stylesheet URLs for <link> tags in <head>
scriptsstring[]JavaScript URLs for <script> tags before </body>
headExtrastringExtra HTML to inject into <head> (use to inline highlight + site-tokens CSS)
langstringHTML lang attribute (default: "en")
baseUrlstringBase URL for canonical URLs and absolute OG URLs
siteNamestringHuman-readable site name for og:site_name and JSON-LD entries
defaultImagestringDefault og:image for pages without their own image
logostringSite logo for Organization JSON-LD schema
seoPageSeoSEO 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.

note

@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:

PackageRequiredPurpose
@refrakt-md/transformYesIdentity transform engine, layout transform, renderToHtml
@refrakt-md/typesYesShared TypeScript interfaces
@refrakt-md/behaviorsOptionalClient-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'] });