- 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", // 标记语言为德语
},
},
}));