Skip to content

Commit 6af9621

Browse files
fix(docs): 服务端 308 自愈历史路径,替代客户端 not-found.tsx 跳转
原 not-found.tsx 是 "use client":服务端先回 HTTP 404,JS 再跳。 Googlebot 不执行 JS,GSC 仍报 404,SEO 目标未达成。 修法:去掉 dynamic="force-static",让未知 slug 走 SSR。在 DocPage 里 page == null 时先查后端 resolve 端点,命中历史路径就 permanentRedirect()(Next.js 发 308),再不认识才 notFound()。 已知路径(generateStaticParams 列出的 304 条)仍然 SSG,正常 页面 TTFB 无额外开销。build 表验证:● SSG 路径不变,ƒ Dynamic 新增(专门接收未知路径的 SSR 兜底)。 验收:curl -sI https://involutionhell.com/zh/docs/community/dev-tips/git101 应看到 308 + Location,不是 404。 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 3c6dbcf commit 6af9621

1 file changed

Lines changed: 48 additions & 6 deletions

File tree

app/[locale]/docs/[...slug]/page.tsx

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { safeJsonLdString } from "@/lib/json-ld";
33
import { SITE_URL } from "@/lib/site-url";
44
import { ensureSeoDescription } from "@/lib/seo-description";
55
import { DocsPage, DocsBody } from "fumadocs-ui/page";
6-
import { notFound } from "next/navigation";
6+
import { notFound, permanentRedirect } from "next/navigation";
77
import type { Metadata } from "next";
88
import { setRequestLocale } from "next-intl/server";
99
import { hasLocale } from "next-intl";
@@ -24,18 +24,50 @@ import { DocShareButton } from "@/app/components/DocShareButton";
2424
import { routing } from "@/i18n/routing";
2525
import { type PageData } from "@/app/types/doc";
2626

27+
const BACKEND_URL = process.env.BACKEND_URL ?? "http://localhost:8080";
28+
29+
/**
30+
* 查询后端 resolve 端点,未知路径可能是历史重命名路径。
31+
* 返回 canonical URL(如 /docs/learn/cs/dev-tips/git101)或 null。
32+
* 后端 Caffeine 缓存 TTL=600s,命中率高,延迟可控。
33+
*/
34+
async function resolveDocPath(
35+
locale: string,
36+
slug: string[],
37+
): Promise<string | null> {
38+
const strippedPath = `/docs/${slug.join("/")}`;
39+
try {
40+
const controller = new AbortController();
41+
const timeout = setTimeout(() => controller.abort(), 400);
42+
const res = await fetch(
43+
`${BACKEND_URL}/api/docs/resolve?path=${encodeURIComponent(strippedPath)}`,
44+
{ redirect: "manual", signal: controller.signal, cache: "no-store" },
45+
);
46+
clearTimeout(timeout);
47+
if (res.status === 301 || res.status === 308) {
48+
const loc = res.headers.get("Location");
49+
// loc === strippedPath 意味着当前路径已是 canonical,不跳
50+
if (loc && loc !== strippedPath) {
51+
return `/${locale}${loc}`;
52+
}
53+
}
54+
} catch {
55+
// 超时或后端不可达:降级到 notFound()
56+
}
57+
return null;
58+
}
59+
2760
interface Param {
2861
params: Promise<{
2962
locale: string;
3063
slug?: string[];
3164
}>;
3265
}
3366

34-
// 显式声明 force-static:让 Next.js 严格按 generateStaticParams 预渲染
35-
// 所有 (locale, slug) 组合,未列出的不允许动态生成。
36-
// 没有这条时,build 表里 ƒ Dynamic 标签会让 docs 走运行时渲染(即使加了
37-
// setRequestLocale 也不一定 prerender)。
38-
export const dynamic = "force-static";
67+
// dynamicParams=true(默认值):generateStaticParams 列出的路径 SSG 预渲染,
68+
// 未列出的路径(包括历史旧路径)走运行时 SSR,在 DocPage 里做 resolve → permanentRedirect。
69+
// 不写 dynamic="force-static":让未知路径能够 SSR,而不是直接 404。
70+
// 已知路径仍然走 SSG(generateStaticParams 命中),正常页面 TTFB 不受影响。
3971

4072
export default async function DocPage({ params }: Param) {
4173
const { locale, slug } = await params;
@@ -47,6 +79,11 @@ export default async function DocPage({ params }: Param) {
4779
// 找不到时按 source.ts 配的 fallbackLanguage='zh' 回退到原文。
4880
const page = source.getPage(slug, locale);
4981
if (page == null) {
82+
// slug 不在 SSG 列表里:查历史路径表,命中则服务端 308(Googlebot 可跟)
83+
const redirectTarget = await resolveDocPath(locale, slug ?? []);
84+
if (redirectTarget) {
85+
permanentRedirect(redirectTarget);
86+
}
5087
notFound();
5188
}
5289

@@ -181,6 +218,11 @@ export async function generateMetadata({ params }: Param): Promise<Metadata> {
181218

182219
const page = source.getPage(slug, locale);
183220
if (page == null) {
221+
// generateMetadata 同步 page.tsx 的跳转逻辑,避免 metadata 和页面不一致
222+
const redirectTarget = await resolveDocPath(locale, slug ?? []);
223+
if (redirectTarget) {
224+
permanentRedirect(redirectTarget);
225+
}
184226
notFound();
185227
}
186228

0 commit comments

Comments
 (0)