Next.js 15 + Cloudflare Workers 实战:零中间件多语言 SEO 站点架构

从 0 到 1 搭建一个支持 9 种语言、部署在 Cloudflare Workers 上的游戏攻略网站,核心思路:用文件结构替代中间件,把 Worker 成本砍到最低

一、项目背景

最近做了一个游戏的攻略站点,需要:

  • 支持 9 种语言(英/日/中/西/法/德/韩/意/葡)
  • SEO 友好(hreflang、canonical、sitemap、JSON-LD 全套)
  • 部署在 Cloudflare Workers 免费套餐(10 万请求/天)
  • 尽可能降低 Worker 调用次数

二、技术栈

选型
框架 Next.js 15 (App Router, RSC)
国际化 next-intl v4
样式 Tailwind CSS
部署 Cloudflare Workers (OpenNext)
分析 Google Analytics 4

三、核心架构:零中间件多语言

1. 问题:中间件的代价

传统的 next-intl 方案用中间件检测语言然后重定向。但在 Cloudflare Workers 上,每次重定向 = 2 次 Worker 调用(初始请求 + 重定向目标)。免费套餐每天只有 10 万次,重定向直接吃掉一半配额。

2. 方案:用路由组(Route Group)替代中间件

删掉 middleware.ts,用 Next.js App Router 的路由组天然隔离语言:

bash 复制代码
src/app/
├── layout.tsx              ← 根布局:HTML 骨架 + GA + JSON-LD
├── (root)/                 ← 英文路由组(URL 无前缀)
│   ├── layout.tsx          ← LocaleShell(defaultMessages=en)
│   ├── page.tsx            → /
│   ├── about/page.tsx      → /about
│   └── gameplay/page.tsx   → /gameplay
└── [locale]/               ← 多语言路由组(URL 有前缀)
    ├── layout.tsx          ← LocaleShell(defaultMessages=对应语言)
    ├── page.tsx            → /zh, /ja, /es ...
    ├── about/page.tsx      → /zh/about
    └── gameplay/page.tsx   → /zh/gameplay

关键点:

  • (root) 是 Next.js 的路由组语法,不会出现在 URL 中(root)/about/page.tsx 对应 /about,不是 /(root)/about
  • 两个路由组互斥------一个 URL 要么匹配 (root)(英文),要么匹配 [locale](其他语言),永远不会同时匹配
  • 英文没有前缀(/about),其他语言带前缀(/zh/about),干净直观。

3. 为什么不用 localePrefix: 'always'?

如果所有语言都带前缀(/en/about/zh/about),可以用中间件统一处理。但英文作为默认语言不带前缀是 SEO 最佳实践------Google 推荐 x-default 指向无前缀版本。

四、两个 LocaleShell 的设计

每个页面都有两个 page.tsx ------一个在 (root)/,一个在 [locale]/。它们共享同一个 View 组件,只是获取语言的方式不同:

javascript 复制代码
// (root)/about/page.tsx --- 英文,硬编码
export function generateMetadata() {
  return getMetadata('en');
}
export default function AboutPage() {
  setRequestLocale('en');
  return <AboutView />;
}

// [locale]/about/page.tsx --- 多语言,从 URL 读取
export async function generateMetadata({ params }) {
  const { locale } = await params;
  return getMetadata(locale);
}
export default async function AboutPage({ params }) {
  const { locale } = await params;
  setRequestLocale(locale);
  return <AboutView />;
}

代价 :9 个页面 × 2 = 18 个 page.tsx 文件。但换来的是零中间件、零重定向、每次请求只消耗 1 次 Worker 调用

五、SEO 全套配置

1. hreflang 标签

src/lib/seo.ts 中统一生成,每个页面的 generateMetadata() 返回值自动包含 alternates,Next.js 会在 <head> 中渲染对应的 <link rel="alternate"> 标签。

2. JSON-LD 结构化数据

  • 全站:WebSite + VideoGame(根布局)
  • 每个页面:BreadcrumbList
  • /faq:FAQPage
  • /release-date:Article

3. Sitemap

生成 9 语言 × N 页面 的完整 sitemap,每个 URL 包含 priorityalternates

六、缓存策略

arduino 复制代码
Cache-Control: public, s-maxage=86400, stale-while-revalidate=604800
  • s-maxage=86400:Cloudflare 边缘缓存 HTML 24 小时
  • stale-while-revalidate=604800:过期后 7 天内先返回旧内容,后台重新验证
  • 不设置 max-age:浏览器不缓存,确保语言切换即时生效

静态资源(_next/static/*、SVG、robots.txt)通过 wrangler.jsoncassets binding 直接由 Cloudflare Static Assets 提供,完全不经过 Worker,零成本。

七、部署

使用 @opennextjs/cloudflare 把 Next.js 打包成 Cloudflare Worker:

arduino 复制代码
npm run dev        # 本地开发
npm run deploy     # 部署到 Cloudflare Workers

wrangler.jsonc 关键配置:

  • main.open-next/worker.js
  • assets.directory.open-next/assets(免费,不经过 Worker)
  • compatibility_date:保持运行时更新

八、踩坑记录

1. getMessages() 返回错误语言

[locale]/layout.tsx 中使用 getMessages() 时,即使调用了 setRequestLocale('zh'),返回的仍然是英文消息。

解决:改用显式动态导入:

javascript 复制代码
const messages = (await import(`@/messages/${locale}.json`)).default;

2. 不要在根布局调用 setRequestLocale

根布局中的 setRequestLocale('en') 会污染上下文,导致 [locale] 子路由也收到 'en'

解决 :只在 (root)/[locale]/ 的 layout 中分别设置。

3. max-age vs s-maxage

如果用 max-age,浏览器会缓存 HTML,导致用户切换语言后看到的还是旧语言的页面。

解决 :只用 s-maxage,让 CDN 缓存,浏览器每次都请求 CDN。

九、总结

传统方案 本方案
语言检测 中间件检测 → 重定向 路由组天然隔离
Worker 调用 每次可能 2 次 永远 1 次
状态依赖 需要 cookie / Accept-Language 纯 URL 决定语言
文件结构 单套 page.tsx + 动态路由 双套 page.tsx(文件多但逻辑简单)

核心取舍:用文件数量换 Worker 成本和架构简洁性。对于内容站点(页面数量有限、不经常变动),这个取舍非常值得。

项目链接:The Duskbloods --- Release Date, Gameplay & Network Test Guide | Duskbloods Guide

技术栈:Next.js 15 · next-intl · Tailwind CSS · Cloudflare Workers

相关推荐
计算机魔术师1 小时前
DeepSeek、Qwen3比肩GPT-5,本地跑大模型的时代来了
前端
xcyxiner1 小时前
flutter wsl2安装记录(使用宿主机虚拟机)
前端·flutter
前端炒粉1 小时前
长会话虚拟滚动与渲染优化
前端·javascript·vue.js
花归去2 小时前
ant的a-table更改表格的高度
前端·javascript·html
sibylyue2 小时前
基于 Vben Admin 框架的 Vue 3 前端项目配置文件及常用命令
前端·javascript·vue.js
知几蜗牛2 小时前
0 后端 · 0 数据库 · 0 备案:用 AI 两天搓出的股票管理系统,开源了
前端·后端·llm
雪芽蓝域zzs3 小时前
Element‑Plus icon 图标名称查询 & 和菜单 meta.icon 字段映射
前端
01_ice3 小时前
前端学习css
前端·css·学习
愚公搬代码4 小时前
【愚公系列】《Web应用安全》010-Repeater模块的使用
前端·安全