unify docs

unify — Product Specification

Status: Shipped contract — the composition core plus the production-and-discovery layer; §6 is that layer's design record Role: This document records the shipped product contract — the composition core, plus the production-and-discovery layer built on it — with §6 retained as that layer's design record: what unify is, who it serves, what exists now, and which boundaries future work must preserve. The rule-by-rule mechanism for shipped behavior — every merge rule with worked input→output examples, the algorithms, the error taxonomy — lives in docs/conformance-spec.md, the normative implementation reference. The two documents are written to agree about shipped behavior; a divergence between them is a defect to be fixed, never a license to reinterpret either. docs/authoring-rules.md is the complete core composition surface in under sixty lines.


1. What unify is

Web pages have needed shared headers, footers, and navigation since the beginning — and HTML still has no way to express that. Every existing answer forces a trade the author didn't ask for: a JavaScript framework, a templating language, a config-heavy build system, or copy-paste.

unify is a static site generator for front-end designers and hobbyists — people fluent in HTML and CSS who have no interest in JavaScript frameworks, templating languages, or build tooling. It lets them define a header, footer, nav, or page layout once, in plain HTML files, and have those rendered into every page of the site. The pitch in one line: HTML-native composition — no expression language, no client runtime. Composed HTML remains the author's markup plus visible, deterministic composition and any standard metadata the author explicitly asks unify to derive; unify adds no executable JavaScript of its own. Because the source and output use platform formats, a unify site stays reviewable by the person who owns it — read the source, read the built page and generated discovery files, and see exactly what happened, no matter who or what wrote the files. unify replaces copy-paste chrome, hand-edited HTML, and Apache SSI — it does not compete with Hugo, Eleventy, or Astro (§5).

The core composition surface is five things, learnable in five minutes:

| You want to… | You write… | |---|---| | Reuse a fragment (nav, footer, badge) | <include src="/_includes/nav.html"></include> | | Wrap pages in a layout | nothing (the nearest _layout.html applies) — data-layout="/path.html" to pick one, data-layout="none" to opt out | | Mark where page content lands, or let pages replace a named region | <main> for the default; <slot name="footer">…</slot> in the layout, slot="footer" on a page element | | Keep a page or folder out of the built site | name it with a leading underscore: _draft.html, _includes/ | | Ship a bare snippet exactly as written (for <include>, embeds, fetch) | name it *.fragment.html |

Any new HTML composition capability must reuse these concepts or prove that the core model remains equally small. Build verification, audits, generators, and standard discovery files may add commands or saved flags, but they may not add a second way to compose a page.

The composition vocabulary is deliberately standard. <main>, <slot>, and the slot attribute mean in unify exactly what they mean on the platform and in every framework that borrowed them: a named hole with visible fallback content, filled by content marked with the hole's name. The only two unify-specific HTML tokens in the core composition model are <include src> (a concept HTML never standardized; Apache SSI comments are the valid-HTML alias, §3.1) and data-layout (a standard extension attribute carrying the universal word for the concept). Anyone who has never read unify's documentation can parse a unify source tree on sight.

Design rules that govern every feature decision:

  1. Explainable in one sentence to someone who knows only HTML and CSS. If a rule needs a diagram, it's out.
  2. Every source file is real HTML a browser can parse as written. Layouts and pages are complete documents (a layout's slot fallbacks are its own preview — they render natively in any browser, no script); fragments are well-formed snippets; no template holes, no unbalanced markup. A source file opens and edits anywhere — but composed preview is the built site: unify dev (§4) serves dist/ and reloads on save. Direct preview of an uncomposed source tree is a possible convenience, not a product dependency or a current priority (§6.6).
  3. Polyfill-able: the HTML composition model must be implementable by a small (~200-line) browser script that produces the same DOM at design time as the CLI produces at build time. This is a complexity test, not a promise that the browser polyfill will ship. Any HTML rule too intricate to survive that test is too intricate to ship. Markdown, audits, generators, and discovery artifacts are outside that budget because none changes how HTML pages compose.
  4. Zero configuration. Conventions, not config files.

2. The five-minute site (golden path)

This walkthrough is the product. Every release must keep it true, and the end-to-end test suite builds exactly this site and asserts the output.

unify init          # scaffold a starter site into src/
unify dev           # build, watch, serve, reload — one command, one terminal
# …edit, save, browser reloads…
unify build         # write the final site to dist/
# upload dist/ anywhere: GitHub Pages, Netlify, a $3 shared host

unify init produces:

my-site/
├── AGENTS.md             # outside src/, so it cannot publish (§6.7)
├── DEPLOY.md             # the deployment recipe
└── src/                  # the source root — everything here ships
    ├── _layout.html      # the site chrome — one complete HTML page
    ├── _includes/
    │   └── nav.html      # a fragment
    ├── index.html        # a page
    ├── about.md          # a Markdown page — equal citizen
    ├── contact.html      # a page that overrides a named region
    ├── 404.html          # a page that opts out of the layout
    ├── robots.txt        # minimal and honest: it blocks nothing
    └── assets/
        ├── style.css
        └── share-placeholder.png   # the og:image, at its declared size

Scaffolding into src/ is what makes zero-config safe: the source root holds only what you meant to publish, so nothing outside it — .git/, .env, notes, screenshots, the output directory — can reach the built site. A flat site with no src/ still builds with no flags (§4). The scaffold exercises the composition primitives once each: an include, the automatic layout, a named-slot override, a layout opt-out, and the underscore. (The .fragment.html opt-out is the one primitive it leaves out — §4 documents it.)

_layout.html — a complete page you can open in a browser right now. Its slot fallbacks are its own preview (the starter stylesheet carries slot { display: contents } so the design-time wrapper adds no box; built pages contain no <slot> elements at all):

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>— My Site</title>
    <link rel="stylesheet" href="/assets/style.css">
  </head>
  <body>
    <include src="/_includes/nav.html"></include>
    <main><slot></slot></main>
    <footer class="site-footer">
      <!-- footer: the site byline, or whatever a page puts here instead -->
      <slot name="footer"><p>© My Site</p></slot>
    </footer>
  </body>
</html>

index.html — also a complete page, an ordinary semantic document. It doesn't mention the layout; the nearest _layout.html applies automatically:

<!doctype html>
<html>
  <head>
    <title>Home</title>
  </head>
  <body>
    <main>
      <h1>Home</h1>
      <p>This content lands in the layout's &lt;main&gt;.</p>
    </main>
  </body>
</html>

Built result: the layout, with its <main> content replaced by the page's, and the page's title prepended to the layout's: <title>Home — My Site</title>. The separator lives in the layout, so pages write only their own name.

contact.html — overriding a named region. The layout marked its footer contents with <slot name="footer">, so any page may replace them with one standard attribute:

<!doctype html>
<html>
  <head>
    <title>Contact</title>
  </head>
  <body>
    <h1>Contact</h1>
    <p>Ordinary content as usual.</p>
    <p slot="footer">© My Site — <a href="mailto:hi@example.com">email us</a></p>
  </body>
</html>

Built contact.html — note that the footer contains exactly the element the author wrote, and that no <slot>, no data-layout and no injected script survives into the output (the one unify token a built page may carry is <meta name="schema">, on a page that asked for a generated JSON-LD block — §6.3.6):

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Contact — My Site</title>
    <link rel="stylesheet" href="/assets/style.css">
  </head>
  <body>
    <nav><a href="/">Home</a> <a href="/about.html">About</a> <a href="/contact.html">Contact</a></nav>
    <main>
      <h1>Contact</h1>
    <p>Ordinary content as usual.</p>
    </main>
    <footer class="site-footer">
      <p>© My Site — <a href="mailto:hi@example.com">email us</a></p>
    </footer>
  </body>
</html>

about.md — Markdown pages work identically; frontmatter supplies the head:

---
title: About
description: Who we are
---

# About

Everything here is converted to HTML and dropped into the layout
exactly like an HTML page's content.

That is the whole core authoring model. Production options and audits may improve what unify verifies or generates, but they do not add another way to compose the page.


3. Composition model (normative — the composition rules; §4 carries the file, exclusion, and error rules)

3.1 Fragments: includes

3.2 Layouts: selection

Layout selection (first match wins):

  1. data-layout="none" on the page's <html> or <body>, or Markdown frontmatter layout: noneopt out: the page is emitted as-is, with includes and URL rules still applied. (A layout-less Markdown page is converted and wrapped in a minimal HTML shell — doctype, a head built from its frontmatter, its converted body — because conversion alone yields a fragment, not a document a browser renders correctly.) This is how 404 pages, redirect stubs, embeddable demos, standalone landing pages, and externally supplied documents live in a site that otherwise has a layout.
  2. data-layout="/path.html" on the page's <html> or <body> — explicit choice.
  3. Markdown frontmatter layout: /path.html — the Markdown equivalent.
  4. The nearest _layout.html, looking in the page's directory, then each parent up to the source root.
  5. No layout found: the page is emitted as-is.

Layouts do not chain. A layout that itself declares data-layout is a problem, located, naming the layout file — layout chaining is not supported, and unify says so rather than silently ignoring the attribute. A section that wants its own chrome writes a complete _layout.html in its directory (item 4 above already scopes discovery per directory); the accepted cost is repeating shared chrome across section layouts. Chaining remains explicitly deferred (§6.6). Layout references are paths to .html files — / resolves from the source root, anything else relative to the declaring file. A bare name (layout: default) is a problem naming the fix: layouts are paths, and unify never guesses. data-layout means nothing on any element other than a page's <html> or <body>; anywhere else it is a problem naming <include src="…"> as the replacement — data-layout is never a component import.

Migration from the retired vocabulary: a data-unify attribute anywhere, or a class beginning unify-, is a problem naming the supported spelling (data-layout, <slot name>/slot=). One-edit diagnosis, never silent behavior drift.

3.3 The merge — four rules

Rule 1 — Slots. A layout's <body> may contain <slot> elements. A page element carrying slot="name" fills <slot name="name">: the slot element is replaced by the filling element(s) — the page's markup ships exactly as written, no attribute merging, no discarding (the consumed slot attribute itself is removed). Multiple fills with the same name land in page order. A slot nothing fills is replaced by its own children (its fallback — which is also what a browser shows when the layout is opened directly). slot= is honored on the page's own top level — the children of <body>, and the children of the <main> you wrote, wherever you put it. That scope is what keeps unify's hands off slot= inside an author's own web-component markup: the parent of a fill is always <body> or your <main>, never a component. Slots inside <template> elements are never touched (that is an author's declarative shadow DOM, not unify's). Slots are recognized only in layouts and only in <body>; a <slot> anywhere else is an advisory and is replaced by its own children. The slotted-include experiment may add *.fragment.html as one deliberately bounded second host (§6.4), without changing page-to-layout scope. A second bare <slot> in a layout, or a repeated slot name, is an advisory and the first wins.

The layout author controls the replacement boundary by where they put the slot, with no additional rule:

<!-- Replace the whole element: the page's element ships, tag and all -->
<slot name="hero"><section class="hero">Default hero</section></slot>

<!-- Replace only the children: the styled wrapper persists -->
<footer class="site-footer"><slot name="footer"><p>© My Site</p></slot></footer>

Rule 2 — Main, the zero-vocabulary default. Page content not addressed to a named slot goes to the layout's bare <slot>; if the layout has no bare slot, it replaces the children of the layout's <main>. Before the merge, incoming body content is unwrapped once: if it contains a <main>, that element is replaced by its children — so a page written as a complete semantic document composes without nesting <main> inside <main>. No other element is unwrapped. If the layout has named slots but neither a bare <slot> nor a <main>, unaddressed page content would vanish: that is a problem, located, naming the fix. A layout with no slots and no <main> contributes its head and passes the page's body through unchanged — a head-only layout (shared stylesheet, shared metas, no body chrome) is a legitimate construct, not a mistake.

The precedence rule, in one sentence: named fills go to named slots, everything else to the bare slot, else into <main>.

A layout that wants persistent content inside <main> alongside page content writes it explicitly — every such case is visible in the layout's own markup, previews correctly in a browser, and needs zero additional rules:

<main>
  <slot name="hero"><section class="hero">Default hero</section></slot>
  <slot></slot>
</main>

Rule 3 — Head merge. Start with the layout's <head>. The page's <title> is prepended to the layout's, joined with a space, so the separator is written once, in the layout: layout <title>— My Site</title> plus page <title>Home</title> emits <title>Home — My Site</title>. The site name and the separator both live in one file, pages write only their own name, and a page with no title keeps the layout's alone. The separator stays the author's choice — an em dash, a pipe, a middot, or nothing at all. A page <meta> replaces a layout <meta> with the same name/property; a page <link rel="canonical"> or <link rel="icon"> replaces the layout's same-rel element — one canonical, one icon set, never two. Every other page head element is appended after the layout's, so page CSS loads last and wins the cascade. Exact-duplicate stylesheet/script references are deduplicated, compared after §3.6 URL resolution — so a page's assets/style.css and a layout's /assets/style.css are one reference, not two downloads. A page <meta charset> is dropped in favor of the layout's, which stays first in the head; if the layout declares none, the page's is kept and moved first. Identical charsets are silent (every complete document has one); a page declaring a different charset from the layout's is an advisory.

Rule 4 — Root attributes. On <html> and <body>, the page's classes are added to the layout's, and any other attribute the page explicitly sets wins over the layout's — so a page can carry class="home" styling hooks or set its own lang, dir, or data-theme. Attribute merging exists nowhere else: Rule 1's replace-element semantics make elementwise attribute rules unnecessary, because the author's markup ships as written.

Edge rules, for determinism: a fill addressed to a slot the layout doesn't have is an advisory, and its content flows to the default slot instead — nothing is lost, so the build still publishes. A slot never appears inside another slot's fallback — that is a problem (the nested slot would silently vanish the moment the outer slot is filled, which the content-loss law forbids). A page top-level <header> or <footer> outside any slot is an advisory (it probably meant slot=). A duplicated construct of which only the first counts — a second bare <slot>, a repeated slot name, a second <main> — is an advisory naming the duplicate; the first wins.

Content the author wrote is never dropped without failing the build. Any case where page content or a head element would not appear in the output is a problem, located, naming the fix — and advisories never involve losing something the author wrote. That rule assigns every case above, and every case not yet enumerated.

data-layout attributes are removed from output, and a <script> carrying data-polyfill is removed with it — an author-signed request to strip a design-time aid, not unify deciding to touch the author's JavaScript. That stripping behavior is part of the shipped contract even though shipping a browser polyfill is not a current priority (§6.6). Built output contains no <slot> elements and no unify vocabulary of any kind — except inside <template> elements, which unify never touches.

3.4 Why this vocabulary

3.5 Markdown

Markdown pages are equal citizens: converted to HTML, then processed by the same layout rules as any page. Frontmatter keys work as follows: title sets the page's <title> (prepended to the layout's, §3.3), layout picks the layout (§3.2), class adds classes to the page's <body>, lang and dir set those attributes on <html> (all via §3.3 rule 4), and any other key becomes a <meta name="…" content="…"> tag (description, author, robots). Namespaced metadata is a nested block, plain YAML: keys under og: become <meta property="og:image" …> tags (property= is what Facebook's crawler reads); keys under any other block — twitter:, say — become name= tags (twitter:card). Synthesized tags merge with the layout's head by the §3.3 rules — page wins. A list value emits one <meta> per item, in order. Markdown output filenames swap .md for .html.

A Markdown page with no frontmatter title uses the text of its first <h1> as the page title — the most common frontmatter chore, removed. A page with neither keeps the layout's title alone.

Headings converted from Markdown get an id derived from their text (lowercase; each run of whitespace becomes one hyphen; every remaining character that is not a letter, digit, or hyphen is dropped; leading and trailing hyphens trimmed; a repeat within the page gets -2, -3), so every heading is a deep link — the one thing documentation cannot do without. A heading that already carries an explicit id keeps it. HTML pages are untouched: unify never rewrites headings the author wrote.

Those keys are the only ones with special behavior. date, tags, categories, draft, permalink, and slug create no collections, taxonomies, or draft state. A leading underscore (_draft.md) is how a page is held back (§1, §4). That last sentence is enforced rather than explained: §6.3.9 shipped, so draft, permalink, and slug are located problems naming the mechanism the author was reaching for, tags and categories still build nothing and unify audit says so once per page, and only date (with lastmod) went the other way — §6.3.6 reads it. Values ship exactly as written — featured: true becomes content="true", date: 2026-01-01 stays 2026-01-01; unify never reinterprets your metadata through YAML's type system. Frontmatter flattens exactly one level of blocks: a value nested deeper (a mapping under og:image) has no honest <meta> form and is a problem, located, naming the key. The roadmap may additionally consume a bounded schema declaration and explicit author/image/date metadata for JSON-LD and feeds (§6.3, §6.5); it does not turn drafts, tags, permalinks, or arbitrary keys into collection controls.

Two hard errors close the two highest-frequency cross-generator reflexes, both silently wrong otherwise: frontmatter atop an .html page is a problem (it would render as visible text — HTML pages have no frontmatter; use <head>), and a literal <head> element in a Markdown body is a problem (it would land in the body — Markdown heads come from frontmatter). A Markdown page cannot express arbitrary head markup (rel="preload" and kin) directly; put that in the layout or write the page in HTML. unify closes the two commonest cases without opening that door: --canonical auto completes a canonical from the page's final public URL, and schema: writes bounded JSON-LD from what the page already declares (§6.3) — nothing else is inferred.

3.6 URLs

Write paths that are correct for the file you're editing — relative (hero.jpg) or root-relative (/assets/style.css); both work anywhere. URLs inside layouts and fragments are resolved against the file that wrote them and emitted root-relative, so composed markup is correct at every page depth: authors never compensate for where an include will land, and editor click-through keeps working. Rewriting applies to href, src, srcset, and poster, on the final composed page — after includes and layouts, before --pretty-urls and --base-url. It does not reach inside <style> blocks or style attributes: a url() written in a layout or fragment must be root-relative, or live in a stylesheet file. (Unreachable is not unchecked — the post-build reference check audits those url()s like every other URL, so one that points at nothing fails the build instead of 404ing quietly.) Stylesheets never need rewriting: mirror-copy ships every CSS file at its source-relative location, so url() references inside them keep working untouched.

Always link the real file. Write href="about.html", never a hand-written pretty URL like /about/ — the real file previews correctly, works without any flags, and --pretty-urls (§4) rewrites every internal link to the pretty form at build time. A hand-written pretty URL is a link to a file that does not exist in the source tree, and the reference check — which validates the emitted tree — reports it as exactly that, unless --pretty-urls happened to move a page to that address. That exception is the argument rather than a loophole: the link the flag writes for you is correct in both modes, and the one you hand-write is correct in one.


4. CLI (the complete surface)

unify [build]              build the site (default command)
unify audit                evaluate the site the build would publish — writes nothing
unify dev                  build, watch, serve, and reload — the inner loop
unify watch                build + rebuild on change, no server (pair with your own)
unify init [template]      scaffold a starter site (default, basic, blog, docs, portfolio)

Options:
  -s, --source <dir>       source directory (default: src/ if it exists, else .)
  -o, --output <dir>       output directory (default: dist)
      --clean              empty the output directory first
      --exclude <glob>     globs never emitted, still usable by the build (repeatable; default: _*)
      --pretty-urls        about.html → about/index.html, and rewrite internal links to match
      --base-url <url>     the site's whole address (https://site.example/repo/): prefix root-relative links, make og:/canonical absolute for share crawlers, and generate sitemap.xml and feed.xml
      --canonical auto     add a canonical link to pages that author none, from the site address
      --feed-full          include each entry's full rendered content in feed.xml (needs --base-url)
      --search-index       write search-index.json for a client-side search library
      --generate <path>    run one JavaScript file from your source tree before the build
      --dry-run            run the full build and every check, print the report, write nothing
      --strict             advisories count as problems for the exit code (with `audit`, findings too)
      --format <kind>      `audit` report shape: human (default), json, or sarif
      --external           `audit` only: fetch every off-origin URL the site emits and report the ones that don't resolve
  -p, --port <n>           port for `unify dev` (default: 3000)
  -v, --version            print version
  -h, --help               print help

That is the entire shipped CLI — there are no other commands or flags, and docs/cli-reference.md documents every one. Section 6 is the design record the production-and-discovery additions shipped from. Behavior notes:


5. Non-goals

Things unify deliberately does not do, even if asked:


6. Beyond the composition core — design record

This layer should make the site production-ready, inspectable, and discoverable while keeping page-and-layout composition stable. The sole composition experiment reuses the existing slot model inside explicit fragments (§6.4); the rest of the roadmap operates on the final output rather than adding authoring syntax. Its one-sentence mental model is: write complete HTML or Markdown pages, reuse fragments with includes, fill holes with slots, and give unify the site's public address; unify composes the pages, verifies the final site, and generates the standard files search engines and feed readers need. This section was written as a roadmap and is retained, in substance, as the design record: every item in §6.2–§6.5 has shipped, and the agreement condition it set — conformance spec, authoring rules, CLI reference, implementation, and tests agreeing — is exactly what the release gates verify. Read "proposes"/"should" below as the record of what was decided, not as open questions.

6.1 Design constraints

6.2 Foundation: one final-output page manifest

After composition and URL rewriting, unify should derive one internal record for every public page. At minimum it carries the source path, output path, public URL, canonical URL, title, description, language, robots directives, first h1, heading outline, visible main text, representative image, author, published/modified dates, declared schema type, and incoming/outgoing internal links. The manifest is an implementation boundary, not a new file format authors must learn. Sitemap, canonical generation, feeds, structured-data checks, search output, orphan detection, and machine-readable reports must all consume it.

The extractor must inspect the emitted DOM rather than frontmatter alone, so HTML and Markdown remain equal citizens and layout-provided metadata is represented exactly as it ships. A page with conflicting declarations produces one located finding and one deterministic manifest value; downstream features never each choose their own winner.

6.3 Production and discovery layer

  1. Sitemap from --base-url. When the site's public address is known, generate an output-root sitemap.xml from final pretty URLs. Include only internal, indexable, canonical pages; exclude assets, fragments, 404.html, noindex pages, and pages canonically pointing elsewhere. URLs are absolute, an authored lastmod is preserved only when a real value exists, and large sites split at protocol limits. If the source tree already contains a sitemap, preserve and validate it instead of overwriting it; a generated-path collision is a problem.
  2. Optional canonical completion. --canonical auto (and the identical saved flag in unify.yaml) adds a canonical only when a page does not already author one, using that page's final public URL. Authored canonicals always win. Missing internal canonical targets, multiple canonicals, canonical/noindex conflicts, and disagreement with the sitemap are reported.
  3. Robots consistency, not invented policy. Validate an authored robots.txt, its sitemap declaration, page-level robots directives, and whether referenced paths exist. Templates may scaffold a minimal robots file, but unify never decides what an author should block.
  4. unify audit. Add an explicit evaluation of the prospective final output, separate from build problems and the capped advisory list. It runs the full pipeline without publishing to dist/. Initial findings cover missing or duplicate titles/descriptions, missing or multiple primary headings, title/heading mismatch, orphan pages, broken fragment links, duplicate IDs, canonical/sitemap/robots conflicts, absent document language, invalid JSON-LD, incomplete declared schema, missing social-image targets or dimensions, and substantially duplicated page text. It prints evidence and a fix, never a score. unify audit --strict is the opt-in CI gate; ordinary build does not reject subjective content quality findings.
  5. Final-output verification. Extend the existing reference check to fragment identifiers, duplicate IDs, normalized public-URL collisions, metadata placement, redirects, and URLs emitted into sitemap or JSON-LD. All generated artifacts participate in transactional publishing and are visible in --dry-run.
  6. Structured-data validation and bounded generation. Always parse and validate authored JSON-LD, verify local URL references, report contradictory entities, and compare factual fields with visible content where the relationship is unambiguous. For Markdown, schema accepts exactly one of WebPage, Article, or BlogPosting (case-sensitive) and may generate JSON-LD from title, description, final canonical, image, author, date, lastmod, and lang; no other key is inferred. date maps to datePublished, lastmod maps to dateModified, both must be authored ISO 8601 values, and neither falls back to the build clock, filesystem time, filename, or Git history. No field is guessed and authored JSON-LD wins. HTML authors continue to write ordinary <script type="application/ld+json"> when they need any other vocabulary.
  7. SEO-complete offline templates. Add focused init templates for a local service, product, publication, portfolio, event, and catalogue. Every template includes semantic visible content, unique titles and descriptions, canonical/social metadata, representative-image dimensions, appropriate authored or bounded JSON-LD, robots.txt, a clean audit, and deployment recipes. Templates teach the platform artifacts; they do not introduce a unify-only content schema.
  8. Local audit view. unify dev may expose a reserved /_unify/ report assembled from the manifest and audit findings. It is served only by the development server, writes nothing to dist/, and adds no script to published pages.
  9. Counter-prior diagnostics. Frontmatter that authors and coding agents routinely expect other generators to interpret must not silently imply behavior unify does not have. draft, permalink, and slug are located problems naming the unify mechanism: prefix the source name with _ to hold a page back; rename or move the source file to choose its output path; and use --pretty-urls only to apply unify's site-wide .html-to-directory rewrite. tags and categories carry an audit finding stating that they create neither collections nor taxonomies. Existing diagnostics for bare layout names, path-only --base-url values, hand-written pretty URLs, and non-empty includes remain mandatory. These findings exist to prevent confident cross-generator assumptions from publishing or addressing the wrong page, not to reserve ordinary metadata names without cause.

6.4 Constrained reuse and a one-process generator seam

  1. Slotted includes, experimental. Recover the useful part of the earlier codebase's customizable fragments without reviving area matching or a component DSL: content inside <include> may fill <slot> elements in a *.fragment.html target using the same slot="name" and fallback model authors already learned for layouts. An empty include remains verbatim. A non-empty include is valid only when its fragment declares slots; fragments may not contribute a document head or root attributes; unmatched or content-losing fills are located findings; include cycles remain errors; and fill scope is lexical. There are no props, expressions, loops, conditionals, attribute merging, style scoping, or implicit data. This ships only if authoring trials show that the complete rule still fits comfortably on the authoring-rules page.
  2. --generate <path> instead of --run <cmd>. Let build, watch, dev, and audit run one author-owned JavaScript generator from the source tree before scanning pages. The path is explicit and saved like any other flag; it is not an arbitrary shell command. The standalone binary supplies the runtime, sets the working directory to the source root, and invokes the script with the absolute source path and a temporary generated-source directory as its two positional arguments (process.argv[2] and [3]); a generator needs no imported unify API. Only files written to the generated directory join the scan as an overlay, so the supported workflow does not mutate src/, audit remains read-only, generated paths participate in collision checks, and watch mode cannot trigger on its own temporary outputs. Generator failures use unify's located diagnostics, and generated pages then follow the ordinary build contract. The filesystem remains the extension interface—this only removes the second runtime and second watcher from the common workflow.
  3. Recipes remain first-class documentation. Ship deployment workflows plus short, readable examples for the generator context, image optimization, an external CMS over the source tree, and interoperability with post-build tools. State plainly that per-page expressions, i18n policy, application bundling, and arbitrary pipelines remain outside unify.

6.5 Publication outputs

  1. RSS/Atom from explicit articles. Generate a feed only from pages explicitly declaring Article or BlogPosting, using the shared manifest rather than a new collection or query language. Feed URLs and stable IDs use canonicals, dates are emitted only when authored, referenced assets are checked, and full-content inclusion is an explicit option. Start with one site feed; scoped feeds wait for demonstrated demand.
  2. Optional search manifest. Emit a documented, static JSON search input from final public pages so a small client-side search library or external indexer can consume titles, URLs, headings, and text without reparsing the site. It is a versioned projection of §6.2—not a second extractor or vocabulary—with top-level schemaVersion and pages, and page fields named url, title, description, headings, and text. unify does not ship a search runtime.
  3. Opt-in network audit and reports. unify audit --external checks external links without making normal builds network-dependent. unify audit --format json exposes the page manifest and findings for CI and other tools without creating a plugin API. The report has a schemaVersion; each finding has a stable machine identifier, severity, source/output locations where available, public URL, evidence, suggested fix, and stable fingerprint. Human output remains plain language and never requires memorizing those identifiers. A SARIF serializer is optional only if it is a mechanical view of the same findings rather than another analysis path.

6.6 Explicitly deferred or rejected

Do not restore DOM Cascade area matching, component props, expressions, loops, conditionals, a collections/query DSL, keyword rewriting, built-in generative prose, numeric SEO scores, guessed structured data, network access during build, or a plugin API — §5's minimal public API is not one: it exposes the CLI's own work to callers and grants no hooks into composition. Layout chaining remains deferred: it adds recursive merge semantics to every page for a reuse problem slotted includes may solve more locally. The browser preview polyfill, HTML minification, and Markdown include shorthand remain possible later conveniences, but they do not close the production, self-containment, or authoring gaps that define this layer and are not roadmap priorities.

6.7 AI-agent authoring and implementation guidance

The team cannot measure the contents of a proprietary model's training corpus, so “training-data density” is an engineering proxy, never a model-specific claim. Treat a surface as dense when it uses a mature standard, appears across common web tools, and has a name whose ordinary meaning predicts the right result. Treat it as sparse when unify combines familiar tokens in a novel way, hides important precedence, or deliberately disagrees with conventions learned from other generators. High density reduces explanation cost; it does not remove the need for validation, and high-volume SEO advice in particular may be confidently wrong.

| Surface | Expected density and prior | Implementation consequence | |---|---|---| | Standard HTML metadata, sitemap.xml, canonical links, JSON-LD syntax, RSS/Atom, broken links/fragments, duplicate IDs | High; mostly aligned. Agents usually know the artifact and its basic shape. | Keep the standard name and format, generate conservative values, validate the final output, and spend documentation on unify's trigger and precedence rather than reteaching the standard. | | SEO guidance, robots/indexing, canonical/sitemap interaction, JSON-LD facts, lastmod | High but noisy. Agents often produce plausible myths: fixed title lengths, keyword density, robots as noindex, build-time dates, or rich-result promises. | State negative rules explicitly and enforce factual consistency. Never convert a common answer into a product rule merely because models repeat it. | | Page manifest, --base-url-driven generation, unify audit, --strict, feed membership, /_unify/ | Medium; concept aligned, policy custom. Agents infer the goal but not exact activation, inclusion, or failure rules. | Give each feature one canonical command/example, show it in --help or --dry-run, and emit a located correction when the inferred policy is wrong. | | Slotted includes, *.fragment.html, the generated-source overlay, search/report schemas, unify's underscore and real-file-link conventions | Low or counter-prior. Familiar syntax points agents toward component props, source mutation, pretty source URLs, or other-generator frontmatter behavior. | Make the rule local and unavoidable: scaffold example, agent guide, conformance fixture, and hard diagnostic. Do not rely on the feature name being self-explanatory. |

Repository-local guidance is part of the feature, not optional marketing. Every init template should place a concise AGENTS.md at the project root, outside src/ so it cannot publish. It repeats only the high-conflict rules: layouts are paths; source links name real .html files; --base-url is a complete public URL; a leading underscore excludes source; draft, permalink, and slug are not silently honored; an empty <include> performs verbatim inclusion; slotted includes, when enabled, accept markup fills but no props or attribute merging; structured data uses visible explicit facts; generated source goes to the supplied overlay directory; and unify audit plus --dry-run are the pre-publish checks. The README and CLI help must lead humans and agents that do not discover AGENTS.md to the same rules — the README links docs/authoring-rules.md rather than duplicating it; no behavior may be documented only in the agent guide, and there is one rule set rather than tool-specific variants.

Diagnostics carry the instruction at the moment it matters. For every sparse or counter-prior rule, identify the source location, state what unify actually did or refused to do, and show the smallest valid replacement. “Unsupported” alone is not sufficient. The diagnostic tests must cover at least: draft: true; bare layout names; path-only --base-url; hand-written pretty URLs; non-empty includes with no target slots; fragment fills at the wrong depth; fragment head/root content; generated-source collisions; unknown schema values; inferred or malformed dates; and canonical/noindex/sitemap disagreement. An author-owned generator remains responsible for side effects outside the supplied overlay (§6.1); unify documents that boundary rather than claiming to sandbox arbitrary JavaScript.

Examples are executable few-shot material. Each author-facing feature ships with one smallest correct example and one high-probability counterexample whose diagnostic is asserted. Templates use conspicuous placeholders for business identity, author, dates, images, prices, ratings, and other factual claims; a template must never make an invented placeholder look publishable. The examples, help text, diagnostics, and conformance fixtures use identical vocabulary and field names.

Machine surfaces are self-describing and shared. The page manifest is the only semantic record. Sitemap, feed, search, the local report, JSON audit output, and optional SARIF serialization are projections of it. Every unify-defined JSON document carries schemaVersion; authored or generated JSON-LD follows its own standard vocabulary and does not. Every audit finding carries a stable identifier and fingerprint; public field names do not depend on terminal prose. Within a major schema version, fields may be added but existing meanings and types do not change; a breaking machine-contract change increments the version. Adding a second extractor, renaming the same fact between outputs, or asking an agent to scrape human diagnostics is a design defect.

Agent authoring trials are a release gate for sparse features. Before a candidate graduates, run representative current coding agents with only the scaffold, repository-local guide, and CLI help—no hidden unify explanation. The fixed trial set asks them to build and audit: a subpath-hosted brochure page, an article with truthful JSON-LD and feed membership, a fragment link, a reusable card/callout using any proposed slotted-include syntax, and a generator that emits one page into the supplied overlay. Store the prompts, source trees, expected outputs, and expected diagnostics as versioned fixtures. A candidate passes when correct work needs no undocumented assumption, common mistakes receive a one-edit diagnosis, no authored content is silently lost or published against the stated intent, and the core composition rules still fit on the authoring-rules page. An agent's successful guess is evidence about usability, never a substitute for a written rule or deterministic test.

Implementation should follow the primary standards rather than secondary SEO folklore: the HTML slot model, Sitemaps protocol, Robots Exclusion Protocol, JSON-LD 1.1, Schema.org, Atom, and RSS 2.0. Search-specific checks follow current primary crawler guidance, including Google's canonical guidance and structured-data policies, and must be reverified when implemented; no search-engine behavior is frozen into the HTML composition model.


7. What the contract requires

Sections 1–5 describe the contract the implementation is held to; §6 is explicitly excluded until individual candidates graduate. src/ implements the shipped contract, and the suite holds the two in lockstep. What the document set demands, without hedging:

  1. The composition model of §3, exactly — slots, the <main> default, the unwrap, the four merge rules, layout discovery — with docs/conformance-spec.md as the normative mechanism reference. Its worked examples are the test fixtures; an implementation conforms when it reproduces them exactly in structure, attributes, and text content (whitespace between block-level elements is not normative — conformance spec §3).
  2. The retired vocabulary is diagnosed, never honored. data-unify and unify-* classes produce located problems naming the supported spelling. No code path composes by area classes, landmarks (beyond <main>), ordered fill, or component imports.
  3. The engine contract of §4, in full: transactional all-or-nothing publish, the post-build reference check, output collision detection, mirror copy, URL provenance rewriting, the --exclude default and its guard, the never-shipped list, the watch contract, unify dev, --dry-run composition reporting, exit codes 0/1/2, and the two-severity error contract with the advisory discipline.
  4. The golden path is executable and tested: unify init && unify dev works end to end, the E2E suite builds the §2 site and asserts its output, and unify init && unify build --dry-run --strict exits zero.
  5. The documents stay in lockstep: docs/authoring-rules.md states every authoring rule in under sixty lines and the README links it rather than duplicating it (both asserted by tests); no shipped behavior may contradict any document in this set.

8. Success criteria