Pages are data
A page is one JSON record of words and choices, never markup. What that buys an agent, and the four layers that turn it into HTML.
A page is one JSON record that PageSchema accepts or refuses, and renderPage turns it into one HTML document. The record holds content only: no markup, no class names, no CSS. Open the Record tab to read the same page as data:
{
"$schema": "https://next.mdxui.dev/schema.json",
"theme": "quaypoint",
"template": "api-product",
"title": "Harbor tides: every berth, every tide",
"description": "Tide tables for every berth in the harbor, as one API: heights, times and the window your keel clears.",
"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": "hero",
"shell": { "inset": "contained", "density": "hero" },
"block": {
"type": "hero",
"layout": "centered",
"stack": 560,
"title": "Every berth, every tide.",
"lede": "One call returns the tide table for any berth in the harbor: the times, the heights and the window your keel clears.",
"actions": [
{ "href": "/pricing", "label": "See the plans", "kind": "solid" },
{ "href": "/contact", "label": "Talk to the harbor", "kind": "ghost" }
],
"fine": ["1,000 calls a month free", "Heights to the chart datum"]
}
},
{
"kind": "band",
"shell": { "inset": "contained", "density": "compact" },
"block": { "type": "command-row", "command": "npm i harbor-tides", "note": "Node.js 22 or later" }
},
{
"kind": "section",
"shell": { "inset": "contained", "density": "section" },
"block": {
"type": "feature-grid",
"cellHead": "icon",
"head": { "eyebrow": "One call returns", "title": "The whole tide, per berth." },
"items": [
{ "icon": "waves", "title": "High and low water", "text": "Times and heights for 14 days, at the berth itself." },
{ "icon": "anchor", "title": "Keel clearance", "text": "The hours a given draft clears, from the charted depth." },
{ "icon": "map-pin", "title": "Datum named", "text": "Every height names its chart datum." }
]
}
},
{
"kind": "section",
"shell": { "inset": "contained", "density": "section", "surface": "plate" },
"block": {
"type": "api-call",
"method": "GET",
"path": "/v1/berths/{berth}/tides",
"note": "One berth, 14 days",
"examples": [
{
"label": "Pier 4, today",
"request": "curl https://api.harbor-tides.example/v1/berths/pier-4/tides?days=1",
"status": "200 OK",
"response": "{\n \"berth\": \"pier-4\",\n \"datum\": \"chart datum\",\n \"tides\": [\n { \"at\": \"04:12\", \"type\": \"low\", \"m\": 0.6 },\n { \"at\": \"10:31\", \"type\": \"high\", \"m\": 4.1 },\n { \"at\": \"16:40\", \"type\": \"low\", \"m\": 0.8 },\n { \"at\": \"22:58\", \"type\": \"high\", \"m\": 3.9 }\n ]\n}"
}
]
}
},
{
"kind": "section",
"shell": { "inset": "contained", "density": "section" },
"block": {
"type": "endpoints",
"head": { "eyebrow": "The API", "title": "Four routes, one shape." },
"rows": [
{ "method": "GET", "path": "/v1/berths", "text": "Every berth in a harbor, with its charted depth" },
{ "method": "GET", "path": "/v1/berths/{berth}/tides", "text": "High and low water for one berth" },
{ "method": "GET", "path": "/v1/berths/{berth}/window", "text": "The hours a given draft clears" },
{ "method": "POST", "path": "/v1/alerts", "text": "A webhook before a window closes" }
]
}
},
{
"kind": "section",
"shell": { "inset": "contained", "density": "section" },
"block": {
"type": "feature-split",
"head": { "eyebrow": "For the bridge", "title": "Plan the passage, not the math." },
"items": [
{ "icon": "clock", "title": "Windows, not tables", "text": "Ask for a draft and get the hours it clears." },
{ "icon": "bell", "title": "Alerts before the turn", "text": "A webhook fires an hour before a window closes." },
{ "icon": "shield-check", "title": "One source per berth", "text": "Each answer names its station and its datum." }
],
"specimen": {
"blocks": [
{
"type": "terminal",
"variant": "plain",
"title": "A 3.2 m draft at pier 4",
"lang": "json",
"lines": ["{", " \"draft\": 3.2,", " \"opens\": \"08:05\",", " \"closes\": \"12:50\"", "}"]
}
]
}
}
},
{
"kind": "section",
"shell": { "inset": "contained", "density": "section" },
"block": {
"type": "bento",
"head": { "eyebrow": "Where it runs", "title": "In the tools a crew opens." },
"panels": [
{
"title": "In a script",
"text": "One package, typed.",
"specimen": {
"type": "terminal",
"variant": "plain",
"title": "window.ts",
"lang": "ts",
"lines": ["const w = await tides.window('pier-4', 3.2);"]
}
},
{
"title": "In an agent",
"text": "One prompt, one berth.",
"specimen": {
"type": "agent-prompt",
"label": "Ask an agent",
"lines": ["Fetch the tide window for a 3.2 m draft at pier 4 today."]
}
},
{
"title": "On a chart plotter",
"text": "Through the Chartplot integration.",
"specimen": {
"type": "api-call",
"method": "POST",
"path": "/v1/alerts",
"examples": [
{ "label": "Alert", "request": "{ \"berth\": \"pier-4\", \"draft\": 3.2 }", "status": "201 Created", "response": "{ \"id\": \"al_42\" }" }
]
}
}
]
}
},
{
"kind": "section",
"shell": { "inset": "contained", "density": "section" },
"block": {
"type": "compare",
"head": { "eyebrow": "Against station tables", "title": "The berth, not the nearest station." },
"cons": {
"label": "Station tables",
"items": ["Heights at the station", "One PDF per month", "Datum in the small print", "Math done by hand"]
},
"pros": {
"label": "Harbor tides",
"items": ["Heights at the berth", "Fourteen days per call", "Datum on every height", "The window computed"]
}
}
},
{
"kind": "section",
"shell": { "inset": "contained", "density": "section" },
"block": {
"type": "steps",
"head": { "eyebrow": "How it works", "title": "From berth to window." },
"steps": ["Name the berth", "Give the draft", "Read the window", "Set an alert before it closes"]
}
},
{
"kind": "section",
"shell": { "inset": "contained", "density": "section" },
"block": {
"type": "text-stack",
"layout": "stack",
"head": { "eyebrow": "Where the numbers come from", "title": "Charted, measured, named." },
"items": [
{ "title": "Charted depth", "text": "Each berth carries the depth from the latest harbor survey." },
{ "title": "Measured tide", "text": "Predictions are corrected every 6 minutes from the harbor gauge." },
{ "title": "Named datum", "text": "Every height says which datum it stands on." }
]
}
},
{
"kind": "section",
"shell": { "inset": "contained", "density": "section" },
"block": {
"type": "agent-prompt",
"label": "Paste into your agent",
"lines": ["Install harbor-tides from npm.", "Fetch the tide window for a 3.2 m draft at pier 4 today.", "Print when it opens and when it closes."]
}
},
{
"kind": "closing",
"shell": { "inset": "contained", "density": "closing" },
"block": {
"type": "closing",
"layout": "stack",
"head": { "eyebrow": "Free to start", "title": "Ask for your berth.", "lede": "A harbor master adds any berth in the harbor within one working day." },
"actions": [
{ "href": "/contact", "label": "Talk to the harbor", "kind": "solid" }
]
}
},
{
"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."]
}
}
]
}
What a record buys an agent
| An agent writing markup | An agent writing a record |
|---|---|
| Redraws the layout on every edit | Changes words; the layout is the Section type's |
| Drifts from the brand page by page | Every page wears the one Theme it names |
| Produces a diff of tags and classes to review | Produces a JSON diff of the words that changed |
| Breaks the layout with a long headline | Is refused at the field, with its maximum, before anything renders |
The smallest record
{
"theme": "quaypoint",
"title": "Harbor tides",
"description": "Tide tables for every berth.",
"frame": "none",
"sections": [
{
"kind": "hero", "shell": { "inset": "contained", "density": "hero" },
"block": {
"type": "hero", "layout": "centered", "stack": 560,
"title": "Every berth, every tide.",
"lede": "One call returns the tide table for any berth.",
"actions": [], "fine": []
}
},
{
"kind": "band", "shell": { "inset": "contained", "density": "section" },
"block": { "type": "statement", "text": "A tide table is a *promise*." }
},
{
"kind": "section", "shell": { "inset": "contained", "density": "section" },
"block": { "type": "command-row", "command": "npm i harbor-tides" }
}
]
}| Field | Says |
|---|---|
theme | Which bundled Theme the page wears |
frame | The frame around every section: none, one ruled sheet, or a split with an aside |
sections | Three to twenty sections, in page order |
kind | What the section is to its neighbors; it picks the landmark and the divider |
shell.inset, shell.density | The section's width and its vertical pace |
block.type | The Section type that lays the content out |
stack | The width in pixels below which the hero's actions stack |
Four layers
Block is the renderer's word for a Section type's code; these docs say Section type everywhere else. Each layer owns one thing and never reaches into another.
| Layer | Holds | Never knows |
|---|---|---|
| Block | One Section type's layout: one root element, colors by role | What is painted behind it, its neighbors, the Theme, the page |
| Shell | What is painted behind and between: surface, background, inset, density, stickiness, the frame | What the content says |
| Theme | Tokens and policies, compiled to custom properties | Which page it dresses |
| Composition | The record: Theme, frame and sections | Markup and CSS |
The rules the layers keep
| Rule | It prevents |
|---|---|
| No model writes markup or CSS; a Section type is drawn once by hand | A page that re-guesses its design on every edit |
| A token never changes layout; a layout change is a variant | A Theme that breaks a page it never saw |
| Painted behind content is shell; positioning content is block | Two layers fighting over one box |
| A Section type never knows its neighbors; the composer derives each divider | A divider right between two sections and wrong between two others |
| Every string is bounded | A headline three times the length the design was drawn for |
Edit with the schema
PageSchema.toJSONSchema() writes every bound as maxLength, minItems and maxItems, the schema this site serves at /schema.json:
import { PageSchema } from '@mdxui/site';
const schema = PageSchema.toJSONSchema();A record may open with a $schema key naming that address; PageSchema strips unknown keys as it parses.
startupbuilder.dev defines the Page; the record is how mdxui holds one. Page maps each part, and How mdxui fits a Business places it in a Business.