bgm004'blog

一个二次元宅男的无聊日常

我的博客重构之旅

2024-05-25
React
Nextjs

本文记录了从博客从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-collectionsVelite

Velite功能多一点,支持资源管理。content-collections支持文章中使用import export,因为它底层使用mdx-bundler。这会使文章数据体积增大。2个库使用方法差不多,切换也不费时间,用哪个都无所谓。

_posts --------------------- // 文章目录
  ├─[slug] ----------------- // 每篇文章一个文件夹
  │ ├─index.md ------------- // 文章
  | |-components.ts -------- // 文章中使用的组件
  | |- * ------------------- // 图片等资源

以上是我的文章目录结构。由于Velitecontent-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。更新数据后调用Nextjsrevalidatetag刷新首页数据。

bangumi页面使用bangumi的api获取数据,由于数据太多又不想弄分页,所以只展示前21条数据。二次元老婆部分因为官方api不能获取目录中收藏的角色。所以使用Cheerio爬取数据。

为markdown赋能

我只需要在文章中添加<BgmSubject id="395378" />即可实现如下效果,通过替换a标签,直接贴bangumi链接更是方便。

迷宫饭
迷宫饭
迷宫饭
TV
迷宫饭,不是吃就是被吃… 妹妹在迷宫深处被赤龙吃掉了! 冒险者莱欧斯侥幸逃过一命回到了地面。 他想要再度挑战迷宫,但是钱和食物都留在了迷宫深处… 在妹妹随时可能会被消化掉情况下,莱欧斯下定了决心: 「食物要在迷宫内就地取材」 史莱姆、鸡尾蛇、宝箱怪、然后是龙! 冒险者啊,一边吃掉袭来的魔物,一边通关迷宫吧!

文章中渲染自定义内容这个功能在Hexo中叫 tagHugo中类似的功能叫 shortcode

在mdx中实现同样的功能更是简单,写好组件传递给<MDXContent />然后在文章中直接使用。替换<pre>标签通过自定义组件实现代码高亮,替换默认img实现图片lcp优化并添加点击放大预览。