Preview sections with `dev`
Preview sections in a browser as you author them — the canvas, the live template rendering, and the render endpoints an agent can drive.
See real output while you author: template-kit dev renders every section against
its fixtures the same way the platform does — server-side, with your template’s
theme and compiled Tailwind utilities — and reloads as you edit.
Its flags, exit codes and constraints are on its reference page. Run it from the workspace root or from inside a template’s own folder; running from inside a template makes it the template the canvas opens on.
Prerequisites
Section titled “Prerequisites”- A workspace with at least one section and its fixtures — Install creates one.
What it needs installed
Section titled “What it needs installed”npm i -D @homepages/template-cli # provides the `template-kit` commandnpm i -D vite @vitejs/plugin-react tailwindcss @tailwindcss/clinpm i -D @homepages/dev-media # sample photos/plans/videos for the media libraryLike check, dev runs your toolchain, resolved from your
workspace — Vite renders and reloads your sections, and Tailwind compiles your
theme’s utilities. None of these are dependencies of the kit; they resolve from your
install, so the versions you pinned are the ones that run. A missing one is reported
by name with the line to install it. @homepages/dev-media is optional — the server
still runs without it, but every media library preview 404s.
Screenshots (below) additionally need Playwright, which is optional:
npm i -D @playwright/test && npx playwright install chromiumWhat you get
Section titled “What you get”The browser lands directly on the canvas (/ normalizes to
/canvas/<template>): the whole template inside the editor canvas, opening on
the default template. The edit panel’s Template selector switches templates,
and the panel’s footer carries the platform’s own actions:
Which actions appear depends on the output class, exactly as they do in the platform — a template’s footer here offers what a user of that template will be offered, and nothing else:
- View live — a website only:
/template/<template>, its home page as it publishes, in a new tab. A rendered class has no View live, because its output is a file to keep rather than a page to visit. - Download — the file a user receives, for the rendered classes. An image template downloads every page of the canvas as one zip; a document template downloads its whole page set as one PDF. Both require Playwright (below).
- Publish — a website only, rendered as the platform renders it but permanently disabled: the playground has nothing to publish to. A rendered class shows no Publish at all, because it has none.
Both surfaces are near-chromeless and URL-keyed, so a link to either reloads and shares exactly the view you’re looking at.
Live reload
Section titled “Live reload”The server watches your template files. Editing a section reloads any open preview of it; editing a theme recompiles its Tailwind utilities and hot-swaps the stylesheet without a full reload.
Interactivity (islands)
Section titled “Interactivity (islands)”A section’s "use client" components hydrate in the preview. The server renders
the section — including each client component’s initial HTML — then the browser
loads React and hydrates only those components, so they become interactive while
the rest of the page stays static. A section with no "use client" component
loads no React at all. Editing a client component reloads the preview, re-rendered
and re-hydrated. See Islands for the client-component contract.
The preview hydrates on the same viewport priority schedule a published page runs: on the full-template view, islands on screen come alive first, an island hydrates as you scroll toward it, and the rest follow in idle slices. Dev is still slower in absolute terms — Vite serves an unbundled module graph, and an off-screen island’s code is fetched as you approach it rather than in the background — so treat this as the shape of deployment’s loading, not its speed.
Overriding content
Section titled “Overriding content”A preview page accepts an overrides query parameter — base64-encoded JSON,
deep-merged onto the fixture’s resolved props — so you can probe a section against
content the fixtures don’t carry without editing fixtures.ts. The decoded JSON
shape mirrors the props themselves:
{ "slots": {}, "nav": { "selfAnchor": "", "anchors": {} }, "variant": "" }All three top-level keys are optional, and the set is closed — a key outside it is
rejected rather than ignored. nav.pages is deliberately not among them: the page list is
the composition’s, and an invented entry would render as a real page. Merging follows one
rule at every level: a
plain object merges key by key with the fixture’s value; anything else — a scalar,
an array, or null — replaces the fixture’s value outright. A key the override
omits keeps the fixture’s value; a key set explicitly to null replaces it with
null, so you can probe an empty/missing state a fixture doesn’t otherwise cover.
The encoded payload is capped at 16 KB.
Diagnostics
Section titled “Diagnostics”--diagnostics-cmd "<cmd>" wires an external command into the section preview
as a live violations banner. It’s unset by default — nothing changes unless you
configure it.
When set, opening /sections/<template>/<section>/<fixture> runs
<cmd> --template <t> --section <s> (the command string is split on
whitespace — no shell, no quoting — and --template/--section are always
appended as separate arguments). The command must print JSON to stdout:
{ "violations": [{ "rule": "some-rule", "message": "what's wrong", "file": "section.ts", "line": 12 }] }file and line are optional. A non-zero exit or unparseable stdout surfaces
as an error in the banner rather than crashing the server. If any violations
come back, the preview shows a dismissible banner at the top of the page; it
re-fetches (and reappears, if still violated) on every file-triggered reload,
the same way the rest of the preview already goes live on a change.
GET /api/diagnostics?template=§ion= is the underlying endpoint, if you
want to query it directly: {available: false} when no command is configured,
{available: true, violations: [...]} on success, {available: true, error}
when the command failed or its output didn’t parse.
Views: the full template, and the section render behind the canvas
Section titled “Views: the full template, and the section render behind the canvas”One page renders per URL, exactly as the published site serves it: the home page
at /template/<template>, and every other declared page at
/template/<template>/<page-path> (/template/aurora/gallery). Each renders that
page’s sections in template.ts composition order, at each section’s base fixture, with
the template theme and page shell applied and islands hydrating. A section listed on
several pages renders on each of them. Open the home page from the canvas
panel’s View live button (a website template’s, the only class with one), or
point a browser tool at any page’s URL directly.
Cross-page links work here as they do live: nav.pages and a nav.anchors entry
for a section on another page resolve against the harness’s own /template/<key>
mount, so clicking one lands on the page that serves it. (Cross-section reconcile
rules are a publish-time behavior and are not applied here.)
Every section also has a standalone render at
/sections/<template>/<section>/<fixture> — the document the /api/screenshot
endpoint targets for a single section. It is not a navigation destination in its
own right.
Canvas mirror
Section titled “Canvas mirror”/canvas/<template> — where the browser lands, or open it directly — renders
the template inside the platform’s own editor canvas
frame: rounded page cards that scale to fit the available width, on the
same dotted canvas surface and with the same selection chrome the platform’s
content editor uses. Each composed section instance loads independently and
is injected into the page in composition order as it arrives, so instances can appear
progressively rather than all at once; one instance failing to render shows an
error placeholder in its own spot without blocking the rest. A paint-only
overlay sits on top of the page, scoped per section instance —
the same slot name in two different sections selects independently. Click a slot
to select it (a gold ring); hover to preview one (a blue ring). Only the slots that
instance’s schema leaves editable select individually — a slot the section declared
locked: true does not — and clicking
anywhere else in a section selects that section instance as a whole. The overlay
never changes the rendered markup. To see the clean published render of a
website template instead, use the edit panel’s View live button; for a
rendered class, Download the artifact itself.
What you navigate between is the template’s canvas, not its page. A canvas is a
group of pages edited on one surface, declared by canvases in template.ts or
defaulted per output class: a website gets one canvas per page, an image or document
template one canvas over its whole page set. A template with more than one canvas
gets a canvas bar along the bottom of the stage, one tab per canvas in declared
order; a template resolving to a single canvas renders no bar at all. Switching
canvases clears the selection, since a selection belongs to the canvas it was made
on.
The canvas on view shows every one of its pages at once — one frame per page, stacked in a single centred column, each captioned with its page label, and each with its own selection chrome and hidden-section tabs. Every page renders with its own nav, so a section shared across pages shows each page’s own menu and cross-page links, and editing one of its slots updates every frame that renders it.
Every developer option sits in one card at the top of the edit panel: the Template selector, and a Fixture picker for whichever instance the panel is showing. An image or document template gets two more there — a Format switcher over its declared formats, and a Trim & bleed guides toggle — and its footer hands over the artifact rather than opening a page. What those two are for is Frames and formats.
The current template, canvas and selection are encoded in the URL as
/canvas/<template>[/<canvas-id>]?sel= — the third segment is the canvas’s id, and
the bare /canvas/<template> is the template’s first canvas. (A website’s default
canvas ids are its page ids, so that segment still names a page there.) sel names a
section instance (<instanceId>), a slot within it (<instanceId>~<slotId>), or
one item of a repeating slot (<instanceId>~<slotId>~<itemIndex>) — so a canvas
link reloads and shares exactly the view you’re looking at.
A floating edit panel, a fixed-width card anchored over the canvas’s right
edge, is always open and shows one section instance at a time: whichever
instance is currently selected, or — before any selection, or after Escape
clears the ring — the last instance shown, defaulting to the template’s first
composed instance on initial load. It shows that instance’s
editable slots as the platform’s own accordion cards, in the
platform’s card order and grouping — a group(...) card collapses its members
into one card, every other slot gets its own — and each slot inside a card gets
the real typed editor for its type: the same combobox, bullet-list builder, price
field or POI picker a user gets. Selecting a slot opens its card
and scrolls it into view. The media slot types — image, video, document, and lists
of any of them — get the same real asset library and crop editors a user gets, listing the
workspace’s dev media pack across its Photos, Floor plans, Videos and Documents tabs;
picking a library item and cropping it both work, with cropping producing a
real cropped preview rather than the uncropped original. A tour slot’s picker
lists the kit’s dev tour pool on its Tours tab, and a pasted public provider
link works there too (and on the Videos tab, for a video slot that admits
links): the dev server checks the URL against the platform’s admitted providers,
builds the embed, fetches the provider’s published thumbnail where there is one,
and saves the row in the workspace’s own gitignored .template-kit/ folder — so
a pasted tour survives a restart, stays out of your commits, and never joins the
dev media pack. A link no tour or video provider owns is refused with a hint
naming the providers that tab takes. A document slot is an attachment
card — file thumb or type glyph, filename, and a PDF · 2.4 MB meta line — with Replace
and Remove, and a list of them renders one such card per row, reorderable by drag. Two things the
playground cannot do: uploading a new file (there is no upload backend, so the
picker offers no Upload tile) and assigning a floor plan to a unit (a
property-editing act — the playground has no property, so that picker opens an
inert dialog explaining why instead of doing nothing). The POI picker searches a
small fixed set of obviously-synthetic places rather than a live places service;
picking one adds the same complete row a real pick would, and those places
deliberately differ in which optional fields they carry — one has no rating,
another no address — so a layout you build here meets the gaps real neighborhood
data has. The panel’s Fixture picker lists the whole fixture corpus of the
section the panel is showing, including its synthetic empty-content fixtures, so
you can drive a slot into an empty or missing state without touching
fixtures.ts. Applying an edit or switching the fixture re-renders that section
instance alone, the same way it renders in the stack, and swaps its HTML into
the iframe in place — no page reload, no other instance disturbed — then
re-anchors the selection ring to the refreshed markup, and re-hydrates that
instance’s islands. The kit dev server is the harness’s only backend, so this
loop never touches fixture files, a database, or another service. Edit state
lives only in the tab: nothing is written to disk, switching the fixture drops
the edits made against that instance — the way back to unedited content — and
reloading the page loses them all.
An island whose component declares editor.live: true (see Islands)
hydrates in the canvas the same way it does on a published page, but behind a
click-through shield until it’s selected: the first click on a live island
selects its owning slot or section instance instead of reaching the
component, dropping the shield; a second click reaches the component itself.
Pressing Escape clears the select ring and re-arms every shield; it does not
close the edit panel, which keeps showing the instance it was last showing.
An island with no editor export, or editor.live: false, stays static
markup in the canvas — same as a published page’s non-live islands. Some
islands own no slot of their own (e.g. a slider or map sitting beside a
marked element rather than carrying a marker itself); their shield only ever drops under a
section instance selection. Shields are derived from an island’s own markup —
one over each box the island root renders, one over each collection item copy
inside it — so what you already mark for editing is what a click selects.
The canvas publishes the selection exactly as the platform’s editor does, so
useEditor() (see Islands) reports the same thing in both — an
island that reveals what the author selected reacts here, and one that holds a
published-only takeover behind inCanvas stays held here rather than opening a
modal over the canvas. The mirror’s own selections stop at a collection item, so
a slot selection it broadcasts never carries a field name.
Agent-facing endpoints
Section titled “Agent-facing endpoints”These render a section — or the whole template — without a browser, for an agent verifying its work from a terminal.
/api/inspect— the low-context semantic view of a section×fixture as JSON: each declared slot with itstype,fillState(filled/empty), whether it actuallyrendered, a compactvaluepreview, how it resolved (provenance: thekind—fact/computed/authored/override— plus therefit came from), and the line its declaration sits on insection.ts; plus the resolvedvariantandnullCorpus(the synthetic empty-state fixtures, one per declared slot, you can walk to review every missing-content branch).provenanceis absent when you inspect one of thosenullCorpusfixtures directly — it is synthesized from the schema, not resolved from any binding, so it has no origin of its own to report. Anempty/missingslot that still showsrendered: trueis a broken empty shell. A slot you replace throughoverridesreportsprovenance: { kind: "override" }, so the view never claims a binding produced a value you wrote yourself. Takestemplate,section,fixture, the same base64overridesparameter as a preview page, andformat=textto add the indented structure outline./api/render— the full structured render payload for one section×fixture (html,resolvedProps,slotTree,islandMap,sectionSchema— that section’s schema as the platform serializes it: the full per-slot schema,slot_orderandgroups); GET takesoverrides, POST takes an unencoded JSON body. POST also accepts a composedinstanceIdin place ofsection+fixture: the server resolves that instance’s section and its default fixture the same way the whole-template render does, so re-rendering one instance matches how it renders in the stack. This is what the canvas mirror’s edit panel calls to apply a slot edit or switch a fixture. Both verbs also takepage=<page-path>, which renders the section with that page’snav— the menu and cross-page links a page’s own URL would give it; without it the section keeps the standalone preview nav, whose page list is empty./api/template-schema?template=<t>— the samesectionSchemafor the whole template: every composed section instance with its section schema, which is both the overlay’s editability map and the sidebar’s card model, pluspages— each declared page’s id, label, path and its ordered instance ids — andcanvases, the stamped editing surfaces (id,label, and the page ids each one stacks) that group them. The canvas mirror builds its overlay, sidebar and canvas bar from this endpoint, not from/api/render’s per-section copy, so none of them depends on which sections have rendered yet. A schema is listed once per instance however many pages carry it./api/screenshot— a PNG. Give ittemplate+section+fixturefor a section, ortemplatealone for the whole template; those are full-page shots andwidth=<px>sets the viewport (default 1280).template+page(rendered classes; optionalformat, default the first declared) instead captures the page’s frame at the format’s declared geometry — one page of the image class’s final output. Requires Playwright (npm i -D @playwright/test && npx playwright install chromium); without it the endpoint answers with the install line./api/bundle— an image template’s whole canvas as one zip, each page captured at the format’s declared geometry and named by its page label. Give ittemplate(+ optionalcanvasandformat, each defaulting to the first declared). This is what the canvas’s Download hands over for an image template, and what the platform hands a user. Requires Playwright, like/api/screenshot./api/print-preview— a document template’s whole page set as one PDF, printed by Chromium at the format’s trim+bleed geometry and streamed to the browser’s PDF viewer. Give ittemplate(+ optionalformat, defaulting to the first declared format); adddownload=1for the same PDF as a saved file, which is what the canvas’s Download asks for. Requires Playwright, like/api/screenshot./api/media-library.json— the sample media library’s listing: every photo, floor plan, library asset, video and document, each with anid,kind, a display name, and asrcunder/__dev-media/— a preview image, which for a video is its poster. A document instead carries its MIME type and byte count, and an emptysrc: it has no preview image at all, which is what every surface branches on to draw a type glyph. It answers even without@homepages/dev-mediainstalled; without it everysrc404s./api/catalog.json— the full template × section × fixture matrix.
Commands
Section titled “Commands”template-kit dev # every template in the workspacetemplate-kit dev <template> # the template the canvas opens ontemplate-kit dev --port <n> # serve somewhere other than 5180template-kit dev --diagnostics-cmd "<cmd>" # wire the violations bannerVerify
Section titled “Verify”Open the URL the server prints and you land on the canvas with your sections
rendered and the edit panel beside them. Browser-less, answer
the same two questions from the agent-facing endpoints
above: /api/catalog.json for what the server sees, /api/inspect for how one
section resolved.
This command is the second step of the author loop, so a preview that reflects your edit is what every other guide means by it.
Rules that can fire
Section titled “Rules that can fire”None — this guide changes no file check inspects.
See also
Section titled “See also”devreference — its flags, defaults and exit codes.- Frames and formats — the format switcher, the trim and bleed guides, and the authoring loop they exist for.
TokenTheme— the theme the preview compiles.fixtures.ts— the fixtures the preview renders and the props theoverridesparameter merges onto.- Run
check— the acceptance gate to run once a section previews cleanly. - Static assets — the preview server derives
photo ladders through the same on-disk cache
checkdoes, so a restart doesn’t re-encode a workspace’s photos from scratch.