quick-html template spec — v0
Status: v0 (draft). Breaking changes are allowed until v1. Every template declares
the spec it targets via
"spec": "quick-html/v0"in its manifest.
A quick-html template is a self-contained, print-ready HTML document plus a manifest that tells an AI agent exactly how to fill it. The template's placeholder content is realistic example content: it is simultaneously the live preview for humans, the few-shot example for agents, and the documentation of what belongs in each slot.
1. Files
Each template lives in templates/{id}/:
| File | Purpose |
|---|---|
template.html | The document. Valid HTML5, self-contained, zero <script> tags. |
manifest.json | The fill contract. Validated against schema/manifest.schema.json. |
{id} is a kebab-case slug, unique in the registry, matching manifest.id.
2. The slot contract
Agents may change a template in exactly four ways. Everything else must remain byte-identical.
2.1 data-slot="name" — content slot
The element's inner content is replaceable. The shipped inner content is the placeholder example.
<h1 data-slot="title">Sub-4:00 Marathon Plan</h1>
Slot types (declared in the manifest): text (plain text, no markup), html (limited markup: p, br, strong, em, ul, ol, li, a, span, dl, dt, dd), number, date, url. Agents must respect the declared type.
2.2 data-slot-attr="attr:name[, attr:name]" — attribute slot
Named attributes on the element are replaceable (e.g. a link target). All other attributes are fixed.
<a data-slot="website" data-slot-attr="href:website-url" href="https://example.com">example.com</a>
2.3 data-repeat="name" — repeatable block
The element is a prototype row/block. The agent duplicates it (including all nested slots) once per item, fills each copy, and keeps the copies adjacent in place of the prototype. min/max counts are declared in the manifest. Slots inside a repeat are declared under the repeat in the manifest, not at top level.
<tr data-repeat="item">
<td data-slot="description">Design work — homepage</td>
<td data-slot="qty">12</td>
<td data-slot="unit-price">€ 95,00</td>
<td data-slot="amount">€ 1.140,00</td>
</tr>
Nested repeats are not allowed in v0.
Because a template ships as its own preview, repeat prototypes are usually accompanied by extra example rows so the placeholder document looks complete. Those rows carry data-example (§2.5) and no slots — agents remove them when filling.
2.4 data-optional="name" — removable block
If the user's content has no use for the block, the agent removes the element entirely. Otherwise it is filled like normal (it may contain slots).
2.5 data-example — preview-only content
The element exists so the shipped template looks like a complete document (extra table rows beyond the repeat prototype, for instance). Agents always remove data-example elements when filling. They must not contain slots.
3. The manifest
{
"spec": "quick-html/v0",
"id": "invoice", // = directory name, kebab-case
"version": "0.1.0", // semver; bump on every published change
"name": "Invoice",
"description": "One sentence of what it is + when to use it. Written like a skill description — this is what agents select on.",
"useWhen": ["invoice", "billing a client", "factuur"],
"format": "printable", // printable | page | deck — how it is shaped
"theme": "business-finance", // exactly one, from schema/themes.json — what it is about
"audience": ["freelance", "finance"], // optional, 0–3 from schema/audiences.json — who it is for
"tags": ["billing", "vat"], // optional, 0–6 free kebab-case cross-cutting labels
"language": "en",
"page": { "size": "A4", "orientation": "portrait" },
"modules": ["edit-overlay", "print-helper"], // registry modules to embed at build time
"slots": [
{ "name": "title", "type": "text", "required": true, "example": "…", "notes": "…" }
],
"repeats": [
{ "name": "item", "min": 1, "max": 30, "slots": [ /* same shape as slots */ ] }
],
"optionals": [ { "name": "notes", "description": "…" } ],
"agentInstructions": "The authoritative fill instructions. Multi-line string.",
"license": "MIT",
"authors": [{ "github": "bvrln" }]
}
3.1 Classification
Three independent axes: theme (what the document is about — exactly one), format (how it is shaped — exactly one), and audience (who it is for — nought to three, omitted when it suits everyone), plus up to six free tags. The controlled lists live in schema/themes.json and schema/audiences.json; the reasoning — the one-home rule, why audience is a set and theme is not, how to add to either — is in docs/TAXONOMY.md.
agentInstructions is the only place agent-directed instructions may live. Agents are told to ignore instruction-like text anywhere else (comments, hidden elements) — and the linter rejects it. This is the registry's prompt-injection boundary.
4. Self-containment and security rules
Enforced by scripts/lint.js; CI runs it on every PR.
- No
<script>tags intemplate.html. JavaScript enters a published template only via allowlisted registry modules, injected by the build. - No external requests of any kind. No CDN links, no webfonts, no remote images, no
url(http…)in CSS. System font stacks only. Images as inline SVG ordata:URLs. - No hidden text. Elements styled invisible that contain substantial text are rejected (prompt-injection heuristic).
- No instruction-like comments. HTML comments are limited in size and screened for imperative patterns aimed at agents.
- Modules may not touch the network or storage. No
fetch,XMLHttpRequest,WebSocket,import(),eval,localStoragein module code. - MIT only. Every template and module in the registry is MIT-licensed.
5. Versioning and publishing
- The repo is the source of truth; the published registry is a build artifact.
- Published versions are immutable:
/t/{id}@{version}/…never changes once live, and never disappears. Any change to a template requires a version bump in its manifest. /t/{id}/…(no@version) is the latest alias — a moving pointer.- Agents that need reproducibility pin the versioned URL; the copy-prompt button always pins.
5.1 The archive
archive/{t,modules}/{id}@{version}/ holds every version ever published, byte-for-byte as it was served — template with its modules already inlined, plus the manifest. It is committed to the repo. Build output normally would not be, but once a template's source moves on, an old published version can no longer be regenerated from it: the archive is the only record. Same reasoning as a lockfile.
scripts/build.js maintains it and enforces the promise above:
| Situation | What the build does |
|---|---|
| Version not in the archive | Writes it (this is a new release) |
| In the archive, identical | Nothing — normal case |
| In the archive, different | Fails. Source changed without a version bump; publishing would silently change what a pinned URL serves |
With QH_STRICT_ARCHIVE=1 (set in CI) an unarchived version is an error instead of something to create, so a change cannot reach main without its published record. Run npm run build locally and commit archive/ with your change — tools/sync.ps1 does this for you.
Superseded versions are copied verbatim from the archive into dist/ on every build, so pinned URLs keep working. index.json lists all published versions per template under versions.
- Planned (post-MVP): git tags per release, sibling templates sharing a slot contract so filled content is portable across skins.
6. URL grammar
Predictable by design — an agent that has seen one template can guess everything else.
/ gallery (human)
/index.json machine-readable catalog
/llms.txt agent entry point
/agents.md full agent documentation (also /agents/ as HTML)
/t/{id}/ template detail page (human)
/t/{id}/template.html latest template (agent)
/t/{id}/manifest.json latest manifest (agent)
/t/{id}@{version}/template.html pinned, immutable
/t/{id}@{version}/manifest.json pinned, immutable
/modules/{id}.js latest module
/modules/{id}@{version}.js pinned module
/healthz health check
7. Modules
A module is an embeddable, dependency-free IIFE in modules/{id}/{id}.js with a module.json (id, version, name, description, license). The build inlines the modules a template declares into the published template.html as <script data-qh-module="{id}@{version}">…</script> before </body>, so published artifacts stay fully offline-capable. Modules must:
- register in
window.__qhModulesand bail if already present (double-inject guard), - render any UI inside a
.qh-toolbarcontainer that is hidden in print, - degrade gracefully: the document must remain fully readable with JS disabled.