Deploying this site
This site is Astro + Starlight,
served by a Workers Assets Worker named gavinf-docs on docs.gavinf.com.
There is no server code — wrangler.json has no main, only an assets
directory, so Cloudflare serves the built files directly.
{ "name": "gavinf-docs", "compatibility_date": "2026-08-23", "assets": { "directory": "./dist" }}Deploying
npm run build # astro build -> dist/npx wrangler deploy # uploads dist/ as the Worker's assetsWrangler only uploads changed files; unchanged assets are reported as “already uploaded” and skipped.
Adding a page
Drop a Markdown file under src/content/docs/. The sidebar is generated from
that tree — astro.config.mjs sets no sidebar key, which is what makes
Starlight walk the filesystem itself.
| Path | Becomes |
|---|---|
src/content/docs/<section>/index.md |
the section landing page |
src/content/docs/<section>/<page>.md |
a child page under it |
src/content/docs/index.mdx |
the splash home page |
Every page must have a title in frontmatter — Starlight will not infer it
from an # H1, and the build fails without it. Starlight renders the title as
the page heading, so don’t also write an H1 in the body or it appears twice.
Quote frontmatter values. An unquoted description containing a colon is
invalid YAML and fails the build with bad indentation of a mapping entry.
---title: "Tunnel & DNS"description: "Zone setup, the tunnel ingress, and Caddy routing."---Order defaults to alphabetical; sidebar: { order: 1 } in frontmatter pins a
page ahead of that.
Custom domain
A hostname can be a Custom Domain on exactly one Worker. Moving
docs.gavinf.com between Workers means detaching it first — attaching over a
live binding fails rather than overriding it.
# list bindings to find the domain idcurl -s "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/domains" \ -H "Authorization: Bearer $CF_API_TOKEN" | jq '.result[] | {hostname, service, id}'
# detach, then attachcurl -X DELETE \ "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/domains/$DOMAIN_ID" \ -H "Authorization: Bearer $CF_API_TOKEN"
curl -X PUT \ "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/domains" \ -H "Authorization: Bearer $CF_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"environment":"production","hostname":"docs.gavinf.com", "service":"gavinf-docs","zone_id":"'"$ZONE_ID"'"}'Constraint: there is a gap of a few seconds between detach and attach where the hostname resolves to nothing. Cut over when nobody is reading.
Cloudflare reuses the same binding id and edge certificate across the move, so
no new cert is issued. Worker routes are different — those can be
reassigned atomically with a PUT, no gap.
Worker names are constrained to ^[a-z0-9_][a-z0-9-_]*$, so no uppercase.
Git-connected builds
Connect the repo in Workers & Pages → gavinf-docs → Settings → Builds:
| Setting | Value |
|---|---|
| Repository | gavinfancher/gavinf-docs |
| Build command | npm run build |
| Deploy command | npx wrangler deploy |
| Root directory | / |
The build image has Node preinstalled, which is why a Node-based generator is the low-friction choice — a Python one such as MkDocs would need its runtime verified in the build image first.
Propagation
Cloudflare’s asset store is eventually consistent. A freshly deployed path can 404 on first request and 200 moments later. Don’t judge a deploy by one curl.