The user-facing documentation site for Guaranate, published at guaranate.dev: Astro Starlight, deployed with Zephyr Cloud.
This directory is self-contained — it holds the only package.json in the
repository. The Swift package at the repository root does not know it exists, and
swift build / swift test are unaffected by it.
Not to be confused with docs/ at the repository root, which holds agent and
contributor configuration (docs/agents/, docs/adr/) and is not published here.
npm install # once
npm run dev # local dev server with hot reload
npm run build # static build — and a Zephyr deployment (see below)
npm run preview # serve the built site from dist/
npm run gen:cli # regenerate the CLI reference from the guaranate binary
npm run gen:changelog # regenerate the changelog from the root CHANGELOG.mdsrc/content/docs/reference/cli.md is generated by
scripts/gen-cli-reference.mjs from guaranate --experimental-dump-help, the
Swift Argument Parser's own structured help output. Do not edit it by hand: every
flag, short alias, and default on that page comes from the binary, so it cannot
drift from what ships.
Regenerate it after any change to the CLI surface:
npm run gen:cli # builds the binary if needed, then rewrites the page
npm run gen:cli:check # non-mutating: fails if the page is staleThe page is committed because this site builds on Linux, where the macOS-only
binary cannot be built. The Swift Build & Test CI job runs gen:cli:check on
macOS, so a CLI change that forgets to regenerate the page fails there.
src/content/docs/reference/changelog.md is generated from the repository root
CHANGELOG.md, the sole source for release history. npm run gen:changelog
regenerates it, and npm run build and npm run dev run the generator first.
If CHANGELOG.md changes while a dev server is already running, rerun
npm run gen:changelog to refresh the page.
The page links “Edit page” to the root CHANGELOG.md. Generated output is
ignored rather than committed: the docs build reads the source directly, so
there is no second changelog to keep in sync. Unlike this page, the binary-generated
cli.md is committed because the macOS CLI cannot be built on the Linux docs
builder.
src/assets/brand/source-sheet.png is the original branding sheet: six mascot
variants in a 3x2 grid on a black background. Everything else is cut from it by
scripts/gen-brand-assets.sh (ImageMagick + pngquant), which paints out the few
stray specks the sheet leaves outside a mascot's outline, flood-fills the black
background to transparency, trims, and quantizes:
| Asset | Variant | Used by |
|---|---|---|
src/assets/brand/mascot.png |
plain mascot | site header logo, README.md |
src/assets/brand/terminal.png |
mascot in a terminal window | landing hero, README.md |
src/assets/brand/sticker.png |
die-cut sticker | 404 page hero |
public/brand/wink.png |
winking mascot | install guide |
public/brand/happy.png |
happy mascot | sessions guide |
public/brand/badge.png |
round badge | source of public/favicon.png |
Assets under src/assets/ are imported, so Astro optimizes and hashes them.
Assets under public/ are referenced by absolute path from plain Markdown, where
Astro's image pipeline does not apply.
./scripts/gen-brand-assets.sh # only needed if the sheet or a crop changesVariants used as a Starlight hero are padded to a square canvas: hero images are rendered at a fixed 400x400, and Astro crops to fill when both dimensions are set, which otherwise shaves the top and bottom off a non-square asset.
public/brand/social-card.jpg is the 1200x630 image link unfurlers show for
every page — X/Twitter, Slack, Discord, LinkedIn. Starlight emits
twitter:card: summary_large_image but no image of its own, so the og:image /
twitter:image tags are declared in astro.config.mjs (head:), as absolute
URLs built from site: unfurlers do not resolve relative paths.
Three details are load-bearing, all learned the hard way on X:
- The card is a truecolor JPEG. It shipped first as a pngquant-palettized
PNG, which X fetched (200,
image/png, 1200x630, 73 KB) and then refused to render. Slack and Discord were fine with it. - The
twitter:namespace is spelled out, not left to fall back toog:.src/components/Head.astrooverrides Starlight'sHeadto add per-pagetwitter:title/twitter:descriptionplustwitter:site/twitter:creator. public/robots.txtallows everything explicitly. Without a file of its own the domain served Cloudflare's managed robots.txt, which is nothing but content-signal comments — noAllow, no sitemap.
X has no cache purge since the Card Validator was retired, and it caches the
page metadata and resolved image as separate records. If X has fetched the image
but omitted its image bindings, increment the version query on socialCard in
astro.config.mjs to force a new media record; use a fresh page URL to make X
read that updated tag instead of its cached page metadata.
./scripts/gen-social-card.sh # only needed if the branding or the tagline changesThe card is composed by ImageMagick from src/assets/brand/terminal.png and the
theme palette, then flattened (no alpha) and written at quality 88. Its type is
set in Avenir Next and Menlo rather than the site's Space Grotesk / JetBrains
Mono: Fontsource ships those as WOFF2 only, which FreeType — and so ImageMagick
— cannot read.
src/styles/theme.css is the whole visual identity, wired through Starlight's
customCss:
- Colour. Starlight builds its palette from HSL ramps at hue 224 (cool grey).
The same ramps are rotated to a warm hue and the accent ramp is rebuilt around
the mascot's berry red (
#d31a0e), which preserves Starlight's contrast steps. Aside and card accents are remapped to leaf green, guaraná amber, and berry red instead of Starlight's blue/purple rotation. - Type. Space Grotesk for UI and
prose, JetBrains Mono for code,
both self-hosted via Fontsource — no requests to a font CDN. Swapping the UI face
means changing one Fontsource package in
astro.config.mjsand--sl-fontinsrc/styles/theme.css. - Details. Tighter heading tracking, a leaf-green rule under
h2section headings, hero art at 80% of Starlight's default size with a berry glow behind it, and a berry-red primary call to action.
Zephyr deploys during npm run build — there is no separate upload step, and
a git repository must be initialized (CI checkouts satisfy this). Consequences:
outputmust stay'static'; Zephyr supports Astro's SSG mode only.- Authentication is a Zephyr token in
ZE_SECRET_TOKEN, stored as the repository secret of the same name. ZE_FAIL_BUILD: trueis set in CI. Without it, Zephyr logs deployment errors and lets the build pass.SKIP_ZEPHYR=truedrops the integration from the Astro config, producing a plain static build. CI sets it whenever noZE_SECRET_TOKENis available, because with no credentials the plugin blocks on an interactive auth flow until it times out — five minutes for a build that takes two seconds.
.github/workflows/docs.yml deploys every merge to main — which Zephyr
serves from the production domain, https://guaranate.dev — and internal pull
requests, which get their own immutable preview URL. It build-verifies pull
requests from forks, which cannot read repository secrets, and so intentionally
run without a token and without deploying.
Deployment is intentionally detached from the binary's release: publishing the
site never waits for a version to be cut, and cutting a release never
republishes the site. Whatever is on main is what guaranate.dev serves.
Building locally deploys if you are logged in to Zephyr. astro preview loads the
integration too, so with an expired session it stops and waits for a browser
login. Either way, SKIP_ZEPHYR=true gives you a plain local site:
SKIP_ZEPHYR=true npm run build
SKIP_ZEPHYR=true npm run preview