Jamdesk Documentation logo

Cloudflare Workers 代理

通过 Cloudflare Worker 将 /docs 请求代理到 Jamdesk 文档站点,涵盖 Worker 设置、路由模式和缓存配置。

Cloudflare Worker 会在你的域名上拦截 /docs 请求,将其重写到你的 Jamdesk 子域名并返回响应。整个过程都在边缘完成,无需源站服务器。你可以使用 npx jamdesk deploy-proxy cloudflare 自动创建 Worker,也可以按照下面的步骤手动设置。

工作原理

Worker 会将请求转发到你的 Jamdesk 子域名,并在 X-Jamdesk-Forwarded-Host 标头中传递你的域名。Jamdesk 使用此标头验证域名并应用相应设置。这是一次性设置——以后如果你在仪表板中更改域名或配置,无需更新 Worker。

前置条件

  • 已配置域名的 Cloudflare 账户
  • 已安装 Wrangler CLI v3.0+
  • 你的 Jamdesk 子域名(可在仪表板设置中找到)
  • 已将自定义域名添加到 Jamdesk 仪表板中的项目(在域名注册并验证之前,Worker 会返回 403)
  • 为提供文档的主机名配置了代理(橙色云朵)DNS 记录。Worker 只能在已代理的记录上运行,因此即使该域名没有托管其他内容,也必须配置一条记录——添加一条指向 100:: 的占位 AAAA 记录,并将其设为代理状态。

使用 CLI 快速设置

设置 Cloudflare Worker 的最快方式:

npx jamdesk deploy-proxy cloudflare

此交互式命令将:

  1. 检查是否已安装 wrangler 3.0+
  2. 验证你的 Cloudflare 账户并显示可用域名
  3. docs.json 中关联的项目解析你的 Jamdesk 子域名
  4. 让你从 Cloudflare 区域中选择目标域名
  5. 生成所有必需文件
  6. 可选部署到 Cloudflare

如果你可以访问多个 Cloudflare 账户(代理机构或团队常见此情况),CLI 会在选择区域之前提示你选择账户。选择拥有你要部署域名的账户——Worker 路由只能为所选账户中的域名创建。

非交互式设置

要在无需提示的情况下运行——例如在 CI 中或从脚本运行——请通过标志传入答案:

jamdesk deploy-proxy cloudflare --slug myproject --domain example.com --yes

--yes 会生成 Worker 文件,然后停止。它不会执行部署:由于区域是根据你的域名推断的,而不是通过 Cloudflare 账户确认的,因此将该配置推送到线上仍是一个明确的步骤。使用以下命令完成部署:

cd cloudflare-worker
npx wrangler deploy

使用 --yes 时 CLI 无法进行提示,因此需要知道你的子域名。它会从 docs.json 中关联的项目读取子域名;如果缺少该关联(运行一次 jamdesk deploy 即可添加),请显式传入 --slug,否则命令会停止而不是进行猜测。

如果输出目录已存在,--yes 会停止而不是替换该目录。添加 --force 可覆盖现有目录,或使用 --output-dir 写入其他位置。

选项描述
--slug你的 Jamdesk 子域名,即 X.jamdesk.app 中的 X(跳过自动检测)
--domain目标域名(例如 yoursite.com)
--path路径前缀;必须与仪表板中的子路径完全一致(默认值:/docs)
--output-dir输出目录(默认值:cloudflare-worker/)
--skip-deploy在交互式运行中跳过“现在部署吗?”提示(--yes 永不部署)
--force如果输出目录已存在,则覆盖该目录
--yes所有提示均采用默认答案(CI 模式)。永不部署,也永不覆盖现有目录——与 --force 结合使用可覆盖目录

如果你更喜欢手动设置,请继续执行以下步骤。


手动设置

步骤 1:创建 Worker

为 Worker 创建新目录并初始化:

mkdir docs-proxy && cd docs-proxy
npm init -y

步骤 2:添加 Worker 代码

创建包含以下代码的 index.js 文件:

index.js
/**
 * Jamdesk Documentation Proxy Worker
 *
 * Generated by: jamdesk deploy-proxy cloudflare
 * Proxies /docs/* requests AND their assets to YOUR_SLUG.jamdesk.app
 *
 * Assets under /_jd/* (images, fonts, branding, analytics) must also be proxied
 * since they use absolute paths in the HTML.
 */

const JAMDESK_HOST = "YOUR_SLUG.jamdesk.app";

// Paths that are always proxied to Jamdesk
const PROXY_PATHS = [
  "/docs",  // Documentation pages
  "/_jd/",  // All Jamdesk assets (images, fonts, branding, analytics)
];

function shouldProxy(pathname) {
  return PROXY_PATHS.some(prefix => {
    // For the docs path, require exact match or prefix with slash (not /docs.json)
    if (prefix === "/docs") {
      return pathname === "/docs" || pathname.startsWith("/docs/");
    }
    return pathname.startsWith(prefix);
  });
}

function proxyToJamdesk(request, url) {
  // Rewrite the request to Jamdesk
  const proxyUrl = new URL(request.url);
  proxyUrl.hostname = JAMDESK_HOST;
  // Pin the scheme: an http:// visitor proxied as http:// gets a 308 from
  // Vercel's edge pointing at JAMDESK_HOST, and redirect:"manual" hands that
  // redirect straight to the browser — bouncing the visitor off this domain.
  proxyUrl.protocol = "https:";
  // Setting .protocol leaves a non-default .port in place, so :8787 (wrangler
  // dev) or Cloudflare's alternate http ports (8080, 8880, 2052…) would follow
  // us to the https upstream and fail there.
  proxyUrl.port = "";

  // Clone headers and add proxy headers
  const headers = new Headers(request.headers);
  headers.set("Host", JAMDESK_HOST);
  headers.set("X-Forwarded-Host", url.hostname);
  headers.set("X-Forwarded-Proto", "https");
  // Custom header for domain verification (Vercel strips standard forwarding headers)
  headers.set("X-Jamdesk-Forwarded-Host", url.hostname);

  // Don't follow redirects — let the browser handle them so the URL updates.
  // Without this, redirects happen internally and the browser URL doesn't change,
  // which causes the sidebar to mis-highlight the active page.
  const proxyRequest = new Request(proxyUrl, {
    method: request.method,
    headers,
    body: request.body,
    redirect: "manual",
  });

  // Cache all content types at Cloudflare edge (CF doesn't cache HTML by default).
  // Cache duration is controlled by upstream Cache-Control headers.
  // Never cache redirects or errors — they must always hit origin.
  return fetch(proxyRequest, {
    cf: {
      cacheEverything: true,
      cacheTtlByStatus: { "300-399": 0, "400-499": 0, "500-599": 0 },
    },
  });
}

export default {
  async fetch(request) {
    const url = new URL(request.url);

    if (shouldProxy(url.pathname)) {
      return proxyToJamdesk(request, url);
    }

    // /_next/ is ambiguous: the customer's root site may itself be a Next.js app
    // serving its own /_next/ assets. Try the origin first and fall back to
    // Jamdesk when the origin doesn't have it (404) or can't answer at all
    // (5xx — a docs-only domain has no real origin, so Cloudflare returns 522).
    // Hashed asset filenames never collide between the two apps.
    if (url.pathname.startsWith("/_next/")) {
      let originResponse;
      try {
        originResponse = await fetch(request);
      } catch (err) {
        // Log rather than swallow: a genuine platform fault and a domain with
        // no origin at all are indistinguishable in `wrangler tail` otherwise.
        console.warn("origin fetch threw, serving from Jamdesk:", err);
        return proxyToJamdesk(request, url);
      }
      if (originResponse.status !== 404 && originResponse.status < 500) {
        return originResponse;
      }
      return proxyToJamdesk(request, url);
    }

    return fetch(request);
  },
};

YOUR_SLUG 替换为你的实际 Jamdesk 子域名(例如,如果你的文档地址为 acme.jamdesk.app,则替换为 acme)。

如果你在仪表板中使用的是自定义子路径,而不是默认的 /docs,请在 PROXY_PATHS 中以及 shouldProxy() 内部的精确匹配检查中,将 "/docs" 替换为你的子路径。CLI 的 --path 标志会在生成文件时自动完成此操作,但只适用于未修改的模板;重新生成文件会覆盖你手动添加到 PROXY_PATHS 的自定义条目。如果你已自定义此 Worker,请直接编辑其中的 /docs 条目。

X-Jamdesk-Forwarded-Host 标头是必需的,而缺少该标头会静默失败——请求仍会成功,但页面会以 noindex 提供,规范链接会指向 YOUR_SLUG.jamdesk.app 而不是你的域名,因此搜索引擎永远不会将你的文档编入索引。403 则是相反的问题:标头确实存在,但其中指定的域名尚未为此项目注册并激活。

步骤 3:配置 wrangler.toml

创建 wrangler.toml 以配置 Worker:

wrangler.toml
name = "docs-proxy"
main = "index.js"
compatibility_date = "2024-01-01"

# Serve only via the routes below, not on the public <name>.workers.dev URL —
# that URL is a second way into the same proxy and is worth closing.
workers_dev = false

# Single catch-all route; the worker handles path filtering internally
routes = [
  { pattern = "yoursite.com/*", zone_name = "yoursite.com" },
]

如果你的 Cloudflare 登录账户可以访问多个账户,请同时添加 account_id = "<your account id>"——否则 wrangler deploy 会停止,而不是猜测要部署到哪个账户。npx wrangler whoami 会列出你的账户 ID。

如果你的网站也在 www.yoursite.com 上提供流量,请添加第二条路由,以便 Worker 同时处理两个域名:

routes = [
  { pattern = "yoursite.com/*", zone_name = "yoursite.com" },
  { pattern = "www.yoursite.com/*", zone_name = "yoursite.com" },
]

步骤 4:部署

将 Worker 部署到 Cloudflare:

npx wrangler deploy

步骤 5:验证

访问 https://yoursite.com/docs,确认文档能够正常提供服务。

故障排除

如果你在仪表板中重命名了子路径(例如从 /docs/help),但 Worker 的 PROXY_PATHS 仍只列出 /docs,则对 /help/* 的请求永远不会到达 Jamdesk:它们会继续执行 fetch(request),并在你自己的源站返回 404。与此同时,/docs/* 仍然可以正常工作(Jamdesk 会同时提供两个前缀),这正是该问题容易被忽略的原因。

**修复方法:**将新子路径添加到 PROXY_PATHS。如果 Worker 仍是未修改的模板,请重新运行 jamdesk deploy-proxy cloudflare --path <subpath>;如果你已经自定义了 Worker,请手动编辑数组。

Wrangler 优先使用 CLOUDFLARE_API_TOKEN,而不是 OAuth 登录;设置该令牌后,它无法启动 OAuth 登录。此命令需要 OAuth 来列出你账户中的区域,因此它会显示令牌的位置并停止,而不是在 wrangler 内部失败。

Wrangler 还会从运行命令的目录读取 .env,因此令牌可能已为 wrangler 设置,却不在你的 shell 环境中——即使 echo $CLOUDFLARE_API_TOKEN 没有输出,也不能排除这种情况。请同时检查当前目录中的 .env 文件。

**修复方法:**为令牌授予 account:readzone:read 权限后重新运行,或者在不使用令牌的情况下运行:

env -u CLOUDFLARE_API_TOKEN jamdesk deploy-proxy cloudflare

如果令牌来自 .envenv -u 不会生效——请从没有此类 .env 的目录运行命令,或在本次运行期间暂时移走该文件。

CLI 会在选择区域之前显示可用域名。如果看到“No domains found”:

  1. 确认你已登录正确的 Cloudflare 账户
  2. 检查你的域名是否已添加到 Cloudflare 仪表板并处于激活状态
  3. 再次运行 CLI,并在询问是否继续使用当前账户时选择“No”,以切换账户

如果你有多个 Cloudflare 账户:

  1. 运行 jamdesk deploy-proxy cloudflare
  2. 出现账户选择提示时,选择拥有你域名的账户
  3. 如果需要完全不同的登录信息,请选择 "Switch to different login"
  4. CLI 会将你登出,并提示你使用正确的凭据登录

此错误表示所选区域与 Cloudflare 账户不匹配。原因可能是:

  • 你选择的区域属于其他账户
  • 该区域已从 Cloudflare 中移除

修复方法:重新运行 CLI 并从列表中选择正确的区域,或切换到拥有该区域的账户。

确保你的路由模式使用全匹配:yoursite.com/*(不要只使用 yoursite.com/docs*)。Worker 内部的 shouldProxy() 函数会处理路径筛选。

两个常见原因:

  1. Worker 未运行。确保你的 DNS 记录在 Cloudflare 中设置为代理(橙色云朵)。Worker 只能在已代理的记录上运行。
  2. **缺少 X-Forwarded-Host 标头。**Worker 必须设置此标头,以便 Jamdesk 生成正确的资源 URL。

如果看到“Domain is not authorized to serve this content”:

  1. 确认你的域名已在 Jamdesk 仪表板中注册
  2. 为你的域名完成 DNS 验证(TXT 记录)
  3. 确保已在 Worker 代码中设置 X-Jamdesk-Forwarded-Host 标头
  4. 检查你的域名是否映射到正确的项目

必须先验证域名,Worker 才能提供文档。

Worker 只能在代理(橙色云朵)DNS 记录上运行。如果你的 A 记录设置为“仅 DNS”(灰色云朵),请求会直接发送到源站,完全跳过 Worker。

**修复方法:**在 Cloudflare DNS 中,将 A 记录切换为代理状态(橙色云朵)。子域名同样适用:任何配置了 Worker 路由的记录都必须启用代理。

Jamdesk 会直接读取 DNS 记录值来验证所有权。Cloudflare 的代理(橙色云朵)会隐藏这些值,因此验证无法完成。

修复方法:

  1. 将 DNS 记录设置为“仅 DNS”(灰色云朵)
  2. 等待验证完成(仪表板中的状态变为 active
  3. 切换回代理(橙色云朵),使 Worker 运行

简而言之:验证时使用灰色云朵 → 提供服务时使用橙色云朵

Jamdesk 使用 Cache-Control: no-store 提供文档 HTML,因此 Cloudflare 不会在边缘缓存页面(cf-cache-status: BYPASS)。每个请求都会渲染当前版本,发布的更改会立即显示,不会有缓存延迟。

/_next//_jd/ 下的静态资源(JavaScript、CSS、字体、图像)使用长期有效的 immutable 缓存标头,因此 Cloudflare 会在边缘缓存它们。它们的文件名包含内容哈希,因此每次构建都会生成新的 URL,并自动获取更新后的资源。无需清除缓存。

cacheEverything: true 允许 Cloudflare 在代理路由上缓存这些静态资源;它不会覆盖 HTML 的 no-store 设置。要手动清除边缘缓存,请使用 Cloudflare 的 Purge Cache(Caching → Configuration → Purge Everything)。

CLI 要求 wrangler 3.0+。使用以下命令更新:

npm install -g wrangler@latest

下一步

仅自定义域名

停止直接响应你的子域名

自定义域名

验证 DNS 并排查问题

子路径托管

在 /docs 提供文档