我的博客网站使用的是 Nuxt 的框架。由于支持了多语言,原本比较简单的问题,在多语言的前提下会变得复杂一些。比如:文件的管理。
Nuxt Content 与 i18n 多语言框架结合时,官方推荐的目录管理结构是:
content/
en/
index.md
about.md
blog/
post-1.md
zh-CN/
index.md
about.md
blog/
post-1.md
通过目录结构来分别管理语言的内容。这种结构的优点很明显:
- 结构目录清晰
- 天然的跟 i18n 的 route 生成是对应的,切换语言时,url 跟文件路径是关联的。比如
content/en/blog/post-1.md生成的 url 就会是/en/blog/post-1。
但是!在我使用了半年后,这种结构有很明显的弊端:
- 不同语言的文件分散到了不同的文件夹,导致我要修改某篇文章的时候,需要同时去两个文件夹下找文件
- 对于文章的基础信息,比如时间、分类、标签,没有提取成公共信息,导致修改的时候会有重复工作。有时候会导致中文改了分类,英文忘记了的情况
上面两点在日常内容管理时非常的不方便。所以我想改成这样的结构:
content/blog
post-a/
index.en.md
index.zh-CN.md
meta.json
post-b/
index.en.md
index.zh-CN.md
meta.json
不按照语言分类,而是按照内容进行聚合。对于一篇文章,它所有的语言文件放在一起,然后要把基础信息抽离出来做成公共部分。这样能够更方便的定位到文件,同时修改基础信息时也不会遗漏。
方向定好后,我就开始琢磨如何实现:
“文件目录直接改问题不大,最关键的路由的处理”
“新的结构按照现有的路由机制,会生成 /blog/post-a/index.en 这样的路由,语言标识放到末尾了,明显不是我想要的”
“所以关键的关键,就是要重新修改路由生成的机制”
于是我开始吭哧吭哧跟 AI 聊,看看怎么修改路由机制。
AI 非常的认真、敬业,绞尽脑汁给我出主意:一会儿建议我根据 nuxt 框架 router 劫持做调整,一会儿又觉得应该从 i18n 路由生成机制的源头处理,过一会儿又推荐我重点调整 nuxt content 的配置。
经过了好几轮的修改后,AI 开始说车轱辘话,我意识到不对劲了。我现在警惕性比之前高很多了。于是我立马放弃 AI,开始自行去搜索如何自定义 url,去找 i18n 的目录管理。果不其然,有其他人遇到过一样的问题,甚至还贴心的给出了代码。但是,就是没有路由的调整。只有目录结构,以及 slug 文件的写法。
pages/blog/[...slug].vue 这种写法是表示页面渲染的时候 /blog 路径下的页面都会匹配到这个文件进行处理。
为什么都没有路由配置的说明?只有 slug 文件的逻辑,只有 slug,slug...... slug?!
哦嚯,我悟了!所以根本可以不用管什么路由生成的机制。只需要做两点处理就好了:
- 显示在页面的超链接,从
/blog/post-a/index.en替换成/en/blog/post-a - 在
[...slug].vue文件中,解析文件的时候反过来解析,把拿到的/en/blog/post-aurl 解析为文件地址/blog/post-a/index.en再去获取文件就好拉!
所以压根一开始我处理的方向就偏了,修改路由生成规则目前应该是没有很好的办法的。但是通过修改页面显示和内容获取的部分,同样可以做到。
具体的代码实现
前面其实漏了一点没说明,公共信息如何处理?在 content.config.ts 中抽离出新的一层结构定义:
collections: {
article: defineCollection(
asSitemapCollection({
type: "page",
source: "blog/*/index.{en,zh-CN}.md",
schema: z.object({
// xxxx
}),
} as any),
),
// 新增一个数据合集,到时候代码里通过文件路径进行匹配找到同一个文章下的公共信息
articleMetadata: defineCollection({
type: "data",
source: "blog/*/meta.json",
schema: z.object({
// xxxxx
}),
}),
}
在 pages/blog/[...slug].vue 中,修改获取文件信息的方法,这里只列关键代码:
const { data: post } = await useAsyncData(
`page-${normalizedPath.value}-${locale.value}`,
async () => {
const slug = currentSlug.value; // 从 url 上 /en/blog/post-a 获取到 post-a 这个唯一标识
const localeValue = locale.value as "en" | "zh-CN";
if (!slug) return null;
// 重新拼接成原始文件的地址,如 /blog/post-a/index.en
const contentStem = buildArticleContentStem(slug, localeValue);
const metadataStem = buildArticleMetadataStem(slug);
const [articleMetadata, articleFile] = await Promise.all([
queryCollection("articleMetadata")
.where("stem", "=", metadataStem)
.first(),
queryCollection("article").where("stem", "=", contentStem).first(),
]);
if (!articleFile) return null;
// 将两边的数据融合在一起,最终提供出去渲染
return combineArticleData(articleMetadata || {}, articleFile, localeValue);
},
);
url 唯一需要处理的,应该就是 sitemap 的生成了(如果你安装了 sitemap 模块的话)。在 nuxt.config.ts 中添加:
sitemap: {
// 排除掉不需要在 sitemap 出现的原始路径(例如带语言后缀的)
exclude: [
"/zh-CN/**", // 我默认 /blog/post-a 就是中文,默认语言不加前缀
new RegExp(".*/index.en/?$"),
new RegExp(".*/index.zh-cn/?$"),
],
},
回过头看,这次重构最值得记下来的,不是那几行代码,而是那个“哦嚯”的瞬间 —— 当你在一条路上走不通的时候,不一定要硬刚,换个角度问自己:“一定要在这里解决吗?”往往答案就藏在别处。
这次把目录从按语言分改成按内容聚,文章管理顺手多了。如果你也在折腾 Nuxt Content + i18n,希望这篇能帮你少绕几个弯。