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 包含 priority 和 alternates。

六、缓存策略

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.jsonc 的 assets 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

​

相关推荐
zhangzeyuaaa几秒前
深入 Ruby:Block、Proc、Lambda 核心区别与最佳实践
开发语言·前端·ruby
liangshanbo121529 分钟前
前端高级面试题:WebSocket 双向流式通信怎么设计?
前端·websocket·网络协议
可乐鸡翅yeah_37 分钟前
新手梳理:HLS 线上问题,哪些该提给 CDN,哪些该找后端切片服务
开发语言·前端·javascript·vue.js·网络协议·http·m3u8在线
zwd200537 分钟前
Manim arrange 和 arrange_in_grid 用法详解:buff、aligned_edge、rows/cols(0.21.0 实测)
前端·python·edge
小小龙学IT1 小时前
Go 语言 reflect 反射包深度解析:从 Type/Value 到三大定律与工业实践
前端·golang
小溪学编程1 小时前
C语言篇:枚举类型
java·c语言·前端
xingpanvip1 小时前
星盘接口开发文档:推算星座接口指南
前端·php
Da Da 泓1 小时前
HTML浅浅入门
前端·html
এ慕ོ冬℘゜1 小时前
es6基础
前端·javascript·es6
熊猫钓鱼>_>1 小时前
从2D平铺到3D沉浸:我用HarmonyOS 7端侧AI做了一个全程数据不出设备的空间化私密相册
前端·人工智能·3d·华为·华为云·harmonyos·鸿蒙