反向代理设置

以 Markdown 格式查看

当您将 Fern 文档托管在子路径(如 mydomain.com/docs)上时,您的基础设施必须将该路径的请求代理到 Fern 的源站。子域名设置(docs.mydomain.com)使用 CNAME 记录,不需要反向代理。

工作原理

有效的反向代理需要完成两件事:

  1. 路由请求:将来自子路径的请求路由到 Fern 源站 app.buildwithfern.com,保留原始路径。x-fern-host 头告诉 Fern 要提供哪个文档站点。
  2. 禁用缓存:禁用 HTML 响应的缓存,确保访客始终收到最新部署的内容。

每个代理请求需要两个头:

用途
x-fern-host不含子路径的裸域名(例如 mydomain.com,而非 mydomain.com/docs告诉 Fern 要提供哪个文档站点
Hostapp.buildwithfern.com将请求路由到 Fern 源站

Fern 在 HTML 响应上设置 Cache-Control: public, max-age=0, must-revalidate,大多数提供商默认会遵守该设置。如果您的提供商覆盖了源站缓存头或应用了自己的 TTL,请为代理路径显式禁用缓存。

不要缓存来自 Fern 的 HTML 响应。缓存的 HTML 引用旧部署的 JavaScript 和 CSS — 这些文件已不存在,页面将无法加载。静态资源(/_next/static/*)由 Fern 的 CDN 直接提供,不经过您的代理。

配置您的提供商

1

创建 Worker

创建一个 Cloudflare Worker,拦截发往文档子路径的请求并代理到 Fern:

1const SUBPATH = "/docs";
2const FERN_HOST = "mydomain.com";
3
4export default {
5 async fetch(request) {
6 const url = new URL(request.url);
7
8 if (
9 url.pathname !== SUBPATH &&
10 !url.pathname.startsWith(`${SUBPATH}/`)
11 ) {
12 return new Response("Not found", { status: 404 });
13 }
14
15 const upstreamUrl = new URL(request.url);
16 upstreamUrl.protocol = "https:";
17 upstreamUrl.hostname = "app.buildwithfern.com";
18 upstreamUrl.port = "";
19
20 const proxiedRequest = new Request(upstreamUrl, request);
21 proxiedRequest.headers.set("x-fern-host", FERN_HOST);
22
23 return fetch(proxiedRequest);
24 },
25};

SUBPATHFERN_HOST 替换为您的子路径和裸域名。

2

将 Worker 附加到路由

在 Cloudflare 控制台的 Workers Routes 下,将 Worker 附加到路由模式,如 mydomain.com/docs/*

3

检查冲突的缓存规则

Worker 会转发 Fern 的 Cache-Control 头,因此缓存自动生效。在 Cloudflare 控制台中确认没有 Page Rule 或 Cache Rule 为 /docs* 设置了”Cache Level: Cache Everything” — 如果存在,请移除或使用”Cache Level: Bypass”覆盖。

1

添加 Fern 源站

在 CloudFront 分配中,前往 Origins 并创建新源站:

设置
Origin domainapp.buildwithfern.com
ProtocolHTTPS only
HTTPS port443
Minimum origin SSL protocolTLSv1.2

Add custom header 下添加:

头名称
x-fern-hostmydomain.com

mydomain.com 替换为您的裸域名(不含子路径)。

2

为文档路径创建缓存行为

前往 Behaviors 并创建新行为:

设置
Path pattern/docs*
Origin刚创建的 Fern 源站
Viewer protocol policyRedirect HTTP to HTTPS
Cache policyCachingDisabled(AWS 托管)
Origin request policyAllViewerExceptHostHeader(AWS 托管)

AllViewerExceptHostHeader 将所有查看器头、查询字符串和 Cookie 转发到源站,同时让 CloudFront 将 Host 头设置为 app.buildwithfern.com。这是必需的 — Fern 源站使用 Host 头进行路由,并拒绝不识别的主机名请求。

不要使用 AllViewer 源站请求策略。它会转发查看器的 Host 头(您的域名)而非源站的,这会导致 Fern 源站返回错误或原始的 React Server Component 数据而非 HTML。

3

验证行为顺序

CloudFront 按顺序评估行为。确保 /docs* 行为位于默认(*)行为之上,以便文档请求被路由到 Fern 源站而非您的主站点。

CloudFront 忽略 CDN-Cache-ControlSurrogate-Control — 只读取标准的 Cache-Control 头。如果您使用自定义缓存策略而非 CachingDisabled,请将默认、最小和最大 TTL 设置为 0。非零默认 TTL 会无视 Fern 的 Cache-Control: max-age=0 指令缓存 HTML 响应,可能导致过期内容和错误。

1

添加重写规则

netlify.toml(或 _redirects)中:

netlify.toml
1[[redirects]]
2 from = "/docs/*"
3 to = "https://app.buildwithfern.com/docs/:splat"
4 status = 200
5 force = true
6
7 [redirects.headers]
8 x-fern-host = "mydomain.com"
2

为文档路径禁用速率限制

Fern 通过并发请求预热页面缓存来重新验证文档站点。Netlify 的流量规则可能将此视为恶意流量并阻止请求,导致重新验证失败。

如果您在 Netlify 控制台的 Security > Traffic rules 下启用了速率限制,请添加规则将文档子路径(/docs/*)排除在速率限制之外。

3

检查代理超时

Fern 的重新验证端点在预热页面缓存时流式传输长时间运行的响应。Netlify 将代理重写请求限制为 26 秒,对于超过几百页的站点,这可能在预热完成前终止流。

如果重新验证持续失败并显示”terminated”状态,此超时很可能是原因。请联系 Netlify 支持请求增加站点超时,或联系 Fern 支持获取替代预热方案。

Netlify 的 status = 200 重写充当反向代理 — 访客的浏览器看到的是您的域名,内容来自 Fern。Netlify 遵守源站的 Cache-Control 头,因此不需要额外的缓存配置。

1

添加带转换的路由

使用带转换规则的路由在一步中重写路径并设置 x-fern-host

vercel.json
1{
2 "$schema": "https://openapi.vercel.sh/vercel.json",
3 "routes": [
4 {
5 "src": "/docs(/.*)?",
6 "dest": "https://app.buildwithfern.com/docs$1",
7 "transforms": [
8 {
9 "type": "request.headers",
10 "op": "set",
11 "target": { "key": "x-fern-host" },
12 "args": "mydomain.com"
13 }
14 ]
15 }
16 ]
17}

Vercel 对重写目标遵守源站的 Cache-Control 头,因此不需要额外的缓存配置。

1

添加 location

1location /docs {
2 proxy_pass https://app.buildwithfern.com;
3 proxy_set_header Host app.buildwithfern.com;
4 proxy_set_header x-fern-host mydomain.com;
5 proxy_set_header X-Real-IP $remote_addr;
6 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
7 proxy_set_header X-Forwarded-Proto $scheme;
8 proxy_ssl_server_name on;
9
10 # 防止 Brotli 响应的编码问题
11 proxy_set_header Accept-Encoding "gzip, deflate";
12
13 # 不缓存 HTML
14 proxy_no_cache 1;
15 proxy_cache_bypass 1;
16 add_header Cache-Control "public, max-age=0, must-revalidate" always;
17}

Nginx 不原生支持 Brotli 解压。上面的 Accept-Encoding 覆盖可防止由 Fern 默认 Brotli 压缩响应引起的 HTTP/2 传输错误。

1

添加路由规则

Site DeliveryIon 属性中,添加匹配路径 /docs* 的规则:

  • Origin Serverapp.buildwithfern.com
  • Forward Host Header:Origin Hostname

添加 Modify Outgoing Request Header 行为:

操作头名称
Addx-fern-hostmydomain.com
2

在规则上禁用缓存

在同一规则中添加 Caching 行为:

设置
Caching OptionNo Store

或者,将 Honor Origin Cache-Control 设置为 Yes,让 Akamai 遵守 Fern 的 Cache-Control: public, max-age=0, must-revalidate 头。

1

添加到 Caddyfile

mydomain.com {
handle /docs* {
reverse_proxy https://app.buildwithfern.com {
header_up Host app.buildwithfern.com
header_up x-fern-host mydomain.com
}
}
}

Caddy 默认不缓存响应,因此除非您使用 cache 指令或外部模块显式启用了缓存,否则不需要额外的缓存配置。

验证设置

对您的生产子路径运行以下检查:

$# 确认页面返回 HTML(而非错误或 RSC 数据)
$curl -sI https://mydomain.com/docs | grep -i content-type
$# 预期:content-type: text/html; charset=utf-8
$
$# 验证 HTML 响应的 Cache-Control
$curl -sI https://mydomain.com/docs | grep -i cache-control
$# 预期:cache-control: public, max-age=0, must-revalidate
$
$# 验证页面未被代理缓存(age 应为 0 或不存在)
$curl -sI https://mydomain.com/docs | grep -i "^age:"

如果 content-typetext/x-component 而非 text/html,说明您的代理将查看器的 Host 头转发给了 Fern 源站。确保发送给源站的 Host 头是 app.buildwithfern.com

如果 age 头存在且非零,说明您的代理正在提供缓存的响应。请重新检查提供商配置,确保 HTML 缓存已禁用。