Skip to content

Commit e61c5a0

Browse files
docs+seo: 首页 generateMetadata + 加新路由 boilerplate
补两条 i18n 收尾的零碎: 1. app/[locale]/page.tsx 加 generateMetadata 覆盖 root layout 默认 alternates,给首页输出当前 locale 的 canonical (/zh 或 /en),并且 hreflang 三向声明(zh-CN / en-US / x-default)。 每个 locale 各自 canonical,不再共享 root 的 fallback。 2. dev_docs/i18n_url_routing.md 加「如何加新 user-facing 路由」章节 贴出 page.tsx + generateMetadata 的 boilerplate,列出 5 条容易踩的坑: - setRequestLocale 排序 - @/i18n/navigation vs next/navigation 选错的后果 - components 不在 [locale] 下 - server fetch 让 page 退回 dynamic 的应对 - layout 嵌套也要逐层 setRequestLocale 后续加 page 直接抄就能保证 SSG + i18n 一起对。
1 parent 8517332 commit e61c5a0

2 files changed

Lines changed: 101 additions & 0 deletions

File tree

app/[locale]/page.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Metadata } from "next";
12
import { setRequestLocale } from "next-intl/server";
23
import { hasLocale } from "next-intl";
34
import { notFound } from "next/navigation";
@@ -41,3 +42,33 @@ export default async function HomePage({ params }: Props) {
4142
</>
4243
);
4344
}
45+
46+
/**
47+
* 首页 metadata:覆盖 root layout 的 alternates。
48+
*
49+
* canonical 指向当前 locale 的首页(/zh 或 /en),让两个 locale 各自有独立
50+
* 的 canonical URL,避免 Google 把它们当成重复内容互相争 PageRank。
51+
*
52+
* languages(hreflang)三向声明,告诉 Google 同一首页的另一语言版本在哪。
53+
*/
54+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
55+
const { locale } = await params;
56+
if (!hasLocale(routing.locales, locale)) notFound();
57+
setRequestLocale(locale);
58+
59+
return {
60+
alternates: {
61+
canonical: `/${locale}`,
62+
languages: {
63+
"zh-CN": "/zh",
64+
"en-US": "/en",
65+
"x-default": "/zh",
66+
},
67+
},
68+
openGraph: {
69+
url: `/${locale}`,
70+
locale: locale === "en" ? "en_US" : "zh_CN",
71+
alternateLocale: locale === "en" ? ["zh_CN"] : ["en_US"],
72+
},
73+
};
74+
}

dev_docs/i18n_url_routing.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,76 @@ frontmatter 不需要写 `lang` 字段。fumadocs 按文件名后缀识别。
143143
`lib/source.ts``fallbackLanguage: "zh"`:访问 `/en/docs/<slug>``.en.mdx`
144144
不存在时,自动渲染原文(zh)。文档站合理体验(缺译显示中文 > 显示空白)。
145145

146+
## 加新 user-facing 路由
147+
148+
新建 page 时直接抄这个 boilerplate,就能保证 SSG + i18n 同时正确:
149+
150+
```tsx
151+
// app/[locale]/<your-route>/page.tsx
152+
import type { Metadata } from "next";
153+
import { setRequestLocale } from "next-intl/server";
154+
import { hasLocale } from "next-intl";
155+
import { notFound } from "next/navigation";
156+
import { routing } from "@/i18n/routing";
157+
158+
interface Props {
159+
params: Promise<{ locale: string }>;
160+
}
161+
162+
// 没 server fetch 才能加 force-static;如果 page 里有 await fetch(...)
163+
// 就别加(会和 fetch 冲突报错),靠 setRequestLocale 也能让 RSC 静态化。
164+
export const dynamic = "force-static";
165+
166+
export default async function Page({ params }: Props) {
167+
const { locale } = await params;
168+
if (!hasLocale(routing.locales, locale)) notFound();
169+
setRequestLocale(locale); // ← 必须,且必须排在任何 next-intl hook 之前
170+
171+
// ... 业务逻辑
172+
return <div>...</div>;
173+
}
174+
175+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
176+
const { locale } = await params;
177+
if (!hasLocale(routing.locales, locale)) notFound();
178+
setRequestLocale(locale);
179+
180+
return {
181+
alternates: {
182+
canonical: `/${locale}/<your-route>`,
183+
languages: {
184+
"zh-CN": "/zh/<your-route>",
185+
"en-US": "/en/<your-route>",
186+
"x-default": "/zh/<your-route>",
187+
},
188+
},
189+
};
190+
}
191+
```
192+
193+
**几条容易踩的坑**
194+
195+
1. **`setRequestLocale` 必须在第一位**。排在 `useTranslations` / `getMessages`
196+
/ `getTranslations` 之前。否则 next-intl 会回退到从 cookies/headers 推断
197+
locale,整页变 dynamic。
198+
2. **导航 API 用 `@/i18n/navigation` 而不是 `next/navigation`**。在 [locale]
199+
段下的客户端组件如果 `import { useRouter } from 'next/navigation'`
200+
`router.push("/foo")` 会跳到 `/foo` 而不是 `/<locale>/foo`,丢 locale 段。
201+
```tsx
202+
// ✅ 正确
203+
import { useRouter, Link } from "@/i18n/navigation";
204+
// ❌ 错误(在 [locale] 段下不要用)
205+
import { useRouter } from "next/navigation";
206+
import Link from "next/link";
207+
```
208+
3. **components 在 `app/components/`(不在 [locale] 下)**。组件本身不需要
209+
locale 段,导入用 `@/app/components/X`
210+
4. **server fetch 让 page 退回 dynamic**。如果非要 fetch backend,参考首页
211+
的做法:建一个 `/api/public/<x>` ISR 代理(revalidate=300),组件改 client
212+
useEffect fetch,page 本身保持纯静态。
213+
5. **layout.tsx 嵌套时也要调 `setRequestLocale`**。Next.js 独立渲染 layout
214+
和 page;page 调了 layout 没调照样退化 dynamic。每层都要补。
215+
146216
## 切换语言
147217

148218
`<LocaleToggle />` 用 next-intl 的 `useRouter().replace(pathname, { locale })`

0 commit comments

Comments
 (0)