You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Added
npm distribution.npx zigapagos now scaffolds, serves and builds a content
site with no Zig toolchain. Three packages, released together at build.zig.zon's version: @zigapagos/cli-<platform> carrying the prebuilt
binary, @zigapagos/cli (canonical) resolving the right one at run time through optionalDependencies, and the unscoped zigapagos as a thin alias so npx zigapagos works. Prebuilt for macOS x64 and Linux x64 — the two targets build/release.zig ships. Every other host is refused with the reason rather
than the bare fact, and arm64 (macOS or Linux) is refused rather than served the
x64 binary: npm install fails with EBADPLATFORM because the launcher packages
declare the os/cpu they have binaries for, so an unsupported host cannot end
up with an install that looks clean and has no binary in it. Everything builds
from npm i zigapagos alone — content, islands, native SPAs and zigapagos dev: @zigapagos/cli ships the @z/runtime sources and the Bun SSR sidecar, and
declares bun, typescript and @zigbase/server as optional dependencies, so
the tools it shells out to are installed rather than asked for. npm puts node_modules/.bin on PATH, and the launcher appends it to the child's, so the
zigbase locator finds the server with no flag, no global install and nothing
downloaded. --omit=optional still builds; it loses dev's server and the SPA
runtime slice. The remaining difference from a Zig build is caching, not
capability. The published READMEs say so. See npm/README.md.
The zigbase dependency is the scoped@zigbase/server, at exactly the pinned_version in src/cli/zigbase.zig (currently ZigBase v0.12.0) — the same
release --download-zigbase fetches, so zigapagos dev runs one zigbase however
it was installed. npm/check-toolchain.mjs fails the build if those two ever
disagree.
zigapagos doctor [DIR]: audits a BUILT output tree (default public, read-only — never
builds, never touches site source) for authoring mistakes that are only visible in the final
emitted HTML. Ships two checks: abs-url-meta (a root-relative Open Graph / Twitter / canonical
URL — crawlers can't resolve it, so this is an error) and dangling-internal-link (a
root-relative href/src with no file behind it in the tree, including under --url-prefix —
a warn, since a client-routed SPA route legitimately has no file). Exit code: any error
finding, or a file doctor could not read, exits non-zero; warn-only findings exit 0 unless --strict is passed.
zigapagos validate [OPTIONS]: a fast, in-memory subset of zigapagos release's checks (issue #45). Parses and analyzes the site — frontmatter/Ziggy schema, SuperMD parse, layout resolution,
content-side $link.page/asset references, output-URL collisions, template SuperHTML/Scripty
parse, the : directive lint, and template RENDER errors (a failing Scripty expression, a $site.page(...) naming no page) — WITHOUT bundling islands, spawning the Bun sidecar, or
writing an output tree. It does not cover island SSR, the typed island props check, SPA route
enumeration/spec checks, asset installation, or CSS minification — those stay release-only, so
a green validate is a subset guarantee, not a green release. Measured (this repo's examples/tsx-site, warm caches): a content-only edit loop goes from zig build's ~2s to validate's ~0.02–0.03s — and unlike zig build, validate needs no bun, node_modules, build.zig, or consumer build graph, and never writes the output tree.
zigapagos explain <route>: resolves one output route to its content source, layout extends
chain (for a route that is one of a page's alternatives, that alternative's OWN layout, not the
page's), effective frontmatter (after schema defaults), islands (as declared in the markup, not
SSR-verified), page-owned assets (referenced vs. pruned), and EMITTED PATHS relative to the
output directory (issue #47). Runs the same kind of fast in-memory build as validate. Content
routes only — a memory build never prerenders SPAs, so a client-routed SPA route is not covered;
the miss message says so.
zigapagos languages: lists every code-fence language registered for syntax
highlighting.
zigapagos release --format=json emits build diagnostics as NDJSON on stderr — one
minified JSON object per line, {"code","severity","file","line","col","message","help"}
— instead of the historical multi-line prose. The consumer this is for is an unattended
agent: it can now tell which diagnostic fired without pattern-matching English. Default
is --format=text and text mode is byte-for-byte unchanged.
The diagnostic code is the stability guarantee; message and help explicitly are not. src/diag-codes.frozen is the append-only ledger that makes that a gate rather than a
promise — a code is never renamed and never reused for a different meaning after
retirement, enforced against the enum on every build.
zigapagos explain-code <CODE> prints the long form of any code: what condition produced
the diagnostic and what to change in the source. zigapagos explain-code with no argument lists
every registered code with a one-line summary. Every code is required by the compiler to
have both, so the listing cannot go stale relative to what the build emits.
The two : directive lints get one code each rather than a shared one — ZP_TEMPLATE_ELSE_DIRECTIVE and ZP_TEMPLATE_BRANCHING_WITHOUT_END_TAG — because they
are unrelated failures with unrelated fixes and code is what a consumer switches on.
docs/diagnostics.md is the consumer contract: the wire schema, what is and is not
stable, and an explicit inventory of what is not converted and why — including the
rule that matters most, skip a stderr line that does not parse as JSON rather than
failing the run, since the Bun sidecar and the usage-menu path legitimately write
prose to the same stream.
Islands can now be embedded directly in .smd content, not only in
layouts: inside a fenced code block whose fence info is =html (SuperMD's
existing validated raw-HTML escape hatch), use the hyphenated <z-island src="…" client:load :props='…'></z-island> spelling — the
islands pass treats it identically to <island> in a layout (SSR, data-z-props, the import map, the runtime script, the tsc props gate,
and the dev island-usage manifest all apply unchanged). The hyphen is
required: superhtml's .html-mode validator (used to vet the fence body)
rejects a non-hyphenated custom element name per the HTML spec, unlike the
lax .superhtml layout mode where <island> has always worked. See docs/islands.md, "Islands in content (.smd)".
Opt-in auto_heading_ids site setting (Site/MultilingualSite in zigapagos.ziggy):
injects a GitHub-compatible slug id into every heading that doesn't already carry an
explicit $heading.id(...)/$section.id(...), so a same-page #anchor or cross-page /page#anchor link written against a doc's existing GitHub rendering keeps working
without hand-writing an id on every heading. Off by default; an explicit id always wins
and is never overwritten. See docs/migration/astro-to-zigapagos.md's "Heading anchors: auto_heading_ids" section.
$site.asset(...).absLink() / $page.asset(...).absLink(): like link(),
but always returns an absolute URL (host_url + url_path_prefix + asset
path), and installs the asset the same way link() does. Use it for URLs
consumed outside the page itself — og:*/twitter:* meta tags, canonical
links, feeds — since link()'s output is root-relative and scrapers do not
resolve those (#25).
.asset_fingerprint = true in zigapagos.ziggy installs every linked site asset under a
content-hashed filename (assets/style.css → /style.a1b2c3d4.css), and every seam that
prints a site-asset URL — .link()/.absLink(), the  content directives, and spa.head hrefs — resolves to that name through one shared formatter, so an installed file
and a link to it cannot drift apart. A changed file is a changed URL, which is what lets a
deploy put Cache-Control: immutable on the asset tree. Opt-in and release-only; static_assets entries, build assets, page assets and the in-memory live server keep verbatim
names. See docs/assets.md.
--allow-missing-pages (zigapagos release and the live server; for a zigapagos dev
loop set allow_missing_pages in your build.zig, since dev re-runs your rebuild
command rather than building the site itself — the tolerance is identical either way,
so a green dev preview and a CI release agree):
tolerate a $link.page/$link.sibling/$link.sub (content) or $site.page(...) (template) reference to a page that doesn't exist YET, instead of
hard-failing the build. The reference renders as the real, url_prefix-aware href
the target page will have once it's written (a 404 until then), and the build log gets
a warning naming the ref and the computed href instead of a fatal error. This is the
fix for incremental authoring: previously, adding a navigation link before its target
page existed broke the entire build (one dangling link → zero pages built), which is
exactly what "site under construction" always looks like.
A relative .aliases entry that basenames as 404.html, robots.txt, or sitemap.xml
now prints a build-time warning showing where it actually resolves. Alias resolution
itself is unchanged — a relative entry still joins to the page's own output directory,
exactly as before; this only flags the common mistake of meaning a site-wide override
(e.g. "/404.html" to replace the SPA fallback) but writing the bare relative form
instead.
A layout route now receives its matched child as a children prop as well as
through <Outlet/> — the two are the same channel (childrenis an <Outlet/>), so a layout written as <div>{children}</div> renders its child
instead of an empty container. Rendering both warns, and so does rendering
neither.
zigapagos warns at build time when a SPA declares no spa.head on a site
that has stylesheet assets, since SPA shells have a fixed <head> and do not
inherit site styles. head: [] declares the omission deliberate and silences
it.
docs/generated-content.md: documents the generated-content pattern this site's own
docs pages use as a copyable recipe (a registry, a deterministic generator, per-file .gitignore entries, and a freshness gate), instead of a built-in content_generators
config hook. The verdict on #34 is that a hook would only automate the cheap part
(invoking a script); the actual cost is the five SuperMD transformations a generator has
to apply, which are documented here in full instead.
Changed
:else is now a build error. SuperHTML validates it at parse time and then never
evaluates it — the renderer null-unwraps its (mandatorily absent) value, so no template
using :else has ever rendered. The error names the fix: write the negated condition on
a second <ctx>, <ctx :if="$cond">…</ctx><ctx :if="$cond.not()">…</ctx>.
:if / :loop on an element with no end tag — a void element like <img>, <br>, <input>, or a self-closing <item/> in an .xml alternative layout — is now a build
error. SuperHTML restarts a conditional or a loop by rewinding to the element's end tag;
with none it rewinds to the start of the file and splices the whole raw template
source into the page (previously with exit code 0), or slices backwards and panics.
The error names the fix: wrap the element in <ctx>.
A <Link> rendered outside a <Router> is now a build error rather than a
silently dead anchor: without router context the href cannot resolve against
the SPA base and the click is never intercepted, so the prerendered shell
shipped a link that 404s on a path-prefixed host. On the client the same
situation warns once per href instead of throwing. Use a plain <a> for a
non-router anchor.
The build error for a dynamic route with no skeleton now names the concrete
pathname the shell is prerendered at.
Fixed
absLink() on a multilingual site returned a root-relative URL for page
assets ($page.asset(...)). It is now absolute in every locale, and stays
correct across locales too: $page.locale('de').asset(...).absLink() emits
the target locale's host exactly once, including when that locale sets host_url_override.
On a multilingual site whose locale sets host_url_override, a site asset
linked with link() lost the separator after assets_prefix_path and came
out as https://example.com/staticfoo.css (or https://example.comfoo.css
with no prefix). This affected link() on those sites before absLink()
existed, and is fixed for both.
A full build now names the site assets it pruned. An asset installs only when something bumps
its refcount, and everything else was dropped in silence — a hand-authored SVG vanished from a
build when its last .link() went away, and finding out why meant reading the refcount logic.
The report is a sorted, capped list with the true total and both fixes spelled out. It stays a
warning, since staging a file ahead of the page that will use it is legitimate, and it is
suppressed wherever it would fire on correct code: incremental rebuilds, a build whose render
pass failed, assets consumed by .bytes()/.size()/.sriHash()/.ziggy(), .keep/.gitkeep placeholders, and an assets_dir_path that doubles as a content dir.
A content directory that holds .smd pages but no index.smd now produces a build-log
warning. Such a directory never becomes a section, so its pages join the enclosing
section with deeper URLs, no page is built at the directory's own URL, and $page.subpages() aimed at it returns an empty list — which previously looked like
"my section is empty" with nothing pointing at the cause. The warning names the
directory, the URL that is not built, and the index.smd to create; when a sibling <dirname>.smd already occupies that URL it says so, since that is the usual shape of
the mistake. It is a warning, not an error: an index-less directory is a legitimate
URL-shaping tool.
An unknown code-fence language (e.g. a typo like ```zig++) is now a build-log
WARNING instead of a fatal error. The fence still renders — as escaped, unhighlighted
text, the same output enable_treesitter=false already produces for every language —
and the warning includes a did-you-mean suggestion when one is available (run zigapagos languages to see the full registered list).
A $link reference starting with a leading . (SuperMD's syntax for "subpage of this
section") that fails because the current page isn't a section now includes a note
clarifying that a leading . means "subpage of this section", not a relative path, and
points at $link.page(...) for linking a sibling page instead.
$link.page('') — which looks like it should work, because $site.page('') accepts an
empty ref for the homepage — now fails with a note pointing at $link.site(), the
correct builtin for linking to the site's homepage, instead of just SuperMD's bare
"path is empty".
Under --format=json, a fatal.msg no longer aborts a Debug build with SIGABRT: it
emits one ZP_FATAL object and exits 1. The std.Progress bar and the
Debug/tracy/tsan warning banners are suppressed in that mode too, since all three write
to the same stderr the NDJSON stream uses.
The site's url_path_prefix is now composed into Router.base in both
environments, so a path-prefixed deploy (a GitHub project-pages site) emits
prerendered <a href> values that carry the prefix, works without JavaScript,
and soft-navigates to a URL that survives a hard refresh. The prefix reaches
the build's SSR pass over the sidecar protocol and the browser over a data-z-prefix attribute on the shell's hydration root, so the two can never
disagree. Sites with no url_path_prefix are unaffected, byte for byte.
zigapagos serve prefixes the SPA bundle and runtime URLs it bakes into dev
shells, which its own request handler already required.
An island's SSR pathname (host.pathname(), useLocation()) now carries the
site's url_path_prefix, matching what the browser reports. An island that
branches on the path — active-nav highlighting, breadcrumbs — used to render
one thing at build time and another after hydration.
The generated nginx, Apache and ZigBase host configs now account for a site's url_path_prefix, each according to its own semantics rather than by
prepending the prefix everywhere: nginx prefixes its location selectors and try_files targets; Apache emits a RewriteBase and keeps its per-directory
patterns relative; ZigBase prefixes its .match patterns but leaves .serve
targets pointing at the output tree, which has no prefix directory. routing-manifest.json carries the prefix as its own url_path_prefix field
for them to apply — its route values stay tree-relative.
The migration guide now spells out the three separate :if traps, including the one
that is still legal and still surprising: :if on a real element gates only its BODY,
so the tag and every one of its attributes are emitted either way (this is how a
documentation sidebar shipped aria-current="page" on all 14 nav items with a green
build). Wrap the element in <ctx> to make the element itself conditional.
Known limitations
With auto_heading_ids on, a same-page reference through the $link.ref('slug') Scripty
directive still fails with unknown ref — SuperMD's own invalid_ref check runs inside Ast.init, before ids can be injected. Plain Markdown links ([t](#slug), [t](/other#slug)) are validated later and work fine; $link.unsafeRef('slug') is the
workaround for the Scripty-directive case.
A content-authored <z-island> only accepts static props (:props Ziggy
literals and literal prop-NAME="value" attributes). prop-NAME="$page.*"
Scripty expressions do not resolve in content — Scripty is evaluated by
SuperHTML at layout render time, and an =html fence's body is emitted
verbatim, never run through SuperHTML's template evaluator. A page-bound
prop still needs a layout.
No Windows support until the Zig 0.17 port. Inherited upstream code
(src/cli/serve/watcher/WindowsWatcher.zig, src/wuffs.zig) does not compile on
stable Zig 0.16.0, and the fix rides upstream's 0.17-dev branch.
FreeBSD needs 15 or newer for live reload: the watcher reuses the
inotify-based LinuxWatcher, and inotify entered the FreeBSD base system in 15.
There is no kqueue backend. Building and serving static output is unaffected.
Strict CSP requires deploying the emitted header. The build writes the
hash-strict policy, but serving it (and re-serving it after a rebuild, since the
hashes are byte-exact) is the host's job. style-src still needs unsafe-inline for the framework's inline style attributes.
host_url_override on a locale is not supported by the live server.
Prebuilt binaries cover x64 only. GitHub Releases ship an x86_64-linux-musl.tar.xz, an x86_64-macos.zip and SHA256SUMS, from v0.1.1
onward, and the npm packages repackage those same two binaries. An arm64 host —
Apple Silicon included — and any commit earlier than v0.1.1 still need a source
build.
Pre-1.0: APIs may change between minor versions.
Internal
zig build test-assets had been compiling and running zero tests while exiting 0, for as
long as the step has existed: filters is a compile-time --test-filter, and no test in main.zig matched assets:, so nothing past main.zig was ever analysed. It now carries the
anchor the other suites already had. Fallout: that finally compiled src/PathTable.zig's
inherited test PathTable, which had rotted against a getPath → getPathNoName rename and
no longer built — repaired in place.
CI no longer resolves an npm package at workflow runtime. browser-e2e.yml's site job
served the built site with bunx serve, which downloads whatever the registry has at
the moment the job runs, in a repository that pins its toolchain in mise.toml, passes --frozen-lockfile to every bun install and materializes its Zig dependencies from
hashes. It now uses python3 -m http.server, already present on every runner, and tests/meta/ci-package-pins.sh fails the build on an unpinned npx / bunx / bun x / pnpm dlx in any workflow so the hole cannot reopen. (#50)
CI builds site/ on the pull-request path (new site job in ci.yml), running the four
assertions — build.sh, docs-mirror.sh, links.sh, js-budget.sh — that previously ran
only as deploy gates in pages.yml and in the scheduled browser-e2e.yml. It reuses the zigapagos binary the build-binary job already publishes, so nothing compiles.
The branding gate takes an inline opt-out. <!-- branding-ok: why --> sanctions the
upstream project's name on that line and <!-- branding-ok:begin why --> / <!-- branding-ok:end --> sanctions a block, for the cases where naming it literally is
the accurate thing to do — this repository's fork-point tag is named after the upstream
release it marks, so the passage in CHANGELOG.md explaining which tags exist here can
now say so instead of gesturing at it. A reason is required, an unbalanced block fails,
a marker that exempts nothing fails as stale, and every sanctioned mention is printed on
success. The gate also no longer excludes itself from its own search, and tests/branding.test.sh pins each of those rules from both sides. (#60)
Extracted the SuperMD transformer out of site/scripts/gen-docs-mirror.ts into site/scripts/md-to-smd.ts, a repo-agnostic module with no repo-specific constants
(paths, URLs, or fence-language remaps are all passed in via TransformOptions), so it
is the thing docs/generated-content.md tells a reader to copy. Verified byte-identical
output against the pre-extraction generator across all 9 existing mirrors.
That transformer tracked fenced code blocks by toggling a boolean on any line that was
exactly three backticks (or tildes) followed by a bare [A-Za-z0-9_-]* language. A doc
that shows fenced Markdown nests a three-backtick block inside a four-backtick one, and
SuperMD's own raw-HTML escape hatch is the fence info string =html — neither is that
shape, so the inner closing fence was read as an opener and the tracker stayed inverted
for the rest of the file, silently dropping every $heading.id(...) and every link
rewrite after it. docs/islands.md hit this, and the two links whose targets had lost
their ids then failed the site build with unknown ref. Fence recognition now follows
CommonMark: a run of three OR MORE delimiters, an arbitrary info string (with no backtick
in a backtick fence's), and a closer that must match the opener's character, be at least
as long, and carry no info string.
Templated site/test/docs-mirror.sh's repo-specific paths behind variables at the top,
and fixed its rendered-HTML directive check, which used to grep the built page for
a literal Scripty directive with no way to tell a real leak from a directive shown as a
documented code sample — a false positive docs/generated-content.md would have tripped
immediately. It now strips <pre>/<code> before matching.
Added site/test/md-to-smd.test.ts, unit tests for the extracted transformer covering
heading-slug edge cases (the em-dash double-hyphen, dedup, an indented fence), link
rewriting, the leading-title strip, and the Ziggy frontmatter emitter, wired into site/test/docs-mirror.sh so CI runs them without a workflow change.
The release target matrix is declared in three places — build/release.zig, npm/cli/targets.json and release.yml's build matrix — and npm/check-targets.mjs now fails when they disagree, deriving each npm
key/cpu/os and archive name from the zig triple rather than trusting the JSON.
Wired into CI through tests/npm/targets.sh and into the release workflow before
anything is packed. A stale targets.json would otherwise publish a platform
package whose binary nobody built.
release.yml gained an npm-package job that assembles and install-tests the
packages from the archives the release already builds — on pull requests too, so
a packaging defect is caught before a tag rather than by a published version that
cannot be replaced. Publishing is a separate job gated on a v* tag, the NPM_PUBLISH_ENABLED repository variable and the NPM_TOKEN secret.