Internal notes for https://documentation.openclaw.ai.
Vincent's design is the desired steady state:
- Cloudflare R2 bucket
openclaw-docsstores the full generated docs site. documentation.openclaw.aiis served from R2 through Cloudflare's CDN, not through a Worker on normal page traffic.documentation.openclaw.ai/ask-molty/*stays on the separate Ask Molty Worker.- The docs site stays static/CDN-first, with full locale HTML, locale markdown, Pagefind search, 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 - Route:
documentation.openclaw.ai/* - Router origin:
docs2.openclaw.ai - Header:
X-OpenClaw-Docs-Origin: cloudflare-r2 - Cache-Control follows the same policy as the R2 manifest.
Why a Worker still exists:
- Plain R2 custom domains do not serve
/as/index.html. - Plain R2 custom domains do not redirect non-root trailing slash docs paths to slashless paths.
- Plain R2 custom domains cannot negotiate markdown from
Accept: text/markdown. - 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:
91b59577e757131d68d55a471fe32aca - zone:
openclaw.ai
Required Cloudflare API token scopes for bucket/domain/DNS setup:
Account: R2 Storage: 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 S3 upload credentials:
OPENCLAW_R2_ACCESS_KEY_IDOPENCLAW_R2_SECRET_ACCESS_KEY
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/accounts/$OPENCLAW_CLOUDFLARE_ACCOUNT_ID/tokens/verify"The secret access key is the SHA-256 hex digest of the R2 token value. These are stored locally in ~/.profile and should be added to GitHub Actions secrets before enabling the R2 workflow in CI.
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.ymlnpx wrangler@4.88.0 deploy --config wrangler.tomldocs-live-smoke.yml
Local R2 build:
npm run docs:build:r2Local R2 upload:
source ~/.profile
CLOUDFLARE_ACCOUNT_ID=91b59577e757131d68d55a471fe32aca \
CLOUDFLARE_R2_BUCKET=openclaw-docs \
OPENCLAW_R2_ACCESS_KEY_ID="$OPENCLAW_R2_ACCESS_KEY_ID" \
OPENCLAW_R2_SECRET_ACCESS_KEY="$OPENCLAW_R2_SECRET_ACCESS_KEY" \
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 fetching objects from R2. 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 R2 upload secrets are present:
OPENCLAW_R2_ACCESS_KEY_IDOPENCLAW_R2_SECRET_ACCESS_KEY
-
Confirm the bucket exists:
source ~/.profile CLOUDFLARE_ACCOUNT_ID=91b59577e757131d68d55a471fe32aca \ 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. -
Attach the R2 custom domain for
documentation.openclaw.ai. -
Deploy
openclaw-docs-routerwithR2_ORIGIN_HOST=docs2.openclaw.ai. -
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
documentation.openclaw.ai/*route fromopenclaw-docs-router. - Purge Cloudflare cache.
- Live-test the URLs below.
Use these after every deploy:
curl -I https://documentation.openclaw.ai/
curl -I https://documentation.openclaw.ai/start/getting-started
curl -I https://documentation.openclaw.ai/concepts/models
curl -I https://documentation.openclaw.ai/concepts/models.md
curl -I https://documentation.openclaw.ai/docs/platforms/digitalocean
curl -I https://documentation.openclaw.ai/llms-full.txt
curl -I https://documentation.openclaw.ai/assets/docs-site.css
curl -i https://documentation.openclaw.ai/ask-molty/api/sessionExpected after R2 cutover:
- slashless HTML paths return
200. .mdpaths returntext/markdown.- 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
documentation.openclaw.ai/*route 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.