Skip to content

Commit df23329

Browse files
feat(docs): not-found 自动解析历史路径,resolver 命中即 301 单跳 (#354)
- 新建 app/[locale]/docs/not-found.tsx(segment-level not-found): 查询 /api/docs/resolve,命中 doc_paths 历史表就 router.replace 到 新路径,端点返回 404 或 500ms 超时则显示标准 404 UI - next.config.mjs 追加 /api/docs/resolve → backend rewrite 防重定向环三层: 前端层:location === strippedPath 时不跳(canonical == current) 端点层:canonical 来自 path_current(当前文件必然存在) 超时层:500ms AbortController,端点异常直接降级 404 `router.replace` 而非 `push`,后退不回到 not-found 页。 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 1e4ccc1 commit df23329

2 files changed

Lines changed: 111 additions & 0 deletions

File tree

app/[locale]/docs/not-found.tsx

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import { usePathname, useRouter } from "next/navigation";
5+
import { useEffect, useRef, useState } from "react";
6+
import { Button } from "@/app/components/ui/button";
7+
8+
/**
9+
* /[locale]/docs/** 段专属 not-found。
10+
*
11+
* 行为:
12+
* 1. 用当前 pathname 查 /api/docs/resolve(后端走 doc_paths 历史表)
13+
* 2. 端点返回 301+Location → router.replace 到新 URL(自动补回 locale)
14+
* 3. 端点返回 404 / fetch 失败 / 超时 → 显示标准 404 UI
15+
*
16+
* 超时兜底 500ms:端点慢或挂了不让用户白屏等待。
17+
*
18+
* 防重定向环三层:
19+
* - 前端层:location === strippedPath 时不跳(canonical 等于当前路径)
20+
* - 端点层:canonical 来自 path_current,当前文件必然存在,正常不再触发 not-found
21+
* - 超时层:500ms abort,异常直接降级 404
22+
*/
23+
export default function DocsNotFound() {
24+
const pathname = usePathname();
25+
const router = useRouter();
26+
const [showNotFound, setShowNotFound] = useState(false);
27+
// 防止 React StrictMode 双调用 useEffect 时发两次请求
28+
const didResolve = useRef(false);
29+
30+
useEffect(() => {
31+
if (didResolve.current) return;
32+
didResolve.current = true;
33+
34+
// 去掉 locale 前缀,把 /zh/docs/community/... 变成 /docs/community/...
35+
const strippedPath = pathname.replace(/^\/(zh|en)/, "");
36+
37+
const controller = new AbortController();
38+
const timeout = setTimeout(() => {
39+
controller.abort();
40+
setShowNotFound(true);
41+
}, 500);
42+
43+
fetch(`/api/docs/resolve?path=${encodeURIComponent(strippedPath)}`, {
44+
// manual 让我们自己处理 301,而不是浏览器自动跟随
45+
redirect: "manual",
46+
signal: controller.signal,
47+
})
48+
.then((res) => {
49+
clearTimeout(timeout);
50+
51+
if (res.status === 301 || res.status === 308) {
52+
const location = res.headers.get("Location");
53+
if (location && location !== strippedPath) {
54+
// 拼回原始 locale,防止语言丢失
55+
const locale = pathname.startsWith("/en") ? "en" : "zh";
56+
// replace 而非 push:用户按后退不会回到 not-found 页
57+
router.replace(`/${locale}${location}`);
58+
return;
59+
}
60+
}
61+
// 其余情况(404、301 但 location 等于当前路径)→ 显示 404
62+
setShowNotFound(true);
63+
})
64+
.catch(() => {
65+
clearTimeout(timeout);
66+
// fetch 失败(abort、网络错误)→ 降级显示 404
67+
setShowNotFound(true);
68+
});
69+
70+
return () => {
71+
clearTimeout(timeout);
72+
controller.abort();
73+
};
74+
}, [pathname, router]);
75+
76+
// 查询中:轻量 skeleton,避免闪白屏
77+
if (!showNotFound) {
78+
return (
79+
<div className="flex h-screen items-center justify-center">
80+
<span className="animate-pulse font-mono text-xs uppercase tracking-widest text-neutral-400">
81+
正在检索...
82+
</span>
83+
</div>
84+
);
85+
}
86+
87+
// 真 404 UI —— 和根 not-found.tsx 视觉保持一致
88+
return (
89+
<div className="flex h-screen w-full flex-col items-center justify-center bg-background text-foreground">
90+
<div className="bg-[url('/cloud_2.png')] bg-cover bg-center absolute inset-0 opacity-10 pointer-events-none" />
91+
<div className="z-10 flex flex-col items-center space-y-6 text-center">
92+
<h1 className="text-9xl font-black italic tracking-tighter">404</h1>
93+
<h2 className="text-2xl font-bold uppercase tracking-widest">
94+
页面不存在 · Page not found
95+
</h2>
96+
<p className="max-w-md text-muted-foreground">
97+
你访问的页面可能已被移动或不存在。Try going back home.
98+
</p>
99+
<Button asChild size="lg" className="mt-8">
100+
<Link href="/">返回首页 · Back to home</Link>
101+
</Button>
102+
</div>
103+
</div>
104+
);
105+
}

next.config.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,12 @@ const config = {
445445
source: "/api/posts/:path*",
446446
destination: `${backendUrl}/api/posts/:path*`,
447447
},
448+
{
449+
// docs 历史路径解析器:not-found.tsx 查询旧 URL 是否有现行映射
450+
// 后端查 doc_paths 表,返回 301+Location(找到)或 404(不认识)
451+
source: "/api/docs/resolve",
452+
destination: `${backendUrl}/api/docs/resolve`,
453+
},
448454
];
449455
},
450456
images: {

0 commit comments

Comments
 (0)