From e691537a6112c01d5ad1e676869023fb9e3c0441 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 17:57:52 +0000 Subject: [PATCH] =?UTF-8?q?feat(docs):=20not-found=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=8E=86=E5=8F=B2=E8=B7=AF=E5=BE=84=EF=BC=8C?= =?UTF-8?q?resolver=20=E5=91=BD=E4=B8=AD=E5=8D=B3=20301=20=E5=8D=95?= =?UTF-8?q?=E8=B7=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新建 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 页。 --- app/[locale]/docs/not-found.tsx | 105 ++++++++++++++++++++++++++++++++ next.config.mjs | 6 ++ 2 files changed, 111 insertions(+) create mode 100644 app/[locale]/docs/not-found.tsx diff --git a/app/[locale]/docs/not-found.tsx b/app/[locale]/docs/not-found.tsx new file mode 100644 index 00000000..e73b12bb --- /dev/null +++ b/app/[locale]/docs/not-found.tsx @@ -0,0 +1,105 @@ +"use client"; + +import Link from "next/link"; +import { usePathname, useRouter } from "next/navigation"; +import { useEffect, useRef, useState } from "react"; +import { Button } from "@/app/components/ui/button"; + +/** + * /[locale]/docs/** 段专属 not-found。 + * + * 行为: + * 1. 用当前 pathname 查 /api/docs/resolve(后端走 doc_paths 历史表) + * 2. 端点返回 301+Location → router.replace 到新 URL(自动补回 locale) + * 3. 端点返回 404 / fetch 失败 / 超时 → 显示标准 404 UI + * + * 超时兜底 500ms:端点慢或挂了不让用户白屏等待。 + * + * 防重定向环三层: + * - 前端层:location === strippedPath 时不跳(canonical 等于当前路径) + * - 端点层:canonical 来自 path_current,当前文件必然存在,正常不再触发 not-found + * - 超时层:500ms abort,异常直接降级 404 + */ +export default function DocsNotFound() { + const pathname = usePathname(); + const router = useRouter(); + const [showNotFound, setShowNotFound] = useState(false); + // 防止 React StrictMode 双调用 useEffect 时发两次请求 + const didResolve = useRef(false); + + useEffect(() => { + if (didResolve.current) return; + didResolve.current = true; + + // 去掉 locale 前缀,把 /zh/docs/community/... 变成 /docs/community/... + const strippedPath = pathname.replace(/^\/(zh|en)/, ""); + + const controller = new AbortController(); + const timeout = setTimeout(() => { + controller.abort(); + setShowNotFound(true); + }, 500); + + fetch(`/api/docs/resolve?path=${encodeURIComponent(strippedPath)}`, { + // manual 让我们自己处理 301,而不是浏览器自动跟随 + redirect: "manual", + signal: controller.signal, + }) + .then((res) => { + clearTimeout(timeout); + + if (res.status === 301 || res.status === 308) { + const location = res.headers.get("Location"); + if (location && location !== strippedPath) { + // 拼回原始 locale,防止语言丢失 + const locale = pathname.startsWith("/en") ? "en" : "zh"; + // replace 而非 push:用户按后退不会回到 not-found 页 + router.replace(`/${locale}${location}`); + return; + } + } + // 其余情况(404、301 但 location 等于当前路径)→ 显示 404 + setShowNotFound(true); + }) + .catch(() => { + clearTimeout(timeout); + // fetch 失败(abort、网络错误)→ 降级显示 404 + setShowNotFound(true); + }); + + return () => { + clearTimeout(timeout); + controller.abort(); + }; + }, [pathname, router]); + + // 查询中:轻量 skeleton,避免闪白屏 + if (!showNotFound) { + return ( +
+ 你访问的页面可能已被移动或不存在。Try going back home. +
+ +