- Published on
Contentlayer + Markdown 渲染
- Authors
- Name
- Shelton Ma
Contentlayer + Markdown 渲染
1. 安装
pnpm add contentlayer next-contentlayer2
pnpm add date-fns
pnpm add remark-gfm rehype-slug rehype-pretty-code
2. 目录结构
├── components.json
├── contentlayer.config.ts
├── data
│ └── blog
│ ├── en
│ │ └── hi.mdx
│ └── zh
│ │ └── hi_zh.mdx
├── src
│ ├── app
│ │ ├── [locale]
│ │ │ ├── blog
│ │ │ │ ├── [slug]
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── favicon.ico
│ │ └── globals.css
创建
contentlayer.config.ts
import { defineDocumentType, makeSource } from "contentlayer/source-files"; import rehypePrettyCode from "rehype-pretty-code"; import rehypeSlug from "rehype-slug"; import remarkGfm from "remark-gfm"; export const Blog = defineDocumentType(() => ({ name: "Blog", filePathPattern: `blog/**/*.mdx`, contentType: "mdx", fields: { title: { type: "string", required: true }, date: { type: "date", required: true }, description: { type: "string", required: false }, slug: { type: "string", required: true, }, published: { type: "boolean", default: true }, tags: { type: "list", of: { type: "string" }, required: false }, }, computedFields: { url: { type: "string", resolve: (doc) => `/blog/${doc.slug}`, }, }, })); export default makeSource({ contentDirPath: "content", documentTypes: [Blog], mdx: { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeSlug, rehypePrettyCode], }, });
配置
tsconfig.json
, 为了导入allBlogs
{ "compilerOptions": { ... "paths": { ... "contentlayer/generated": ["./.contentlayer/generated"] }, }, ... "include": [ ... ".contentlayer/generated", ".contentlayer/generated/**/*.json", ... ], ... }
创建列表页
src/app/[locale]/blog/page.tsx
import { allBlogs } from "contentlayer/generated"; import { compareDesc } from "date-fns"; import Link from "next/link"; export default function BlogListPage() { const posts = allBlogs .filter((p) => p.published) .sort((a, b) => compareDesc(new Date(a.date), new Date(b.date))); return ( <main className="max-w-3xl mx-auto py-16 px-4"> <h1 className="text-4xl font-bold mb-8">Blog</h1> <ul className="space-y-8"> {posts.map((post) => ( <li key={post.slug}> <Link href={post.url} className="block group"> <h2 className="text-2xl font-semibold group-hover:underline"> {post.title} </h2> <p className="text-sm text-muted-foreground">{post.date}</p> {post.description && ( <p className="mt-1 text-muted-foreground">{post.description}</p> )} </Link> </li> ))} </ul> </main> ); }
创建详情页
src/app/[locale]/blog/[slug]/page.tsx
import { allBlogs } from "contentlayer/generated"; import { format } from "date-fns"; import { useMDXComponent } from "next-contentlayer2/hooks"; import { notFound } from "next/navigation"; type Params = { params: { slug: string }; }; export default function BlogDetailPage({ params }: Params) { const post = allBlogs.find((p) => p.slug === params.slug); if (!post) notFound(); const MDXContent = useMDXComponent(post.body.code); return ( <article className="max-w-3xl mx-auto py-16 px-4"> <h1 className="text-4xl font-bold mb-2">{post.title}</h1> <p className="text-sm text-muted-foreground mb-8"> {format(new Date(post.date), "yyyy-MM-dd")} </p> <div className="prose dark:prose-invert"> <MDXContent /> </div> </article> ); }
配置
next.config.ts
import type { NextConfig } from "next"; import { withContentlayer } from "next-contentlayer"; import createNextIntlPlugin from "next-intl/plugin"; const nextConfig: NextConfig = {}; const withNextIntl = createNextIntlPlugin(); export default withNextIntl(withContentlayer(nextConfig));
3. 内容添加
在根目录创建 content/blog/
目录,并添加一个 Markdown 文件 first-post.mdx
, 根目录参考 contentlayer.config.ts的配置(contentDirPath: "content",)