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. 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 usesrenderer-html. - A typed write-API client,
@wisepunk/sdk, is optional (used in step 6 to pull a live page).
renderMidl(doc, { pack, theme }) → { html, themeCss, validation }
1. Prerequisites
- Node 18+ (for built-in
fetch, used in step 6). Verify:
node -v # expect v18 or higher
2. Create the project
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.
// 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:
// 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(
`<!doctype html><html><head><meta charset="utf-8">` +
`<title>My MIDL site</title><style>${themeCss}</style></head>` +
`<body>${html}</body></html>`
);
}).listen(PORT, () => console.log(`MIDL site → http://localhost:${PORT}`));
Run it:
node server.mjs
Verify (in another terminal):
curl -s http://localhost:3000 | grep -o '<main>.*</main>'
# → <main><section><h1>My first MIDL site</h1></section><button>Get started</button></main>
The Hero's title renders as a real <h1> (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 +themeCssCSS 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 withminimal-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.
npm install @wisepunk/sdk
// 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);
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;uipoints at the UI component vocabulary.frames— array of components. Each:{ id, type, ...fields }. Aui:Pagelists childids inchildren; children render in that order.- Verified starter components:
ui:Page(children),ui:Hero(title→<h1>),ui:Heading(level1–6 +text→<h1>–<h6>),ui:Text(text→<p>),ui:Button(text,variant). The authoritative component set + their allowed fields live in the pack's manifest — if a field/type isn't allowed,renderMidlreturnsvalidation.valid === falsewithvalidation.errors(an array of messages) describing the problem. Always checkvalidationrather 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 toresult.themeCss(:root { … }).result.html— the rendered HTML string.result.themeCss—:rootCSS variables for the theme (empty string when no theme given).result.validation—{ valid, errors }from checking the doc against the pack manifest (errorsis a string array).
Machine-readable surfaces (for discovery)
Against a live deployment (e.g. https://midl-web.rene-dad.workers.dev):
GET /render/<slug>.midl.json— the raw MIDL document for a page.GET /render/<slug>.facts.json— extracted facts (agent/RAG view).GET /api/agents/openapi.jsonand/.well-known/agent.json— the API + tool descriptions.
Agent gotchas
- ESM only: set
"type": "module"(npm pkg set type=module) or theimports fail. - Packs aren't npm packages: don't
npm install @midl/pack-*— pass the id string torenderMidl; it's bundled in@wisepunk/core. renderMidlreturns a string, not a server — you servehtml(+<style>${themeCss}</style>) however you like.minimal-htmlneeds no CSS;tailwind-shadcndoes — useminimal-htmlfor the first run to avoid an unstyled page.- Node 18+ for the SDK's
fetch(step 6). - Trust
validation— ifvalidation.validis false, fix the document to match the manifest before assuming a render bug.
Done check
node server.mjslogsMIDL site → http://localhost:3000.curl -s http://localhost:3000 | grep -q 'My first MIDL site'exits 0.- No "validation issues" in the server log.
All three → you've built and served your first MIDL site. Next: read 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.