Internal notes for https://docs.openclaw.ai.
Vincent's design is the desired steady state:
- Cloudflare R2 bucket
openclaw-docsstores the full generated docs site. docs.openclaw.aiis served from R2 through Cloudflare's CDN, not through a Worker on normal page traffic.docs.openclaw.ai/ask-molty/*stays on the separate Ask Molty Worker.documentation.openclaw.aiis legacy and redirects todocs.openclaw.ai.docs2.openclaw.aiis the old Mintlify backup hostname.mintlify.openclaw.airedirects todocs2.openclaw.ai.- The docs site stays static/CDN-first, with full locale HTML, locale markdown, Pagefind search, the
/api/searchCLI endpoint, and source indexes.
The repo-side pieces are in place:
npm run docs:build:r2scripts/docs-site/r2-prepare.mjsscripts/docs-site/r2-upload.mjs.github/workflows/r2-pages.yml
r2-prepare.mjs writes dist/docs-r2-manifest.json. The manifest includes each object key, source file, SHA-256, content type, cache policy, and slashless HTML aliases such as:
/concepts/models->concepts/models/index.html/concepts/models.md->concepts/models.md
r2-upload.mjs downloads .openclaw-docs-r2-manifest.json from R2, compares hashes and metadata, uploads only changed objects through the R2 S3 API, and then writes the new manifest back. The first upload seeds everything; later uploads should be small.
Production is cut over to R2-backed storage with a small Worker router in front:
- Worker:
openclaw-docs-router - Routes:
docs.openclaw.ai/*,documentation.openclaw.ai/* - Router storage: native
DOCS_BUCKETR2 binding to bucketopenclaw-docs - Header:
X-OpenClaw-Docs-Origin: cloudflare-r2 - Cache-Control follows the same policy as the R2 manifest.
Why a Worker still exists:
- R2 object storage does not serve
/as/index.htmlwithout router logic. - R2 object storage does not redirect non-root trailing slash docs paths to slashless paths.
- R2 object storage cannot negotiate markdown from
Accept: text/markdownwithout router logic. - The CLI search endpoint
/api/searchreadsdocs-search.jsonfrom R2 and needs Worker logic. - The available Cloudflare auth can manage R2, DNS, custom domains, and Worker routes, but not zone Rulesets/Page Rules. Dashboard-session replay via
mcporter chrome-devtoolsalso returned Cloudflare API auth error10000for/rulesets.
The pure Vincent target remains possible after a Cloudflare token/session with Zone: Rulesets: Edit is available. Until then, the Worker is the compatibility layer and R2 is the storage/source of truth.
The old Worker Static Assets build remains the rollback path in git history.
Cloudflare account:
- account:
Services@openclaw.org - account id: stored in the private
CLOUDFLARE_ACCOUNT_IDsecret/local environment variable - zone:
openclaw.ai
Required Cloudflare API token scopes for bucket/domain/DNS setup:
Account: R2 Storage: EditAccount: Workers Scripts: EditZone: DNS: EditZone: Cache Rules: EditorZone: Rulesets: EditZone: Zone Settings: EditZone: Read
R2 must be enabled for the account before bucket creation works.
Required R2 upload credentials:
CLOUDFLARE_ACCOUNT_IDCLOUDFLARE_API_TOKEN
The Worker router reads through the DOCS_BUCKET R2 binding declared in wrangler.toml; it does not need R2 S3 secrets. Do not commit the account id or credentials to this repository.
For Cloudflare R2 API tokens, the access key id is the account-token id returned by:
curl -H "Authorization: Bearer $OPENCLAW_CLOUDFLARE_API_TOKEN" \
"https://api.cloudflare.com/client/v4/user/tokens/verify"CI verifies CLOUDFLARE_API_TOKEN, tries to mint short-lived R2 credentials, then falls back to the R2 token's direct S3 credential form: the access key id is the token id, and the secret access key is the SHA-256 hex digest of the token value. If the Cloudflare token cannot be verified, CI falls back to OPENCLAW_R2_ACCESS_KEY_ID / OPENCLAW_R2_SECRET_ACCESS_KEY if those upload credentials are rotated directly.
Production docs object deploy:
.github/workflows/r2-pages.ymlnpm run docs:build:r2npm run docs:smokenpm run docs:r2:upload
Production router deploy:
.github/workflows/pages.yml- Pushes validate the Worker bundle with
wrangler deploy --dry-run. - Manual dispatch with
deploy_worker=truerunsnpx wrangler@4.88.0 deploy --config wrangler.toml. docs-live-smoke.yml
Local R2 build:
npm run docs:build:r2Local R2 upload:
source ~/.profile
OPENCLAW_R2_ACCESS_KEY_ID="$(curl -fsS -H "Authorization: Bearer $OPENCLAW_CLOUDFLARE_API_TOKEN" \
"https://api.cloudflare.com/client/v4/user/tokens/verify" \
| node -e 'const fs = require("node:fs"); const data = JSON.parse(fs.readFileSync(0, "utf8")); if (!data.success || !data.result?.id) process.exit(1); process.stdout.write(data.result.id);')"
CLOUDFLARE_ACCOUNT_ID="$CLOUDFLARE_ACCOUNT_ID" \
CLOUDFLARE_R2_BUCKET=openclaw-docs \
OPENCLAW_R2_ACCESS_KEY_ID="$OPENCLAW_R2_ACCESS_KEY_ID" \
OPENCLAW_R2_SECRET_ACCESS_KEY="$(printf '%s' "$OPENCLAW_CLOUDFLARE_API_TOKEN" | shasum -a 256 | awk '{print $1}')" \
R2_UPLOAD_CONCURRENCY=64 \
npm run docs:r2:uploadThe generated R2 manifest uploads both canonical files and slashless aliases:
/concepts/modelsserves HTML from object keyconcepts/models./concepts/models.mdserves markdown from object keyconcepts/models.md./docs/platforms/digitaloceanserves the compatibility redirect HTML.
The Worker router preserves Accept: text/markdown negotiation and root / behavior while reading objects from R2 through the bucket binding. Pure R2 custom-domain serving still needs Cloudflare URL rewrite/redirect rules.
r2-prepare.mjs assigns per-object Cache-Control:
- hashed/static assets:
public, max-age=31536000, immutable - HTML and slashless HTML aliases:
public, max-age=60, s-maxage=86400, stale-while-revalidate=604800 - markdown, JSON, JSONL, and text indexes:
public, max-age=300, s-maxage=3600, stale-while-revalidate=86400 - upload manifest:
private, max-age=0, no-store
The Worker router splits browser and edge cache headers so cached HTML does not become stale in users' browsers:
- HTML and slashless HTML aliases:
Cache-Control: public, max-age=60, stale-while-revalidate=60CDN-Cache-Control/Cloudflare-CDN-Cache-Control: public, s-maxage=86400, stale-while-revalidate=604800
- markdown, JSON, JSONL, and text indexes:
Cache-Control: public, max-age=300, stale-while-revalidate=300CDN-Cache-Control/Cloudflare-CDN-Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400
- hashed/static assets:
Cache-Control: public, max-age=31536000, immutable
The Worker also uses caches.default for production router responses. Recommended Cloudflare cache rules for the later pure-R2 path:
- Cache static assets and Pagefind files for one year.
- Cache HTML at the edge for one day with short browser TTL.
- Cache
.md,.txt,.json, and.jsonlfor one hour at the edge. - Bypass cache for
/ask-molty/*.
After router deploy, verify repeated requests show X-OpenClaw-Docs-Cache: MISS then HIT. After pure-R2 ruleset cutover, verify repeated requests show cf-cache-status: MISS then HIT.
-
Confirm R2 is enabled on the Services@openclaw.org account.
-
Confirm the GitHub Cloudflare secrets are present:
CLOUDFLARE_ACCOUNT_IDCLOUDFLARE_API_TOKEN
-
Confirm the bucket exists:
source ~/.profile CLOUDFLARE_ACCOUNT_ID="$CLOUDFLARE_ACCOUNT_ID" \ CLOUDFLARE_API_TOKEN="$OPENCLAW_CLOUDFLARE_API_TOKEN" \ npx wrangler@4.88.0 r2 bucket list
-
Run the manual
R2 Pagesworkflow, or run the local upload command above. -
Deploy
openclaw-docs-routerfrom the manual Pages workflow. -
Live-test the URLs below.
Pure R2 follow-up, blocked on Zone: Rulesets: Edit:
- Add or verify Cloudflare rules:
/rewrites to/index.htmlif needed.- non-root trailing-slash docs paths redirect to slashless paths.
- cache rules match the policy above.
/ask-molty/*remains routed toopenclaw-docs-chat-proxy.
- Remove the
docs.openclaw.ai/*anddocumentation.openclaw.ai/*routes fromopenclaw-docs-router. - Purge Cloudflare cache.
- Live-test the URLs below.
Use these after every deploy:
curl -I https://docs.openclaw.ai/
curl -I https://docs.openclaw.ai/start/getting-started
curl -I https://docs.openclaw.ai/concepts/models
curl -I https://docs.openclaw.ai/concepts/models.md
curl -I https://docs.openclaw.ai/docs/platforms/digitalocean
curl -I https://docs.openclaw.ai/llms.txt
curl -I https://docs.openclaw.ai/.well-known/llms.txt
curl -I https://docs.openclaw.ai/robots.txt
curl -I https://docs.openclaw.ai/sitemap.xml
curl -sS 'https://docs.openclaw.ai/api/search?q=heartbeat'
curl -I https://docs.openclaw.ai/llms-full.txt
curl -I https://docs.openclaw.ai/.well-known/llms-full.txt
curl -I https://docs.openclaw.ai/assets/docs-site.css
curl -i https://docs.openclaw.ai/ask-molty/api/session
curl -I https://documentation.openclaw.ai/start/getting-started
curl -I https://docs2.openclaw.ai/
curl -I https://mintlify.openclaw.ai/Expected after R2 cutover:
- slashless HTML paths return
200. .mdpaths returntext/markdown./llms.txtand/.well-known/llms.txtreturn the lightweight docs index./robots.txtreturns200 text/plain./sitemap.xmlreturns200 application/xmlwith mutable cache headers, notimmutable./api/search?q=heartbeatreturns JSON search results fromX-OpenClaw-Docs-Origin: cloudflare-r2./llms-full.txtand/.well-known/llms-full.txtreturn200 text/plainafter the scheduledLLMs Full Corpusworkflow has uploaded the nightly corpus.- docs responses include
X-OpenClaw-Docs-Origin: cloudflare-r2. - repeated router requests become
X-OpenClaw-Docs-Cache: HIT. /ask-molty/api/sessionreturns401when logged out.- no
X-OpenClaw-Docs-Origin: cloudflare-static-assetsheader on normal docs pages.
Expected before R2 cutover:
- the same URLs work through the Worker Static Assets fallback.
- docs responses include
X-OpenClaw-Docs-Origin: cloudflare-static-assets. - repeated requests should show Cloudflare
cf-cache-status: HIT.
If R2 cutover misbehaves:
-
Re-add the
docs.openclaw.ai/*anddocumentation.openclaw.ai/*routes toopenclaw-docs-router. -
Re-run
.github/workflows/pages.ymlor deploy locally:source ~/.profile CLOUDFLARE_API_TOKEN="$CRABBOX_CLOUDFLARE_API_TOKEN" npx wrangler@4.88.0 deploy --config wrangler.toml
-
Purge Cloudflare cache.
-
Re-run the live smoke.