Docs Start
Local development
Getting the site running: toolchain, commands, the dev-versus-production runtime gap, and environment variables.
Toolchain
Bun for packages and scripts, Node.js >= 22.12 for everything that shells
out to wrangler. .node-version pins 22. A Node 18 shell does not fail loudly —
wrangler commands just silently misbehave, which is a worse outcome than a
crash. Check before debugging anything Cloudflare-shaped:
node --version # must be >= 22.12
bun install
Running
bun dev -- --background # http://localhost:4321, does not hold the terminal
bunx astro dev status
bunx astro dev logs --follow
bunx astro dev stop
The background form is the default because most work here needs the terminal free.
Plain bun dev runs in the foreground when you actually want to watch the log
stream.
Surface-scoped variants boot the same server with a DEV_SURFACE hint, which lets
a route skip work it does not need:
| Command | Surface |
|---|---|
bun dev:home |
Home page only |
bun dev:mood |
Mood feed and detail |
bun dev:richtext |
Mood with the rich-text fixture loaded |
bun dev:preview |
Draft preview routes |
bun dev:portal |
Admin portal with the auth bypass on |
bun dev:api |
Proxy /api/* at a local site-api instead of production |
The dev/production runtime gap
This trips people up, so it is worth stating plainly: astro dev runs on Astro’s
native Node SSR. The Cloudflare adapter only applies during build. The workerd
runtime, its bindings, and the API service binding do not exist in dev.
What that means in practice:
/api/*,/v2/*, and/oauth*are proxied over plain HTTP in dev, toAPI_DEV_ORIGIN(defaulthttps://buxx.me). So a freshbun devtalks to production APIs unless told otherwise.- To develop against a local API: run
bun run devin../site-api(it boots wrangler on127.0.0.1:8787), thenbun dev:apihere. - Set
API_DEV_ORIGINin.env.localto point at a preview deployment instead. - Anything that depends on real Worker behavior — cache keys, headers, bindings —
must be checked with
bun preview, which builds and runswrangler devon the built Worker.
There is a second gap worth knowing: mood pages read from the live source in
dev and the D1 archive in production. Profiling or debugging /mood without
?source=archive means measuring a code path that production never takes.
Performance diagnostics
Append ?debug=performance to any site page to load the opt-in performance
panel. debug=observe is retained as an alias for older investigation links.
The production build includes the diagnostic module, but ordinary visits do not
load or run it.
The panel records layout shifts and their source nodes, LCP, long tasks, slow resources, frame gaps, image frame geometry, fonts, visual viewport changes, scroll calls, and meaningful element resizes. Copy exports the complete log; the visible panel keeps only the latest entries. Agents can read the same data without scraping the UI:
window.__BUXX_PERF_DEBUG__.snapshot()
window.__BUXX_PERF_DEBUG__.text()
Resource entries contain only origins and paths. Page text and resource query parameters are deliberately excluded from the log.
Checks
bun run check # astro sync + type check
bun run build # production build (adapter, agent markdown, pagefind)
bun run test:unit
bun run test:e2e:site # Playwright; needs test:e2e:install once
bun run test:registry # installs every published component with the real CLI
bun run test:ops # scheduled health checks
No linter is configured. That is on purpose — the type checker and the tests are the gate, and a third opinion about formatting was not earning its keep.
Environment variables
Read through import.meta.env.*. Put local values in .env.local; Worker secrets
go in Cloudflare, never in the repo.
| Variable | Purpose |
|---|---|
PUBLIC_GHOST_URL |
Ghost CMS origin (default https://blog.buxx.me) |
GHOST_CONTENT_API_KEY |
Content API key. Required in the Cloudflare build env, or the Writing section prerenders empty |
GHOST_ADMIN_API_KEY |
Server-only. Draft previews. Never prefix it PUBLIC_ |
PUBLIC_BLOG_OG_IMAGE_ENDPOINT |
OGIS endpoint for generated blog OG images |
GITHUB_TOKEN |
GitHub GraphQL token |
PUBLIC_HD_IMAGE_URL |
HD mood image base URL served by site-api |
MOOD_READ_SOURCE |
archive (default) or live |
CHANNEL, TELEGRAM_HOST |
Telegram channel slug and host |
LASTFM_API_KEY, LASTFM_USER |
Home listening widget |
PUBLIC_SITE_URL, SITE_URL |
Canonical base URLs |
API_DEV_ORIGIN |
Dev-only. Where /api/* is proxied |
Bindings and non-secret vars live in
wrangler.jsonc.
Two repositories
This repo (site) is the public Worker. The private Worker site-api lives in
the sibling repo ../site-api and owns D1, KV, R2, queues, crons, admin and OAuth,
notify, the Telegram webhook, the image proxy, and the concrete public API
implementations. Production buxx.me/api/* routes directly to site-api; this
repo keeps only a thin service-binding fallback for preview environments.
Keep them split. The boundary is a security boundary, not an organizational preference.
@bunizao/contracts is duplicated byte-for-byte in both repos and this repo is
canonical. After editing a contract here, run bun run sync:contracts in
../site-api.