-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmiddleware.ts
More file actions
112 lines (99 loc) · 4.18 KB
/
Copy pathmiddleware.ts
File metadata and controls
112 lines (99 loc) · 4.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { locales, defaultLocale, getBaseLanguage, Locale } from "./i18n-config";
// 获取用户首选语言
function getPreferredLanguage(request: NextRequest): string {
// 先读用户选择的语言(cookie)
const savedLang = request.cookies.get("i18nextLng")?.value;
if (savedLang) {
const mappedLang = getBaseLanguage(savedLang);
if (locales.includes(mappedLang as Locale)) {
return mappedLang;
}
}
// 再从 Accept-Language 头部获取语言偏好
const acceptLanguage = request.headers.get("accept-language");
if (acceptLanguage) {
const preferredLangs = acceptLanguage
.split(",")
.map((lang) => {
const [language, weight] = lang.split(";");
return {
language: language.trim(),
weight: weight ? parseFloat(weight.split("=")[1]) : 1.0,
};
})
.sort((a, b) => b.weight - a.weight);
for (const { language } of preferredLangs) {
const mappedLang = getBaseLanguage(language);
if (locales.includes(mappedLang as Locale)) {
return mappedLang;
}
}
}
// 默认返回英语
return defaultLocale;
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// 如果是静态资源或 API 路由,直接返回
if (
pathname.startsWith("/_next") ||
pathname.startsWith("/api") ||
pathname.startsWith("/static") ||
pathname === "/favicon.ico" ||
pathname === "/manifest.json" ||
pathname === "/sw.js" ||
pathname.startsWith("/workbox-") ||
pathname.startsWith("/locales/") ||
pathname.startsWith("/emoji/") ||
pathname.match(/^\/icon-[\w-]+\.png$/) ||
pathname === "/robots.txt" ||
pathname === "/sitemap.xml" ||
pathname === "/llms.txt" ||
// 兜底:任何带扩展名的路径都是文件,不是页面。此前这里是一份白名单,
// 每加一类静态资源就得补一行,漏了就会被当成「语言前缀写错」补上
// 前缀、变成 404——图标、hreflang-sitemap、演示视频都栽过一次。
// 站内路由不带扩展名(/en、/en/faq、/en/996),所以不会误伤。
/\.[a-z0-9]+$/i.test(pathname)
) {
return NextResponse.next();
}
// 注意:这里不能把「首段不是合法语言码」当成语言前缀写错来处理。曾经的实现
// 会把首段剥掉再重定向到默认语言,导致 /faq 变成 /en 而不是 /en/faq —— 路径
// 被吃掉了。以前站内只有 /[lang] 一种路由所以没暴露,加了内容页后就会出问题。
// 统一交给下面的逻辑:没有合法语言前缀就整体补上首选语言。这样 /faq 得到
// /en/faq;而真正写错的 /xx/faq 会得到 /en/xx/faq 并 404,这是诚实的结果。
// 检查 URL 是否已经包含有效的语言代码
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) return NextResponse.next();
// 获取用户首选语言
const locale = getPreferredLanguage(request);
// 用 clone 而不是 new URL(path, base):后者的第一个参数是绝对路径时会连同
// 查询串一起替换掉。分享链接指向根路径并带着 ?s= 与 utm_*,走旧写法会在这次
// 重定向中被整串丢弃——分享归因此前一直没生效,正是这个原因。
const newUrl = request.nextUrl.clone();
newUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(newUrl);
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - manifest.json
* - sw.js (Service Worker)
* - workbox-*.js (Workbox files)
* - locales
* - robots.txt
* - sitemap.xml
* - baidu_verify_codeva-SXZydSeYe0.html
*/
"/((?!api|_next/static|_next/image|favicon.ico|manifest.json|sw.js|workbox-[^/]+|locales|emoji|robots.txt|sitemap.xml|llms.txt|baidu_verify_codeva-SXZydSeYe0.html).*)",
],
};