Nextjs 引入 contentlayer 支持 MDX 文件渲染

如今大多静态文档网站生成技术都支持 mdx 格式文件的渲染。这得益于像 contentlayer 这样的库的支持,将非结构化内容转换为类型安全的 json 数据结构。

静态站点生成技术:built static site

这里要介绍的一种解决方案是借助 contentlayer 这个工具库。

安装

安装 contentlayer

在 Next 项目中,你需要额外安装 next-contentlayer,它提供了对 contentlayer 接口的封装以便对 Nextjs 框架的支持。

bash 复制代码
npm install contentlayer next-contentlayer --save

使用 withContentLayer 函数对 Next 配置进行包装

next.config.js 文件中导入 withContentLayer 函数并使用它对 Next 配置进行包装。

js 复制代码
import { withContentLayer } from "next-contentlayer";
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
};
 
module.exports = withContentlayer(nextConfig);

添加编译选项以及生成路径

tsconfig.json 文件中添加以下配置,以满足对生成目录的访问。

js 复制代码
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "contentlayer/generated": ["./.contentlayer/generated"]
    }
  },
  "include": ["next-env.d.ts", "**/*.tsx", "**/*.ts", ".contentlayer/generated"]
}

定义结构

首先在项目根目录下创建了一个 /content 文件夹。然后在 /content 文件夹下,创建了两个文件夹 /definitions/posts

  • definitions:放置对数据结构的定义文件
  • posts:放置以 mdx 格式撰写的文章
css 复制代码
content
├── definitions
└── posts

这里以 Post 为例,我们在 /definitions 目录下新建 post.tsx,用它来定义单篇文章的内容结构:

jsx 复制代码
import { defineDocumentType } from "contentlayer/source-files";
 
export const Post = defineDocumentType(() => ({
  name: "Post",
  filePathPattern: "posts/*.mdx",
  contentType: "mdx",
  fields: {
    title: { type: "string", required: true },
    publishedAt: { type: "string", required: true },
    description: { type: "string" },
    status: {
      type: "enum",
      options: ["draft", "published"],
      required: true,
    },
  },
}));

还有另外两个模型需要定义:TagSeriesTag 将用于给文章定义标签,而 Series 定义与某篇文章相关的文章,用于推荐。

jsx 复制代码
import { defineNestedType } from "contentlayer/source-files";
 
// define tags elsewhere (in a constants file)
import { tagNames, tagSlugs } from "../../lib/contentlayer";
 
export const Tag = defineNestedType(() => ({
  name: "Tag",
  fields: {
    title: {
      type: "enum",
      required: true,
      options: tagNames,
    },
    slug: {
      type: "enum",
      required: true,
      options: tagSlugs,
    },
  },
}));
jsx 复制代码
import { defineNestedType } from "contentlayer/source-files";
 
export const Series = defineNestedType(() => ({
  name: "Series",
  fields: {
    title: {
      type: "string",
      required: true,
    },
    order: {
      type: "number",
      required: true,
    },
  },
}));

定义好 SeriesTag 之后,就可以把它们导入到 Post 中使用它们来完成字段的定义。

jsx 复制代码
import { defineDocumentType } from "contentlayer/source-files";
 
import { Tag } from "./tag";
import { Series } from "./series";
 
export const Post = defineDocumentType(() => ({
  // ...
  fields: {
    title: { type: "string", required: true },
    publishedAt: { type: "string", required: true },
    description: { type: "string" },
    status: {
      type: "enum",
      options: ["draft", "published"],
      required: true,
    },
    series: {
      type: "nested",
      required: false,
      of: Series,
    },
    tags: {
      type: "list",
      required: false,
      of: Tag,
    },
  },
}));

配置文件

现在,我们需要将定义好的模型提供给 Contentlayer ,在项目根目录下新建 contentlayer.config.js 文件。

js 复制代码
import { makeSource } from "contentlayer/source-files";
 
import { Post } from "./content/defintions/post";
 
export default makeSource({
  contentDirPath: "content",
  documentTypes: [Post],
  mdx: {
    esbuildOptions(options) {
      options.target = "esnext";
      return options;
    },
    remarkPlugins: [],
    rehypePlugins: [],
  },
});

插件

请注意上面 contentlayer.config.js 文件中的 remarkPluginsrehypePlugins 两个字段,Contentlayer 非常强大,我们可以在内容生成的过程中 使用各种插件。

Github Flavored Markdown

Github Flavored Markdown 是 Github 在处理 Md 文件时使用的工具其中之一。我们可以使用 remark-gfm 来启用它。

bash 复制代码
npm install remark-gfm

然后把它引入 contentlayer.config.js 配置文件中

js 复制代码
import { makeSource } from "contentlayer/source-files";
import { Post } from "./content/defintions/post";
import remarkGfm from "remark-gfm";
 
export default makeSource({
  // ...
  mdx: {
    // ...
    remarkPlugins: [[remarkGfm]],
    rehypePlugins: [],
  },
});

处理标题文章链接

给标题文本添加上超链接便签,便于点击跳转

bash 复制代码
npm install rehype-autolink-headings github-slugger rehype-slug
js 复制代码
import { makeSource } from "contentlayer/source-files";
import { Post } from "./content/defintions/post";
import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-heading";
 
export default makeSource({
  // ...
  mdx: {
    // ...
    remarkPlugins: [[remarkGfm]],
    rehypePlugins: [
      [rehypeSlug],
      [
        rehypeAutolinkHeadings,
        {
          behavior: "wrap",
          properties: {
            className: ["<insert class names here>"],
          },
        },
      ],
    ],
  },
});

最后你需要到 Post 模型中,从内容中获取文章的链接

jsx 复制代码
import { defineDocumentType } from "contentlayer/source-files";
import { Tag } from "./tag";
import { Series } from "./series";
import GithubSlugger from "github-slugger";
 
export const Post = defineDocumentType(() => ({
  // ...
  computedFields: {
    headings: {
      type: "json",
      resolve: async (doc) => {
        const slugger = new GithubSlugger();
 
        // https://stackoverflow.com/a/70802303
        const regex = /\n\n(?<flag>#{1,6})\s+(?<content>.+)/g;
 
        const headings = Array.from(doc.body.raw.matchAll(regex)).map(
          // @ts-ignore
          ({ groups }) => {
            const flag = groups?.flag;
            const content = groups?.content;
            return {
              heading: flag?.length,
              text: content,
              slug: content ? slugger.slug(content) : undefined,
            };
          }
        );
 
        return headings;
      },
    },
  },
}));

Slug

为了在 Next.js 中给每篇文章渲染一个页面,需要生成一个对应的 slug,这时在 Post 模型中添加另一个 computedField 字段。

jsx 复制代码
import { defineDocumentType } from "contentlayer/source-files";
import { Tag } from "./tag";
import { Series } from "./series";
import GithubSlugger from "github-slugger";
 
export const Post = defineDocumentType(() => ({
  // ...
  computedFields: {
    // ...
    slug: {
      type: "string",
      resolve: (doc) => doc._raw.sourceFileName.replace(/\.mdx$/, ""),
    },
  },
}));

渲染内容

我们可以使用 Next.js 中的动态路由来渲染文章页面,在 /pages 目录下,我们创建 post 目录和 [slug].tsx 文件。用大括号包起来表示它是一个参数。

然后我们可以直接从 contentlayer/generated 中获取由 contentlayer 生成的一个 json 格式的包含所有文章的一个数组。

js 复制代码
import { allPosts } from "contentlayer/generated";

通过这个导入,您可以获取所有生成的文章,并根据需要进行渲染。这里为了便于直接从页面中获取,把它放在页面的 getStaticProps 方法中。

jsx 复制代码
import { allPosts } from "contentlayer/generated";
import { compareDesc } from "date-fns";
 
export async function getStaticProps() {
  const posts = allPosts
    .sort((a, b) => {
      return compareDesc(new Date(a.publishedAt), new Date(b.publishedAt));
    })
    .filter((p) => p.status === "published");
 
  return { props: { posts: posts } };
}
相关推荐
10年前端老司机1 小时前
什么!纯前端也能识别图片中的文案、还支持100多个国家的语言
前端·javascript·vue.js
摸鱼仙人~1 小时前
React 性能优化实战指南:从理论到实践的完整攻略
前端·react.js·性能优化
程序员阿超的博客2 小时前
React动态渲染:如何用map循环渲染一个列表(List)
前端·react.js·前端框架
magic 2452 小时前
模拟 AJAX 提交 form 表单及请求头设置详解
前端·javascript·ajax
小小小小宇7 小时前
前端 Service Worker
前端
只喜欢赚钱的棉花没有糖8 小时前
http的缓存问题
前端·javascript·http
小小小小宇8 小时前
请求竞态问题统一封装
前端
loriloy8 小时前
前端资源帖
前端
源码超级联盟8 小时前
display的block和inline-block有什么区别
前端
GISer_Jing8 小时前
前端构建工具(Webpack\Vite\esbuild\Rspack)拆包能力深度解析
前端·webpack·node.js