新时代独立博客走弯路式搭建的最佳实践

我已经不记得这是自己第几次重写博客的代码，但我很确定这是（短时间内）最后一次了。无论如何，是时候给我这在自定义性、易维护性和成本三者之间找不到任何平衡点的完美主义画上一个暂时的句号了。

重写的原因

因为越来越看不惯旧博客的页面设计，我决定直接推翻重新设计。但在我这么做之前，我突然意识到我已经有好久没更新博客了，最近的更新还都是一些写得让人摸不着头脑的小说，而且大部分是直接搬运自我在另一个网站的创作 —— 总之，我已经很久没有静下心来认真地写博客了。我认为时不时用较长篇幅的文字记录一下自己在某方面的摸索历程还是很有必要的，这和我做手帐时的日常记录有很大区别，针对事物本身而非时间的记述能帮助我在理清思路的同时加深记忆。

我不小心丢掉了写博客的这个习惯，有很大原因是更新静态博客时繁琐的操作流程。我前几次重写博客代码，使用的都是纯粹的 Next.js 或者 Svelte 框架，算是一种「无服务器」（Serverless）的应用。由于没有数据库，博客所有的文章都以 Markdown 文件的形式，和博客的源代码储存在一起。这就导致，在我需要更新博客的时候，我要先在一个 Markdown 文件里写好文章内容，还要在文件的开头编写必须的 Front Matter 用于表示文章标题、创建日期、标签等元信息（而且我每次都记不住格式和属性的命名，要先找到以前的文件，然后复制到新的文件里）。当文件准备好之后，我要把它放进 Git 仓库里对应的目录下，在本地运行 npm run dev 测试是否会产生问题，无误后推送到 GitHub 并等待 Vercel 将新的博客版本部署在生产环境中。之后，如果我还需要修改文章内的某些错误，或者进行额外的更新，我无法在移动设备上进行这些操作，我需要打开我的电脑，打开 GitHub Desktop 和 VS Code，编辑我的内容，然后测试，再推送，再等待部署。

初次接触静态博客的开发者可能会觉得这很有意思，很极客，但我很快就厌倦了，因为有的时候我只是想要写一篇文章传达一些想法或者是纯粹地记录，可我却需要打开一个光是看着就觉得自己要开始写 Bug 的界面，然后再进行一系列十分「黑客」的操作，才能发表我的文章。这简直太反人类了。

为了保留写博客的好习惯，同时不委屈自己，我决定重写一个让自己觉得更舒服的博客系统。

我的思路

我需要一个功能齐全的图形化博客管理后台，但我不想自己造轮子，也不想用市面上花里胡哨的 CMS 来「大炮打蚊子」，我只是想要一个简单、易上手的，适用于个人博客的内容管理程序。

符合这个描述的，答案当然是 ——Typecho。

不过问题是，Typecho 是一个用 PHP 编写的，前后端一体的博客程序。对我这个已经享受过用 JavaScript 写前端是多么舒爽的人而言，回到 PHP 时代无异于现代人到山里住岩洞，我还得重新适应 PHP 并重新写一个博客主题。这实在是令我不能接受。

我既想享受传统博客傻瓜式操作带来的便利，又不愿意离开现代化前端开发的优雅和高效。那么解决方案就很明显了 —— 使用一个无头 CMS，同时重新设计博客的前端。但问题又回来了，市面上大部分的无头 CMS 都有些臃肿，或者说是相对于我要解决的问题，它们都具备了太多我不需要的功能。不过，Typecho 虽然不能作为无头 CMS 使用，但它的量级却刚刚好满足我的需求。

那么，我只需要想办法把 Typecho 变成一个无头 CMS，一切问题就都引刃而解了。

开始实践

我很容易就找到了一个现有的插件，它能为 Typecho 提供 RESTful 化的 API。这样一来，Typecho 就能作为纯粹的后端为我自己设计的前端提供数据了，而我只需要在 Typecho 的控制台更新博客内容就行了。

接下来，我只需要把重点放在前端的设计上就好了。

选择工具

我决定使用我熟悉的 Next.js 编写前端，因为我决定把前端托管在 Vercel 上，而 Vercel 的 Next.js 的支持显然更好。

在 CSS-in-JS 这方面，我选择了最近很火的 Tailwind.css 而不是自己用 SCSS 手写每一个类。一方面，新版本的 Next.js 默认支持 Tailwind.css，省去了自己配置的时间；另一方面，有了 React 对模块化开发的支持，每个相同或相似的元素都可以被编写成组件，在 CSS 层面做到语义化就显得有些没必要了，这时候有更方便快捷的方法当然是最好的。

顺带一提，我有好一段时间没用 Next.js，上一版博客（Isla）使用的是 Svelte。新版本的 Next.js 添加了新的页面路由方法，即 App Router，与以往的 Page Router 区分开来。照理来说使用 App Router 是更好的，但刚回坑的我显然还没有反应过来，所以继续采用 Page Router 编写博客。不过，能跑就行。

像是 React Icons 图标库这样的额外工具就没必要提了。

获取文章

使用 Next.js Page Router 提供的 getStaticProps() 函数可以在页面加载之前获取来自无头 CMS 的数据。使用 fetch() 获取 API 内容，记得使用 await 关键词。

插件提供的 RESTful 风格的 API 可以直接用 json 解析，不要忘记解析时也需要加上 await 关键词。

Copy
export async function getStaticProps() {
const res = await fetch('https://blog.guhub.cn/api/posts')
const posts = await res.json()

return { props: { posts } }
}

完成之后将文章列表数据作为 Props 返回给主函数即可。

不过，在这里我遇到了一个后端的问题，代码这样正常跑了数十次之后我才发现前端只展示了前五篇文章，原因是插件给 API 提供了分页功能，每页默认五篇，需要在 URL Query 中用 ?page= 标明正在查看第几页。不过我目前的设计并不需要分页功能，所以我用 API 提供的另一个方法增加了每页显示的文章数量，算是一个比较蠢的解决方案。

Copy
const res = await fetch('https://blog.guhub.cn/api/posts?pageSize=9999')

展示文章

从后端得到的数据中，重要的数据在 data.dataSet 下，里面包含了文章的标题、创建时间戳、CID、分类、Slug 等。值得一提的名为 digest 的属性，这个和 Typecho 的设置挂钩，如果设置了在首页展示完整的 $this->content()，digest 就会包含全文内容的 HTML 字符串而不只是摘要。这个插件在文章列表的 API 中没有专门输出全文内容的属性，如果在 digest 只输出摘要的情况下需要获取全文，就要用 slug 或者 cid 等唯一的属性到另一个路径中获取更详细的文章信息。

这显然有些太麻烦了，于是我决定不更改 Typecho 的设置，把 digest 当作全文内容使用。不过，我仍然有在文章列表输出真正的摘要的需求，这就意味着我需要在前端截取一段摘要。

我是这样实现的：

Copy

function stripDigest(digest) {
//删除空行和空格
digest = digest.replace(/\ +/g,"").replace(/[ ]/g,"").replace(/[\r\n]/g,"")

//删除标题
digest = digest.replace( /<h.*?>(\S|\s)*?<\/h.*?>/g,"")

//在文章内容中寻找 标签
// 若存在，则截取 之前的内容
// 若不存在，则截取前 150 个字符
var moreTag = digest.search(/ /)
var sliceEnd = (moreTag>0) ? moreTag+2 : 150

//删除 html 标签，只保留文字内容，然后执行截取操作
digest = digest.slice(0,sliceEnd).replace(/<\/?[^>]+(>|$)/g, "") + "......"

return digest
}

摘要应当是一段连续的文字，没有分行和空格，所以要先删去这些空白；标题最好也删去；然后是喜闻乐见的 标签，这个是用来手动截取摘要的，如果有 标签，就将其作为分界线截取前面的文本作为摘要；如果没有，就截取前 150 个字符。然后需要删去 HTML 字符串中的标签，只保留纯文字内容。

如果你有闲心仔细看了上面的代码，你可能会对这一段代码感到疑惑：

Copy
var moreTag = digest.search(/ /)
var sliceEnd = (moreTag>0) ? moreTag+2 : 150

其中，变量 moreTag 用来表示 所在位置的索引。如果存在，索引就大于 0，照理就应该以索引直接作为之后 slice() 方法，但我在这里加了 2，原因是 —— 不加这个 2 的话，截取的位置就不对。

很经典的问题，我不知道为什么要写这段代码，但不写的话程序跑起来就有问题。

虽然加上了之后跑起来也不完全对，但不加的话问题更大。我一直没搞明白为什么，然后就摆烂了。现在想想，最佳的处理方式是按照 RESTful API 设计的逻辑走，直接获取服务端提供的摘要。这个问题留到之后有空再改吧。

页面设计

能够获取文章数据并展示在前端就已经完成博客的基本功能了，接下来轮到页面设计。

在之前几个版本的博客设计中，我都在刻意地追求简洁（一个已经被用烂了的设计风格）。当时的页面组成就是白底黑字，加上一些同样简单的黑色线条图标，和一些淡灰的色块简单地划分一下区域。

这样的设计确实让我在花里胡哨的网站和 App 中找到了一丝清爽的感觉，但问题在于，这种过于简单的设计很容易被「刻奇」，更准确地，是我在对这种被许多装作内行的博主广泛认可的设计风格进行刻奇。这样的风格缺乏新意和个性，现在想来，也是我想要把博客前端推翻重写的主要原因。

我已经忘记了是什么给了我灵感，但在我纠结数日后，我对新博客的外观设计有了新的构想。我想要一个简洁大方，但同时特征鲜明，色彩明显，排版富有新意的设计。在融入了一些报刊头条和拼贴的元素之后，我首先在 Figma 上做好了一个概念图。

在之后的实装过程中我又做了一些调整，加上了类似网格手帐本内页的底纹，逐渐变成了现在的样子。

RSS 订阅

在这个没什么人看博客，写博客的大多数更新也经常挤牙膏的时代，给愿意关注自己的读者一个订阅的途径，在自己终于更新的时候提醒一下读者是有必要的。

起初我觉得这并不难，因为 Typecho 本身提供了 RSS 订阅源。但问题又来了，我把后端部分（Typecho）放在了 blog.guhub.cn 这个域下，前端则在 www.guhub.cn，而 Typecho 本身并不是为前后端分离的方案设计的，所以在 Typecho 提供的订阅源中，所有文章链接都指向了 blog.guhub.cn 这个域，而不是我现在使用的 www.guhub.cn。

我以为我只需要在前端把 RSS 的 XML 抓过来，然后把所有的 blog.guhub.cn 替换为 www.guhub.cn 就可以了。不过，Next.js 的设计者大概怎么也不会想到有个爱走弯路的傻子想要干出这种事情，它没有办法直接处理 XML 数据，我也没有找到直接获取页面内容的方法。这照理来说是可行的，但我不想在这一步多花时间了，于是......

Copy
npm i rss

我安装了一个 RSS 库，用我从 API 获取到文章数据重新生成了一个订阅源。

Copy
export default async function generateRssFeed({ posts }) {
const feedOptions = {
//...
}

const feed = new RSS(feedOptions);

posts.map((item) => {
let post = parseBlogPost(item);
feed.item({
title: post.title,
description: post.content,
url: `${site_url}/blog/${post.slug}`,
date: post.date,
});
});

fs.writeFileSync('./public/feed/index.xml', feed.xml({ indent: true }));
}

这样 Next.js 就会在需要的时候生成一个 XML 文件作为 RSS 订阅源。现在，如果你愿意的话，你可以用这个链接订阅我的博客。

其他

细枝末节的实现步骤就不在这里赘述了。我还有诸如分类、标签页面等功能没有做出来，文章列表的设计也有些过于简陋，这些会慢慢地在之后补上的。光是做这些功能就够我忙活一阵子了，我应该还不至于很快就觉得无聊然后把整个博客删掉。

另外，如果你觉得这个博客的前端看着不舒服，尤其是目前我还没有把深色模式做出来，你可以到 Typecho 侧阅读文章，那边我使用的是 Matcha 主题，也是我编写的，功能较为完善，阅读体验应该比现在的博客要好很多。

哦对，差点忘了，我把这个项目叫做 Taco，也就是把 Typecho 中间的音素拆掉一部分之后得到的单词。

备案和网站加速

因为 Typecho 侧的服务器使用的是腾讯云的国内服务器，所以我终于给 guhub.cn 这个域名备案了。不过最主要的原因还是想要使用 CDN 和对象储存这样的服务提高博客的访问速度。

ICP 备案和公安备案的具体步骤就不细谈了。CDN 和对象储存服务我使用的是又拍云，对于独立博客这种低访问量网站是性价比很高的选择了，用了半个多月只扣了不到一块钱的费用。

全国都是绿油油的感觉很舒服。

最近倒腾博客的成果大概就是这些了。

插几句题外话，如果你细心的话，你会发现目前博客还没有友情链接页面，这个我会尽快加上的。我打算重新开放友链申请，并删除一部分不常交流的友链。对博客往后的发展我也有了初步的构想。这些内容或许我都会单独写一篇文章谈一谈，在这里就不展开说明了。

好了，感谢你读到这里，希望你过得还愉快。