Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
66 changes: 66 additions & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Deploy Docs
permissions:
contents: read

on:
push:
branches: [main]
# For redeploying the current main without an empty commit -- a rolled-back
# Cloudflare deployment, or a failed run that needs another go.
workflow_dispatch:

# One deploy at a time, and no cancelling. Cancelling would let an older run
# finish after a newer one and leave stale content live; queueing keeps the last
# push to main the last thing deployed.
concurrency:
group: deploy-docs
cancel-in-progress: false

jobs:
deploy:
if: ${{ github.repository_owner == 'cloudflare' }}
timeout-minutes: 10
runs-on: ubuntu-latest
environment: docs

steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # PageActions reads `git log %at` for each page's "Updated" date

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "npm"
# packages/docs is excluded from the workspaces and the React example's
# client is its own install, so both have lockfiles the root one does not
# cover. Missing one here means a cache key that does not change when its
# dependencies do.
cache-dependency-path: |
package-lock.json
packages/docs/package-lock.json
examples/worker-react/client/package-lock.json

# Three installs, because there are three lockfiles. The docs build bundles
# each example's real source, so the React example's client needs its own
# node_modules for esbuild to resolve react out of.
- name: Install dependencies
run: |
npm ci
npm ci --prefix packages/docs
npm ci --prefix examples/worker-react/client

# `build:docs` is the library build followed by the site build, in that order
# on purpose: the playgrounds vendor `dist/index.js` and the prose substitutes
# the measured bundle size, so the site is built against the library from this
# same commit rather than whatever was there last.
- name: Build docs
run: npm run build:docs

- name: Deploy to Cloudflare
run: npx wrangler deploy -c packages/docs/wrangler.jsonc
env:
# Account ID is committed in packages/docs/wrangler.jsonc; only the
# credential is a secret.
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
36 changes: 36 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,42 @@ jobs:
- name: Lint Markdown
run: npm run lint:md

# The docs site is deployed from main by deploy-docs.yml, so without this the
# first place its build ever runs is the deploy. Fail it on the pull request
# instead.
build-docs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v7

Comment on lines +37 to +45
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "npm"
# packages/docs is excluded from the workspaces and the React example's
# client is its own install, so both have lockfiles the root one does not
# cover.
cache-dependency-path: |
package-lock.json
packages/docs/package-lock.json
examples/worker-react/client/package-lock.json

# The docs build bundles each example's real source, so the React example's
# client needs its own node_modules for esbuild to resolve react out of.
- name: Install dependencies
run: |
npm ci
npm ci --prefix packages/docs
npm ci --prefix examples/worker-react/client

- name: Build docs
run: npm run build:docs

- name: Check docs
run: npm --prefix packages/docs run check

test:
runs-on: ubuntu-latest
container:
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
"build:watch": "tsdown --config-loader native --watch",
"dev": "npm run build:watch",
"dev:docs": "npm run build && npm --prefix packages/docs run dev -- --port 4321",
"build:docs": "npm run build && npm --prefix packages/docs run build",
"deploy:docs": "npm run build:docs && wrangler deploy -c packages/docs/wrangler.jsonc",
"lint:md": "markdownlint-cli2 && node scripts/align-markdown-tables.mjs --check",
"lint:md:fix": "node scripts/align-markdown-tables.mjs && markdownlint-cli2 --fix",
"setup": "npm install && npm --prefix packages/docs install && npm --prefix examples/worker-react/client install",
Expand Down
30 changes: 22 additions & 8 deletions packages/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -943,20 +943,29 @@ is worth repeating after any framework change.

## Deployment

`npm run build` emits a plain static site to `dist/`, deployable anywhere. `site` defaults to the
preview deployment so that canonical URLs, Open Graph URLs, the sitemap and the links inside
`/llms.txt` are all valid; point it at a real domain with `DOCS_SITE_URL`:
The site is `https://capnweb.com`, served from a Cloudflare Worker named `capnweb-docs` in the
`capnweb` account. Every push to `main` redeploys it: `.github/workflows/deploy-docs.yml` builds the
library, builds the site, and runs `wrangler deploy`. The same build runs as a `build-docs` job on
every pull request, so a site that does not build fails the PR rather than the deploy.

`npm run build` emits a plain static site to `dist/`, deployable anywhere. `site` is
`https://capnweb.com` unless `DOCS_SITE_URL` overrides it, and it is what canonical URLs, the
absolute OG image URLs, `robots.txt`, the sitemap and the links inside `/llms.txt` are all built
from -- which is why a preview build has to override it rather than publish a sitemap claiming to be
production:

```sh
DOCS_SITE_URL=https://example.com npm run build
```

`wrangler.jsonc` deploys that output to a Cloudflare Worker. There is no `main`, so no Worker script
runs: every request is served from the asset store, which is all a static site with in-browser
playgrounds needs.
playgrounds needs. Deploy by hand from the **repo root**, not from here -- the playgrounds vendor
`dist/index.js` and the prose substitutes the measured bundle size, so the library has to be built
first and only the root script does both:

```sh
npm run deploy # rebuilds first, via predeploy
npm run deploy:docs # at the repo root: library build, then site build, then wrangler deploy
```

`public/_headers` is part of the deployment rather than decoration. Workers' default for static
Expand All @@ -966,6 +975,11 @@ stylesheet, revalidated but answered with a 304, arrived first, and that gap was
header over bare background between navigations. The file gives documents a minute of freshness and
fingerprinted assets a year.

Pick the account with `CLOUDFLARE_ACCOUNT_ID` if your token can see more than one. Note that an
account may put Cloudflare Access in front of its whole `*.workers.dev` subdomain, in which case the
deployed URL prompts for SSO until a bypass policy is added for the hostname.
`account_id` is committed in `wrangler.jsonc` rather than left to `CLOUDFLARE_ACCOUNT_ID`. It is an
identifier, not a credential, and pinning it is what stops a deploy from an operator who can see
several accounts landing in the wrong one -- wrangler refuses to guess and fails the deploy instead.

`workers_dev` and `preview_urls` are both off. Every URL the build emits names `https://capnweb.com`,
so a second live origin serving those same pages is a duplicate for crawlers and a link people paste
by accident. The cost is that there is no throwaway URL to check a deploy on: verify a change with
`npm run build && npm run preview` locally, or deploy to a scratch Worker with `--name`.
2 changes: 0 additions & 2 deletions packages/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
"check": "astro check",
"lint:docs": "nimbus-docs lint",
"lint:docs:fix": "nimbus-docs lint --fix",
"predeploy": "npm run build",
"deploy": "wrangler deploy",
"astro": "astro"
},
"dependencies": {
Expand Down
22 changes: 22 additions & 0 deletions packages/docs/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@
"name": "capnweb-docs",
"compatibility_date": "2026-02-05",

// The `capnweb` account. An account ID is an identifier rather than a secret --
// nothing can be done with it without a credential -- and committing it is what
// lets both a laptop and CI deploy without being told which of the several
// accounts an operator can see is the right one.
"account_id": "b14f364f7066ca93045436e8450ce7e2",

// The site is the apex domain and nothing else. `custom_domain` makes Cloudflare
// own the DNS record and the certificate for it, so the record cannot drift away
// from the Worker by hand.
"routes": [{ "pattern": "capnweb.com", "custom_domain": true }],

// No `*.workers.dev` copy. Canonical URLs, the sitemap and the links inside
// `/llms.txt` all name `https://capnweb.com` (see `site` in astro.config.ts), and a
// second origin serving the same pages with the same canonicals is a duplicate for
// crawlers and a link people paste by accident.
"workers_dev": false,

// Same reasoning, made explicit: with `workers_dev` off, wrangler turns these off
// anyway and warns that it is guessing. A per-version URL serving the production
// canonicals is the same duplicate problem, one hostname at a time.
"preview_urls": false,

// A static site: no `main`, so there is no Worker script and every request is
// served from the asset store. The playgrounds run their "server" inside the
// browser, so nothing here needs to execute server-side.
Expand Down
Loading