Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions .github/workflows/deploy-sites.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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/**
# 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
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
33 changes: 28 additions & 5 deletions apps/docs/content/1.guide/16.deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
13 changes: 13 additions & 0 deletions apps/docs/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -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,
}
2 changes: 1 addition & 1 deletion apps/marketing/modules/shared/utils/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
14 changes: 14 additions & 0 deletions apps/marketing/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -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,
}
9 changes: 7 additions & 2 deletions turbo.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"],
Expand Down
Loading