我如何用 MDX 和 Next.js 构建完全自定义博客
Source: Dev.to
小小的背景故事
我一直想把我觉得有趣的东西——我学到的东西、让我好奇的话题、经验教训、教育内容,或者只是一些搞笑的随机事物——发布出来。我对自己说,如果真的要做的话,就在 我的个人博客 上发布。
为什么是个人博客?
目前的平台在自定义和功能方面受到很大限制。有些平台甚至需要订阅才能使用某些功能(甚至阅读文章)。我想要完全掌控内容的呈现方式:设计、布局、字体、配色方案等。
Source: …
日常工作
我最近大学毕业,幸运地在毕业后直接获得了工作机会。工作几个月后,我必须适应岗位的高强度要求,尤其是在试用期结束后,需要了解系统中极其复杂的部分。
这让我把写博客的想法暂时搁置在脑后。现在我对自己的角色更加熟悉,能够更好地应对压力和挑战,并且对自己的职责有了更清晰的认识,我终于可以回到那个未完成的博客构想上(写博客应该不难,对吧?)。
注意: 我在工作中做了很多很酷的事,想和大家分享,但那是另一个故事,留待以后再说。
我仍未达到我对这个应用设想的“完全自定义”水平。虽然有很多想法,但因为一直拖延构建,导致我失去了方向。现在在创建了一个简易版本后,我可以重新聚焦于最初的愿景。
从学生转变为全职员工本身就值得写一篇帖子——也许以后再写。
现在,让我们稍微技术一点。
Tech Stack
该博客是一个 monorepo 的一部分,monorepo 还托管了我的作品集(是的,有点多余)。它使用 Next.js 构建,并使用 MDX 来编写博客文章。文章以 Markdown 编写,并通过 remark 编译为 React 组件。
- Styling: 使用 Tailwind CSS,并配合
@tailwindcss/typography插件来处理标题、段落、列表等。 - MDX: 系统的核心——没有它,要让博客上线运行将需要更多的工作。衷心感谢这款优秀工具的贡献者们。
- Deployment: Vercel.
数据库(或缺失)
我 没有 使用数据库来存储博客文章的元数据(绝对不是因为想省钱)。有人可能会说 SQLite 或免费托管的数据库是合适的选择,但我不想让项目变得过于复杂;我只想把它从“愿望清单”里搬出来。
于是,我编写了一个 自定义脚本,它:
- 遍历 MDX 文件。
- 使用
gray‑matter提取 front‑matter。 - 将收集到的数据保存到一个 TypeScript 文件中。
生成的文件在构建时被应用程序导入,因此不需要在运行时进行繁重的处理(比如读取文件)。数据只有在我重新构建应用时才会改变,这种做法是合理的。
生成对象的示例
// THIS FILE IS AUTO‑GENERATED BY compile‑mdx‑data SCRIPT, DO NOT EDIT
export const blogPostsObject: Record = {
"mdx-nextjs-blog-setup": {
title: "My MDX + Next.js Blog Setup",
description: "This is a description",
tags: ["nextjs", "mdx", "tailwind"],
date: "2025-11-21",
readingTime: "5 min",
},
};
该脚本在 构建脚本之前 运行。
博客主页
列出所有文章的主页使用 blogPostsObject。这确保了一致性,并消除了在多个位置手动更新文章元数据的需求。
代码片段
我使用 Expressive Code 来高亮代码块。它是一个强大的插件,能够添加语法高亮、文件标签、行号,甚至差异视图。
-
文件标签示例
// file: src/components/Button.tsx export const Button = () => Click me; -
显示行号并高亮某行
// file: server.ts const port = 3000; // and let me know what you think!
使用 withTocExport 生成目录
withTocExport 函数负责触发导出目录(TOC)数据。
import withToc from "@stefanprobst/rehype-extract-toc";
import withTocExport from "@stefanprobst/rehype-extract-toc/mdx";
const withMdx = nextMdx({
options: {
// …
rehypePlugins: [
withToc,
[withTocExport, { name: "toc" }],
],
},
});
toc 变量随后会从 MDX 导入本身中导出,我们将其传递给 Toc 组件来渲染目录树:
const { default: Post, toc } = await import(`@/app/(blog)/mdx/${slug}.mdx`);
return (
<>
{/* render Post and Toc here */}
</>
);
Post– 将作为博客文章渲染的组件。toc– 包含生成的目录(table of contents)的变量。Toc– 负责渲染目录 UI 的组件。
结束语
我对这篇博客的完成感到非常满意。虽然仍有许多工作要做,但我已经完成的部分值得花点时间欣赏其中的辛勤付出。
我对这个小项目的下一步充满期待,也希望你们阅读本文时感到愉快!如有任何错误或拼写失误,敬请见谅——我仍在学习如何撰写此类内容 😅