Published on

如何使用 Contentlayer 和 Next.js, Tailwindcss 实现i18n多语言支持

引言

Contentlayer 是一款内容 SDK,它能验证并将 markdown, mdx 内容转换为类型安全的 JSON 数据,轻松导入应用程序中。 本文将介绍如何使用 Contentlayer 和 Next.js 实现 i18n 多语言支持。以及过程中遇到的问题和解决方案。

实现

基本的安装及配置本文不做介绍,请参考Contentlayer 官方文档

1. 配置 Contentlayer

首先需要了解contentlayer.config.ts配置文件。这里会定义文档类型,文档类型对应的文件路径, 文档类型对应的字段,以及文档类型对应的计算字段。

文档类型对应的fields字段和计算字段computedFields会在后续的React组件代码中使用,可以理解为文档的属性字段。 contentType字段默认是markdown,这里我们改为markdown,因为我们的内容是 markdown 格式。

// contentlayer.config.ts
import { defineDocumentType, makeSource } from 'contentlayer/source-files'

export const Post = defineDocumentType(() => ({
  name: 'Post',
  contentType: "markdown", // 文档类型
  filePathPattern: `**/*.md`, // 文件路径
  fields: {
    title: { type: 'string', required: true },
    date: { type: 'date', required: true },
  },
  computedFields: {
    url: { type: 'string', resolve: (post) => `/posts/${post._raw.flattenedPath}` },
  },
}))

export default makeSource({ contentDirPath: 'posts', documentTypes: [Post] })

踩坑:

contentType字段如果设置成mdx,在某些Nextjs版本中获取post.body.code并渲染时,会报错。可能是由于 React 版本不兼容导致的。

修复措施为将contentType字段设置为markdown。直接使用 post.body.html 渲染。文章渲染的样式问题,可以在tailwind.config.js中添加插件的配置require('@tailwindcss/typography')。 插件内置了丰富的样式,可以满足大部分的文章样式需求。还可以使用className属性自定义样式,使用 prose 覆盖插件的默认样式。

export function BlogContent({ html, className }: BlogContentProps) {
  return (
    <article
      className={cn(
        // 基础排版样式
        "prose dark:prose-invert max-w-none",
        // 标题样式
        "prose-headings:scroll-mt-20 prose-headings:font-bold prose-headings:tracking-tight",
        "prose-h2:text-2xl prose-h2:mt-10 prose-h2:mb-4 prose-h2:mt-0",
        "prose-h3:text-xl prose-h3:mt-8 prose-h3:mb-4",
        ...,
        // 自定义类名
        className
      )}
      dangerouslySetInnerHTML={{ __html: html }}
    />
  );
}

2. Contentlayer 的 i18n 支持

我的项目中多语言有不同的语言子路径,如/blog/post/de/blog/post。所以直接在contentlayer.config.ts中配置多个Post文档,filePathPattern根据语言来区分路径,并根据lang字段来区分不同的语言。

const Post = defineDocumentType(() => ({
  name: "Post",
  filePathPattern: `/en/**/*.mdx`,
  // contentType: "mdx",
  contentType: "markdown",
  fields: {
    type: { type: "string", required: true },
    title: { type: "string", required: true },
    date: { type: "date", required: true },
    summary: { type: "string", required: true },
  },
  computedFields: {
    ...computedFields,
    url: {
      type: "string",
      resolve: (post) => {
        console.log("post._raw:", post._raw);
        // return `/blog/${post._raw.flattenedPath}`;
        return `/blog/${post._raw.sourceFileName.replace(".mdx", "")}`;
      },
    },
    lang: {
      type: "string",
      resolve: () => "en", // 标记语言为英文
    },
  },
}));

const DePost = defineDocumentType(() => ({
  name: "DePost",
  filePathPattern: `/de/**/*.mdx`,
  // contentDirPath: "src/posts/de",
  // contentType: "mdx",
  contentType: "markdown",
  fields: {
    type: { type: "string", required: true },
    title: { type: "string", required: true },
    date: { type: "date", required: true },
    summary: { type: "string", required: true },
  },
  computedFields: {
    ...computedFields,
    url: {
      type: "string",
      // resolve: (post) => `/blog/${post._raw.flattenedPath}`,
      resolve: (post) =>
        `/blog/${post._raw.sourceFileName.replace(".mdx", "")}`,
    },
    lang: {
      type: "string",
      resolve: () => "de", // 标记语言为德语
    },
  },
}));