---
name: midl
description: Build personalization-first, agent-native websites with MIDL — author pages as JSON "frames", render them to HTML with the public @wisepunk/* npm packages, theme them, validate with a round-trip loop, and serve/manage them via the MIDL write API. Use when the user wants to build, render, validate, theme, or deploy a MIDL site or page; when they mention MIDL documents, frames, packs, the @wisepunk packages, or describeVocabulary; or when they want a self-hostable, machine-readable web page.
---

# Building with MIDL

MIDL is a **personalization-first, agent-native web format**. A page is a plain-JSON document of semantic **frames** (components); a **pack** turns it into HTML — the document stays the same, the pack decides the look. Every MIDL page is dual-use: human HTML and a machine-readable document off the same source. You build with the public **`@wisepunk/*`** packages (no auth, plain `npm install`).

## The golden rule

**Don't guess the spec — introspect it, and round-trip through the validator.** The component vocabulary is machine-readable, and every render/validate call returns structured feedback (`errors` AND `warnings`) that names the fix. 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.

## 1. Install

```sh
npm install @wisepunk/renderer-html @wisepunk/core   # render to an HTML string (framework-free)
# optional: @wisepunk/renderer-svelte (Svelte components) · @wisepunk/sdk (typed write-API client)
```
ESM only — set `"type": "module"` (`npm pkg set type=module`). Node 18+.

## 2. The document

```js
const doc = {
  midl: '0.1',
  ns: { ui: 'https://midl.ai/ui#' },
  frames: [
    { id: 'page', type: 'ui:Page', children: ['hero', 'features', 'cta'] },
    { id: 'hero', type: 'ui:Hero', title: 'Build with MIDL' },      // title → <h1>
    { id: 'features', type: 'ui:Heading', level: 2, text: 'Why MIDL' },
    { id: 'cta', type: 'ui:Button', text: 'Get started', href: '/start', variant: 'default' }, // href → <a>
  ],
};
```
- A frame is `{ id, type, ...fields, children? }`. `type` is namespaced (`ui:Hero`).
- **`children` is an array of frame IDs**, not nested objects — those frames exist elsewhere in `frames`.
- Structure lives in the document: use `ui:Heading` (`level` 1–6), `ui:Hero` (`title`→`<h1>`, `size`), `ui:Label` (`text`+`for`), `ui:Stack` (`direction: row|column`), `ui:Section` (`width: content|full`), `ui:Select` (`options: string[]`), `ui:Accordion` (`summary`→`<details>`). Don't push structure/typography into a host shell.

### Multilingual (native i18n)

Any human-text prop may be a plain string **or** a locale map `{ en, de }` — declare `defaultLocale` + `locales` at the document root and the renderer resolves each value to the visitor's language. Plain strings are unchanged (backward-compatible).

```js
const doc = {
  midl: '0.1', ns: { ui: 'https://midl.ai/ui#' },
  defaultLocale: 'en', locales: ['en', 'de'],
  frames: [
    { id: 'hero', type: 'ui:Hero', title: { en: 'Ship faster', de: 'Schneller liefern' } },
    { id: 'cta', type: 'ui:Button', text: { en: 'Get started', de: 'Loslegen' } },
  ],
};
```
- Visitor language = `?lang=` → `Accept-Language` → `defaultLocale` (only declared `locales` are honored). The render path sets `<html lang>` + `hreflang`, and the JSON-LD/Facts twin is localized too.
- Resolution: `active → primary(active) → defaultLocale → primary(defaultLocale) → first → ""`. A missing translation for a declared locale is a **warning**, not an error. Full reference: `/docs/localization`, and `describeVocabulary().localization`.

## 3. Render

```js
import { renderMidl } from '@wisepunk/renderer-html';
const { html, themeCss, validation } = renderMidl(doc, { pack: '@midl/pack-minimal-html', theme });
```
- `options.pack` — a **literal pack id string**, NOT an npm package: `@midl/pack-minimal-html` (bare semantic tags, zero CSS — best to start) or `@midl/pack-tailwind-shadcn` (Tailwind + shadcn classes + `themeCss` vars).
- `result.html` — the HTML string; `result.themeCss` — `:root { … }` vars; `result.validation` — `{ valid, errors, warnings }`.

## 4. Discover the vocabulary (don't read docs — introspect)

```js
import { describeVocabulary, EXAMPLE_DOCS } from '@wisepunk/core';
const vocab = describeVocabulary();
// vocab.components[]: { type, requiredFields, leaf, allowsChildren, container, acceptsText, textProp?, localizable, variants?, packs: { <id>: { tag } }, example }
// vocab.localization: { model, defaultLocaleProp, localesProp, localizableProps[], resolution } — the native-i18n contract
```
`EXAMPLE_DOCS` is an array of `{ name, description, doc }` (`landing`, `contact-form`, `article`) — every `doc` is test-asserted valid against both packs; copy and mutate them.

Against a live MIDL deployment, the same data is served (origin-rooted): `GET /llms.txt`, `/api/agents/vocabulary.json`, `/api/agents/examples.json`, `/api/agents/openapi.json`, `/.well-known/agent.json`, `/render/<slug>.midl.json`, `/render/<slug>.facts.json`. Read `https://<deployment>/docs` for the full reference.

**Importing a Figma design?** Use the **`figma-to-midl`** skill (`skills/figma-to-midl/SKILL.md`) — it drives the Figma MCP server, maps the design to MIDL frames + a generated theme, and runs the validate→repair→fidelity loop. Deterministic transform CLI: `pnpm import:figma`.

## 5. The authoring loop: validate → repair → render

```js
import { validateMIDL, validateAgainstManifest, repairAgainstManifest, getPack } from '@wisepunk/core';
const pack = getPack('@midl/pack-minimal-html');
validateMIDL(doc);                               // structural: { valid, errors, warnings }
validateAgainstManifest(doc, pack.manifest);     // + pack constraints + unconsumed-prop warnings
const { doc: fixed, repairs } = repairAgainstManifest(doc, pack.manifest); // auto-fix structural issues
```
Write → validate → fix what `errors`/`warnings` say (or `repairAgainstManifest`) → re-render. Trust the validator over your assumptions.

## 6. Theming

Pass `options.theme` (DESIGN.md tokens; a plain string `{ 'colors.primary': '#34d399' }` or `{ value }` both work), or embed a `ui:StyleToken` frame and the document self-themes. Foregrounds auto-derive to pass WCAG AA; override one with its `*.text` key (e.g. `colors.muted.text`). For `tailwind-shadcn`, spread `tailwindThemeExtend()` (from `@wisepunk/core`) into your `tailwind.config`.

## 7. Serve a localhost site (minimal, no framework)

```js
// server.mjs
import { createServer } from 'node:http';
import { renderMidl } from '@wisepunk/renderer-html';
import { doc } from './home.mjs';
createServer((_req, res) => {
  const { html, themeCss, validation } = renderMidl(doc, { pack: '@midl/pack-minimal-html' });
  if (!validation.valid) console.warn('MIDL issues:', JSON.stringify(validation.errors));
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
  res.end(`<!doctype html><html><head><meta charset="utf-8"><style>${themeCss}</style></head><body>${html}</body></html>`);
}).listen(3000, () => console.log('→ http://localhost:3000'));
```

## 8. Author/manage stored pages (write API)

```js
import { createClient } from '@wisepunk/sdk';
const midl = createClient({ baseUrl: 'https://<deployment>', 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' });                          // reads are public
```

## Checklist before you ship
1. `renderMidl(doc, { pack }).validation.valid === true` (fix `errors` first).
2. `validation.warnings` is empty, or you understand each (a warned prop won't render).
3. Headings/labels/structure are in the document, not the host CSS.
4. Pack id is a string passed to `renderMidl` (you did NOT `npm install @midl/pack-*`).
