你好,世界:这个博客是怎么搭起来的
从毛坯房到能住人——记录一次用 Next.js 静态导出 + TinaCMS + Cloudflare Pages 搭个人博客的完整过程,以及为什么最后选了纯静态。
为什么又开一个博客#
写作这件事,我断断续续试过好几次。前面几次都死在同一件事上:发布太麻烦。
Markdown 写完了还要 git add、commit、push,然后等 CI 跑完。兴致来的时候这不算什么,
但更多时候我只是想改一个错别字——为了改错别字走一遍完整的部署流程,热情就消磨掉了。
所以这次的目标很明确,一共四条:
- 网页后台直接写作,手机上也能改
- 国内访问要快
- 托管必须免费
- 源码可以公开
这四条其实互相打架。网页后台意味着要有服务端,服务端就意味着不能纯静态、不能白嫖 CDN。 转折点是 TinaCMS:它把「后台」做成了纯前端应用,编辑时直接调 GitHub API 提交, 不需要我维护任何服务器。
技术选型#
最后定下来的组合是这样:
| 层级 | 技术 | 作用 |
|---|---|---|
| 框架 | Next.js 16 | App Router + 静态导出 |
| UI | React 19 | 组件化开发 |
| 样式 | Tailwind CSS 4 | 原子化 CSS |
| 内容 | MDX | Markdown 里嵌 React 组件 |
| 后台 | TinaCMS | 网页编辑器,保存即提交 GitHub |
| 托管 | Cloudflare Pages | 全球 CDN,国内速度尚可 |
为什么不是 Vercel
Vercel 的体验确实好,但它的默认域名和部分 CDN 节点在国内访问不稳定。 Cloudflare Pages 免费额度更大,国内实测也更稳一些。 代价是构建产物必须是纯静态的,不能用 Server Actions、ISR 这些东西。
静态导出到底做了什么#
next build 执行的时候,Next.js 会把每个路由都渲染成一份 HTML 文件。
import createMDX from "@next/mdx";
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export", // 关键:生成静态 HTML
images: { unoptimized: true }, // 静态导出必须关掉默认图片优化
trailingSlash: true, // 生成 posts/hello-world/index.html
};
export default createMDX({ options: { remarkPlugins: ["remark-gfm"] } })(nextConfig);构建完之后,out/ 目录里就是一棵可以直接扔给任何静态服务器的文件树:
out/
├── index.html
├── posts/
│ ├── index.html
│ └── hello-world/
│ └── index.html
└── _next/static/…访客打开页面时,服务器(其实是 CDN 边缘节点)只是把已经生成好的文件发出去, 不执行任何代码、不查任何数据库。这就是它快的原因,也是它便宜的原因。
静态导出的边界
一旦选了 output: "export",下面这些能力就用不了了:
Server Actions、请求相关的 Route Handler、cookies()、ISR、默认的图片优化、
以及任何的 rewrites / redirects / headers 配置。这些都需要一个常驻的 Node 进程。
视频与音乐#
国内写博客绕不开一个问题:YouTube 打不开。所以视频统一走 B 站的 iframe 嵌入, 播放器直接连接 B 站的 CDN,我这边不需要承担任何流量。
<BilibiliVideo bvid="BV1xx411c7mD" title="示例视频" />渲染出来是这样:
音乐同理,用原生 <audio> 就够了。侧栏那个迷你播放器读的是
src/lib/music.ts 里的歌单,把 mp3 丢进 public/uploads/ 再登记一条就行。
示例曲目一 · 琶音
0:00 / 0:00本站生成
这只是占位音频
上面这条指向 public/uploads/demo-01.wav,是仓库里自带的一段几秒钟的示例音,
用来让播放器「点开就有声音」。
换成你自己的音频:把文件放进 public/uploads/,再把 src 改成对应路径即可。
注意路径要对得上——指向不存在的文件时,浏览器控制台会看到一条 404。
写作时的完整链路#
打开 /admin → TinaCMS 编辑器 → 写文章 → 保存
↓
调用 GitHub API 提交到仓库
↓
Cloudflare Pages 检测到更新 → 重新构建 → 部署到 CDN
↓
大约 1 分钟后线上生效整个过程中我唯一需要做的事情就是「写字」和「点保存」。这才是一个博客应该有的摩擦力。
后来补上的两件事
写这篇文章时评论和搜索都还没做,现在都已经落地了: 搜索是构建期生成一份静态索引,浏览器本地匹配(不需要 Pagefind); 评论则是三条通道——本机评论(零配置)、仓库里自带的互动服务 (Cloudflare Worker + KV,部署后所有人可见)、以及可选的 Giscus。 两者的共同点是:都不需要常驻服务器,符合这个站点的定位。
小结#
如果把建站比作装修,那大概是这样分工的:
.html是承重墙,决定房子里有什么.css是装修,决定好不好看.js是电器开关,决定能不能互动.mdx是我的家具说明书——写清楚每件东西怎么用
这套组合没有哪一项是新技术,但拼在一起刚好满足了我那四条互相打架的需求。 如果你也想搭一个类似的博客,直接从仓库 clone 下来改就行,源码本来就是公开的。
评论0
还没有评论,来写第一条吧。
访客的评论先存在你自己的浏览器里(纯静态站没有收件箱), 只有站长发布的留言与回复是写进仓库、所有人都能看到的;点赞也只记在本机。
想要「所有访客都看得见、站长能回复」的评论区,两条路选一条:部署仓库里自带的互动服务 (workers/blog-api,见 README),或者配置 Giscus。 现在既没配也没部署,所以上面的评论只存在各自的浏览器里。