Nuxt Content 多语言文件管理,我换了一种组织方式

我的博客网站使用的是 Nuxt 的框架。由于支持了多语言,原本比较简单的问题,在多语言的前提下会变得复杂一些。比如:文件的管理。

Nuxt Content 与 i18n 多语言框架结合时,官方推荐的目录管理结构是:

content/
  en/
    index.md
    about.md
    blog/
      post-1.md
  zh-CN/
    index.md
    about.md
    blog/
      post-1.md

通过目录结构来分别管理语言的内容。这种结构的优点很明显:

  1. 结构目录清晰
  2. 天然的跟 i18n 的 route 生成是对应的,切换语言时,url 跟文件路径是关联的。比如 content/en/blog/post-1.md 生成的 url 就会是 /en/blog/post-1

但是!在我使用了半年后,这种结构有很明显的弊端:

  1. 不同语言的文件分散到了不同的文件夹,导致我要修改某篇文章的时候,需要同时去两个文件夹下找文件
  2. 对于文章的基础信息,比如时间、分类、标签,没有提取成公共信息,导致修改的时候会有重复工作。有时候会导致中文改了分类,英文忘记了的情况

上面两点在日常内容管理时非常的不方便。所以我想改成这样的结构:

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?!

哦嚯,我悟了!所以根本可以不用管什么路由生成的机制。只需要做两点处理就好了:

  1. 显示在页面的超链接,从 /blog/post-a/index.en 替换成 /en/blog/post-a
  2. [...slug].vue 文件中,解析文件的时候反过来解析,把拿到的 /en/blog/post-a url 解析为文件地址 /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,希望这篇能帮你少绕几个弯。