我的博客重构之旅
本文记录了从博客从Hexo迁移到Next.js的技术选型过程。
技术选型
Hexo用了7年,折腾了很多,越发感觉这个框架不能满足我的需求。从有重构的想法到新博客上线过了一年半。过程中了解了很多框架,最后选择了现在的技术栈。
最开始打算使用 gatsbyjs
gridsome
,但是数据源只能是graphql
,实在用不来,且框架前途不乐观。Astro
虽然支持使用其他框架,但是我都引入其他框架的runtime了还不如直接使用其他框架。而Astro
的语法我欣赏不来。VuePress
VitePress
我见过的博客都是文档样式,是只能用文档布局吗?纠结了这么多框架后我选择了Nextjs
。
最终博客的技术栈如下:
- 渲染引擎:
Nextjs
- 页面样式:
Tailwindcss
- 文章数据管理:
Velite
- MDX 编译:
@mdx-js/mdx
- 图片放大预览:
react-medium-image-zoom
- 模糊占位图:
plaiceholder
- 评论功能:
Waline
(前端部分使用solidjs
重写) - 代码高亮:
Shiki
- 数据解析:
Cheerio
数据源
最开始有想过和sukaka一样让Hexo
充当CMS的角色,但是我对Hexo
的api不熟悉,而且ts支持是个大问题。
本地文章数据管理推荐使用content-collections或Velite。
Velite
功能多一点,支持资源管理。content-collections
支持文章中使用import
export
,因为它底层使用mdx-bundler
。这会使文章数据体积增大。2个库使用方法差不多,切换也不费时间,用哪个都无所谓。
_posts --------------------- // 文章目录
├─[slug] ----------------- // 每篇文章一个文件夹
│ ├─index.md ------------- // 文章
| |-components.ts -------- // 文章中使用的组件
| |- * ------------------- // 图片等资源
以上是我的文章目录结构。由于Velite
和content-collections
都不支持远程数据。为了方便使用和渲染一致性,我没有使用Velite
自带的mdx编译函数。参照Velite
的源码自己写了一个。文章在transform中调用自己封装的函数进行编译,首页memos通过fetch拿到数据后编译。
// 封装通用的mdx编译函数
import { compile } from '@mdx-js/mdx'
import { minify } from 'terser'
export const compileMDX = async ({
source,
path,
remarkPlugins,
rehypePlugins
}: {
source: string
path?: string
remarkPlugins?: PluggableList
rehypePlugins?: PluggableList
}) => {
const code = await compile(
{ value: source, path },
{
format: 'mdx',
outputFormat: 'function-body',
remarkPlugins: [remarkGfm, remarkUnwrapImages, ...(remarkPlugins || [])],
rehypePlugins: [...(rehypePlugins || [])]
}
)
const minified = await minify(code.toString(), {
module: true,
compress: true,
keep_classnames: true,
mangle: { keep_fnames: true },
parse: { bare_returns: true }
})
return minified.code ?? code.toString()
}
// 在transform编译文章内容
const posts = defineCollection({
name: 'posts',
pattern: '**/*.md',
schema: s.object({
content: s.custom().transform(async (data, { meta }) => {
return await compileMDX({
source: meta.content || '',
path: meta.path
})
})
})
})
编译后的文章数据如何使用请参考rendering-mdx-content。
Velite
不支持在文章中使用import
通过下面的方法动态传递文章中依赖的组件,避免加载不必要的js。
// _posts/refactor-my-blog-using-nextjs/components.ts
import BgmSubject from '@/components/BgmSubject'
export { BgmSubject }
// app/posts/[slug]/page.tsx
export default async function Page({ params }: props) {
let postComponents = {}
try {
postComponents = await import(`@/_posts/${params.slug}/components`)
} catch (e) {}
/* ... */
return (
<MDXContent
className="markdown-lg"
code={post.content}
components={postComponents}
/>
)
}
首页短博客通过自己写的简陋版memos进行发布管理,数据存储在leancloud。更新数据后调用Nextjs
的revalidatetag
刷新首页数据。
bangumi页面使用bangumi的api获取数据,由于数据太多又不想弄分页,所以只展示前21条数据。二次元老婆部分因为官方api不能获取目录中收藏的角色。所以使用Cheerio
爬取数据。
为markdown赋能
我只需要在文章中添加<BgmSubject id="395378" />
即可实现如下效果,通过替换a标签,直接贴bangumi链接更是方便。
文章中渲染自定义内容这个功能在Hexo
中叫 tag,Hugo
中类似的功能叫 shortcode。
在mdx中实现同样的功能更是简单,写好组件传递给<MDXContent />
然后在文章中直接使用。替换<pre>
标签通过自定义组件实现代码高亮,替换默认img
实现图片lcp优化并添加点击放大预览。