Marketing site and public documentation for nan.builders. Built with Astro 6 (SSR mode) on Cloudflare Workers, vanilla scripts (and a few Preact islands on the events pages), Tailwind CSS v4, and an EN/ES i18n catalog. Signup is open and lives on cloud.nan.builders: the site links there (hero, nav, pricing, footer), and every product unlocks with a plan.
- Astro 6 (
output: 'server', Cloudflare adapter) - Preact 10 for the few client-side islands (events)
- TypeScript (strict)
- Tailwind CSS v4 (Vite plugin)
rehype-pretty-code+ Shiki for code highlighting in docsastro-i18nplus a local helper (src/lib/i18n.ts) for ES/EN strings- Vitest 4 for unit tests
- Cloudflare Workers (assets binding + SSR entrypoint)
- Node.js
>=22.12.0, npm (lockfile ispackage-lock.json)
The site is a single Astro app deployed as a Cloudflare Worker. The landing (/, /es), the community page, events, projects and the docs (/docs/*) are SSR-rendered at the edge. There is no signup form or route on the site: the hero's Create account button links to https://cloud.nan.builders/signup (carrying a valid ?plan= from the landing URL; the legacy ?premium=1 maps to plan=premium), Log in links to https://cloud.nan.builders/, and pricing cards link to signup?plan=<product>. Links are built in src/lib/signup.ts. Static assets are served from the ASSETS binding configured in wrangler.jsonc.
See ARCHITECTURE.md for the full breakdown of rendering modes, route inventory, security model, and member lifecycle.
website/
├── src/
│ ├── pages/ # Landing, community, legal, events, projects, docs (EN root, ES under /es)
│ │ └── api/ # auth/login-request, events proxy, docs manifest
│ ├── components/ # nan/ (shell + home sections), docs/, events/
│ ├── scripts/ # Vanilla client scripts (reveal, dither, ...)
│ ├── layouts/ # NanBase, NanPage, Docs, NanLegal
│ ├── lib/ # signup links, i18n, docs, events, rate limits
│ ├── styles/ # Tokens, system CSS, Tailwind theme
│ ├── tests/ # Vitest suites
│ └── env.d.ts # Cloudflare env typings
├── i18n/ # en.json, es.json translation catalogs
├── public/ # Static assets, favicons, _routes.json
├── .github/workflows/ # ci.yml, deploy.yml
├── astro.config.mjs
├── wrangler.jsonc
├── vitest.config.ts
├── tsconfig.json
└── package.json
- Node.js
>=22.12.0(uses native--env-file=in scripts) - npm (the lockfile is
package-lock.json; do not switch package managers) - Access to a
cloud-apiinstance (default:https://cloud-api.nan.builders) for the events pages. WithoutCLOUD_API_URLthe events pages fail to load, but the landing and docs render fine.
git clone <repo>
cd website
npm install
cp .env.example .env
# fill in values (see "Environment variables" below)
npm run devThe dev server runs on http://localhost:4321.
| Script | Command | Description |
|---|---|---|
dev |
astro dev |
Start the Astro dev server with HMR. |
build |
astro check --quiet || true && astro build && cp public/_routes.json dist/client/ |
Type-check (non-blocking), build for Cloudflare Workers, then copy the routing rules into dist/client/. |
preview |
astro preview |
Preview the production build locally. |
astro |
astro |
Run the Astro CLI passthrough. |
generate-types |
wrangler types |
Regenerate worker-configuration.d.ts from wrangler.jsonc (file is gitignored). |
test |
vitest run |
Run the full Vitest suite once. |
test:watch |
vitest |
Run Vitest in watch mode. |
Runtime variables are declared in src/env.d.ts and consumed via Astro.locals.runtime.env (Cloudflare adapter). Local development reads them from .env; production values live in the Cloudflare dashboard (set with wrangler secret put).
| Name | Required | Where it's used | Description |
|---|---|---|---|
CLOUD_API_URL |
runtime | src/lib/events.ts, src/pages/api/events/[...path].ts, src/pages/api/auth/login-request.ts |
Base URL of the cloud-api backend (e.g. https://cloud-api.nan.builders). |
RATE_LIMIT_RPM / RATE_LIMIT_PARALLEL |
optional | src/lib/rateLimits.ts |
Per-key limits shown in the docs; defaults apply when unset. |
RESEND_API_KEY, RESEND_FROM_EMAIL and CLOUD_API_WAITLIST_KEY are no longer read by the site and can be removed from the Worker. None of these values should ever be committed.
npm test # run all Vitest suites
npm run test:watchVitest config: vitest.config.ts (a unit project for src/**/*.test.ts and a render project for src/**/*.render.test.ts). The signup-link suites are src/tests/lib/signup.test.ts, src/tests/landing/heroCta.test.ts, src/tests/landing/heroCta.server.test.ts and src/tests/landing/openSignup.test.ts.
npm run build # astro check (non-blocking) → astro build → copy _routes.json
npm run preview # serve the production build locallyastro check is invoked with --quiet and || true so type warnings do not fail the build; full type checking happens in CI via npx astro check.
The site is deployed to Cloudflare Workers under the name nan-website (see wrangler.jsonc). Pushes to main trigger the Deploy workflow, which runs tests, builds, and then npx wrangler deploy. The production site is served at https://nan.builders.
wrangler.jsonc highlights:
main: "@astrojs/cloudflare/entrypoints/server"— Astro's Cloudflare SSR entrypoint.assets: { directory: "./dist", binding: "ASSETS" }— static assets binding for the Worker.compatibility_date: "2026-03-17"andcompatibility_flags: ["global_fetch_strictly_public"]— outboundfetchis restricted to public addresses.observability.enabled: true— Workers observability is on.
All runtime secrets must be set in the Cloudflare dashboard (or via wrangler secret put), not in the repository.
Because every wrangler deploy publishes a numbered Version, Cloudflare keeps
the history and a rollback needs no rebuild. It is a code-and-assets rollback
only: it does not revert secrets, vars, or anything living in cloud-api.
Before merging anything large, write down the version that is live:
npx wrangler deployments status # shows the Version ID currently serving
npx wrangler versions list # the 10 most recent VersionsTo roll back:
npx wrangler rollback <version-id> # omit the id to pick the previous one
npx wrangler deployments status # confirm what is serving nowGive it a minute and re-check the site: the Worker flips immediately, but a CDN-cached asset can outlive the rollback for a short while.
wrangler deploy sends 100% of traffic to the new Version straight away. To see
a build on real Cloudflare infrastructure first, upload a Version without
deploying it. This is worth doing for releases that replace many pages at once:
npm run build
npx wrangler versions upload # prints a preview URL, serves no trafficOnce it looks right, either merge as usual or promote that exact Version:
npx wrangler versions deploy <version-id>@100wrangler versions deploy also splits traffic (for example <id>@10 for 10%),
which is the safer path for a change that touches routing or URLs.
Two GitHub Actions workflows live in .github/workflows/:
- Trigger:
pull_requesttomain. - Job
build-and-test(Ubuntu, Node 22, npm cache):npm cinpm testnpx astro checknpm run build
- Trigger:
pushtomain. - Job
deploy(Ubuntu, Node 22, npm cache,deployments: write):npm cinpm testnpm run buildnpx wrangler deploy
Required repository secrets (consumed by deploy.yml):
CLOUDFLARE_API_TOKENCLOUDFLARE_ACCOUNT_ID
The site supports Spanish (es, default) and English (en).
- Translation catalogs:
i18n/es.jsonandi18n/en.json. - Helpers:
src/lib/i18n.tsexportst(key, locale)for strings,tArrfor arrays,tObjfor nested objects, andgetLocale(URLSearchParams)which reads the?lang=query param and defaults toes. - Locale switcher UI:
src/components/landing/LanguageSwitcher.astro, embedded inBase.astroand on/community.
To add a new string, add the key (matching the dotted path used in t(...)) to both i18n/es.json and i18n/en.json, then reference it from the component with t('your.key', locale). Both catalogs must stay in structural sync.
- Branch from
mainand open a PR. - Commit messages follow Conventional Commits (
feat(scope): ...,fix(scope): ...,docs(scope): ...). Match the existinggit logstyle. - CI must be green:
npm test,npx astro check, andnpm run buildall pass. - For UI changes, run
npm run devand smoke-test the affected flows before requesting review. - Never commit secrets or any
.envfile.