Skip to content
happyfenglife
Go back

我的技术栈拆解:这个博客是怎么搭起来的

Updated:

这个博客从写下第一行 Markdown,到能在公网被任何人打开,中间串了十来项技术。这篇文章把它们逐个拆开讲清楚 —— 不只是「用了什么」,更重要的是「每一项到底解决了什么问题」,以及我在哪几个地方摔过跤。

配合作者本人的背景(日常写 C 和 Linux 系统编程,前端是业余),下面的解释会尽量往”它本质上在干什么”这个方向讲。

Table of contents

Open Table of contents

先看一张全景图

一次访问要穿过这样一条链路:

写作 → 构建 → 提交 → 部署 → 分发 → 访问

对应到技术上:

环节用的东西它解决的问题
写作Markdown + frontmatter内容和排版彻底分离
构建Astro(承重) + AstroPaper(骨架) + Tailwind(样式)把纯文本编译成静态网页
依赖pnpm装得快、装得准、锁得住
搜索Pagefind纯静态站也能做站内搜索,无需后端
版本控制Git + GitHub每次改动可追溯,顺便当部署触发器
部署Vercel推一次代码,自动构建、自动上线
域名DNS(腾讯云 DNSPod)把域名翻译成 IP
传输HTTPS + 自动签发的证书加密 + 身份验证
统计Vercel Analytics知道有多少人来过,且不用 Cookie
评论Giscus借用 GitHub Discussions 当评论区

一项一项说。

一、Markdown:让内容不受排版绑架

Markdown 的本质是用纯文本表达结构。## 表示二级标题、**加粗** 表示强调 —— 它描述的是”这是什么”,而不是”这长什么样”。

这个区别很关键。如果你用 Word 写博客,文字和格式(字号、缩进、行距)是绑死在一起的;等哪天想换一套主题,几百篇文章就得重新排一遍。而 Markdown 文件里没有一个字节跟视觉有关,换主题只是换了一层渲染规则,内容一个字都不用动。

这也是为什么这个博客的文章能被同时输出成网页、RSS 和 sitemap —— 它们都只是同一份 Markdown 的不同渲染结果。

frontmatter:文章的”元数据”

每篇文章开头那段被 --- 夹住的 YAML,叫 frontmatter。它不属于正文,而是给程序看的结构化信息:

---
title: "文章标题"
pubDatetime: 2026-10-13T11:00:00+08:00
description: "摘要,会用在首页卡片、搜索结果和 SEO 描述里"
tags:
  - 技术
draft: false
---src/content/posts/example.md

这一小段是整个博客”自动化”的基础:因为标题、时间、标签是结构化的,程序才能自动生成首页列表、标签页、归档页、RSS 和 sitemap。如果这些信息藏在一段自由文本里,机器就读不懂了。

二、Astro:把 Markdown 编译成网站

Astro 是这个博客的承重结构。它的核心工作是在构建阶段,把你写的内容渲染成一批完整的 HTML 文件。

静态生成,而不是每次现算

传统的博客系统(比如 WordPress)是请求时才现算:用户打开页面 → 服务器查数据库 → 拼 HTML → 返回。这要求服务器一直在线,并且每次访问都要付出这部分计算。

我的做法相反:在构建的时候一次性算完,产出纯静态的 HTML,然后丢给 CDN。用户来访问时,拿到的是已经生成好的文件,中间没有任何程序在跑。好处是显而易见的 —— 没有数据库、没有后端进程、没有要维护的东西,快且几乎不可能崩。

代价是:内容变了必须重新构建。对博客这种更新频率的东西,这个代价可以忽略。

岛屿架构:默认零 JavaScript

前端圈这两年流行”什么都在浏览器里跑”,代价是首屏要下载几百 KB 的 JS 才能看见内容。Astro 反其道而行:默认一个字节的 JS 都不发,页面就是纯粹的 HTML + CSS。

需要交互的组件(这个主题里只有主题切换、搜索框这些)才被单独”激活”,Astro 管这叫岛屿架构(Islands Architecture)—— 静态的海洋里浮着几个需要跑 JS 的小岛。

对一个文字为主的博客来说,这几乎是最优解:文章页打开就是”立刻”。

内容集合与类型校验

Astro 的 Content Collections 让我能给 frontmatter 定义 schema,写错了会直接报错,而不是等到线上出现空白页面:

const posts = defineCollection({
  loader: glob({ pattern: "**/[^_]*.{md,mdx}", base: "./src/content/posts" }),
  schema: ({ image }) =>
    z.object({
      author: z.string().default(config.site.author),
      pubDatetime: z.date(),
      title: z.string(),
      draft: z.boolean().optional(),
      tags: z.array(z.string()).default(["others"]),
      description: z.string(),
    }),
});src/content.config.ts

上面这段里有个细节值得单独说:glob 模式写的是 **/[^_]*.{md,mdx},意思是文件名以 _ 开头的 markdown 会被完全忽略。我正是靠这条规则,在文章目录里放了一个 _template.md 当发文模板 —— 它永远不会被当成正文发布出去。

三、AstroPaper:一套现成的骨架

AstroPaper 是社区里一套成熟的博客主题。它提供的不是”库”,而是一整套已经写好的页面和组件:首页、文章列表、标签页、归档页、404、RSS 生成、sitemap、暗色模式、动态 OG 图……

我选择它而不是从零写,理由很朴素:我想写文章,不想设计网页。

Warning

用现成主题有个必须知道的前提:它的配置不一定都在配置文件里。 我踩过这个坑 —— 首页的大标题和简介是硬编码在 src/pages/index.astro 里的,改 astro-paper.config.ts 完全不起作用,导致首页很长一段时间显示着主题自带的模板文案。换主题时,务必先确认哪些内容是”可配置的”,哪些是”写死的”。

四、Tailwind CSS:样式写在标签上

Tailwind 的思路是把 CSS 拆成一个个极小的原子类,直接写在 HTML 标签上:

<h1 class="text-2xl font-bold sm:text-3xl">标题</h1>

乍看很反直觉(这不是把样式和结构又混在一起了吗),但实际用下来有两个好处:改样式时不用在文件之间跳来跳去,以及不用再为类名发愁。

这个主题用的是 Tailwind v4,配置方式已经变成”在 CSS 里写 @theme”而不是 JS 配置文件。配合 CSS 变量,暗色模式的切换就只是换一组变量值 —— 不用维护两套样式表。

五、pnpm:快、省,而且严格

pnpm 是包管理器,和 npm 干同一件事,但有两个本质区别。

一是存储方式:npm 会把每个项目的依赖各存一份;pnpm 用一个全局的内容寻址仓库,项目里通过硬链接指过去。十个项目用同一个库,磁盘上只有一份实体。

二是对锁文件的态度:pnpm-lock.yaml 记录的是精确到版本和哈希的依赖树。CI 环境里默认加 --frozen-lockfile,意思是”锁文件和 package.json 必须完全一致,否则直接构建失败”。

Caution

这个严格性是双刃剑。我就被它拦住过一次:往 package.json 里加了依赖,但没同步更新 pnpm-lock.yaml,Vercel 直接以 ERR_PNPM_OUTDATED_LOCKFILE 报错退出。 教训:换包管理器必须同步换锁文件。 这个博客的约定是——新增依赖一律用 pnpm add,绝不用 npm install 生成 package-lock.json。

六、Pagefind:纯静态站也能搜索

前面说了这个站没有任何后端,那搜索框怎么办?

Pagefind 的思路很巧妙:它在构建完成之后,去扫描生成的 HTML 文件,离线建立一份索引,然后在浏览器里加载这份索引完成搜索。整个过程没有服务器参与。

代价是两条:索引是构建产物,新增文章必须重新构建才会被搜到;以及只有带有 data-pagefind-body 标记的页面才会被索引 —— 这样导航栏、页脚之类的重复内容就不会污染搜索结果。

七、Git + GitHub:不只是代码仓库

Git 负责版本控制,这部分没什么可说的。真正有意思的是它在部署链路里扮演的角色:GitHub 仓库是整条流水线的触发源 —— 每次往 main 分支推送,就自动触发一次构建部署。

也就是说,“提交代码”和”发布网站”被我合并成了同一个动作。

Note

顺带记录一个网络层的坑:我这台机器上 GitHub 的 HTTPS(443) 和 SSH(22) 都连不上,最后是走 SSH over 443(ssh.github.com:443)才通的。如果你的网络环境也这样,~/.ssh/config 里加一段 Host github.com / Hostname ssh.github.com / Port 443 就能解决。

八、Vercel:从推送到上线

Vercel 是整条链路的部署平台。我把 GitHub 仓库连上去之后,它做的事是:

  1. 监听到 main 分支有新提交
  2. 拉取代码,自动识别出这是个 Astro 项目
  3. 跑安装和构建命令
  4. 把产物分发到全球边缘节点
  5. 绑定我的自定义域名并签发证书

实测数据(就用这篇文章的发布来测的):推送后 3 秒被接住,46 秒构建完成并上线。 整个过程我没有点过任何一个按钮。

Vercel 还会给每次部署分配一个 xxx.vercel.app 的临时地址,用来预览。但要注意:那只是默认域名,不是你的门牌号。真正对外的地址是你自己绑的域名,两者是完全独立的两回事。

九、DNS:这本”电话簿”的两个关键记录

DNS 是这整条链路里最容易被忽视、但最需要理解的一层。它的工作就一句话:把域名翻译成 IP 地址。因为人是靠名字记东西的,而机器只认 IP。

配置时只需要两条记录,但它们的机制完全不同:

A 记录:直接给出 IP

happyfenglife.club  →  216.198.79.1

A 记录的答案是一个 IP 地址,一次查询就拿到终点,直接连过去。代价是 IP 被写死在 DNS 里 —— Vercel 那边要换 IP,得我去 DNS 后台手动改。

CNAME 记录:给出一个”别名”

www.happyfenglife.club  →  <项目标识>.vercel-dns-017.com  →  216.198.79.65

CNAME 的答案不是 IP,而是另一个域名。所以查询要绕一圈:先问 www,得到”你去找那个名字”,再查一次才拿到 IP。

好处是:IP 是什么由 Vercel 决定,我完全不用管。 它想换就换。

那 apex 为什么不能用 CNAME?

因为 DNS 协议规定,每个域名的顶点(happyfenglife.club 本身)天生就必须携带 NS 和 SOA 两种记录,而 CNAME 的规则恰恰是”这个名字上只能有它自己”。两条规定直接冲突,所以顶点只能填 A 记录。

而 www 只是个普通子域,不背这个包袱,所以可以用 CNAME。

这也是 Vercel 官方推荐”用 www 作主域名、apex 重定向过去”的原因:走 CNAME 路径能让 CDN 对流量有更强的调度能力。不过 apex 作主域同样是被支持的(靠 Anycast 做就近路由),这个博客最后就选了 apex —— 域名短一点,看着舒服。

Tip

排查 DNS 时有个容易犯的错误:不要只用一个解析器就下结论。 改完记录后,我实测发现阿里 DNS 还在返回旧 IP(缓存没到期),而 DNSPod 已经是新值了。递归解析器各自按 TTL 缓存,多个解析器交叉验证才可靠。

十、HTTPS 与证书:你什么都没做,它就上线了

现在的浏览器对没有 HTTPS 的站点会直接标”不安全”。但这件事我一行配置都没写 —— Vercel 在域名验证通过后自动向 Let’s Encrypt 申请证书,并且自动续期。

背后是 ACME 协议在起作用:平台需要证明”我确实控制这个域名”,然后就能拿到证书。证书同时覆盖 apex 和 www 两个名字,所以两个地址都是绿的。

站点还带了 HSTS 响应头(max-age=63072000,即两年),意思是告诉浏览器”以后访问这个域名只准用 HTTPS”,从根上杜绝降级攻击。

我想知道有多少人来看过,但不想为此引入 Cookie 和随之而来的隐私声明麻烦。

Vercel Analytics 是 cookieless 的:它不往用户浏览器里写任何标识,而是用匿名化的请求特征做聚合统计。所以这个站没有、也不需要那个烦人的”本网站使用 Cookie”横幅。

十二、Giscus:把评论区”外包”给 GitHub

评论区通常是博客里唯一需要后端的东西 —— 要存评论、要防垃圾广告、要审核。

Giscus 的思路很取巧:它把评论直接存进仓库的 GitHub Discussions。 文章的评论区本质上就是一条 Discussion,读者用 GitHub 账号登录后留言。存储、鉴权、通知全部由 GitHub 负责,我这边零成本、零维护。

前提条件是两条:仓库必须是公开的,以及必须开启 Discussions 功能。

这套架构的代价

说好处说了一路,也得把代价讲清楚:

一句话总结:这套方案是为”以文字为主的个人博客”量身定做的,换个场景就未必合适。

小结

回头看,这十来项技术其实各自只做一件很小的事:Markdown 管内容、Astro 管编译、Pagefind 管搜索、Git 管版本、Vercel 管构建和分发、DNS 管寻址、证书管加密。

真正的巧妙之处不在任何单项,而在于它们能首尾相接成一条完整的流水线 —— 我在本地写一个 .md 文件,执行一条 git push,几十秒后全世界就能访问。中间没有一步需要我去点按钮。

这也是我这次搭博客最大的收获:好的架构不是把问题解决得多么聪明,而是让正确的路径变得足够短。

如果这篇对你有点用,欢迎在下方留言聊聊你的方案。


Share this post:

Comments


Previous Post
你好,世界:这是我的第一篇博客