From 113383e45166984b306e3cd911c47cf27fe6f10b Mon Sep 17 00:00:00 2001 From: RedStar Date: Mon, 19 Oct 2026 12:14:08 +0200 Subject: [PATCH 1/3] ci: deploy the marketing and docs sites to Cloudflare Workers Marketing goes to code-zero.dev and the docs to docs.code-zero.dev as two static Workers. Both sites are generated with `nuxt generate`, so each wrangler.jsonc only uploads .output/public: no script, no bindings, the generated 404 page for unknown paths, and trailing slashes redirected to the canonical URL. Docs search keeps working because Docus queries its content dump in the browser. deploy-sites.yml builds and deploys both on pushes to main that touch them and on manual dispatch, using the CLOUDFLARE_API_TOKEN secret and the CLOUDFLARE_ACCOUNT_ID variable. Wrangler runs through npx at a pinned version rather than through an action, which would try to install it with the package manager the lockfile implies. The marketing Docs links now point at docs.code-zero.dev instead of the README, and the vercel.json files stay for pull request previews. --- .github/workflows/deploy-sites.yml | 86 +++++++++++++++++++++ .gitignore | 5 +- AGENTS.md | 1 + apps/docs/content/1.guide/16.deployment.md | 33 ++++++-- apps/docs/wrangler.jsonc | 13 ++++ apps/marketing/modules/shared/utils/site.ts | 2 +- apps/marketing/wrangler.jsonc | 14 ++++ 7 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/deploy-sites.yml create mode 100644 apps/docs/wrangler.jsonc create mode 100644 apps/marketing/wrangler.jsonc diff --git a/.github/workflows/deploy-sites.yml b/.github/workflows/deploy-sites.yml new file mode 100644 index 00000000..dc6fa0aa --- /dev/null +++ b/.github/workflows/deploy-sites.yml @@ -0,0 +1,86 @@ +name: Deploy sites + +# Publishes the marketing site to code-zero.dev and the docs to docs.code-zero.dev, as two static +# Cloudflare Workers (see each app's `wrangler.jsonc`). Both are generated ahead of time, so a +# deploy is a build plus an asset upload; nothing runs on Cloudflare at request time. + +on: + push: + branches: + - main + paths: + - apps/marketing/** + - apps/docs/** + - docs/*.md + - packages/build-env/** + - packages/i18n/** + - packages/ui/** + - pnpm-lock.yaml + - .github/workflows/deploy-sites.yml + workflow_dispatch: + +permissions: {} + +concurrency: + # One deploy per site at a time, and never cancel one halfway through an upload. + group: ${{ github.workflow }} + cancel-in-progress: false + +env: + # Pinned so a deploy is reproducible; bump together with the compatibility dates in wrangler.jsonc. + WRANGLER_VERSION: 4.151.1 + +jobs: + deploy: + name: Deploy ${{ matrix.site }} + if: github.repository == 'wolfstar-project/code-zero' + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + environment: + name: ${{ matrix.site }} + url: ${{ matrix.url }} + strategy: + fail-fast: false + matrix: + include: + - site: marketing + package: '@code-zero/marketing' + url: https://code-zero.dev + - site: docs + package: '@code-zero/docs' + url: https://docs.code-zero.dev + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup toolchain + uses: ./.github/actions/setup-toolchain + + - name: Build workspace dependencies + env: + PACKAGE: ${{ matrix.package }} + run: aube exec turbo run build --filter="${PACKAGE}^..." + + - name: Generate static site + working-directory: apps/${{ matrix.site }} + env: + # Absolute URLs in the sitemap, canonical links and robots.txt. + NUXT_SITE_URL: ${{ matrix.url }} + MARKETING_SITE_URL: https://code-zero.dev + MARKETING_DASHBOARD_URL: https://app.code-zero.dev + CODE_ZERO_BUILD_COMMIT: ${{ github.sha }} + CODE_ZERO_BUILD_BRANCH: ${{ github.ref_name }} + # The Docus prerender needs more than Node's default heap. + NODE_OPTIONS: --max-old-space-size=6144 + run: aube exec nuxt generate + + - name: Deploy to Cloudflare + working-directory: apps/${{ matrix.site }} + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} + run: npx --yes "wrangler@${WRANGLER_VERSION}" deploy diff --git a/.gitignore b/.gitignore index 2122c2de..ee9ef67b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,14 @@ # Also matches a node_modules symlink, which the trailing-slash form does not. node_modules .turbo/ -dist/ +# Also matches the `dist` symlink `nuxt generate` points at `.output/public`. +dist .output/ .nuxt/ # Written by the Nitro `vercel` preset and by the Vercel CLI's own local link file. .vercel/ +# Local state of `wrangler dev` and `wrangler deploy`. +.wrangler/ .maizzle/ .vitehub/ *.tsbuildinfo diff --git a/AGENTS.md b/AGENTS.md index 33555dbd..007a4000 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -183,6 +183,7 @@ aube run build | ---------------------------- | --------------------------------------------------------- | ------------------------------------- | | `ci.yaml` | Lint, repository metadata, typecheck, tests, build, i18n | PR, push to `main`, merge group | | `cd.yml` | Builds and publishes the dashboard image to GHCR | Push to `main`, `vX.Y.Z` tags, manual | +| `deploy-sites.yml` | Deploys marketing and docs to Cloudflare Workers | Push to `main` (site paths), manual | | `autofix.yml` | Pushes formatting and lint fixes back to the pull request | PR, merge group | | `zizmor.yaml` | Static analysis of GitHub Actions workflows | PR, push to `main`, merge group | | `semantic-pull-requests.yml` | Validates PR titles against Conventional Commits | PR opened, edited, synchronized | diff --git a/apps/docs/content/1.guide/16.deployment.md b/apps/docs/content/1.guide/16.deployment.md index 6ac2bc68..66ee4859 100644 --- a/apps/docs/content/1.guide/16.deployment.md +++ b/apps/docs/content/1.guide/16.deployment.md @@ -83,9 +83,32 @@ expects the preset's own layout. `apps/marketing` sets `NITRO_PRESET` the same way. It composes no ViteHub integration, so the variable only picks its Nitro preset, and it has no KV store to configure. -## Documentation site +## Marketing and documentation sites -`apps/docs` is not deployed with the dashboard. It ships its own `vercel.json` so it can be -deployed as a separate Vercel project with **Root Directory** set to `apps/docs`: the install and -build commands `cd` back to the workspace root so `pnpm` and Turborepo resolve the monorepo -correctly, and the build output is `.output`. +`apps/marketing` and `apps/docs` are not deployed with the dashboard. Production runs them as two +static Cloudflare Workers, `code-zero-marketing` on `code-zero.dev` and `code-zero-docs` on +`docs.code-zero.dev`. Both are generated ahead of time with `nuxt generate`, and each app's +`wrangler.jsonc` only uploads `.output/public`: no script runs at request time, so there are no +bindings or secrets on the Workers themselves. Unknown paths get the generated `404.html`, and a +trailing slash redirects to the canonical URL without one. + +The `deploy-sites.yml` workflow builds and deploys both on every push to `main` that touches them, +and on manual dispatch. It needs two repository settings: + +- the `CLOUDFLARE_API_TOKEN` secret, a token limited to **Workers Scripts: Edit** on the account and + **Workers Routes: Edit** on the `code-zero.dev` zone; +- the `CLOUDFLARE_ACCOUNT_ID` variable. + +To deploy by hand, generate the site and run `wrangler deploy` from its directory: + +```bash +aube exec turbo run build --filter='@code-zero/marketing^...' +cd apps/marketing +MARKETING_SITE_URL=https://code-zero.dev MARKETING_DASHBOARD_URL=https://app.code-zero.dev aube exec nuxt generate +npx wrangler deploy +``` + +The Docus prerender needs more memory than Node's default heap, so give `nuxt generate` in +`apps/docs` about 2.5 GB (`NODE_OPTIONS=--max-old-space-size=2560`) or more. + +Each app also keeps its `vercel.json`, which the preview deployments on pull requests still use. diff --git a/apps/docs/wrangler.jsonc b/apps/docs/wrangler.jsonc new file mode 100644 index 00000000..a5c05ac1 --- /dev/null +++ b/apps/docs/wrangler.jsonc @@ -0,0 +1,13 @@ +{ + // Docus is generated as a static site (`nuxt generate`): every page is prerendered and search + // runs in the browser over the content dump, so the Worker only serves files and needs no D1. + "name": "code-zero-docs", + "compatibility_date": "2026-10-01", + "assets": { + "directory": ".output/public", + "not_found_handling": "404-page", + "html_handling": "drop-trailing-slash", + }, + "routes": [{ "pattern": "docs.code-zero.dev", "custom_domain": true }], + "workers_dev": true, +} diff --git a/apps/marketing/modules/shared/utils/site.ts b/apps/marketing/modules/shared/utils/site.ts index ba2cc59e..c4ea05bc 100644 --- a/apps/marketing/modules/shared/utils/site.ts +++ b/apps/marketing/modules/shared/utils/site.ts @@ -20,6 +20,6 @@ export const links = { issues: 'https://github.com/wolfstar-project/code-zero/issues', security: 'https://github.com/wolfstar-project/code-zero/blob/main/SECURITY.md', architecture: 'https://github.com/wolfstar-project/code-zero/blob/main/docs/architecture.md', - docs: 'https://github.com/wolfstar-project/code-zero#readme', + docs: 'https://docs.code-zero.dev', contactEmail: 'hello@code-zero.dev', } as const; diff --git a/apps/marketing/wrangler.jsonc b/apps/marketing/wrangler.jsonc new file mode 100644 index 00000000..1d51d1a9 --- /dev/null +++ b/apps/marketing/wrangler.jsonc @@ -0,0 +1,14 @@ +{ + // The marketing site is prerendered end to end (`nuxt generate`) and has no server routes, so the + // Worker only serves the generated files: no script, no bindings, nothing to keep warm. + "name": "code-zero-marketing", + "compatibility_date": "2026-10-01", + "assets": { + "directory": ".output/public", + // `nuxt generate` writes `404.html`; serve it with a 404 status for unknown paths. + "not_found_handling": "404-page", + "html_handling": "drop-trailing-slash", + }, + "routes": [{ "pattern": "code-zero.dev", "custom_domain": true }], + "workers_dev": true, +} From c22e00cec055a964ab86a09b55d40d50e07fb79e Mon Sep 17 00:00:00 2001 From: RedStar Date: Mon, 19 Oct 2026 12:40:26 +0200 Subject: [PATCH 2/3] ci: redeploy the sites when a shared build input changes Both sites build workspace packages through scripts/tsdown.config.ts, the root package and workspace manifests, turbo.jsonc and the setup-toolchain action. A change to any of them alone did not start deploy-sites.yml, so the live sites kept the old build until something else triggered it. --- .github/workflows/deploy-sites.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/deploy-sites.yml b/.github/workflows/deploy-sites.yml index dc6fa0aa..5a399984 100644 --- a/.github/workflows/deploy-sites.yml +++ b/.github/workflows/deploy-sites.yml @@ -15,7 +15,13 @@ on: - packages/build-env/** - packages/i18n/** - packages/ui/** + # Shared build inputs: a change to any of them changes what the sites are built from. + - scripts/tsdown.config.ts + - package.json - pnpm-lock.yaml + - pnpm-workspace.yaml + - turbo.jsonc + - .github/actions/setup-toolchain/** - .github/workflows/deploy-sites.yml workflow_dispatch: From ee5b19ddfa7cad01fe53bc5f795b3e584a48d56b Mon Sep 17 00:00:00 2001 From: RedStar Date: Mon, 19 Oct 2026 12:48:05 +0200 Subject: [PATCH 3/3] fix(ci): cache the docs Vercel bundle and key builds on the site URL The docs build listed only .output as a Turbo output, while the Nitro vercel preset writes .vercel/output. A cache hit on Vercel therefore restored no bundle and the preview failed looking for a dist directory, as it did on a commit that only touched the deploy workflow. NUXT_SITE_URL now changes the generated sitemap and canonical links, so it joins the env of both site builds, and the docs build also keys on NITRO_PRESET and VERCEL so a Vercel and a Cloudflare build never share a cache entry. --- turbo.jsonc | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/turbo.jsonc b/turbo.jsonc index 424dd8bf..33ab0360 100644 --- a/turbo.jsonc +++ b/turbo.jsonc @@ -86,6 +86,7 @@ "env": [ "MARKETING_SITE_URL", "MARKETING_DASHBOARD_URL", + "NUXT_SITE_URL", "NITRO_PRESET", // Read by packages/build-env to resolve what the build is (version, commit, branch, // deploy channel). Declared so strict env mode does not hide them from `nuxt build`, and @@ -133,9 +134,13 @@ "dependsOn": ["^build"], // The canonical architecture and provider references are pulled in from the // repository-root docs/ directory by the @include hook in nuxt.config.ts. - "env": ["NUXT_APP_BASE_URL"], + // NUXT_SITE_URL is the absolute origin in the sitemap and canonical links; NITRO_PRESET and + // VERCEL pick the output layout, so a Vercel build never reuses a Cloudflare one or back. + "env": ["NUXT_APP_BASE_URL", "NUXT_SITE_URL", "NITRO_PRESET", "VERCEL"], "inputs": ["$TURBO_DEFAULT$", ".env*", "../../docs/*.md"], - "outputs": [".output/**"], + // `.vercel/output/**` is where the `vercel` preset writes; without it a cache hit on Vercel + // restores no bundle and the deployment fails looking for a `dist` directory. + "outputs": [".output/**", ".vercel/output/**"], }, "typecheck": { "dependsOn": ["^build"],