
基于 Next.js + ContentLayer + MDX 构建无后端博客系统
引言在构建个人博客或技术文档时,无后端方案因其简单、高效和低成本而备受青睐。本文将介绍如何使用 Next.js、ContentLayer 和 MDX 构建一个无后端博客系统(本博客当前采用的方案),并探讨如何后续迁移到 MDX-Bundler 和 GitHub API,利用 GitHub Issues 作为文章存储源。技术栈简介Next.js:React 框架,支持 SSR、SSG 和 API 路由,适合构建高性能博客。ContentLayer:将 Markdown 或 MDX 文件转换为类型安全的 JSON 数据,方便在 Next.js 中使用。MDX:支持在 Markdown 中嵌入 React 组件,增强文章的表现力。MDX-Bundler:将 MDX 文件编译为 React 组件,支持动态加载和自定义组件。GitHub API:通过 GitHub Issues 存储文章内容,实现无后端数据管理。使用 Next.js + ContentLayer + MDX 构建博客系统初始化项目npx create-next-app@latest my-blog cd my-blog npm install contentlayer @mdx-js/loader配置 ContentLayer在项目根目录创建 contentlayer.config.ts 文件:import { defineDocumentType, makeSource } from "contentlayer/source-files"; export const Post = defineDocumentType(() => ({ name: "Post", filePathPattern: `**/*.mdx`, fields: { title: { type: "string", required: true }, date: { type: "date", required: true }, }, computedFields: { slug: { type: "string", resolve: (post) => post._raw.flattenedPath, }, }, })); export default makeSource({ contentDirPath: "data/posts", documentTypes: [Post], });创建文章在 data/posts 目录下创建 Markdown 或 MDX 文件:--- title: "Hello, Next.js!" date: 2023-10-01 --- This is a blog post written in **MDX**.实现文章列表页在 src/app/page.tsx 中实现文章列表:import { allPosts } from "contentlayer/generated"; import Link from "next/link"; export default function Home() { return ( <div> <h1>Blog Posts</h1> <ul> {allPosts.map((post) => ( <li key={post.slug}> <Link href={`/posts/${post.slug}`}>{post.title}</Link> </li> ))} </ul> </div> ); }创建 mdx 渲染组件在 src/components/mdx.tsx 中实现 mdx 渲染组件:import { useMDXComponent } from "next-contentlayer/hooks"; export interface MdxProps { code: string; } export default function Mdx({ code }: MdxProps) { const Component = useMDXComponent(code); return <Component />; }实现文章详情页在 src/app/posts/[slug].tsx 中实现文章详情页:import Mdx from "@/src/components/mdx"; import { allPosts } from "contentlayer/generated"; interface IProps { params: { slug: string[] }; } export default function Post({ params }: IProps) { const slug = decodeURIComponent(params.slug.join("/")); const postIdx = allPosts.findIndex((p) => p.slug === slug); const post = allPosts[postIdx]; if (!post) return notFound(); return ( <div> <h1>{post.title}</h1> <div> <Mdx code={post.body.code} /> </div> </div> ); }迁移到 MDX-Bundler + GitHub API安装 MDX-Bundlernpm install mdx-bundler根据 repo 和 name 获取 issuesconst REPO = "your repo"; const NAME = "your name"; export const fetchGithubIssueList = (type: "page" | "post", current: number) => fetch( `https://api.github.com/search/issues?q=repo:${REPO}+state:open+author:${NAME}+${encodeURIComponent( `[${type}]` )}+in:title&per_page=10&sort=updated&page=${current}` ).then((res) => res.json()); export const fetchGithubIssueDetail = (id: number) => fetch(`https://api.github.com/repos/${REPO}/issues/${id}`).then((res) => res.json() );使用 MDX-Bundler 编译文章import { bundleMDX } from "mdx-bundler"; export const parseMDX = (content: string) => bundleMDX({ source: content, esbuildOptions: (opts) => { opts.target = "es2020"; return opts; }, });改写文章列表页面import { fetchGithubIssueList } from "@/src/utils/github"; import Link from "next/link"; export default async function Home() { const allPosts = await fetchGithubIssueList("post", 1); return ( <div> <h1>Blog Posts</h1> <ul> {allPosts.items.map((post) => ( <li key={post.id}> <Link href={`/posts/${post.number}`}>{post.title}</Link> </li> ))} </ul> </div> ); }改写文章详情页面import { parseMDX } from "@/src/utils/parse-mdx"; import { fetchGithubIssueDetail } from "@/src/utils/github"; interface IProps { params: { slug: string }; } export default async function Post({ params }: IProps) { const post = await fetchGithubIssueDetail(params.slug); if (post.status === "404") return notFound(); const { code } = await parseMDX(post.body); return ( <div> <h1>{post.title}</h1> <div> <Mdx code={code} /> </div> </div> ); }总结通过 Next.js + ContentLayer + MDX,我们可以快速构建一个无后端博客系统。而迁移到 MDX-Bundler + GitHub API 后,我们可以利用 GitHub Issues 作为文章存储源,实现更灵活的内容管理。这种方案不仅简单高效,还能充分利用现有的工具和平台,适合个人开发者和小型团队。

手写题盘点
广度遍历深克隆const getEmpty = (o) => { if (Array.isArray(o)) return []; if (o !== null && typeof o === "object") return {}; return o; }; const cloneBfs = (a) => { const root = getEmpty(a), queue = [{ origin: a, copy: root }], map = new Map(); while (queue.length) { const { origin, copy } = queue.shift(); for (let i in origin) { const empty = getEmpty(origin[i]); if (empty === origin[i]) { copy[i] = origin[i]; } else { if (map.has(origin[i])) { copy[i] = map.get(origin[i]); continue; } copy[i] = empty; queue.push({ origin: origin[i], copy: copy[i] }); map.set(origin[i], empty); } } } return root; };限制 promise 并发数量const pLimit = (limit) => { let count = 0; const task = []; return async (fn) => { if (count >= limit) { await new Promise((r) => task.push(r)); } count++; try { const ret = await fn(); return ret; } finally { count--; if (task.length) { task.shift()(); } } }; };

分享一些前端好用的库和网站
| 网址 | 简介 | | ----------------------------------------------------- | --------------------------------------------------------------------- | | React Bits | 最大且最具创意的动画 React 组件库 | | react-spring | react-spring 是一个跨平台的 spring-physics first 动画库 | | uiverse | 使用 CSS 或 Tailwind 制作的开源组件合集 | | Whirl | 轻松搞定 CSS 加载动画 | | css-loaders | CSS 加载动画合集 | | free-icons | 免费开源 icon 合集 | | Lucide | 美丽&一致的图标 React Icon 组件 | | iconify | 集成所有流行的图标集的框架 | | jotai | 一个采用原子方法进行全局 React 状态管理库 | | Zustand | 一个小型、快速且可扩展的 React 状态管理库 | | shadcn/ui | shadcn/ui 是一组设计精美、可访问的组件和一个代码分发平台 | | css-pattern | CSS 渐变制作背景图合集 | | css-generators | 广泛的 CSS 生成器 | | qr-code | 一个基于 SVG 的无框架、无依赖项、可定制、可制作动画的自定义 html 元素 | | 21st | 一个 ai 生成的基于 shadcn/ui 的组件库&模版 | | Best Of Js | 查找与前端相关的最佳开源项目的地方 | | Logo.surf | 通过文本生成图标的网站 |