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 uses renderer-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 + 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.

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; ui points at the UI component vocabulary.
  • frames — array of components. Each: { id, type, ...fields }. A ui:Page lists child ids in children; children render in that order.
  • Verified starter components: ui:Page (children), ui:Hero (title<h1>), ui:Heading (level 1–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, 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/<slug>.midl.json — the raw MIDL document for a page.
  • GET /render/<slug>.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 imports 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 (+ <style>${themeCss}</style>) 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 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.