HTML
Parse, render each Section type, compose, then wrap: how one Page record becomes one HTML document with one stylesheet and no framework.
renderPage(page, { theme }) runs four steps and returns one HTML string: a document with one <style> element and no framework.
What it renders
Harbor's blog index, one renderPage call. Widen it, narrow it to 390, or pick another Theme: every frame is a file the build wrote.
{
"$schema": "https://next.mdxui.dev/schema.json",
"theme": "quaypoint",
"template": "blog",
"title": "Blog · Harbor tides",
"description": "Notes from the harbor on tides, datums and windows.",
"frame": "none",
"sections": [
{
"kind": "nav",
"shell": { "inset": "bleed", "density": "none", "surface": "glass", "sticky": true },
"block": {
"type": "nav",
"menu": "none",
"brand": { "href": "/", "text": "Harbor tides", "mark": "waves" },
"links": [
["/pricing", "Pricing"],
["/integrations", "Integrations"],
["/blog", "Blog"]
],
"cta": { "href": "/contact", "label": "Talk to the harbor" }
}
},
{
"kind": "section",
"shell": { "inset": "listing", "density": "listing" },
"block": {
"type": "post-list",
"head": { "eyebrow": "Blog", "title": "Notes from the harbor", "lede": "How the tables are made, and how crews read them." },
"read": "Read",
"posts": [
{
"href": "/blog/tide-windows",
"tag": "Windows",
"dateline": {
"date": "2026-10-20",
"read": { "minutes": 3, "unit": "min read" }
},
"title": "A tide window is a promise",
"dek": "Why the API answers with the hours a draft clears, and never with a table to read by hand."
}
]
}
},
{
"kind": "section",
"shell": { "inset": "contained", "density": "section" },
"block": {
"type": "text-stack",
"layout": "grid",
"head": { "eyebrow": "Topics", "title": "What the notes cover." },
"items": [
{ "title": "Tides", "text": "How a berth’s tide differs from its station’s." },
{ "title": "Datums", "text": "Which zero a height stands on, and why it matters." },
{ "title": "Windows", "text": "The hours a keel clears, computed per berth." }
]
}
},
{
"kind": "footer",
"shell": { "inset": "contained", "density": "footer" },
"block": {
"type": "footer",
"layout": "row",
"brand": { "href": "/", "text": "Harbor tides", "mark": "waves" },
"blurb": "Tide tables for every berth, as one API.",
"columns": [
{
"title": "Product",
"links": [
["/pricing", "Pricing"],
["/integrations", "Integrations"]
]
},
{
"title": "Harbor",
"links": [
["/blog", "Blog"],
["/contact", "Contact"],
["/terms", "Terms"]
]
}
],
"note": ["Harbor tides is a worked example on mdxui.dev, not a customer.", "Rendered by @mdxui/site. Forms here do not send."]
}
}
]
}
Four steps
| Step | Export | Does |
|---|---|---|
| 1 | PageSchema.parse | Accepts the record or refuses it, naming each field over its bound |
| 2 | renderBlock | Renders each section's Section type, with a render context for the Theme and the shell surface it sits on |
| 3 | compose | Wraps each section in its shell, derives every divider from its neighbors, and draws the frame |
| 4 | document | Wraps the body in the head lines, the Theme's typefaces and one stylesheet |
Document and head runs the four steps by hand and gets renderPage's output byte for byte.
A string, not a server
renderPage returns a string, so a build writes it to a file, as Ship a Site does, and a Worker returns it per request. Neither needs a runtime of its own.
Without a script
27 of 27 Section types render no <script> element, each measured with every optional field set.
| Mechanism | Section types |
|---|---|
<details> | faq |
| radio inputs | terminal |
popover | product-list |
inline onclick | hero, feature-split, bento, steps, command-row, agent-prompt, closing, article |
Every inline handler is a button's onclick that writes text to the clipboard: a copy control, or the article's copy-link button.