Author semantic Markdown pages
Brix can expose an authored semantic Markdown representation alongside an SSR HTML page. Opt in page by page and project the same successful loader data into intentionally different renderers.
This is not the website's markdown-it prose parser. markdown-it parses trusted documentation
files such as this one for display. Application representations use the closed brix/markdown
builder so CMS and user text cannot become active Markdown or raw HTML.
Export project-bound factories
Keep contracts browser-safe and route bindings server-only:
// src/brix.ts
export const { createMarkdownIndex, createPageContract, getPath } = brix;
// src/brix.server.ts
export const { createMarkdownIndexRoute, createPageRoute } = brixServer;
See pages for the complete contract, loader, component, and route workflow.
Add a page representation
// contract.ts
import { createPageContract } from "#brix";
export const articlePageContract = createPageContract({
path: "/articles/:slug",
representations: ["markdown"],
});
// route.ts
import { md } from "brix/markdown";
import { createPageRoute } from "#brix/server";
import { ArticlePage } from "./Page.tsx";
import { articlePageContract } from "./contract.ts";
// getArticle is an application-specific data dependency.
export const articlePageRoute = createPageRoute(articlePageContract, {
component: ArticlePage,
load: async (c, { params }) => ({ article: await getArticle(c, params.slug) }),
markdown: (_c, { article }) =>
md.document({
body: [md.heading(1, md.text(article.title)), md.paragraph(md.text(article.summary))],
}),
});
The markdown callback is required when the contract declares Markdown and forbidden otherwise. It
receives a readonly view of successful loader props and returns a MarkdownDocument, synchronously or
as a promise. Keep redirects, not-found responses, and other early Response values in load.
Brix invokes exactly one renderer. HTML runs Preact, styling, islands, fragments, and hydration preparation. Markdown runs the callback and deterministic serializer without Preact or a browser runtime. The callback must explicitly include semantics for content that HTML obtains from a fragment; Markdown never fetches fragment endpoints.
Build safe documents
Use md.text for every CMS or user-provided string. Builders are immutable branded values, not plain
objects or raw strings:
const content = md.fragment(
md.paragraph(md.strong(md.text("Status: ")), md.text(status)),
md.list(items.map((item) => md.listItem(md.paragraph(md.link(item.url, md.text(item.title)))))),
md.image(image.url, image.alt),
md.codeBlock(example, { language: "ts" }),
);
The API covers text, paragraphs, headings, emphasis, strong text, strikethrough, inline code, hard breaks, links, images, blockquotes, code blocks, ordered and unordered lists, tables, thematic breaks, fragments, documents, and index documents. It validates structure, URL schemes, depth, node count, and output size, and emits CommonMark plus GFM tables and strikethrough without raw HTML.
Do not cast strings or objects to Markdown types. Extract projection functions returning
MarkdownContent or MarkdownFragment when multiple routes share semantic pieces.
Negotiate and link durable URLs
The canonical page URL defaults to HTML and selects Markdown for Accept: text/markdown. An
unacceptable canonical request receives 406. A durable .md alias always selects Markdown regardless
of Accept; the root alias is /index.md.
Generate both forms with getPath rather than appending a suffix:
const htmlUrl = getPath(articlePageContract, { params: { slug: "example" } });
const markdownUrl = getPath(articlePageContract, {
params: { slug: "example" },
representation: "markdown",
});
Brix supplies Content-Type, Vary, Content-Location, and reciprocal alternate links. Pages that
do not declare a representation stay HTML-only and retain their previous Accept behavior.
Localize the projection
In a translation-enabled app, call the project-bound server getTranslator(c) inside markdown.
Do not use useTranslate, which is a Preact hook. Loader and Markdown calls share request-local
dictionary preparation. Localized canonical and durable URLs resolve the same locale and return
Content-Language; generate paths with the localized getPath(contract, locale, options) overload.
Localized index callbacks receive { locale } and can await the same translator for titles,
summaries, sections, and links. See translations for locale configuration and
test fixtures.
Continue with publish and operate semantic markdown.