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 } };
}
相关推荐
94very几秒前
iframe实践
前端
用户85759414500299 分钟前
产品让你写段炫彩炫酷的字体效果,你该怎么做?回答我?
前端
南北是北北10 分钟前
Flow 热流
前端·面试
一只小风华~10 分钟前
快速搭建一个Vue+TS+Vite项目
前端·javascript·vue.js·typescript·前端框架
m0_7381207210 分钟前
CTFshow系列——命令执行web73-77(完结篇)
前端·安全·web安全·网络安全·ctfshow
呵阿咯咯12 分钟前
前端开发典型问题解决方案:打包冲突、状态更新与性能优化
前端
前端搬运侠17 分钟前
🚀 浏览器原理+网络知识面试必刷!50道高频面试题详解
前端
励扬程序17 分钟前
Cloudflare workers 构建和部署无服务器功能、站点和全栈应用程序。
前端·全栈
YUJIANYUE18 分钟前
纯前端html英文字帖图片生成器自动段落和换行
前端·html
给月亮点灯|20 分钟前
Vue基础知识-Vue中v-cloak、v-text、v-html、v-once、v-pre指令详解
前端·javascript·vue.js