Common build errors

The errors a new project actually hits, with what each one means and what to change. Every message on this page was produced by running the binary — none is paraphrased — and the table at the end pins each one to the source that emits it, so a reworded message fails CI instead of silently making this page a lie.

This page is for humans reading a build log. Its machine-readable counterpart is Diagnostics: --format=json emits the same failures as NDJSON with a stable code, and zigapagos explain-code <CODE> prints a long-form explanation of any code named below.

this page has no subpages (page is not a section)

content/a.smd:9:5: error: this page has no subpages (page is not a section)
|    see [b](.b)
|        ^^^^^^^
|   note: a leading '.' means "subpage of this section", not a relative path -- to link a sibling page, use $link.page("...")

Cause. In SuperMD a leading . in a link target means subpage of this section, not relative path. (.b) on a page that is not a section asks for a child that cannot exist. This is the single most expensive misreading in the project’s own history: it produced 55 build errors in one sitting when publishing existing Markdown, because ./b and .b look like ordinary relative links.

Fix. For a sibling, [b]($link.page("b")). The leading-dot form is only for a section page linking its own children.

Code: ZP_LINK_NOT_A_SECTION.

path is empty

content/index.smd:8:4: [scripty] path is empty
|    go [home]($link.page(""))
|       ^^^^^^^^^^^^^^^^^^^^^^
|   note: to link to the site homepage, use $link.site()

Cause. $link.page("") is rejected. It looks like it should work because $site.page('') does work in a .shtml layout — the shipped base.shtml uses exactly that for its home link — so the prose form reads like the same thing.

Fix. $link.site(), which is the content-side builtin for the site root. The note is fork-added enrichment printed around SuperMD’s own message; if an upstream sync rewords path is empty the note stops firing and tests/rendering/empty-page-path-hint.sh goes red rather than drifting.

Code: ZP_SUPERMD.

unknown page

content/index.smd:9:5: error: unknown page

Cause. $link.page("x") names no page. The argument is a page path under content/, without the .smd extension and without a leading slash — $link.page("blog/first"), not "/blog/first.smd".

Fix. Check the path. If the target is legitimately not built yet, --allow-missing-pages downgrades this to a warning (ZP_MISSING_PAGE_TOLERATED) and emits the link anyway.

Code: ZP_UNKNOWN_PAGE.

unknown ref

content/index.smd:9:6: error: unknown ref

Cause. An in-page anchor ([jump](#nope)) whose target id does not exist. SuperMD resolves anchors at build time rather than shipping a dead fragment, so every #… link must name a real id.

Fix. Give the target heading an id — []($heading.id("nope")) at the start of the heading — or correct the anchor. Headings do not get slugs automatically; see Generated content for how this site’s own pipeline adds them.

Code: ZP_UNKNOWN_REF.

Frontmatter and layouts

missing layout file

content/index.smd:6:11: error: missing layout file

Cause. The page’s .layout names a file that is not under layouts_dir_path, or is not usable as a layout.

Fix. Check the name and the directory. The path is relative to the layouts directory, extension included: .layout = "index.shtml".

Code: ZP_MISSING_LAYOUT. Only title and layout are required in frontmatter; author, date and draft have defaults.

unexpected '.', expected: ',' or '}' or '---' or 'EOF'

content/index.smd:3:1:
    .layout = "index.shtml",
    ^
unexpected '.', expected: ',' or '}' or '---' or 'EOF'

Cause. The block between the --- delimiters is a Ziggy struct literal, not YAML. Every field is .name = value, and fields are comma-separated. The example above is missing the comma after the previous field, so the parser sees the next . where it wanted ,.

Fix. A comma after every field except, optionally, the last one — the error’s own expected-set says so: after a value the parser will also take the closing ---. A trailing comma on the last field is the house style here and is always accepted, so “comma after every field” is the rule that never bites. Strings are double-quoted; dates are @date("2020-01-01T00:00:00"); booleans are bare true/false.

Code: ZP_FRONTMATTER_PARSE.

Assets

field not found on $site.asset(...)

content/index.smd:8:1: [scripty] field not found
|    [css]($site.asset("nope.css").link())
|    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Cause. The asset does not exist under assets_dir_path. The message is Scripty’s generic one for a failed lookup and does not name the file.

Fix. Check the path, which is relative to the assets directory. Note the three asset scopes are distinct builtins with distinct roots: $site.asset(...) for assets_dir_path, $page.asset(...) for files beside the page, and $build.asset(...) for build-generated ones.

Codes: ZP_MISSING_SITE_ASSET, ZP_MISSING_PAGE_ASSET, ZP_MISSING_BUILD_ASSET.

Content

unknown code-fence language 'jsonc'

content/index.smd:9:1: warning: unknown code-fence language 'jsonc'; emitting the block unhighlighted (did you mean 'json'?)

Cause. The fence’s info string is not a registered syntax.

This is a warning, not an error — the block is emitted unhighlighted and the build succeeds. zigapagos languages prints the registered list, and a near-miss gets a did-you-mean, so you do not have to guess.

Code: ZP_UNKNOWN_LANGUAGE.

Islands

unknown island directive

unknown island directive 'client:visable' (expected load|idle|visible|media|only)

Cause. A typo, or an Astro directive with no counterpart here. The build rejects it because the runtime would not: schedule()’s default: branch mounts a directive it does not recognise, so client:visable would hydrate on page init exactly like client:load. You lose the deferral you asked for, and nothing looks broken.

Fix. One of the five. Their semantics are on the islands page.

props mismatch

error: props mismatch on /  <island src="components/Hero.island.tsx" id="z-island-0">
  resolved props: {"headline":5}
  Type 'number' is not assignable to type 'string'. (TS2322)

Cause. The island’s resolved props — :props merged with prop-NAME overrides and any $page.* values — do not typecheck against the component’s exported Props type. A misspelled prop shows up here as an excess-property error.

Fix. The resolved props: line is the actual JSON the island would have received; compare it against Props. --island-props-check=warn downgrades the gate while you work, and off disables it.

failed to spawn island sidecar

error: failed to spawn island sidecar (/usr/bin/bun .../runtime/sidecar/render.ts): FileNotFound

Cause. The build could not start the Bun SSR sidecar. FileNotFound here almost always means the interpreterbun — is not at the path the build was told to use, not that render.ts is missing.

Fix. Check that bun resolves (command -v bun) and matches the version mise.toml pins. A site with no islands and no SPA never spawns the sidecar at all.

SPAs

dynamic route "…" declares no skeleton

error: spa describe failed for app/app.spa.tsx: dynamic route "/club/:id" declares no skeleton — the build SSRs its shell at the base-relative path "/club/_" (every :param and * segment substituted with "_"), so its SSR output differs from the first client render, which breaks hydration. Give the route a param-independent `skeleton` component, or set `skeleton: false` to assert the component is hydration-stable without one.

Cause. A dynamic route is prerendered once, with every :param replaced by _. A component that renders the real parameter would therefore produce different markup on the server than on the first client render, and Preact’s hydrate() does not repair that.

Fix. Give the route a skeleton component that does not depend on the parameter, or skeleton: false to assert the component is hydration-stable. staticPaths is the third option: enumerate the concrete parameter values and each gets its own real shell.

spa declares neither a "/" route nor any dynamic route

Cause. An SPA that prerenders nothing. Every SPA needs at least one shell for a host to serve.

Fix. Add a { path: "/", component: … } entry — the SPA quickstart is the minimum that works.

Layouts

Three template mistakes fail the build with their own codes, all covered on the SuperHTML page: :attr is not a directive (ZP_TEMPLATE_BAD_DIRECTIVE_ATTR), :else is parsed and never evaluated (ZP_TEMPLATE_ELSE_DIRECTIVE), and :if/:loop on a void or self-closing element can never work (ZP_TEMPLATE_BRANCHING_WITHOUT_END_TAG).

Where each message comes from

Machine-checked. tests/meta/build-errors-doc.sh reads this table and fails if a fragment is no longer present in the file that emits it, or if a code is not [ACTIVE] in src/diag-codes.frozen. A row that cannot be greppable is the defect this page exists to prevent: an invented error message is worse than no page at all, because it sends a reader looking for something that never happens.

A fragment must be the stable literal part of the message — never a span crossing a {s} placeholder, which is unfindable — must carry no " (source literals escape it as \", so it could never match) and no edge whitespace inside its backticks (invisible in review, and an editor’s trim would break the gate on a message nobody changed). A | is allowed but must be escaped \|, which is markdown’s own escape and also what makes the row render as one cell.

Message fragmentCodeEmitted by
this page has no subpages (page is not a section)ZP_LINK_NOT_A_SECTIONsrc/context/Page.zig
not a relative path -- to link a sibling page, use $link.page(ZP_LINK_NOT_A_SECTIONsrc/context/Page.zig
to link to the site homepage, use $link.site()ZP_SUPERMDsrc/root.zig
unknown pageZP_UNKNOWN_PAGEsrc/context/Page.zig
unknown refZP_UNKNOWN_REFsrc/context/Page.zig
missing layout fileZP_MISSING_LAYOUTsrc/context/Page.zig
unknown code-fence languageZP_UNKNOWN_LANGUAGEsrc/context/Page.zig
unknown island directive-src/islands/pass.zig
(expected load|idle|visible|media|only)-src/islands/pass.zig
props mismatch on-src/islands/props_check.zig
failed to spawn island sidecar-src/root.zig
declares no skeleton-runtime/src/router.ts
route nor any dynamic route, so nothing-src/spa.zig