unify docs

Authoring a unify site — the complete rules

unify composes plain HTML at build time. No template language, variables, loops, or config: if you reach for {{ }}, {% %}, props, or a config key, you are solving it wrong. The vocabulary is standard HTML — <main>, <slot>, slot= — plus <include> and data-layout. Derived files (a post index) come from a script you write and run yourself: node _scripts/gen.mjs && unify build. A feed at /feed.xml needs no script: declare schema: Article or BlogPosting (below) on any page and build with --base-url, and unify writes it — Atom, from your title/description/canonical/dates; a date: with no time is reported and left out rather than guessed at.

Files

Include — reuse a fragment

<include src="/_includes/nav.html"></include>, always with the closing tag; /… resolves from the source root, anything else relative to the including file. Works in any file — layouts, pages, <head>, fragments, and .md — but never inside <pre>/<code>: there both spellings are left as written (a code sample is content). Empty, it splices the file in verbatim. Content between the tags fills slots — allowed only when the target is a *.fragment.html declaring <slot>, and filled exactly as a page fills a layout's (slot="name" on a top-level element, everything else to the bare slot, an unfilled slot showing its own fallback). Fills reach that fragment's slots and no deeper. No props, no attributes passed, no expressions: an include is still not a component.

Layout — chrome around a page

Every page is wrapped by the nearest _layout.html — its own folder, then each parent; the page says nothing. Pick one with data-layout="/path.html" on the page's <html> or <body> (Markdown: layout: /path.html); opt out with data-layout="none" / layout: none. Layouts are paths ending in .html — a bare name like default is an error. data-layout belongs on pages only: anywhere else it is an error — on a layout too, because layouts don't chain (a section layout is a complete standalone page).

Merging a page into its layout

Markdown

Frontmatter is YAML: quote any value containing a colon — title: "Finish: the last quarter". title, layout, class (on <body>), lang, dir, and schema are the only keys with meaning; every other becomes <meta name=…> with the value as written — except draft, permalink and slug, which are errors naming what unify does instead: a leading underscore holds a page back (_post.md), and a page's address is its source path, so rename or move the file. tags/categories are allowed but build nothing — no index, no archive, no feed, no route — and unify audit says so. A key named og:… emits property= instead (og:image: /card.png, and og:image:width: 1200 is one flat key — a colon inside a name is not nesting; an indented block two levels deep is an error). No title: → first # Heading; headings get slug ids. schema: Article (or WebPage, or BlogPosting — those three, spelled exactly; in HTML, <meta name="schema" content="Article"> in the head, which a layout may carry for a whole section) writes the page's JSON-LD for you, from what the page already declares: its title, description, canonical, og:image, author, date, lastmod, and lang. Nothing else is added and nothing is guessed — a date unify cannot read as 2026-01-02 or 2026-01-02T09:30:00Z is left out and reported, never filled in from the clock or the file. Write your own <script type="application/ld+json"> for any other type, or for more detail: yours wins and unify then generates nothing. A canonical still has no frontmatter key — it is one page's own address, which a layout must never set (that stamps every page with the same URL), so write that page in HTML, use --canonical auto, or leave it off.

Styles, scripts, finishing

unify never scopes, rewrites, or injects CSS/JS, and rewrites only HTML's own URL attributes (href, src) — a url() in CSS and a fetch()/hx-get address ship as written, so a root-relative one misses the --base-url prefix and 404s: keep every url() in a stylesheet file and every fetched address relative to the page — or read it back from an href unify rewrote. Scope fragment styles yourself (@scope, @layer, a class prefix). unify build --dry-run --strict is the whole build and every check, writing nothing: every problem in one pass, and a list naming each page with the layout it resolved to. Then unify build — exit 0 means dist/ is the complete site; non-zero means nothing was published and dist/ is untouched, so never report success on a non-zero exit. --exclude replaces the _* default; keep _* in your list.