feat: move docs to canonical subdomain

This commit is contained in:
Peter Steinberger
2026-06-13 15:38:43 -07:00
parent 749028df12
commit 6afb91b772
32 changed files with 284 additions and 100 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## Unreleased
### Changes
- Docs: serve ClawHub documentation from `docs.clawhub.ai` and permanently redirect legacy `clawhub.ai/docs` URLs while preserving paths and queries.
## 0.21.0 - 2026-06-11
### Changes
+10 -10
View File
@@ -13,7 +13,7 @@ function expectFooterLinksUnderlined(html: string) {
/href="https:\/\/clawhub\.ai"[^>]*style="[^"]*color:#8a8a8e[^"]*text-decoration[^"]*underline[^"]*"[^>]*>ClawHub<\/a>/,
);
expect(html).toMatch(
/href="https:\/\/(?:clawhub\.ai\/docs|docs\.openclaw\.ai)"[^>]*style="[^"]*color:#8a8a8e[^"]*text-decoration[^"]*underline[^"]*"[^>]*>Docs<\/a>/,
/href="https:\/\/(?:docs\.clawhub\.ai|docs\.openclaw\.ai)"[^>]*style="[^"]*color:#8a8a8e[^"]*text-decoration[^"]*underline[^"]*"[^>]*>Docs<\/a>/,
);
}
@@ -57,10 +57,10 @@ describe("moderation notification email copy", () => {
expect(email.html).not.toContain("If you already appealed");
expect(email.html).not.toContain("separate support email");
expect(email.html).not.toContain("You received this email because");
expect(email.html).toContain("https://clawhub.ai/docs");
expect(email.html).toContain("https://docs.clawhub.ai");
expectFooterLinksUnderlined(email.html);
expect(email.text).not.toContain("clawhub scan ./my-skill --output clawhub-scan.zip");
expect(email.text).not.toContain("https://clawhub.ai/docs/cli#scan-path");
expect(email.text).not.toContain("https://docs.clawhub.ai/cli#scan-path");
});
it("does not leak raw manual moderator notes into outbound email", async () => {
@@ -113,7 +113,7 @@ describe("moderation notification email copy", () => {
expect(email.html).toContain("ACCOUNT REINSTATED");
expect(email.html).toContain("API tokens issued before the suspension");
expect(email.html).not.toContain("You received this email because");
expect(email.html).toContain("https://clawhub.ai/docs");
expect(email.html).toContain("https://docs.clawhub.ai");
expectFooterLinksUnderlined(email.html);
});
@@ -148,7 +148,7 @@ describe("moderation notification email copy", () => {
expect(email.text).toContain("Version: 1.2.3");
expect(email.text).toContain("clawhub scan download demo-skill --version 1.2.3");
expect(email.text).toContain("Increment the version number before uploading the fixed skill.");
expect(email.text).toContain("https://clawhub.ai/docs/moderation");
expect(email.text).toContain("https://docs.clawhub.ai/moderation");
expect(email.text).not.toContain("clawhub scan ./my-skill --output clawhub-scan.zip");
expect(email.text).not.toContain("fixed local copy");
expect(email.text).toContain("Repeated malicious rejections may lead to account disablement");
@@ -219,7 +219,7 @@ describe("moderation notification email copy", () => {
scanSource: "publish",
authorRemediation: {
summary: "Replace the legacy before_agent_start hook with current prompt hooks.",
docsUrl: "https://clawhub.ai/docs/plugin-validation-fixes#legacy-before-agent-start",
docsUrl: "https://docs.clawhub.ai/plugin-validation-fixes#legacy-before-agent-start",
},
},
],
@@ -242,7 +242,7 @@ describe("moderation notification email copy", () => {
);
expect(email.text).toContain(" Docs:");
expect(email.text).toContain(
" https://clawhub.ai/docs/plugin-validation-fixes#legacy-before-agent-start",
" https://docs.clawhub.ai/plugin-validation-fixes#legacy-before-agent-start",
);
expect(email.text).not.toContain("ClawHub Security");
expect(email.html).toContain("Validate a local fix");
@@ -251,7 +251,7 @@ describe("moderation notification email copy", () => {
expect(email.html).not.toContain('href="https://clawhub.ai" style="display:inline-block');
expect(email.html).not.toContain("You&#39;re receiving this because");
expect(email.html).not.toContain("You're receiving this because");
expect(email.html).toContain("https://clawhub.ai/docs/plugin-validation-fixes");
expect(email.html).toContain("https://docs.clawhub.ai/plugin-validation-fixes");
expectFooterLinksUnderlined(email.html);
expect(email.html).toContain("OpenClaw Version");
expect(email.html).toContain("0.9.0");
@@ -350,7 +350,7 @@ describe("moderation notification email copy", () => {
expect(email.html).toContain("Open appeal");
expect(email.html).not.toContain("<package>");
expect(email.html).not.toContain("You received this email because");
expect(email.html).toContain("https://clawhub.ai/docs");
expect(email.html).toContain("https://docs.clawhub.ai");
expectFooterLinksUnderlined(email.html);
});
@@ -369,7 +369,7 @@ describe("moderation notification email copy", () => {
expect(email.html).not.toContain("{{primary_action_url}}");
expect(email.html).not.toContain('href="https://clawhub.ai" style="display:inline-block');
expect(email.html).not.toContain("You received this email because");
expect(email.html).toContain("https://clawhub.ai/docs");
expect(email.html).toContain("https://docs.clawhub.ai");
expectFooterLinksUnderlined(email.html);
});
});
+1 -1
View File
@@ -1,5 +1,5 @@
export const APPEALS_URL = "https://appeals.openclaw.ai/";
export const MODERATION_GUIDELINES_URL = "https://clawhub.ai/docs/moderation";
export const MODERATION_GUIDELINES_URL = "https://docs.clawhub.ai/moderation";
export const MALICIOUS_REJECTION_ACCOUNT_WARNING =
"Repeated malicious rejections may lead to account disablement.";
const MAX_EMAIL_FINDING_SUMMARY_LENGTH = 280;
+2 -2
View File
@@ -35,7 +35,7 @@ describe("package inspector publish normalization", () => {
authorRemediation: {
summary:
"Publish the entrypoint declared in OpenClaw package metadata or update the metadata to point at an existing file.",
docsUrl: "https://clawhub.ai/docs/plugin-validation-fixes#package-entrypoint-missing",
docsUrl: "https://docs.clawhub.ai/plugin-validation-fixes#package-entrypoint-missing",
},
},
],
@@ -75,7 +75,7 @@ describe("package inspector publish normalization", () => {
authorRemediation: {
summary: "Declare the OpenClaw plugin API range this package supports.",
docsUrl:
"https://clawhub.ai/docs/plugin-validation-fixes#package-plugin-api-compat-missing",
"https://docs.clawhub.ai/plugin-validation-fixes#package-plugin-api-compat-missing",
},
},
],
+1 -1
View File
@@ -41,7 +41,7 @@ type InspectorReport = {
issues?: unknown[];
};
const AUTHOR_REMEDIATION_DOCS_BASE = "https://clawhub.ai/docs/plugin-validation-fixes";
const AUTHOR_REMEDIATION_DOCS_BASE = "https://docs.clawhub.ai/plugin-validation-fixes";
const LEGACY_AUTHOR_REMEDIATION_SUMMARIES = {
"channel-env-vars":
+11 -11
View File
@@ -6757,7 +6757,7 @@ describe("packages public queries", () => {
evidence: ["src/index.ts:4"],
authorRemediation: {
summary: "Move prompt mutation work to before_prompt_build.",
docsUrl: "https://clawhub.ai/docs/plugin-validation-fixes#legacy-before-agent-start",
docsUrl: "https://docs.clawhub.ai/plugin-validation-fixes#legacy-before-agent-start",
},
},
],
@@ -6851,7 +6851,7 @@ describe("packages public queries", () => {
message: "legacy before_agent_start hook is deprecated",
authorRemediation: {
summary: "Move prompt mutation work to before_prompt_build.",
docsUrl: "https://clawhub.ai/docs/plugin-validation-fixes#legacy-before-agent-start",
docsUrl: "https://docs.clawhub.ai/plugin-validation-fixes#legacy-before-agent-start",
},
}),
],
@@ -6911,7 +6911,7 @@ describe("packages public queries", () => {
fixture: "demo",
authorRemediation: {
summary: "Add a display name to the plugin manifest.",
docsUrl: "https://clawhub.ai/docs/plugin-validation-fixes#manifest-name-missing",
docsUrl: "https://docs.clawhub.ai/plugin-validation-fixes#manifest-name-missing",
},
},
],
@@ -6926,7 +6926,7 @@ describe("packages public queries", () => {
inspectorFindingId: "demo:manifest-name-missing",
authorRemediation: {
summary: "Add a display name to the plugin manifest.",
docsUrl: "https://clawhub.ai/docs/plugin-validation-fixes#manifest-name-missing",
docsUrl: "https://docs.clawhub.ai/plugin-validation-fixes#manifest-name-missing",
},
}),
);
@@ -6982,7 +6982,7 @@ describe("packages public queries", () => {
fixture: "demo",
authorRemediation: {
summary: "Move prompt mutation work to before_prompt_build.",
docsUrl: "https://clawhub.ai/docs/plugin-validation-fixes#legacy-before-agent-start",
docsUrl: "https://docs.clawhub.ai/plugin-validation-fixes#legacy-before-agent-start",
},
},
],
@@ -7040,7 +7040,7 @@ describe("packages public queries", () => {
fixture: "demo",
authorRemediation: {
summary: "Replace registerTool with the current plugin API.",
docsUrl: "https://clawhub.ai/docs/plugin-validation-fixes#missing-expected-seam",
docsUrl: "https://docs.clawhub.ai/plugin-validation-fixes#missing-expected-seam",
},
},
],
@@ -7108,7 +7108,7 @@ describe("packages public queries", () => {
fixture: "demo",
authorRemediation: {
summary: "Move prompt mutation work to before_prompt_build.",
docsUrl: "https://clawhub.ai/docs/plugin-validation-fixes#legacy-before-agent-start",
docsUrl: "https://docs.clawhub.ai/plugin-validation-fixes#legacy-before-agent-start",
},
},
],
@@ -7123,7 +7123,7 @@ describe("packages public queries", () => {
issueClass: "deprecation-warning",
authorRemediation: {
summary: "Move prompt mutation work to before_prompt_build.",
docsUrl: "https://clawhub.ai/docs/plugin-validation-fixes#legacy-before-agent-start",
docsUrl: "https://docs.clawhub.ai/plugin-validation-fixes#legacy-before-agent-start",
},
}),
);
@@ -7148,7 +7148,7 @@ describe("packages public queries", () => {
authorRemediation: {
summary: "Move prompt mutation work to before_prompt_build.",
docsUrl:
"https://clawhub.ai/docs/plugin-validation-fixes#legacy-before-agent-start",
"https://docs.clawhub.ai/plugin-validation-fixes#legacy-before-agent-start",
},
},
]),
@@ -7240,7 +7240,7 @@ describe("packages public queries", () => {
authorRemediation: {
summary: "Replace registerTool with the current plugin API.",
docsUrl:
"https://clawhub.ai/docs/plugin-validation-fixes#missing-expected-seam",
"https://docs.clawhub.ai/plugin-validation-fixes#missing-expected-seam",
},
createdAt: 2,
},
@@ -7410,7 +7410,7 @@ describe("packages public queries", () => {
authorRemediation: {
summary: "Move prompt mutation work to before_prompt_build.",
docsUrl:
"https://clawhub.ai/docs/plugin-validation-fixes#legacy-before-agent-start",
"https://docs.clawhub.ai/plugin-validation-fixes#legacy-before-agent-start",
},
createdAt: 1,
},
+1 -1
View File
@@ -8,7 +8,7 @@ read_when:
# Docs
`docs/` is the publishable source for user-facing ClawHub documentation served
under `https://clawhub.ai/docs`.
at `https://docs.clawhub.ai`.
Keep product, CLI, publisher, API, policy, security, and troubleshooting docs
here. These pages should explain how people use ClawHub: discover, install,
+1 -1
View File
@@ -14,7 +14,7 @@ import {
import type { ReactNode } from "react";
export const CLAWHUB_URL = "https://clawhub.ai";
export const CLAWHUB_DOCS_URL = "https://clawhub.ai/docs";
export const CLAWHUB_DOCS_URL = "https://docs.clawhub.ai";
export const EMAIL_PREFERENCES_URL = "https://clawhub.ai/settings";
export const APPEALS_URL = "https://appeals.openclaw.ai/";
export const CLAWHUB_LOGO_URL =
+1 -1
View File
@@ -83,6 +83,6 @@ BlockedVersionEmail.PreviewProps = {
version: "1.2.3",
findingSummary: "Attempts to exfiltrate credentials.",
validateCommand: "clawhub scan download @scope/demo-plugin --version 1.2.3 --kind plugin",
docsUrl: "https://clawhub.ai/docs/moderation",
docsUrl: "https://docs.clawhub.ai/moderation",
preheader: "ClawHub blocked @scope/demo-plugin@1.2.3 after a security scan.",
} satisfies BlockedVersionEmailProps;
+1 -1
View File
@@ -38,7 +38,7 @@ describe("React Email templates", () => {
expect(html).toContain("ClawHub");
expect(html).toContain("OpenClaw");
expect(html).toContain("https://clawhub.ai/docs");
expect(html).toContain("https://docs.clawhub.ai");
expect(text).toContain("ClawHub");
});
});
+1 -1
View File
@@ -81,7 +81,7 @@ PluginInspectorFindingsEmail.PreviewProps = {
meta: "legacy-before-agent-start · deprecation-warning · P2",
message: "legacy before_agent_start hook is deprecated",
fix: "Replace the legacy before_agent_start hook with current prompt hooks.",
docsUrl: "https://clawhub.ai/docs/plugin-validation-fixes#legacy-before-agent-start",
docsUrl: "https://docs.clawhub.ai/plugin-validation-fixes#legacy-before-agent-start",
},
],
} satisfies PluginInspectorFindingsEmailProps;
@@ -386,7 +386,7 @@ describe("package commands", () => {
evidence: ["src/index.ts:4", { hook: "before_agent_start" }],
authorRemediation: {
summary: "Move the hook to before_prompt_build.",
docsUrl: "https://clawhub.ai/docs/plugin-validation-fixes#legacy-hook",
docsUrl: "https://docs.clawhub.ai/plugin-validation-fixes#legacy-hook",
},
},
{
@@ -414,7 +414,7 @@ describe("package commands", () => {
"WARNING legacy-hook (deprecation-warning) P2: legacy hook is deprecated",
);
expect(output).toContain("Fix: Move the hook to before_prompt_build.");
expect(output).toContain("Docs: https://clawhub.ai/docs/plugin-validation-fixes#legacy-hook");
expect(output).toContain("Docs: https://docs.clawhub.ai/plugin-validation-fixes#legacy-hook");
expect(output).toContain("Evidence:");
expect(output).toContain("- src/index.ts:4");
expect(output).not.toContain("runtime-tool-capture");
@@ -451,7 +451,7 @@ describe("package commands", () => {
message: "legacy hook is deprecated",
authorRemediation: {
summary: "Move the hook to before_prompt_build.",
docsUrl: "https://clawhub.ai/docs/plugin-validation-fixes#legacy-hook",
docsUrl: "https://docs.clawhub.ai/plugin-validation-fixes#legacy-hook",
},
},
{
@@ -489,7 +489,7 @@ describe("package commands", () => {
message: "legacy hook is deprecated",
authorRemediation: {
summary: "Move the hook to before_prompt_build.",
docsUrl: "https://clawhub.ai/docs/plugin-validation-fixes#legacy-hook",
docsUrl: "https://docs.clawhub.ai/plugin-validation-fixes#legacy-hook",
},
},
{
@@ -499,7 +499,7 @@ describe("package commands", () => {
authorRemediation: {
summary: "Declare the OpenClaw plugin API range this package supports.",
docsUrl:
"https://clawhub.ai/docs/plugin-validation-fixes#package-plugin-api-compat-missing",
"https://docs.clawhub.ai/plugin-validation-fixes#package-plugin-api-compat-missing",
},
},
],
@@ -2488,7 +2488,7 @@ describe("package commands", () => {
message: "legacy before_agent_start hook is deprecated",
authorRemediation: {
summary: "Replace the legacy before_agent_start hook with current prompt hooks.",
docsUrl: "https://clawhub.ai/docs/plugin-validation-fixes#legacy-before-agent-start",
docsUrl: "https://docs.clawhub.ai/plugin-validation-fixes#legacy-before-agent-start",
},
},
],
@@ -2510,7 +2510,7 @@ describe("package commands", () => {
" Fix: Replace the legacy before_agent_start hook with current prompt hooks.",
);
expect(mockLog).toHaveBeenCalledWith(
" Docs: https://clawhub.ai/docs/plugin-validation-fixes#legacy-before-agent-start",
" Docs: https://docs.clawhub.ai/plugin-validation-fixes#legacy-before-agent-start",
);
} finally {
await rm(workdir, { recursive: true, force: true });
@@ -67,7 +67,7 @@ const LEGACY_DOT_DIR = ".clawdhub";
const DOT_IGNORE = ".clawhubignore";
const LEGACY_DOT_IGNORE = ".clawdhubignore";
const PACKAGE_PUBLISH_RETRY_COUNT = 5;
const AUTHOR_REMEDIATION_DOCS_BASE = "https://clawhub.ai/docs/plugin-validation-fixes";
const AUTHOR_REMEDIATION_DOCS_BASE = "https://docs.clawhub.ai/plugin-validation-fixes";
const LEGACY_AUTHOR_REMEDIATION_SUMMARIES = {
"channel-env-vars":
"Move legacy channel environment variable metadata into the current setup/config metadata.",
+30
View File
@@ -0,0 +1,30 @@
# ClawHub
Public documentation for finding, installing, publishing, and operating ClawHub skills and plugins.
> Use this file as a lightweight map of the ClawHub documentation. Fetch individual pages as Markdown with `.md` URLs or `Accept: text/markdown`.
## Agent Resources
- [Markdown page export](https://docs.clawhub.ai/acceptable-usage.md): Append `.md` to any docs page URL for clean Markdown.
- [Sitemap](https://docs.clawhub.ai/sitemap.xml): Search crawler URL index.
- [Robots policy](https://docs.clawhub.ai/robots.txt): Bot and crawler policy.
## Documentation Index
- [Acceptable Usage](https://docs.clawhub.ai/acceptable-usage): Marketplace policy: what ClawHub allows and what it will not host.
- [API v1](https://docs.clawhub.ai/api): Public REST API (v1) overview and conventions.
- [Auth](https://docs.clawhub.ai/auth): ClawHub sign-in, API tokens, CLI login, token storage, and revocation.
- [CLI](https://docs.clawhub.ai/cli): CLI reference: commands, flags, config, lockfile, and sync behavior.
- [How ClawHub Works](https://docs.clawhub.ai/how-it-works): How ClawHub listings, versions, installs, publishing, scans, and updates work.
- [HTTP API](https://docs.clawhub.ai/http-api): HTTP API reference (public + CLI endpoints + auth).
- [ClawHub](https://docs.clawhub.ai/): Public ClawHub overview for discovery, install, publish, security, and the clawhub CLI.
- [Moderation and Account Safety](https://docs.clawhub.ai/moderation): How ClawHub reports, moderation holds, hidden listings, bans, and account standing work.
- [Plugin validation fixes](https://docs.clawhub.ai/plugin-validation-fixes): Fix ClawHub plugin package validation findings before publishing
- [Publishing](https://docs.clawhub.ai/publishing): How ClawHub publishing works for skills, plugins, owners, scopes, releases, and review.
- [Quickstart](https://docs.clawhub.ai/quickstart): Start using ClawHub: find, install, update, and publish skills or plugins.
- [Security](https://docs.clawhub.ai/security): How to report ClawHub security issues and when vulnerabilities are publicly disclosed.
- [Security Audits](https://docs.clawhub.ai/security-audits): How to understand ClawHub security audit results before installing a skill or plugin.
- [Skill format](https://docs.clawhub.ai/skill-format): Skill folder format, required files, allowed file types, limits.
- [Telemetry](https://docs.clawhub.ai/telemetry): Install telemetry collected by the ClawHub CLI and how to opt out.
- [Troubleshooting](https://docs.clawhub.ai/troubleshooting): Troubleshooting ClawHub sign-in, install, publish, sync, update, and API issues.
-3
View File
@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
+21 -4
View File
@@ -24,6 +24,18 @@ export function planDocsStage(sourceRels) {
.sort((a, b) => a.stageRel.localeCompare(b.stageRel));
}
export function docsBasePathForEnv(env = process.env) {
return env.VERCEL_ENV === "preview" ? "/docs" : "";
}
export function prepareDocsMarkdown(markdown, sourcePath) {
const absoluteOpenClawLinks = markdown.replaceAll(
"](/plugins/",
"](https://docs.openclaw.ai/plugins/",
);
return injectSourcePath(absoluteOpenClawLinks, sourcePath);
}
export function resolveOpenClawDocsRepo({
env = process.env,
cwd = repoRoot,
@@ -90,7 +102,7 @@ function prepareStage({ docsRepoDir, stageRoot, cwd }) {
if (entry.injectSourcePath) {
fs.writeFileSync(
target,
injectSourcePath(fs.readFileSync(source, "utf8"), entry.injectSourcePath),
prepareDocsMarkdown(fs.readFileSync(source, "utf8"), entry.injectSourcePath),
"utf8",
);
} else {
@@ -109,10 +121,10 @@ function runBuilderPipeline({ docsRepoDir, stageRoot, cwd, commandRunner, env })
const sourceRepoDir = env.DOCS_SOURCE_REPO_DIR?.trim() || cwd;
const builderEnv = {
...env,
DOCS_SITE_BASE_PATH: "/docs",
DOCS_SITE_BASE_PATH: docsBasePathForEnv(env),
DOCS_SITE_LEGACY_BASE_PATH: "",
DOCS_SITE_CANONICAL_ORIGIN: "https://clawhub.ai/docs",
DOCS_SITE_CHAT_AUTH_URL: "/auth/docs",
DOCS_SITE_CANONICAL_ORIGIN: "https://docs.clawhub.ai",
DOCS_SITE_CHAT_AUTH_URL: "https://clawhub.ai/auth/docs",
DOCS_FEEDBACK_ISSUE_REPO: "openclaw/clawhub",
DOCS_SOURCE_REPO_DIR: sourceRepoDir,
DOCS_SOURCE_REPO_URL: "https://github.com/openclaw/clawhub",
@@ -167,6 +179,11 @@ function copyGeneratedDocs({ stageRoot, outputDir }) {
fs.rmSync(outputDir, { recursive: true, force: true });
fs.mkdirSync(path.dirname(outputDir), { recursive: true });
fs.cpSync(built, outputDir, { recursive: true });
const llmsDiscoverySource = path.join(built, ".well-known", "llms.txt");
const llmsDiscoveryTarget = path.join(path.dirname(outputDir), ".well-known", "llms.txt");
fs.mkdirSync(path.dirname(llmsDiscoveryTarget), { recursive: true });
fs.copyFileSync(llmsDiscoverySource, llmsDiscoveryTarget);
}
function shouldIgnoreSourceDoc(sourceRel) {
+24 -1
View File
@@ -4,7 +4,13 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { ensureOpenClawDocsRepo, planDocsStage, resolveOpenClawDocsRepo } from "./docs-builder.mjs";
import {
docsBasePathForEnv,
ensureOpenClawDocsRepo,
planDocsStage,
prepareDocsMarkdown,
resolveOpenClawDocsRepo,
} from "./docs-builder.mjs";
describe("docs-builder", () => {
it("stages ClawHub docs in the shape expected by openclaw/docs", () => {
@@ -37,6 +43,23 @@ describe("docs-builder", () => {
]);
});
it("uses root links in production and /docs links on Vercel previews", () => {
expect(docsBasePathForEnv({ VERCEL_ENV: "production" })).toBe("");
expect(docsBasePathForEnv({ VERCEL_ENV: "preview" })).toBe("/docs");
expect(docsBasePathForEnv({})).toBe("");
});
it("makes OpenClaw plugin guide links absolute while preserving ClawHub source metadata", () => {
expect(
prepareDocsMarkdown(
"See [Plugin manifest](/plugins/manifest) and [Publishing](/publishing).\n",
"clawhub/plugin-validation-fixes.md",
),
).toContain(
"[Plugin manifest](https://docs.openclaw.ai/plugins/manifest) and [Publishing](/publishing)",
);
});
it("resolves an explicit openclaw/docs checkout", () => {
const docsRepoDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawhub-openclaw-docs-"));
fs.mkdirSync(path.join(docsRepoDir, "scripts", "docs-site"), { recursive: true });
+6 -12
View File
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
const HERE = resolve(fileURLToPath(import.meta.url), "..");
const CLAWHUB_ROOT = resolve(HERE, "..");
const PUBLIC_ROOT = resolve(CLAWHUB_ROOT, "public");
const DOCS_ROOT = resolve(CLAWHUB_ROOT, "public", "docs");
const HOST = process.env.DOCS_RUN_HOST || "127.0.0.1";
const PORT = Number(process.env.DOCS_RUN_PORT || "4174");
@@ -22,13 +22,7 @@ const server = createServer((request, response) => {
}
const url = new URL(request.url, `http://${HOST}:${PORT}`);
if (url.pathname === "/") {
response.writeHead(302, { Location: "/docs/" });
response.end();
return;
}
const file = resolvePublicPath(url.pathname);
const file = resolveDocsPath(url.pathname);
if (!file) {
response.writeHead(404);
response.end("Not found");
@@ -40,7 +34,7 @@ const server = createServer((request, response) => {
});
server.listen(PORT, HOST, () => {
console.log(`ClawHub docs preview: http://${HOST}:${PORT}/docs/`);
console.log(`ClawHub docs preview: http://${HOST}:${PORT}/`);
});
function run(command: string, args: string[]) {
@@ -54,12 +48,12 @@ function run(command: string, args: string[]) {
if (result.status !== 0) process.exit(result.status ?? 1);
}
function resolvePublicPath(pathname: string) {
function resolveDocsPath(pathname: string) {
const decoded = decodeURIComponent(pathname);
const normalized = normalize(decoded).replace(/^(\.\.(?:\/|\\|$))+/, "");
let candidate = resolve(PUBLIC_ROOT, `.${sep}${normalized}`);
let candidate = resolve(DOCS_ROOT, `.${sep}${normalized}`);
if (!candidate.startsWith(`${PUBLIC_ROOT}${sep}`) && candidate !== PUBLIC_ROOT) {
if (!candidate.startsWith(`${DOCS_ROOT}${sep}`) && candidate !== DOCS_ROOT) {
return "";
}
+39 -13
View File
@@ -13,22 +13,24 @@ const siteJs = read("assets/docs-site.js");
const cli = read("cli/index.html");
const pluginValidationFixes = read("plugin-validation-fixes/index.html");
const llms = read("llms.txt");
const llmsDiscovery = fs.readFileSync(
path.join(repoRoot, "public", ".well-known", "llms.txt"),
"utf8",
);
const robots = read("robots.txt");
const searchIndex = JSON.parse(read("docs-search.json"));
const sourceIndexMeta = JSON.parse(read("source-index-meta.json"));
const vercelConfig = JSON.parse(fs.readFileSync(path.join(repoRoot, "vercel.json"), "utf8"));
assert(
index.includes("<title>ClawHub - ClawHub</title>"),
"index should render ClawHub as the docs home",
);
assert(
index.includes('href="https://clawhub.ai/docs/"'),
"index should use clawhub.ai/docs canonical URL",
);
assert(
index.includes('href="/docs/quickstart"'),
"index should link to the quickstart child route",
index.includes('href="https://docs.clawhub.ai/"'),
"index should use docs.clawhub.ai canonical URL",
);
assert(index.includes('href="/quickstart"'), "index should link to the quickstart child route");
assert(
!fs.existsSync(path.join(site, "clawhub", "index.html")),
"home page should not publish /docs/clawhub",
@@ -43,8 +45,8 @@ assert(quickstart.includes("Quickstart"), "quickstart child route should render"
assert(index.includes("Ask Molty"), "Ask Molty widget should render");
assert(index.includes("ClawHub docs assistant"), "Ask Molty should use the ClawHub chat label");
assert(
siteJs.includes('new URL("/auth/docs",location.href)'),
"Ask Molty auth should use ClawHub /auth/docs",
siteJs.includes('new URL("https://clawhub.ai/auth/docs",location.href)'),
"Ask Molty auth should use canonical ClawHub auth",
);
assert(
!siteJs.includes("https://hub.openclaw.ai/docs/auth"),
@@ -71,25 +73,26 @@ assert(
"llms.txt should describe the ClawHub docs corpus",
);
assert(
/https:\/\/clawhub\.ai\/docs\/[^)\s]+\.md/u.test(llms) &&
/https:\/\/docs\.clawhub\.ai\/[^)\s]+\.md/u.test(llms) &&
!llms.includes("/start/getting-started.md"),
"llms.txt should advertise a real ClawHub Markdown page",
);
assert(llmsDiscovery === llms, "root .well-known LLMS discovery should match the docs artifact");
assert(
robots.includes("# ClawHub documentation crawler policy") &&
robots.includes("Sitemap: https://clawhub.ai/docs/sitemap.xml") &&
robots.includes("Sitemap: https://docs.clawhub.ai/sitemap.xml") &&
!robots.includes("OpenClaw documentation crawler policy"),
"robots.txt should use ClawHub docs metadata",
);
assert(
pluginValidationFixes.includes('href="/docs/plugin-validation-fixes#package-json-missing"') &&
pluginValidationFixes.includes('href="/plugin-validation-fixes#package-json-missing"') &&
!pluginValidationFixes.includes('href="./plugin-validation-fixes.md#package-json-missing"'),
"relative Markdown links should resolve to docs routes",
);
assert(
pluginValidationFixes.includes('href="/plugins/building-plugins"') &&
pluginValidationFixes.includes('href="https://docs.openclaw.ai/plugins/building-plugins"') &&
!pluginValidationFixes.includes('href="/docs/plugins/building-plugins"'),
"root app links should stay outside the docs base path",
"OpenClaw plugin guide links should use the canonical OpenClaw docs host",
);
assert(
cli.includes(
@@ -105,6 +108,29 @@ assert(
sourceIndexMeta.repository === "openclaw/clawhub",
"source index should identify openclaw/clawhub",
);
assert(
vercelConfig.redirects?.some(
(route) =>
route.source === "/docs/:path*" &&
route.destination === "https://docs.clawhub.ai/:path*" &&
route.statusCode === 308,
),
"legacy ClawHub docs paths should redirect to the canonical docs host",
);
assert(
vercelConfig.rewrites?.some(
(route) =>
route.source === "/:path*" &&
route.destination === "/docs/:path*" &&
route.has?.some(
(condition) =>
condition.type === "header" &&
condition.key === "host" &&
condition.value === "docs.clawhub.ai",
),
),
"docs.clawhub.ai should serve the generated docs artifact",
);
console.log("ClawHub docs smoke ok");
+2 -2
View File
@@ -60,14 +60,14 @@ describe("ask-molty proxy route", () => {
}),
);
const event = mockEvent(
"https://clawhub.ai/ask-molty/sign-in?return_to=https%3A%2F%2Fclawhub.ai%2Fdocs",
"https://docs.clawhub.ai/ask-molty/sign-in?return_to=https%3A%2F%2Fdocs.clawhub.ai%2F",
);
const response = await handler(event);
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe(
"https://clawhub.ai/auth/docs?return_to=https%3A%2F%2Fclawhub.ai%2Fdocs",
"https://clawhub.ai/auth/docs?return_to=https%3A%2F%2Fdocs.clawhub.ai%2F",
);
expect(response.headers.get("set-cookie")).toBe(
"ask_molty_session=signed.value; Max-Age=604800; Path=/ask-molty; HttpOnly; Secure; SameSite=Lax",
+14
View File
@@ -0,0 +1,14 @@
import { defineEventHandler } from "h3";
const robots = `# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
`;
export default defineEventHandler(() => {
return new Response(robots, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
},
});
});
+1 -1
View File
@@ -10,7 +10,7 @@ Status: superseded for the CLAW-201 ClawHub docs migration.
The current migration plan is to build `docs/` with the shared
`openclaw/docs` renderer and serve the generated output from ClawHub under
`https://clawhub.ai/docs`. Do not set up Mintlify for this migration unless a
`https://docs.clawhub.ai`. Do not set up Mintlify for this migration unless a
new issue explicitly reopens that direction.
## Historical note
+1 -1
View File
@@ -21,7 +21,7 @@ Canonical product and registry material now lives in `docs/clawhub.md`:
## Summarize or link from OpenClaw docs
These OpenClaw docs should eventually point to `https://clawhub.ai/docs` rather than
These OpenClaw docs should eventually point to `https://docs.clawhub.ai` rather than
restate the whole registry guide:
- `docs/tools/clawhub.md`: keep as a concise bridge to ClawHub docs plus OpenClaw-native install examples.
+3 -3
View File
@@ -698,7 +698,7 @@ describe("plugin detail route", () => {
scanSource: "nightly",
authorRemediation: {
summary: "Replace the legacy before_agent_start hook with current prompt hooks.",
docsUrl: "https://clawhub.ai/docs/plugin-validation-fixes#legacy-before-agent-start",
docsUrl: "https://docs.clawhub.ai/plugin-validation-fixes#legacy-before-agent-start",
},
createdAt: 1,
},
@@ -779,10 +779,10 @@ describe("plugin detail route", () => {
expect(
screen
.getByRole("link", {
name: "https://clawhub.ai/docs/plugin-validation-fixes#legacy-before-agent-start",
name: "https://docs.clawhub.ai/plugin-validation-fixes#legacy-before-agent-start",
})
.getAttribute("href"),
).toBe("https://clawhub.ai/docs/plugin-validation-fixes#legacy-before-agent-start");
).toBe("https://docs.clawhub.ai/plugin-validation-fixes#legacy-before-agent-start");
expect(screen.getByText("Docs")).toBeTruthy();
expect(screen.getAllByText("OpenClaw 0.9.0").length).toBeGreaterThan(0);
});
+1 -1
View File
@@ -148,7 +148,7 @@ describe("plugins publish route", () => {
renderPublishRoute();
const guideLink = screen.getByRole("link", { name: /Plugin publishing guide/i });
expect(guideLink.getAttribute("href")).toBe("https://clawhub.ai/docs/publishing#plugins");
expect(guideLink.getAttribute("href")).toBe("https://docs.clawhub.ai/publishing#plugins");
expect(guideLink.getAttribute("target")).toBe("_blank");
});
+1 -1
View File
@@ -89,7 +89,7 @@ describe("Upload route", () => {
render(<Upload />);
const guideLink = screen.getByRole("link", { name: /Skill publishing guide/i });
expect(guideLink.getAttribute("href")).toBe("https://clawhub.ai/docs/skill-format");
expect(guideLink.getAttribute("href")).toBe("https://docs.clawhub.ai/skill-format");
expect(guideLink.getAttribute("target")).toBe("_blank");
});
+10 -4
View File
@@ -6,10 +6,13 @@ import { buildDocsAuthCallbackUrl, normalizeDocsReturnTo } from "./docsAuth";
describe("docs auth helpers", () => {
it("allows documentation return URLs and rejects unrelated origins", () => {
expect(normalizeDocsReturnTo("https://clawhub.ai/docs/auth")).toBe(
"https://clawhub.ai/docs/auth",
"https://docs.clawhub.ai/auth",
);
expect(normalizeDocsReturnTo("https://hub.openclaw.ai/docs/auth?from=mobile")).toBe(
"https://clawhub.ai/docs/auth?from=mobile",
"https://docs.clawhub.ai/auth?from=mobile",
);
expect(normalizeDocsReturnTo("https://docs.clawhub.ai/auth?from=mobile")).toBe(
"https://docs.clawhub.ai/auth?from=mobile",
);
expect(normalizeDocsReturnTo("https://documentation.openclaw.ai/concepts/models")).toBe(
"https://documentation.openclaw.ai/concepts/models",
@@ -36,10 +39,13 @@ describe("docs auth helpers", () => {
it("keeps callbacks on the same docs host", () => {
expect(buildDocsAuthCallbackUrl("https://clawhub.ai/docs/auth")).toBe(
"https://clawhub.ai/ask-molty/auth/callback",
"https://docs.clawhub.ai/ask-molty/auth/callback",
);
expect(buildDocsAuthCallbackUrl("https://hub.openclaw.ai/docs/auth")).toBe(
"https://clawhub.ai/ask-molty/auth/callback",
"https://docs.clawhub.ai/ask-molty/auth/callback",
);
expect(buildDocsAuthCallbackUrl("https://docs.clawhub.ai/auth")).toBe(
"https://docs.clawhub.ai/ask-molty/auth/callback",
);
expect(buildDocsAuthCallbackUrl("https://documentation.openclaw.ai/concepts/models")).toBe(
"https://documentation.openclaw.ai/ask-molty/auth/callback",
+8 -7
View File
@@ -1,8 +1,7 @@
const canonicalDocsOrigin = "https://clawhub.ai";
const docsOriginAliases = new Map([["https://hub.openclaw.ai", canonicalDocsOrigin]]);
const canonicalDocsOrigin = "https://docs.clawhub.ai";
const legacyClawHubOrigins = new Set(["https://clawhub.ai", "https://hub.openclaw.ai"]);
const productionDocsOrigins = new Set([
canonicalDocsOrigin,
...docsOriginAliases.keys(),
"https://documentation.openclaw.ai",
"https://docs.openclaw.ai",
]);
@@ -16,12 +15,14 @@ export function normalizeDocsReturnTo(value?: string | null, options: DocsAuthOp
try {
const url = new URL(value);
if (!["http:", "https:"].includes(url.protocol)) return null;
if (!isAllowedDocsOrigin(url.origin, options.currentOrigin ?? getCurrentOrigin())) return null;
const canonicalOrigin = docsOriginAliases.get(url.origin);
if (canonicalOrigin) {
const canonical = new URL(canonicalOrigin);
if (legacyClawHubOrigins.has(url.origin)) {
if (url.pathname !== "/docs" && !url.pathname.startsWith("/docs/")) return null;
const canonical = new URL(canonicalDocsOrigin);
url.protocol = canonical.protocol;
url.host = canonical.host;
url.pathname = url.pathname.slice("/docs".length) || "/";
} else if (!isAllowedDocsOrigin(url.origin, options.currentOrigin ?? getCurrentOrigin())) {
return null;
}
return url.href;
} catch {
+6 -6
View File
@@ -70,7 +70,7 @@ const { DocsAuth, Route } = await import("./docs");
describe("DocsAuth", () => {
beforeEach(() => {
routePath = Route.path;
mockSearch = { return_to: "https://clawhub.ai/docs/concepts/models" };
mockSearch = { return_to: "https://docs.clawhub.ai/concepts/models" };
mockAuthToken = "convex.jwt";
mockAuthStatus = {
isAuthenticated: true,
@@ -88,12 +88,12 @@ describe("DocsAuth", () => {
const form = screen.getByRole("button", { name: /continue to docs/i }).closest("form");
expect(form?.getAttribute("method")).toBe("post");
expect(form?.getAttribute("action")).toBe("https://clawhub.ai/ask-molty/auth/callback");
expect(form?.getAttribute("action")).toBe("https://docs.clawhub.ai/ask-molty/auth/callback");
expect(document.querySelector<HTMLInputElement>('input[name="token"]')?.value).toBe(
"convex.jwt",
);
expect(document.querySelector<HTMLInputElement>('input[name="return_to"]')?.value).toBe(
"https://clawhub.ai/docs/concepts/models",
"https://docs.clawhub.ai/concepts/models",
);
expect(document.querySelector<HTMLInputElement>('input[name="registry"]')?.value).toBe(
"http://127.0.0.1:3211",
@@ -106,9 +106,9 @@ describe("DocsAuth", () => {
render(<DocsAuth autoSubmit={false} />);
const form = screen.getByRole("button", { name: /continue to docs/i }).closest("form");
expect(form?.getAttribute("action")).toBe("https://clawhub.ai/ask-molty/auth/callback");
expect(form?.getAttribute("action")).toBe("https://docs.clawhub.ai/ask-molty/auth/callback");
expect(document.querySelector<HTMLInputElement>('input[name="return_to"]')?.value).toBe(
"https://clawhub.ai/docs/plugins?from=mobile",
"https://docs.clawhub.ai/plugins?from=mobile",
);
});
@@ -119,7 +119,7 @@ describe("DocsAuth", () => {
expect(screen.getByRole("heading", { name: /verify with github/i })).toBeTruthy();
expect(screen.getByRole("button", { name: /verify with github/i }).dataset.redirectTo).toBe(
"/auth/docs?return_to=https%3A%2F%2Fclawhub.ai%2Fdocs%2Fconcepts%2Fmodels",
"/auth/docs?return_to=https%3A%2F%2Fdocs.clawhub.ai%2Fconcepts%2Fmodels",
);
});
+1 -1
View File
@@ -60,7 +60,7 @@ const apiRefs = api as unknown as {
};
const SHOW_CLAWPACK_ONBOARDING_BANNER = false;
const PLUGIN_PUBLISHING_GUIDE_URL = "https://clawhub.ai/docs/publishing#plugins";
const PLUGIN_PUBLISHING_GUIDE_URL = "https://docs.clawhub.ai/publishing#plugins";
function findReadmeFile(files: File[]): File | null {
// Match the same lookup the publish backend uses (readme.md / readme.mdx)
+1 -1
View File
@@ -52,7 +52,7 @@ import {
} from "../upload/-utils";
const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
const SKILL_PUBLISHING_GUIDE_URL = "https://clawhub.ai/docs/skill-format";
const SKILL_PUBLISHING_GUIDE_URL = "https://docs.clawhub.ai/skill-format";
const REQUIRED_FILE_LABEL = "SKILL.md";
const REQUIRED_FILE_ISSUE = `${REQUIRED_FILE_LABEL} is required.`;
// Ordered by sort priority: the canonical name first, the legacy alias second.
+71 -1
View File
@@ -5,7 +5,7 @@
"headers": [
{
"key": "Content-Security-Policy",
"value": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https: wss:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self' https://clawhub.ai https://documentation.openclaw.ai https://docs.openclaw.ai http://localhost:3000 http://127.0.0.1:3000 http://localhost:4173 http://127.0.0.1:4173; upgrade-insecure-requests"
"value": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https: wss:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self' https://clawhub.ai https://docs.clawhub.ai https://documentation.openclaw.ai https://docs.openclaw.ai http://localhost:3000 http://127.0.0.1:3000 http://localhost:4173 http://127.0.0.1:4173; upgrade-insecure-requests"
},
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "DENY" },
@@ -21,6 +21,54 @@
}
],
"redirects": [
{
"source": "/docs",
"has": [
{
"type": "header",
"key": "host",
"value": "clawhub.ai"
}
],
"destination": "https://docs.clawhub.ai/",
"statusCode": 308
},
{
"source": "/docs/",
"has": [
{
"type": "header",
"key": "host",
"value": "clawhub.ai"
}
],
"destination": "https://docs.clawhub.ai/",
"statusCode": 308
},
{
"source": "/docs/:path*",
"has": [
{
"type": "header",
"key": "host",
"value": "clawhub.ai"
}
],
"destination": "https://docs.clawhub.ai/:path*",
"statusCode": 308
},
{
"source": "/docs/:path*/",
"has": [
{
"type": "header",
"key": "host",
"value": "clawhub.ai"
}
],
"destination": "https://docs.clawhub.ai/:path*/",
"statusCode": 308
},
{
"source": "/",
"has": [
@@ -47,6 +95,28 @@
}
],
"rewrites": [
{
"source": "/ask-molty/:path*",
"has": [
{
"type": "header",
"key": "host",
"value": "docs.clawhub.ai"
}
],
"destination": "https://clawhub.ai/ask-molty/:path*"
},
{
"source": "/:path*",
"has": [
{
"type": "header",
"key": "host",
"value": "docs.clawhub.ai"
}
],
"destination": "/docs/:path*"
},
{
"source": "/api/:path*",
"destination": "https://wry-manatee-359.convex.site/api/:path*"