MDX是一种将Markdown和JSX结合的语法,可以在Markdown中使用JSX,一直想独立编写一个博客平台,这次就用nextjs+MDX来写一个博客平台吧。
使用Next.js开发博客平台的主要步骤
使用Next.js创建项目
使用
pnpm create next-app
命令可以快速创建一个Next.js项目。设计目录结构
使用Next.js提供的app目录结构。
实现Markdown解析
可以使用
next-mdx-remote
等MDX解析库,将MDX文件解析为ReactNode。静态页面生成
使用
generateStaticParams
实现文章静态页生成。添加样式
可以使用SASS或tailwindcss给博客及mdx元素添加样式。
部署上线
可以部署到Vercel、Netlify等平台上。
Next.js的静态生成和路由功能可以方便实现一个简单的博客平台。
项目初始化
1 | pnpm create next-app |
默认创建的项目是nextjs 13.4版本(2023-7-20),是最新使用app router
的版本,相较于之前page router
有些改动。
目录结构
由于我的文章文件名都是中文的,不利于网页的SEO,所以我为每一个文章通过文件名生成唯一id,使用id作为文章的路由。
目录结构大致是:
1 | app: |
src:
1 | ├─assets |
实现Markdown解析
实现了两个函数,获取所有meta数据和获取文章内容。
1 | const rootDirectory = path.join(process.cwd(), 'src', 'posts') |
关于生成文章唯一id,我使用了crc32算法,这个算法的特点是速度快,生成的id短,但是会有冲突,不过我觉得这个冲突概率很小,而且我也不会写那么多文章,所以就这样吧。
- 对于文章列表,直接使用
getAllPostsMeta
获取所有文章的meta数据,然后渲染列表即可。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55interface PostProps {
id: number | string
title: string
desc: string
tags: string[]
date: string
}
const Post = ({ ...props }: PostProps) => {
const { title, desc: content, tags, date: time, id } = props
return (
<div className="post-card">
<Link href={`/post/${id}`}>
<div className="post-title">
{title}
</div>
</Link>
<div className="post-content">
{content}
</div>
<div className="post-footer">
<TimeBar time={time} />
<div className="post-footer__tag">
{
tags.length <= 0 && <Tag val={'未归档'} />
}
{
tags.map((tag, idx) => {
if (idx > 3) return null;
return <Tag key={tag} val={tag} />
})
}
{
tags.length > 3 && <Tag isMore />
}
</div>
</div>
</div>
)
}
export default async function PagePostList() {
const posts = await getPostList()
return (
<div className="post-list">
{
posts.map(post => {
return <Post key={post.id} {...post} />
})
}
</div>
)
} - 对于文章详情页,通过id获取文章内容,将内容传入
MDXRemote
组件中,然后渲染即可。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27import { MDXRemote } from 'next-mdx-remote/rsc'
export default async function Post({
params,
}: {
params: { slug: string };
}) {
try {
const postPayload = await getPost(params.slug)
return <>
<div className="post-detail">
{/* ... */}
{/* 文章内容 */}
<div className="markdown-body w-full break-words whitespace-normal ">
<Suspense fallback={<>Loading...</>}>
{/* @ts-ignore */}
<MDXRemote source={postPayload.content} options={{ parseFrontmatter: true, }} components={components} />
</Suspense>
</div>
{/* ... */}
</div>
</>
} catch (e) {
return notFound()
}
}
静态页面生成
在post/[slug]/page.tsx
中,通过导出generateStaticParams
函数,可以实现静态页面生成(app router)。
在app router中,生成静态页面路径的函数已经由原来的getStaticPaths
改为generateStaticParams
1 | export async function generateStaticParams() { |
添加样式
mdx文件中的元素,可以通过components
属性传入<MDXRemote />
组件,然后在components
对象中添加样式。
1 | const components = { |
总结
app router用起来感觉别扭,比如文章生成那块,默认的MDXRemote组件并不是RSC(react-server-component)
,导致外面引用该组件的组件必须使用’use client’,因为nextjs在服务端渲染不了useState函数,这样就导致了组件的复用性变差,不过这个问题可以通过自定义MDXRemote组件解决,官方示例有提供方法,这样就不如page router方便了。
另外,使用motion添加动画效果的时候,必须添加’use client’,这样就导致了动画效果在服务端渲染的时候不会生效,这个问题我还没想到解决方法。