# MIDL — full documentation > Every canonical MIDL doc, concatenated. Source + per-doc pages: https://midl.site/docs > Machine-readable surfaces: https://midl.site/llms.txt ================================================================================ # Agent Quickstart (https://midl.site/docs/quickstart) ================================================================================ # Build your first MIDL site on localhost — agent quickstart A self-contained starter for an AI agent (or developer) to stand up a first **MIDL** website running on `http://localhost:3000`, using the public `@wisepunk/*` packages. Every step has a command to run and a way to verify it worked — follow them in order. **End state:** a tiny Node project that renders a MIDL document to HTML and serves it on localhost, with no framework and no auth. > Want the full capability map + how to discover the component vocabulary programmatically? See the > companion [AGENT-REFERENCE.md](AGENT-REFERENCE.md). You can also enumerate every component without docs: > `import { describeVocabulary, EXAMPLE_DOCS } from '@wisepunk/core'`. --- ## 0. What MIDL is (read once) - A **MIDL document** is plain JSON describing a page as semantic *frames* (components) — not HTML. - A **renderer** + a **pack** turn that JSON into HTML. The pack decides the look; the document stays the same. - Two render targets ship on public npm: **`@wisepunk/renderer-html`** (→ an HTML string, framework-free) and **`@wisepunk/renderer-svelte`** (Svelte components). This guide uses `renderer-html`. - A typed write-API client, **`@wisepunk/sdk`**, is optional (used in step 6 to pull a live page). ```js renderMidl(doc, { pack, theme }) → { html, themeCss, validation } ``` --- ## 1. Prerequisites - **Node 18+** (for built-in `fetch`, used in step 6). Verify: ```sh node -v # expect v18 or higher ``` --- ## 2. Create the project ```sh mkdir my-midl-site && cd my-midl-site npm init -y npm pkg set type=module # the code below is ESM npm install @wisepunk/renderer-html @wisepunk/core ``` **Verify:** `ls node_modules/@wisepunk` lists `core` and `renderer-html`. (These are **public** packages — no token, no `.npmrc`.) --- ## 3. Write a MIDL document Create **`src/home.mjs`**. A document is `{ midl, ns, frames }`; each frame has an `id`, a `type` (namespaced like `ui:Hero`), and type-specific fields. A frame with `children` composes other frames by `id`. ```js // src/home.mjs export const home = { midl: '0.1', ns: { ui: 'https://midl.ai/ui#' }, frames: [ { id: 'page', type: 'ui:Page', children: ['hero', 'cta'] }, { id: 'hero', type: 'ui:Hero', title: 'My first MIDL site' }, { id: 'cta', type: 'ui:Button', text: 'Get started', variant: 'default' } ] }; ``` --- ## 4. Serve it on localhost Create **`server.mjs`** — a zero-dependency Node HTTP server that renders the document on each request: ```js // server.mjs import { createServer } from 'node:http'; import { renderMidl } from '@wisepunk/renderer-html'; import { home } from './src/home.mjs'; const PORT = 3000; const PACK = '@midl/pack-minimal-html'; // bare semantic tags, zero CSS needed createServer((_req, res) => { const { html, themeCss, validation } = renderMidl(home, { pack: PACK }); if (!validation.valid) console.warn('MIDL validation issues:', JSON.stringify(validation.errors)); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end( `` + `My MIDL site` + `${html}` ); }).listen(PORT, () => console.log(`MIDL site → http://localhost:${PORT}`)); ``` Run it: ```sh node server.mjs ``` **Verify** (in another terminal): ```sh curl -s http://localhost:3000 | grep -o '
.*
' # →

My first MIDL site

``` The Hero's `title` renders as a real `

` (the page's primary heading) — heading structure lives in the document, so screen readers, crawlers, and agents all see it. If you see that line, your first MIDL site is live on localhost. The server log should **not** print any "validation issues." --- ## 5. Swap the look (packs) The document never changes — change the **pack**. Set `PACK` in `server.mjs`: - **`@midl/pack-minimal-html`** — plain semantic tags, no CSS. Works out of the box (used above). - **`@midl/pack-tailwind-shadcn`** — Tailwind + shadcn class names + `themeCss` CSS variables. The HTML will carry utility classes; to *see* it styled, your page needs Tailwind (e.g. the Tailwind Play CDN) with the shadcn variables. For a first localhost run, stick with `minimal-html`. > Pack ids are **literal strings**, not packages to install — they're built into `@wisepunk/core`. --- ## 6. (Optional) Pull a live page with the SDK Render isn't only for hardcoded docs — you can fetch a stored page from a MIDL deployment and render it. The public demo API serves the read endpoints without auth. ```sh npm install @wisepunk/sdk ``` ```js // live.mjs import { createClient } from '@wisepunk/sdk'; import { renderMidl } from '@wisepunk/renderer-html'; const midl = createClient({ baseUrl: 'https://midl-web.rene-dad.workers.dev', apiKey: 'public-demo' // GET is public; the SDK just needs a non-empty key }); const page = await midl.frames.get({ slug: 'home' }); // live document const { html } = renderMidl(page.frame.content, { pack: '@midl/pack-minimal-html' }); console.log(html); ``` ```sh node live.mjs # prints the live home page rendered to HTML ``` To **author** pages (create/update/delete) you need a real API key; `midl.frames.create({ content, slug })` returns `{ id, slug }`. See `docs/WRITE-API-MANUAL.md`. --- ## Reference ### The MIDL document - `midl` — schema version (`'0.1'`). - `ns` — namespace map; `ui` points at the UI component vocabulary. - `frames` — array of components. Each: `{ id, type, ...fields }`. A `ui:Page` lists child `id`s in `children`; children render in that order. - Verified starter components: `ui:Page` (`children`), `ui:Hero` (`title` → `

`), `ui:Heading` (`level` 1–6 + `text` → `

`–`

`), `ui:Text` (`text` → `

`), `ui:Button` (`text`, `variant`). The authoritative component set + their allowed fields live in the pack's manifest — if a field/type isn't allowed, `renderMidl` returns `validation.valid === false` with `validation.errors` (an array of messages) describing the problem. **Always check `validation` rather than guessing.** ### `renderMidl(doc, options)` → result - `options.pack` — pack id string (default registry pack if omitted). - `options.theme` — optional token map (DESIGN.md tokens) → mapped to `result.themeCss` (`:root { … }`). - `result.html` — the rendered HTML string. - `result.themeCss` — `:root` CSS variables for the theme (empty string when no theme given). - `result.validation` — `{ valid, errors }` from checking the doc against the pack manifest (`errors` is a string array). ### Machine-readable surfaces (for discovery) Against a live deployment (e.g. `https://midl-web.rene-dad.workers.dev`): - `GET /render/.midl.json` — the raw MIDL document for a page. - `GET /render/.facts.json` — extracted facts (agent/RAG view). - `GET /api/agents/openapi.json` and `/.well-known/agent.json` — the API + tool descriptions. --- ## Agent gotchas - **ESM only:** set `"type": "module"` (`npm pkg set type=module`) or the `import`s fail. - **Packs aren't npm packages:** don't `npm install @midl/pack-*` — pass the id string to `renderMidl`; it's bundled in `@wisepunk/core`. - **`renderMidl` returns a string, not a server** — you serve `html` (+ ``) however you like. - **`minimal-html` needs no CSS; `tailwind-shadcn` does** — use `minimal-html` for the first run to avoid an unstyled page. - **Node 18+** for the SDK's `fetch` (step 6). - **Trust `validation`** — if `validation.valid` is false, fix the document to match the manifest before assuming a render bug. --- ## Done check 1. `node server.mjs` logs `MIDL site → http://localhost:3000`. 2. `curl -s http://localhost:3000 | grep -q 'My first MIDL site'` exits 0. 3. No "validation issues" in the server log. All three → you've built and served your first MIDL site. Next: read [AGENT-REFERENCE.md](AGENT-REFERENCE.md) for the full vocabulary + the validate→repair→render loop, add more frames (try `ui:Heading`), switch to the `tailwind-shadcn` pack with Tailwind, or pull live content with the SDK. ================================================================================ # Agent Reference (https://midl.site/docs/reference) ================================================================================ # MIDL agent reference The capability map for an agent building with MIDL: what the system is, every surface you can use, and the **validate → repair → render** loop that lets you author correctly without memorizing the spec. For a hands-on first run, do [AGENT-QUICKSTART.md](AGENT-QUICKSTART.md) first; this is the companion reference. > **Core principle: don't guess the spec — introspect and round-trip.** The vocabulary is machine-readable > (`describeVocabulary()`), and every render/validate call returns structured feedback that names the fix. --- ## 1. The model in one minute - A **MIDL document** is plain JSON: `{ midl: '0.1', ns: { ui: 'https://midl.ai/ui#' }, frames: [...] }`. - A **frame** is a component: `{ id, type, ...fields, children? }`. `type` is namespaced (`ui:Hero`). `children` is an array of **frame ids** (composition is by reference, not nesting). - A **pack** turns the document into HTML. The document never changes; the pack decides the look. - `@midl/pack-minimal-html` — bare semantic tags, zero CSS. Best for a first run. - `@midl/pack-tailwind-shadcn` — Tailwind + shadcn classes + a `:root` theme via `themeCss`. - Pack ids are **literal strings** built into `@wisepunk/core` — *not* npm packages to install. - A **theme** (optional) is a token map mapped to `themeCss` (`:root { … }`), contrast-checked. ```js import { renderMidl } from '@wisepunk/renderer-html'; const { html, themeCss, validation } = renderMidl(doc, { pack: '@midl/pack-minimal-html', theme }); ``` --- ## 2. Discover the vocabulary (instead of reading docs) ```js import { describeVocabulary, EXAMPLE_DOCS } from '@wisepunk/core'; const vocab = describeVocabulary(); // vocab.components[] — one entry per ui:* type: // { type, requiredFields, leaf, allowsChildren, container, acceptsText, textProp?, // variants?, packs: { : { tag } }, example } ``` So for any component you get its **required fields**; whether it **accepts text** (`acceptsText` + which `textProp` — e.g. `ui:Button` → `text`, `ui:Hero` → `title`); whether it **takes children** (`allowsChildren` = the validator's real rule, `container` = a declared child container); its **variant axes** (e.g. `ui:Button` → `variant`/`size`, `ui:Stack` → `direction`, `ui:Section` → `width`, `ui:Hero` → `size`); the **tag each pack renders it as**; and a **minimal example**. `allowsChildren` and the validator now agree (MID-100) — introspection never forbids a pattern the validator accepts. `EXAMPLE_DOCS` is an **array** of `{ name, description, doc }` — full, valid documents to pattern-match (`landing`, `contact-form`, `article`). Every `doc` is test-asserted valid against both packs, so they're safe to copy and mutate. E.g. `EXAMPLE_DOCS.find(e => e.name === 'landing').doc`. On a live deployment the same data is served (origin-rooted): | Surface | What it gives you | |---|---| | `GET /llms.txt` | The discovery index (this, machine-readable) | | `GET /api/agents/vocabulary.json` | `describeVocabulary()` output | | `GET /api/agents/examples.json` | `EXAMPLE_DOCS` | | `GET /api/agents/openapi.json` | OpenAPI 3.1 for the public API | | `GET /.well-known/agent.json` | Site identity, tools, formats | | `GET /render/.midl.json` | A page's raw MIDL document | | `GET /render/.facts.json` | Extracted facts (RAG/agent view) | --- ## 3. The authoring loop: validate → repair → render MIDL is designed so you author by round-tripping, not by getting it right blind. 1. **Write** a document from a `describeVocabulary()` entry or an `EXAMPLE_DOCS` doc. 2. **Validate** it. The result names every problem (missing required field, unknown type, bad child ref): ```js import { validateMIDL, validateAgainstManifest, getPack } from '@wisepunk/core'; validateMIDL(doc); // structural: { valid, errors, warnings } validateAgainstManifest(doc, getPack(packId).manifest); // + pack constraints: { valid, errors, warnings } ``` Or over HTTP: `POST /api/validate` with the document. Or just read `renderMidl(doc).validation`. **Read `warnings`, not just `errors`:** a prop a component won't render (e.g. a `subtitle` on `ui:Hero`, a hallucinated field) surfaces as a `warning` — it won't fail validation but it *won't render either* (MID-100). 3. **Repair** structural issues automatically instead of hand-fixing: ```js import { repairAgainstManifest, getPack } from '@wisepunk/core'; const { doc: fixed, repairs } = repairAgainstManifest(doc, getPack(packId).manifest); ``` 4. **Render** — `renderMidl(doc, { pack })` → HTML. `validation.valid === false`? Fix what `validation.errors` say and re-render. **Trust the validator over your assumptions.** --- ## 4. Authoring & serving stored pages (the write API) To create/update pages on a deployment, use `@wisepunk/sdk` with a Bearer API key: ```js import { createClient } from '@wisepunk/sdk'; const midl = createClient({ baseUrl: 'https://your-deployment.workers.dev', apiKey: process.env.MIDL_API_KEY }); const { id, slug } = await midl.frames.create({ content: doc, slug: 'home' }); // validated server-side const page = await midl.frames.get({ slug: 'home' }); // read it back (GET is public) ``` Reads (`list`, `get`) are public; writes (`create`, `update`, `delete`) need the key. The full HTTP contract is `GET /api/agents/openapi.json`. See [WRITE-API-MANUAL.md](WRITE-API-MANUAL.md). --- ## 5. Theming Pass `options.theme` (DESIGN.md tokens) to `renderMidl`, or let a document theme itself. - **Token values** accept a plain string OR `{ value }` — `{ 'colors.primary': '#34d399' }` works (MID-102). - **Self-theming documents** — a `ui:StyleToken` frame's `tokens` are read automatically when you don't pass `options.theme`. `renderMidl(doc, { pack })` themes from the document itself. - **Foregrounds are auto-derived to pass WCAG AA** against their own background — you supply background-side colors; `--*-foreground` is computed (and contrast-checked). To override a derived foreground (e.g. make `--muted-foreground` less bright), set the `*.text` key: `colors.muted.text`, `colors.card.text`, `colors.primary.text`, etc. - **Tailwind hosts** (for `@midl/pack-tailwind-shadcn`): don't hand-write the var mapping — `import { tailwindThemeExtend } from '@wisepunk/core'` and spread it into `tailwind.config`'s `theme.extend` so `bg-primary` / `text-muted-foreground` / the radius resolve to the vars `themeCss` sets. --- ## 6. Gotchas - **Children are ids, not objects** — `children: ['hero', 'cta']`, and those frames exist in `frames`. - **Packs aren't installable** — pass the id string to `renderMidl`; it's bundled in `@wisepunk/core`. - **`minimal-html` needs no CSS; `tailwind-shadcn` does** — start with `minimal-html`. - **Express structure in the document, not the host shell:** `ui:Heading` (`level` 1–6) / `ui:Hero` (`title` → `

`, `size`) for outline; `ui:Label` (`text` + `for`) for inputs; `ui:Stack` (`direction: row|column`) / `ui:Section` (`width: content|full`) for layout; `ui:Button` with `href` renders an `` (real CTA); `ui:Select` (`options: string[]`), `ui:Rating` (`value`/`max`), `ui:Accordion` (`summary` → `
`). - **ESM + Node 18+** for the packages. - **Don't assume — round-trip.** If `validation.valid` is false, the document is wrong, not the renderer; if `validation.warnings` is non-empty, a prop you set isn't being rendered. ================================================================================ # Personalization & Optimization (https://midl.site/docs/personalization) ================================================================================ # Personalization & optimization MIDL is **personalization-first**: a page is data, not markup, so the runtime can serve a different *variant* of a page (or a different *style*) to each visitor, measure what works, and promote the winners — **with no redeploys**. This is the loop that makes MIDL more than a renderer. ``` author variants ─▶ SERVE (assign per visitor) ─▶ TRACK (impressions + conversions) ▲ │ └──────────────── OPTIMIZE (promote winners) ◀───────────┘ ``` ## 1. Variants A stored page (frame) can hold several **content variants**, each with a `traffic_percentage`. The same idea applies to **design systems** (styles): several competing DESIGN.md themes can run at once. You author variants once; you don't redeploy to change the split. ## 2. Serve — deterministic, sticky, per-visitor On every request the server picks one variant **deterministically** from a hash of `(visitor key + frame id)` mapped over the variants' cumulative weights: - **Sticky** — the same visitor always sees the same variant for a page, so the experience is stable (and conversions attribute cleanly). - **Weighted** — across visitors the split matches each variant's `traffic_percentage`; change a weight and assignment deterministically re-shuffles. - **Edge-safe** — pure hashing (no `Math.random`, no Node crypto), so it runs on the Cloudflare edge. **Design systems** are assigned the same way but **site-wide** (one style per visitor for the whole site), on an independent seed so a visitor's style and content assignments don't correlate. ### Targeting (who sees what) A variant can carry a **targeting rule** and take precedence over the weighted pool. Rules match on the visitor context the edge derives per request: - `geo` — country (the trusted edge geo, not a spoofable header) - `device` — mobile / desktop - `returning` — first-time vs returning visitor - `referrer` — where they came from Targeted visitors get the matching variant; everyone else falls back to the weighted/sticky pool. The served-page cache is segmented by the resolved segment, so personalization is never cached away. ## 3. Track Each render emits an **impression** (`frame_id` + `variant_id` + visitor context) off the hot path, and the rendered output is tagged (`data-midl-frame` / `data-midl-variant`) so clicks and conversions **attribute to the variant that was actually served**. Style impressions are tracked separately by design-system id. ## 4. Optimize — promote winners automatically A scheduled **optimizer Worker** reads the telemetry and, per variant, computes the conversion + engagement rate, the sample size, and a **statistical confidence** (95% level). Once a variant clears a minimum sample (so you're not acting on noise) and is a confident winner, the optimizer **promotes it** — shifting traffic toward it by adjusting the weights the serve step reads. The loop then repeats with the new split. No human redeploy; the site self-improves on real outcomes. ## How you use it - **Author variants** — via the authenticated write API (`@wisepunk/sdk` → `frames.create` / `frames.update`) or the admin Studio. Add content variants (or competing design systems) and set each one's `traffic_percentage`. - **Target** — attach a `geo`/`device`/`returning`/`referrer` rule to a variant for context-driven selection. - **Let it run** — the optimizer promotes winners on a schedule; watch the admin dashboards for per-variant performance and confidence. - **Machine-readable** — a page's served variant + extracted facts are available at `/render/.midl.json` and `/render/.facts.json` for agents and crawlers. Because selection happens at render time over stored data, you change *who sees what* by editing data — never by shipping code. ================================================================================ # Native i18n (https://midl.site/docs/localization) ================================================================================ # Native i18n (multilingual pages) MIDL is **multilingual by construction**. Because a page is data, not markup, a single document can carry every translation inline and the renderer resolves the right language per visitor — no parallel page trees, no per-language variant duplication, no redeploys. Localization (which **language**) and personalization (which **variant**) are **orthogonal**: a page can be both translated and A/B-tested, and the two never multiply each other. ## 1. Localizable text values Any human-readable text prop is EITHER a plain string (as before) OR a **locale map** keyed by BCP-47 language tag: ```jsonc // before — still valid, renders identically { "id": "hero", "type": "ui:Hero", "title": "Ship faster" } // localized — same prop, a { bcp47: string } map { "id": "hero", "type": "ui:Hero", "title": { "en": "Ship faster", "de": "Schneller liefern" } } ``` Plain strings pass through unchanged, so **every existing document is byte-identical** to before — localization is backward-compatible by construction. You localize a value only when you want to; an untranslated string is simply shared across all languages. The localizable props (per component type) and the resolution order are published in the machine [vocabulary](/api/agents/vocabulary.json) under its `localization` block. Today they are: | Component | Localizable prop | | --- | --- | | `ui:Hero` | `title` | | `ui:Heading`, `ui:Text`, `ui:Button`, `ui:Submit`, `ui:NavItem`, `ui:Link`, `ui:Badge`, `ui:Label` | `text` | | `ui:Accordion` | `summary` | ## 2. Declare the document's languages Set two props at the **document root**: ```jsonc { "midl": "0.1", "ns": { "ui": "https://midl.ai/ui#" }, "defaultLocale": "en", // the canonical language; absent → "en" "locales": ["en", "de"], // the languages this document is translated into "frames": [ /* … */ ] } ``` - `defaultLocale` — the fallback language a value resolves to when the visitor's locale isn't present. - `locales` — the set the runtime is allowed to serve and advertise (`hreflang`, the language switcher). A `?lang=` value or `Accept-Language` outside this set falls back to `defaultLocale`. ## 3. Resolution (how a value becomes one string) For a given visitor locale, a locale map resolves in this order: ``` active → primary(active) → defaultLocale → primary(defaultLocale) → first string → "" ``` Matching is **case-insensitive** and falls back region→primary (`de-AT` → `de`). A plain string always returns itself. This single primitive (`resolveLocalized` in `@wisepunk/core`) is shared by **both** renderers (HTML string emitter and the Svelte components) through the common render plan, so the two can never diverge — the parity gate proves it. ## 4. Which language a visitor sees On every request the server resolves the active locale, in order: 1. an explicit **`?lang=`** query override, 2. the visitor's **`Accept-Language`** primary subtag (the same value targeting can match on as `lang`), 3. the document's **`defaultLocale`**. Only locales the document declares in `locales` are honored. The page then: - renders all text in that language, - sets **``** and the **`Content-Language`** header, - emits **`hreflang`** alternate ``s for every declared locale (`?lang=`), so search engines and agents discover the translations. ## 5. The machine twin is localized too A MIDL page is dual-use — the agent/SEO surfaces resolve text the same way: - **JSON-LD** (`