---
title: "个人网站的再次重建"
date: 2024-07-25T23:00:04+08:00
tags:
  - 技术

---

自从我 [22 年中迁移博客](/posts/blog-migration/) 后，事情发展算是比较符合预期，过往旺盛的表达欲也有所集中，从知识管理、人生体系、个人的方向和规划等视角，随着文章的梳理，也经历了一轮重塑。

但在技术层面，我对于这套方案仍有些不满，只是因为工作生活的精力分配原因一直没有投入时间去改。一方面是 Notion 的表达力的有限，以及生成的站点 CSS 样式的结构略复杂，不好定制；另一方面是其 RSS 订阅的逻辑会消耗大量的 Vercel 的云函数运行时长，频频超限；比较不能忍的是有的 Page 莫名其妙无法访问，但我不知道背后发生了什么，只能复制粘贴重新搞，这也让我对以 Notion 作为博客后端 CMS 的方案信心大减。

这件事情的推进，伴随着许多巧合和水到渠成：某天偶然意外拿到了 `zgq.me` 域名；设计师好友 Yvon 在思考自己新的职业规划，尝试做了自己的 [作品集站点](https://yvon.im/)；去年和 [@johnbanq](https://github.com/johnbanq) 说起要搞个人网站，最近终于[上线](https://johnbanq.github.io/)；外加去年在写 [0xFFFF 计算机入门专题](https://wiki.0xffff.one/getting-started/) 的时候也比较重度地使用了 Markdown 文件和静态网站生成器，重建个人网站的的动机愈发旺盛。在酝酿了好多个周末以后，终于是落地了一个版本。

由此，也在这里简单梳理一下重建个人网站这事儿的考虑和选择，以供参考。

### 目标

大的目标还是聚焦，在 [个人知识管理体系](https://wiki.0xffff.one/getting-started/knowledge-base#%E5%AF%B9%E5%A4%96%E7%9A%84%E7%A2%B0%E6%92%9E) 中，将所有对外的输出，归集到 `zgq.me` 域名下，比如较长的文字、个人状态、项目介绍等等，引用的源头都来自于此（Single source of truth 原则）。与此同时，将内容页面都统一在一套代码仓库，解除第三方依赖，确保数据的完整与安全。

在技术栈的角度，我想把个人的关注点集中在 [Web App 开发](https://zgq.me/posts/web-app-history/)，积累尽可能聚焦在这一领域。所以这里也继续用已有的前端技术去实现，继承 [Nobelium](https://github.com/craigary/nobelium) 的衣钵，把内容来源换成 Markdown 文件。

### 数据导出

这里好像没什么可复用的经验，主要是把 Notion 中存放的文章一股脑导出，然后再针对性地做些处理。

Notion 导出 Database，它会将数据打包到一个 zip 文件中，其中有一个 `csv` 存 Database 的各个字段，以及一系列 Markdown 的 `md` 文件，以及 `md` 文件对应引用的图片资源等。

这里一个核心是，以文章的 slug 作为文章的 key，然后以此建立一个目录，存放其引用的各项资源（主要是图片）。`md` 文件内容参考 Hexo / Jekyll 中名为 [Front Matter](https://jekyllrb.com/docs/front-matter/) 的做法，头部将元数据（发布时间、标签等）以 yaml 的格式输出至 `md` 文件的头部，紧接着才是文章正文。

目录结构上，以年为单位划分（毕竟一年产出也没有几篇），整理出来大概类似以下的结构：

![内容目录的结构](https://cdn.zgq.me/assets/posts/2024/blog-rebuilding/content-directory-structure.jpg)

### 核心
最开始考虑 Hexo，其生态有些复杂，懒人有点不想琢磨，遂先战术放弃。实质上我只是想要一个方便的 Markdown 转 HTML 能力，以及生成网站其他必要部分的框架，并不想在别的方面纠结太多，所以先用成熟的方案跑起来即可。所以这里网站构建上还是会继续用 Next.js，它的 [SSG 方案](https://nextjs.org/docs/pages/building-your-application/rendering/static-site-generation)基本可以满足我这里的需求。当然也保留使用其他方案（比如 [Waku](https://waku.gg/)）的空间，目前看 Next.js 耦合太多 Vercel Only 的东西，并不必要。

然后需要关注的是 Markdown -> HTML DOM 的解析方案，这里就直接用 `@mdx-js/mdx` 梭哈，综合下来还涉及到一些扩展组件，如 Front Matter、数学公式、表格等 Markdown 扩展语法的支持，基本上抄抄默认配置就可以。

接下来是网站的 CSS 样式方案，原则上基础 CSS 样式需要尽可能薄。这里直接用 Tailwind CSS 来做，日常用到的 CSS 样式基本都可以被它 cover 住，并且它背后是一套完整的 PostCSS 工具链支撑，基于它做样式方案代码可以更加简洁，尽可能减少全局的重复样式定义。基础文章排版则直接引用 `@tailwindcss/typography` 的样式。

RSS Feed 与 sitemap 的方案参考 [Nobelium](https://github.com/craigary/nobelium) 的，用 `feed` 和 `next-sitemap` 整一个，也挺简单方便的。

### 图片处理
图片是直接从博文 Markdown 中引用的，但由于 Next.js 的设定，无法直接访问到 public 目录外的静态资源，显然不是太方便。查了一堆资料，暂时没找到更好的解法，只能单独 copy 到 `static` 目录，然后用 `_next/static` 来访问到对应的文件。

``` js
config.plugins.push(
  new CopyPlugin({
    patterns: [{
      from: './content/',
      to: './static/assets/',
      filter: async (resourcePath) => {
        if (resourcePath.includes('.md')) {
          return false;
        }
        return true;
      }
    }],
  })
);
```

另一个问题是在我把网站部署 Vercel 之后，发现每次打开页面都会请求一次图片资源，虽然是 `304 Not Modified`，但其实也很影响体验。检查请求发现 cache 的 `max-age` 竟然是 `0`...

![](https://cdn.zgq.me/assets/posts/2024/blog-rebuilding/vercel-max-age.jpg)

找了很久没有找到 Next.js 和 Vercel 可以怎么调整文件缓存，我还是在 CDN 上配置好了，在 Next 这边做一组 URL 替换，以及禁用默认在 Vercel 烧钱的图片自动压缩服务。另外尽可能在后端读到各个图片的 size，避免页面初始化加载过程发生 Layout Shift（类似我之前在 Flarum 上的 [做法](/posts/flarum-img-layout-shift-optimization/)）。

整体处理代码如下：
``` tsx
const components: ReturnType<UseMdxComponents> = {
  img: (props) => {
    const { src, alt } = props;
    const realSrc = `${CDN_URL || ''}/_next/static/assets/${pathBase}/${src}`;

    // next/image support
    const { width, height } = (src && imageSizeMap?.[src]) || {};
    const useNextImage = width && height;
    const imgWithAltElement = useNextImage ? (
      <Image width={width} height={height} className="mx-auto mb-4 max-h-[300px] rounded-l object-contain" src={realSrc} alt={alt || ''} />
    ) : (
      <img className="mx-auto mb-4 max-h-[300px] rounded-l" src={realSrc} alt={alt} />
    );
    const imgWithOutAltElement = useNextImage ? (
      <Image width={width} height={height} className="mx-auto max-h-[300px] rounded-l object-contain" src={realSrc} alt="" />
    ) : (
      <img className="mx-auto max-h-[300px] rounded-l" src={realSrc} alt="" />
    );

    if (alt) {
      return (
        <span className="block">
          {imgWithAltElement}
          <span className="block mb-4 text-center text-xs text-gray-400 px-4">{alt}</span>
        </span>
      );
    }
    return imgWithOutAltElement;
  },
};
```

### 评论区
不想再用 cusdis，感觉逻辑有些复杂，并且 [Randy Lu](https://github.com/djyde) 没有很积极的维护它。所以我先把原有的评论随着数据导出一起存了一份，准备写一个新的，尽可能极简，减小维护的负担。

一个评论框业务逻辑本身并不复杂，主要是提交评论的组件，以及后端存储的数据库。

嵌入文章的 UI 评论组件，先糊一个 React 组件自用，后续看情况再扩展到 iframe 嵌入的 widgets。

数据库的考虑，个人博客站点的评论量其实很小，SQLite 已足够支撑。核心在于针对某个页面维护一个 comment 列表，以及 comment 之间可能的父子关系。所以用一个 comments 表，补上核心数据以及一些必要的字段，就足够。

然后需要一个针对博主的简单 CRUD 管理后台，实现评论回复，删除 / 审核，并根据用户回复的动态，实现 Email 通知提醒的能力。

准备是用 `Koa` + `SQLite` + `Vite React` 先做一个，写好再整理分享出来。整体结构类似 [weibo-rss](https://github.com/zgq354/weibo-rss)；因为只有我一个用户，所以后台的登陆权限校验相关，用 JWT 认证就足够，hash 直接写死；Email 发送直接走 [SES](https://cloud.tencent.com/product/ses)。主打一个「老夫写代码就是一把梭」。

### Reference

新网站的建设，参考了不少同路人的实现，特此鸣谢：
1. 博客的内容结构，参考了 Limboy 的 [我的博客系统演变之路](https://limboy.me/posts/my-blog-system/)，还有 [板桥工学](https://johnbanq.github.io/) 和 [Sukka's Blog](https://blog.skk.moe/)
2. 关于 content 目录的组织，友情链接的设定，参考了二花的 [This Cute World](https://thiscute.world/)
