Status: the contract behind specification §5.15, published 2026-10-01 with the format’s first
baseline revision; manifest 1.3 and data 1.2 (the project-only clock layout) the same day; data 1.3 and
engine 1.4 (the device’s 12- or 24-hour clock, clock.hourCycle) on 2026-10-02. The reference template and engine are in template/:
template/default/ is the package every reference client carries, and template/engine/ the shim
it embeds (with a vendored, unmodified copy of nunjucks 3.2.4,
BSD-2-Clause).
A session board template is how a Surface draws a session board (specification §4.7): a zip a Surface extracts and loads into a web view, which it feeds with one JSON data document per render. The template’s page renders the board; the Surface’s engine decides what the board says (now, next, the pages of the day) and when it changes. No server renders anything; any modern browser engine draws it. A template may also draw the clock a project-only Surface shows over the Show’s wallpaper (specification §5.15), as a third layout.
A zip, <id>-v<version>.marqueetemplate.zip, whose root holds:
template.json the manifest (§2)
index.html the page shell: the mount, the shared stylesheet, the engine
styles.css styles shared by every layout
layouts/<name>.html one nunjucks source per layout the template offers (§3)
layouts/<name>.css that layout's own styles, optional
fonts/*.woff2 | *.woff the template's typefaces (§6)
engine/marquee-template.js the shim (§4), versioned
engine/nunjucks.js the template language, vendored
preview.html, preview.png a self-contained preview and a picture, for pickers; never what a host loads
Everything a page references is inside the package, by a package-relative path. No external
URL is allowed in index.html, the stylesheets or the layout sources (a sign is offline); a
URL inside a comment is tolerated. A Surface refuses a package that breaks this, by name.
The template is trusted content. nunjucks does not sandbox: the people who author a template and the operator who attaches it are trusted, and the data document is autoescaped. The shim fetches nothing but the package; a host serves the package from an origin of its own (§7).
template.json — format 1.3{
"format": "1.3",
"id": "example-board",
"displayName": "Example — now / next and schedule",
"version": 3,
"basedOn": { "id": "example-board", "version": 2 },
"engine": "1.4",
"data": "1.3",
"layouts": {
"now-next": { "source": "layouts/now-next.html", "style": "layouts/now-next.css" },
"schedule": { "source": "layouts/schedule.html", "style": "layouts/schedule.css" },
"clock": { "source": "layouts/clock.html", "style": "layouts/clock.css",
"textRegion": { "x": 0.12, "y": 0.68, "w": 0.76, "h": 0.24 } }
},
"supports": { "orientations": ["portrait", "landscape"], "aspects": ["9x16", "16x9", "3x4", "4x3"] },
"textRegion": { "x": 0, "y": 0, "w": 1, "h": 1 },
"roles": {
"title": { "label": "Title", "default": "text.auto", "contrast": "large" },
"name": { "label": "Name", "default": "text.auto", "contrast": "large" },
"description": { "label": "Description", "default": "text.autoMuted", "contrast": "large" },
"clock": { "label": "Clock", "default": "text.auto", "contrast": "large" },
"accent": { "label": "Accent", "default": "palette.primary" }
},
"vars": {
"sponsorName": { "label": "Sponsor line", "default": "" }
},
"modifiers": {
"align": { "kind": "choice", "label": "Text alignment", "options": ["left", "center", "right"], "default": "left" },
"compact": { "kind": "toggle", "label": "Compact spacing", "default": false }
},
"brand": {
"displayName": "Example",
"cssFamily": "Inter",
"palette": { "primary": "#3B82F6", "secondary": "#1E3A8A", "tertiary": "#F59E0B", "quaternary": "#64748B" },
"text": { "onLight": "#111827", "onDark": "#F9FAFB", "mutedOnLight": "#4B5563", "mutedOnDark": "#C7CCD4" }
},
"fonts": [
{ "cssFamily": "Inter", "licence": "SIL Open Font License 1.1 (fonts/Inter-OFL-1.1.txt)",
"faces": [
{ "file": "fonts/Inter-Regular.woff2", "weight": 400, "style": "normal", "postScriptName": "Inter-Regular" },
{ "file": "fonts/Inter-Bold.woff2", "weight": 700, "style": "normal", "postScriptName": "Inter-Bold" }
] }
],
"files": { "index.html": "sha256:…", "styles.css": "sha256:…", "layouts/now-next.html": "sha256:…" }
}
| Field | Rule |
|---|---|
format, engine, data |
The manifest, shim and data-document formats, major.minor. A host reads the major it knows; a newer minor is accepted. |
id |
^[a-z0-9-]{1,64}$. version a positive integer. A package is immutable once exported: a change is a new version. basedOn records what it was adapted from. |
layouts |
The layouts the template offers, an object keyed now-next, schedule and / or clock. Each names its source (a nunjucks fragment, .html) and an optional style (.css), package-relative, no .., no dot segment, and may name its own textRegion (below). A sign chooses which of the two boards it shows (specification §5.15); a template offers one board layout or both. clock (manifest 1.3) is the project-only Surface’s date and time over the Show’s wallpaper (§4, §7); a template that does not offer it leaves the clock to the Surface’s default template. |
supports, textRegion |
Advisory: the orientations and aspect families the template was designed for (it still renders elsewhere), and where its text falls on the canvas — { x, y, w, h }, fractions 0…1 — for a host that measures its backing there (§7). layouts.<name>.textRegion (manifest 1.3) overrides the top-level one for that layout: a clock in a band at the foot of the screen is read there, not across a board’s whole canvas. |
roles |
Named colours the template draws with (§5). default is a brand slot (palette.primary …), a text colour (text.onDark …), text.auto / text.autoMuted (the text pair that reads over the backing), or #RRGGBB. |
vars |
Named variables with a label and a string default: content the Show fills (specification §5.15 template_settings.vars). |
modifiers |
The template’s style switches, chosen by the sign (§5). choice: one of options, the class mod-<name>-<option>. toggle: on or off, the class mod-<name> when on. Names and options ^[a-z0-9-]{1,32}$; marquee is reserved as a prefix; default is required and must be an option (or a boolean). |
brand |
The palette and text pair the template was authored against; cssFamily its family. The template’s brand travels inside the package. |
fonts |
Each family’s faces as files in fonts/, with weight, style and postScriptName; the shim writes @font-face from this (§6). |
files |
SHA-256 of every text file and font, so a tampered package is refused by name. template.json, the engine and the previews are never hashed. |
A layout source is a nunjucks template that renders the whole board for that layout into the
page’s mount, <div id="marquee-root">, from the data document (§4) as its context. The shim loads
every declared source from the package at page load, compiles it once, and on each update renders
the one whose name is the document’s board.renderMode. A layout’s style, when named, is linked
into the page before the first render. Nothing else in the page is touched.
The shim’s filters: time and date (an instant in the venue zone, as the Surface’s own board
writes them — time with the document’s clock.hourCycle, data 1.3), role (a role’s resolved colour). The template language is nunjucks as documented
publicly; the include tag is never used with a path (every source is a file the manifest names).
The clock layout renders from the same context as a board: clock.time and clock.date are the
show clock in the venue zone, board.header the Show’s name, and there are no slots.
1.3One JSON object per render. The Surface’s engine produces it from its resolution of the session set (specification §4.7, §5.8): the engine decides what a render needs and delivers that subset. A schedule of several pages is several renders, one per page, each carrying that page’s rows; the template pages nothing and filters nothing.
{
"format": "1.3",
"clock": { "showNow": 1789570800000, "zone": "America/Los_Angeles", "projected": false,
"hourCycle": "h12", "time": "8:45 a.m.", "date": "Tuesday, September 15" },
"board": { "renderMode": "schedule", "header": "Main hall", "resolvedAt": 1789570800000,
"pageCount": 3, "page": 0,
"nowNext": null, "signage": null,
"schedule": { "roomName": "Main hall", "rowCount": 12,
"rows": [ { "id": "s-1", "title": "Opening", "time": "9:00 a.m.", "status": "now" } ] } },
"slots": [ { "id": "s-1", "title": "Opening", "abstract": "…", "startTime": "9:00 a.m.", "endTime": "9:45 a.m.",
"isNow": true, "isNext": false,
"presenters": [ { "name": "A. Example", "company": "Example Co", "title": "CTO" } ], "attributes": [] } ],
"vars": { "sponsorName": "Example Co" },
"modifiers": { "align": "right", "compact": true },
"brand": { "displayName": "Example", "cssFamily": "Inter", "ink": "onDark",
"palette": { "primary": "#3B82F6" }, "text": { "onLight": "#111827", "onDark": "#F9FAFB" } },
"roles": { "title": "#F9FAFB", "name": "#F9FAFB", "description": "#C7CCD4", "clock": "#F9FAFB", "accent": "#3B82F6" },
"canvas": { "width": 2160, "height": 3840, "host": "apple" }
}
| Key | Meaning |
|---|---|
clock |
The engine’s show clock (specification §8), in the venue zone — never the device’s clock. projected is true when the clock is not live (a preview). hourCycle (data 1.3) is the device’s setting for how a time is written, and every time in the document is already written with it: h12 “8:45 a.m.” (no leading zero, lowercase with periods), h23 “08:45” (two-digit hours and minutes, no suffix; European deployments). A document without it is h12. |
board |
The resolved board for THIS render: renderMode names the layout (now-next, schedule or clock); page / pageCount which page this is; nowNext + signage for a now / next render, schedule with this page’s rows for a schedule render; the other is null. A clock render (data 1.2) is one page: { renderMode: "clock", header: <the Show's name>, page: 0, pageCount: 1, resolvedAt, nowNext: null, signage: null, schedule: null }, with slots: []. |
slots |
The sessions this render shows, normalised: presenters as { name, company, title }, times as strings in the venue zone, isNow / isNext. |
vars |
Every declared variable with the Show’s value, else its default (specification §5.15). |
modifiers |
Every declared modifier with the device’s value, else its default: an option name for a choice, a boolean for a toggle. |
brand, roles |
The package’s brand with ink — the text pair that reads over the backing the host measured (onLight or onDark) — and every role resolved to #RRGGBB. |
canvas |
The destination’s own pixel size and the host’s name (apple, browser, or another host’s). Orientation, aspect family and size tier are derived by the shim, not sent. |
Keys are sorted and numbers are integers, so two producers of the same render write the same bytes.
<html>Before every render the shim sets, on the root element only:
marquee-portrait · marquee-landscape · marquee-square;
the nearest aspect family marquee-aspect-9x16 · 16x9 · 3x4 · 4x3 · 1x1 · marquee-aspect-other;
the size tier by the long edge marquee-size-uhd (≥ 3840) · fhd (≥ 1920) · hd (≥ 1280) · marquee-size-small;
the host marquee-host-<name>.marquee-ink-light / marquee-ink-dark (brand.ink);
marquee-layout-now-next / marquee-layout-schedule / marquee-layout-clock (board.renderMode); marquee-projected.mod-<name>-<option> for each choice, mod-<name> for each
toggle that is on — validated against the manifest; a value that is not an option falls to the
default with a warning, never an unknown class. The marquee- prefix is reserved for the shim.--marquee-canvas-width, --marquee-canvas-height, --marquee-scale
(the long edge ÷ 3840); --marquee-role-<role> for every role; --marquee-palette-<slot>;
--marquee-text-<pair>; --marquee-ink, --marquee-ink-muted.A template keys its CSS on these and writes no JavaScript for it. The resolved modifiers are also on
the render context as modifiers.
The shim writes one @font-face per declared face from template.json, loads every face
before reporting ready (a face no text has used yet would otherwise never be fetched), and lists
the faces that did not resolve in ready.missingFonts. A host passes a template’s fonts to no
system font registry: they are the page’s.
A Surface rendering a template:
template.json breaks the rules of §2, or that lacks a declared file, is refused by name; the
next template of the resolution applies (specification §5.15)... and dot-segments are not found). Loading from file:// is not conforming: a browser
engine makes every file its own origin there.index.html in a web view sized to the board’s canvas at CSS scale 1 and a transparent
background, and calls marquee.configure({ host }) once.ready, then pushes one document per render with marquee.update(document):
on the board’s issue (its anchor page), on each page turn, on the venue minute for a now / next
layout, on a cartridge commit, and on a change of orientation or of the device’s board variant.
For the variant both the first page is now-next and the rest schedule
(specification §5.15). A document’s modifiers are the device’s settings for this template;
its vars the Show’s template_settings; its clock.hourCycle the device’s time format (12-
or 24-hour), which the host writes every time with — never the show’s.rendered, presents the page: a native host captures it into its text layer, a browser
host reveals it. The engine’s first-frame rule applies to the first rendered after issue; a
later page that never renders keeps the previous page up.textRegion says its text falls (the brand rules a
Surface already keeps), and sends the reading as brand.ink. A board waits for the reading.template.load_failed, template.render_error,
template.no_first_frame, template.variant_unavailable.The project-only clock (specification §5.15, data 1.2). A Surface with no surface code draws its
date and time with the clock layout of the Show’s template — project.template_item_id in
project.db, taken and extracted as above — when that template offers one, else with its default
template’s. It pushes one clock document on the venue minute and on a cartridge commit, over the
Show’s wallpaper for its orientation (none is black); it measures the wallpaper where the clock
layout’s textRegion says the text falls and sends the reading as brand.ink, and the clock waits
for the reading and for the wallpaper to be on screen, as a board waits for its backing. A clock
that fails to load or render falls to the default template’s, then to nothing drawn over the
wallpaper — never a stale minute.
Host → page: marquee.configure({ host }), marquee.update(document).
Page → host: one channel, { type: "ready" | "rendered" | "error" | "log", … } — a
WKScriptMessageHandler named marquee where one exists, else a CustomEvent named marquee on
window. ready carries { engine, data, layouts, missingFonts, host }; rendered
{ page, pageCount, renderMode }; error the message, never thrown into the host. The page runs no
timers and pages nothing: cadence is the host’s.
template.json not an object · format / engine / data not a known major · id not
[a-z0-9-]{1,64} · layouts: none declared · layouts.<name>: "x" is not one of now-next, schedule, clock ·
[layouts.<name>.]textRegion.<x|y|w|h>: … is not a fraction 0…1 · [layouts.<name>.]textRegion: an empty region ·
layouts.<name>.source: "…" is not a package-relative .html path · <file>: declared as the "<name>"
layout's source, not in the package · fonts[i].faces[j].file: … is not fonts/<name>.woff2 or .woff ·
<file>: declared, not in the package · <file>:<line>: references an external URL — a sign is
offline · <file>: hash … is not template.json's … · modifiers.<name>: a modifier name is 1–32
lowercase letters, digits and dashes, and never starts with "marquee" · modifiers.<name>.default:
"…" is not one of its options · modifiers.<name>.kind: "…" is not choice or toggle.
The reference implementation of these rules, the shim and the default template are in
template/; a host that refuses what they refuse, and renders what they render,
conforms.