Skip to content

Commit bebb9a9

Browse files
committed
feat: cut over docs canonical host
1 parent 4ed9498 commit bebb9a9

16 files changed

Lines changed: 194 additions & 42 deletions

.github/workflows/docs-live-smoke.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ jobs:
2121

2222
- name: Smoke live docs pages
2323
env:
24-
BASE_URL: https://documentation.openclaw.ai
24+
BASE_URL: https://docs.openclaw.ai
2525
GITHUB_SHA: ${{ github.sha }}
2626
PAGES: |
2727
/tools/reactions
28+
/plugins/meeting-notes
2829
/zh-CN/tools/reactions
2930
/de/tools/reactions
3031
/de/gateway/heartbeat

.github/workflows/llms-full.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ jobs:
1919
timeout-minutes: 30
2020
env:
2121
DOCS_SITE_CANONICAL_ORIGIN: https://docs.openclaw.ai
22-
DOCS_SITE_CNAME: documentation.openclaw.ai
22+
DOCS_SITE_CNAME: docs.openclaw.ai
2323
environment:
2424
name: cloudflare
25-
url: https://documentation.openclaw.ai/llms-full.txt
25+
url: https://docs.openclaw.ai/llms-full.txt
2626
steps:
2727
- name: Check out
2828
uses: actions/checkout@v6
@@ -56,7 +56,7 @@ jobs:
5656
if: steps.current-main.outputs.stale != 'true'
5757
id: live-route
5858
env:
59-
BASE_URL: https://documentation.openclaw.ai
59+
BASE_URL: https://docs.openclaw.ai
6060
GITHUB_SHA: ${{ github.sha }}
6161
run: |
6262
node - <<'NODE'
@@ -147,7 +147,7 @@ jobs:
147147
- name: Smoke live corpus
148148
if: steps.current-main.outputs.stale != 'true' && steps.live-route.outputs.available != 'false' && steps.current-main-before-upload.outputs.stale != 'true'
149149
env:
150-
BASE_URL: https://documentation.openclaw.ai
150+
BASE_URL: https://docs.openclaw.ai
151151
GITHUB_SHA: ${{ github.sha }}
152152
run: |
153153
node - <<'NODE'

.github/workflows/pages.yml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ on:
66
- main
77
paths:
88
- "workers/**"
9+
- "scripts/cloudflare-cutover-docs-hosts.mjs"
910
- ".github/workflows/pages.yml"
1011
- "wrangler.toml"
1112
- "CLOUDFLARE.md"
@@ -16,6 +17,11 @@ on:
1617
required: false
1718
type: boolean
1819
default: true
20+
cutover_docs_hosts:
21+
description: "Move docs.openclaw.ai to the generated docs router and mintlify.openclaw.ai to Mintlify."
22+
required: false
23+
type: boolean
24+
default: false
1925

2026
permissions:
2127
actions: write
@@ -32,7 +38,7 @@ jobs:
3238
timeout-minutes: 15
3339
environment:
3440
name: cloudflare
35-
url: https://documentation.openclaw.ai
41+
url: https://docs.openclaw.ai
3642
steps:
3743
- name: Check out
3844
uses: actions/checkout@v6
@@ -59,8 +65,14 @@ jobs:
5965
--tag "${GITHUB_SHA::12}" \
6066
--message "openclaw/docs ${GITHUB_SHA}"
6167
68+
- name: Cut over docs hostnames
69+
if: github.event_name == 'workflow_dispatch' && inputs.cutover_docs_hosts == true
70+
env:
71+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
72+
run: node scripts/cloudflare-cutover-docs-hosts.mjs
73+
6274
- name: Dispatch live smoke
63-
if: github.event_name == 'workflow_dispatch' && inputs.deploy_worker == true
75+
if: github.event_name == 'workflow_dispatch' && (inputs.deploy_worker == true || inputs.cutover_docs_hosts == true)
6476
env:
6577
GH_TOKEN: ${{ github.token }}
6678
run: gh workflow run docs-live-smoke.yml --ref main

.github/workflows/r2-pages.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ jobs:
4747
timeout-minutes: 60
4848
env:
4949
DOCS_SITE_CANONICAL_ORIGIN: https://docs.openclaw.ai
50-
DOCS_SITE_CNAME: documentation.openclaw.ai
50+
DOCS_SITE_CNAME: docs.openclaw.ai
5151
environment:
5252
name: cloudflare
53-
url: https://documentation.openclaw.ai
53+
url: https://docs.openclaw.ai
5454
steps:
5555
- name: Check out
5656
uses: actions/checkout@v6

CLOUDFLARE.md

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
# Cloudflare Hosting
22

3-
Internal notes for `https://documentation.openclaw.ai`.
3+
Internal notes for `https://docs.openclaw.ai`.
44

55
## Target Design
66

77
Vincent's design is the desired steady state:
88

99
- Cloudflare R2 bucket `openclaw-docs` stores the full generated docs site.
10-
- `documentation.openclaw.ai` is served from R2 through Cloudflare's CDN, not through a Worker on normal page traffic.
11-
- `documentation.openclaw.ai/ask-molty/*` stays on the separate Ask Molty Worker.
10+
- `docs.openclaw.ai` is served from R2 through Cloudflare's CDN, not through a Worker on normal page traffic.
11+
- `docs.openclaw.ai/ask-molty/*` stays on the separate Ask Molty Worker.
12+
- `documentation.openclaw.ai` is legacy and redirects to `docs.openclaw.ai`.
13+
- `mintlify.openclaw.ai` is the old Mintlify backup hostname.
1214
- The docs site stays static/CDN-first, with full locale HTML, locale markdown, Pagefind search, and source indexes.
1315

1416
The repo-side pieces are in place:
@@ -30,7 +32,7 @@ The repo-side pieces are in place:
3032
Production is cut over to R2-backed storage with a small Worker router in front:
3133

3234
- Worker: `openclaw-docs-router`
33-
- Route: `documentation.openclaw.ai/*`
35+
- Routes: `docs.openclaw.ai/*`, `documentation.openclaw.ai/*`
3436
- Router storage: native `DOCS_BUCKET` R2 binding to bucket `openclaw-docs`
3537
- Header: `X-OpenClaw-Docs-Origin: cloudflare-r2`
3638
- Cache-Control follows the same policy as the R2 manifest.
@@ -183,7 +185,7 @@ Pure R2 follow-up, blocked on `Zone: Rulesets: Edit`:
183185
- non-root trailing-slash docs paths redirect to slashless paths.
184186
- cache rules match the policy above.
185187
- `/ask-molty/*` remains routed to `openclaw-docs-chat-proxy`.
186-
2. Remove the `documentation.openclaw.ai/*` route from `openclaw-docs-router`.
188+
2. Remove the `docs.openclaw.ai/*` and `documentation.openclaw.ai/*` routes from `openclaw-docs-router`.
187189
3. Purge Cloudflare cache.
188190
4. Live-test the URLs below.
189191

@@ -192,19 +194,21 @@ Pure R2 follow-up, blocked on `Zone: Rulesets: Edit`:
192194
Use these after every deploy:
193195

194196
```sh
195-
curl -I https://documentation.openclaw.ai/
197+
curl -I https://docs.openclaw.ai/
198+
curl -I https://docs.openclaw.ai/start/getting-started
199+
curl -I https://docs.openclaw.ai/concepts/models
200+
curl -I https://docs.openclaw.ai/concepts/models.md
201+
curl -I https://docs.openclaw.ai/docs/platforms/digitalocean
202+
curl -I https://docs.openclaw.ai/llms.txt
203+
curl -I https://docs.openclaw.ai/.well-known/llms.txt
204+
curl -I https://docs.openclaw.ai/robots.txt
205+
curl -I https://docs.openclaw.ai/sitemap.xml
206+
curl -I https://docs.openclaw.ai/llms-full.txt
207+
curl -I https://docs.openclaw.ai/.well-known/llms-full.txt
208+
curl -I https://docs.openclaw.ai/assets/docs-site.css
209+
curl -i https://docs.openclaw.ai/ask-molty/api/session
196210
curl -I https://documentation.openclaw.ai/start/getting-started
197-
curl -I https://documentation.openclaw.ai/concepts/models
198-
curl -I https://documentation.openclaw.ai/concepts/models.md
199-
curl -I https://documentation.openclaw.ai/docs/platforms/digitalocean
200-
curl -I https://documentation.openclaw.ai/llms.txt
201-
curl -I https://documentation.openclaw.ai/.well-known/llms.txt
202-
curl -I https://documentation.openclaw.ai/robots.txt
203-
curl -I https://documentation.openclaw.ai/sitemap.xml
204-
curl -I https://documentation.openclaw.ai/llms-full.txt
205-
curl -I https://documentation.openclaw.ai/.well-known/llms-full.txt
206-
curl -I https://documentation.openclaw.ai/assets/docs-site.css
207-
curl -i https://documentation.openclaw.ai/ask-molty/api/session
211+
curl -I https://mintlify.openclaw.ai/
208212
```
209213

210214
Expected after R2 cutover:
@@ -230,7 +234,7 @@ Expected before R2 cutover:
230234

231235
If R2 cutover misbehaves:
232236

233-
1. Re-add the `documentation.openclaw.ai/*` route to `openclaw-docs-router`.
237+
1. Re-add the `docs.openclaw.ai/*` and `documentation.openclaw.ai/*` routes to `openclaw-docs-router`.
234238
2. Re-run `.github/workflows/pages.yml` or deploy locally:
235239

236240
```sh

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,5 @@ Source of truth lives in [`openclaw/openclaw`](https://github.com/openclaw/openc
4848

4949
- `OPENCLAW_DOCS_SYNC_TOKEN` lives in `openclaw/openclaw` and lets the source repo push into this repo.
5050
- `OPENCLAW_DOCS_I18N_OPENAI_API_KEY` lives in this repo and powers locale translation refreshes.
51-
- `CLOUDFLARE_API_TOKEN` lives in this repo and deploys the `documentation.openclaw.ai` router.
51+
- `CLOUDFLARE_API_TOKEN` lives in this repo and deploys the `docs.openclaw.ai` router.
5252
- R2 uploads verify `CLOUDFLARE_API_TOKEN`, try temporary R2 credentials, and normally fall back to the token-derived direct S3 credential form. `OPENCLAW_R2_ACCESS_KEY_ID` / `OPENCLAW_R2_SECRET_ACCESS_KEY` are only fallback upload credentials when the Cloudflare token cannot be verified.

docs/AGENTS.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
# Docs Guide
22

3-
This directory owns docs authoring, Mintlify link rules, and docs i18n policy.
3+
This directory owns docs authoring, generated-site link rules, and docs i18n policy.
44

5-
## Mintlify Rules
5+
## Docs Rules
66

7-
- Docs are hosted on Mintlify (`https://docs.openclaw.ai`).
7+
- Docs are hosted on the generated R2 site (`https://docs.openclaw.ai`).
8+
- The old Mintlify site lives at `https://mintlify.openclaw.ai` for backup.
89
- Internal doc links in `docs/**/*.md` must stay root-relative with no `.md` or `.mdx` suffix (example: `[Config](/gateway/configuration)`).
910
- Section cross-references should use anchors on root-relative paths (example: `[Hooks](/gateway/configuration-reference#hooks)`).
10-
- Doc headings should avoid em dashes and apostrophes because Mintlify anchor generation is brittle there.
11-
- README and other GitHub-rendered docs should keep absolute docs URLs so links work outside Mintlify.
11+
- Doc headings should avoid em dashes and apostrophes because anchor generation is brittle there.
12+
- README and other GitHub-rendered docs should keep absolute docs URLs so links work outside the docs site.
1213
- Docs content must stay generic: no personal device names, hostnames, or local paths; use placeholders like `user@gateway-host`.
1314

1415
## Docs Content Rules
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#!/usr/bin/env node
2+
const apiToken = process.env.CLOUDFLARE_API_TOKEN;
3+
const zoneName = process.env.CLOUDFLARE_ZONE_NAME ?? "openclaw.ai";
4+
const dryRun = process.argv.includes("--dry-run");
5+
6+
if (!apiToken) {
7+
throw new Error("CLOUDFLARE_API_TOKEN is required");
8+
}
9+
10+
const docsHost = `docs.${zoneName}`;
11+
const legacyHost = `documentation.${zoneName}`;
12+
const mintlifyHost = `mintlify.${zoneName}`;
13+
const docsRouterScript = "openclaw-docs-router";
14+
const chatProxyScript = "openclaw-docs-chat-proxy";
15+
16+
const zone = await findZone(zoneName);
17+
console.log(`zone:${zone.name}`);
18+
19+
await upsertDns(zone.id, {
20+
name: docsHost,
21+
type: "A",
22+
content: "192.0.2.1",
23+
proxied: true,
24+
ttl: 1,
25+
comment: "OpenClaw generated docs router",
26+
});
27+
await upsertDns(zone.id, {
28+
name: mintlifyHost,
29+
type: "CNAME",
30+
content: "cname.mintlify.builders",
31+
proxied: false,
32+
ttl: 1,
33+
comment: "OpenClaw Mintlify backup docs",
34+
});
35+
36+
await upsertRoute(zone.id, `${docsHost}/ask-molty/*`, chatProxyScript);
37+
await upsertRoute(zone.id, `${legacyHost}/ask-molty/*`, chatProxyScript);
38+
await upsertRoute(zone.id, `${docsHost}/*`, docsRouterScript);
39+
await upsertRoute(zone.id, `${legacyHost}/*`, docsRouterScript);
40+
41+
console.log(dryRun ? "dry-run complete" : "cutover complete");
42+
43+
async function findZone(name) {
44+
const data = await cloudflare(`/zones?name=${encodeURIComponent(name)}&status=active`);
45+
const zone = data.result?.find((entry) => entry.name === name);
46+
if (!zone) throw new Error(`active zone not found: ${name}`);
47+
return zone;
48+
}
49+
50+
async function upsertDns(zoneId, desired) {
51+
const existing = await cloudflare(`/zones/${zoneId}/dns_records?name=${encodeURIComponent(desired.name)}&per_page=100`);
52+
const records = existing.result ?? [];
53+
const matching = records.find((record) =>
54+
record.type === desired.type
55+
&& record.content === desired.content
56+
&& record.proxied === desired.proxied
57+
);
58+
for (const record of records) {
59+
if (matching && record.id === matching.id) continue;
60+
await mutate(`/zones/${zoneId}/dns_records/${record.id}`, { method: "DELETE" });
61+
console.log(`dns:deleted:${record.name}:${record.type}`);
62+
}
63+
if (matching) {
64+
await mutate(`/zones/${zoneId}/dns_records/${matching.id}`, {
65+
method: "PATCH",
66+
body: desired,
67+
});
68+
console.log(`dns:ok:${desired.name}:${desired.type}`);
69+
return;
70+
}
71+
await mutate(`/zones/${zoneId}/dns_records`, {
72+
method: "POST",
73+
body: desired,
74+
});
75+
console.log(`dns:created:${desired.name}:${desired.type}`);
76+
}
77+
78+
async function upsertRoute(zoneId, pattern, script) {
79+
const existing = await cloudflare(`/zones/${zoneId}/workers/routes?per_page=100`);
80+
const routes = existing.result ?? [];
81+
const match = routes.find((route) => route.pattern === pattern);
82+
if (match?.script === script) {
83+
console.log(`route:ok:${pattern}`);
84+
return;
85+
}
86+
const body = { pattern, script };
87+
if (match) {
88+
await mutate(`/zones/${zoneId}/workers/routes/${match.id}`, {
89+
method: "PUT",
90+
body,
91+
});
92+
console.log(`route:updated:${pattern}`);
93+
return;
94+
}
95+
await mutate(`/zones/${zoneId}/workers/routes`, {
96+
method: "POST",
97+
body,
98+
});
99+
console.log(`route:created:${pattern}`);
100+
}
101+
102+
async function mutate(path, init) {
103+
if (dryRun) {
104+
console.log(`${init.method}:dry-run:${path}`);
105+
return {};
106+
}
107+
return cloudflare(path, init);
108+
}
109+
110+
async function cloudflare(path, init = {}) {
111+
const response = await fetch(`https://api.cloudflare.com/client/v4${path}`, {
112+
method: init.method ?? "GET",
113+
headers: {
114+
Authorization: `Bearer ${apiToken}`,
115+
"Content-Type": "application/json",
116+
},
117+
body: init.body ? JSON.stringify(init.body) : undefined,
118+
});
119+
const text = await response.text();
120+
const data = text ? JSON.parse(text) : {};
121+
if (!response.ok || data.success === false) {
122+
const message = data.errors?.map((error) => `${error.code}: ${error.message}`).join("; ") || response.statusText;
123+
throw new Error(`${init.method ?? "GET"} ${path}: ${message}`);
124+
}
125+
return data;
126+
}

scripts/docs-site/build.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const md = createMarkdownRenderer();
2222
const basePath = normalizeBasePath(process.env.DOCS_SITE_BASE_PATH ?? "");
2323
const legacyBasePath = normalizeBasePath(process.env.DOCS_SITE_LEGACY_BASE_PATH ?? "/docs");
2424
const canonicalOrigin = (process.env.DOCS_SITE_CANONICAL_ORIGIN
25-
?? (process.env.DOCS_SITE_CNAME ? `https://${process.env.DOCS_SITE_CNAME}` : "https://documentation.openclaw.ai"))
25+
?? (process.env.DOCS_SITE_CNAME ? `https://${process.env.DOCS_SITE_CNAME}` : "https://docs.openclaw.ai"))
2626
.replace(/\/$/, "");
2727
const llmsFullAvailable = process.env.DOCS_SITE_LLMS_FULL_AVAILABLE === "1";
2828
const ogImagePath = "/og-card.png";
@@ -519,7 +519,7 @@ function englishDocsPages() {
519519
}
520520

521521
function docsOrigin() {
522-
return (canonicalOrigin || "https://documentation.openclaw.ai").replace(/\/$/, "");
522+
return (canonicalOrigin || "https://docs.openclaw.ai").replace(/\/$/, "");
523523
}
524524

525525
function chatWidget() {

scripts/docs-site/elements-fixture.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ set -euo pipefail
9999
openclaw status --deep
100100
openclaw gateway restart
101101
102-
curl -fsSL https://documentation.openclaw.ai/llms.txt \\
102+
curl -fsSL https://docs.openclaw.ai/llms.txt \\
103103
| sed -n '1,16p'
104104
\`\`\`
105105

0 commit comments

Comments
 (0)