这篇文章最初写于 2026-10-24,记录的是这个博客刚搭起来时的样子。10-26 我把它更新了一遍:补上了后来长出来的音乐播放器、天气、旅行地图和自建统计,也修正了部署部分——线上实际用的是 Nginx + Cloudflare,不是仓库示例里的 Caddy。
为什么不用现成的
市面上的博客系统大致分两类:一类是 WordPress 这种重后台,插件生态大,但攻击面和维护成本也大;另一类是 Hexo / Hugo 这种纯静态,写得爽,但想在网页上改一篇文章、发一条朋友圈都做不到。
我需要的其实很简单:能在线写 Markdown、有后台、打开要快,最好还能随手加一点自己的东西。
技术选型
最终选的是 Next.js 15 的 App Router 加 SQLite:
- App Router 让前后台在同一个项目里,页面和 API Route 共用一套路由;
- SQLite 单文件、零运维,对个人博客的读写量绰绰有余;
- ISR 让文章页可以静态化,发布时按需刷新;
- Tailwind CSS 负责样式,Shiki 在服务端做代码高亮,Drizzle 做类型安全查询。
后来因为旅行地图,又加了 ECharts。它只在 /travel 页面按需加载,不会拖慢首页。
数据层
用 Drizzle ORM 做类型安全的查询,所有 SQL 都是参数化的:
const list = db
.select()
.from(posts)
.where(eq(posts.status, 'published'))
.orderBy(desc(posts.publishedAt))
.all();
后来表也慢慢多了起来:文章、评论、点赞、朋友圈、相册、访问记录、旅行地点……但都还是同一套思路:小表、明确的索引、需要时再加。SQLite 的 WAL 模式下,备份不要直接 cp,用 sqlite3 .backup 拿到一致性快照更稳。
后来加的东西
博客刚上线时只有文章和后台;后来它慢慢长成了现在这样:
- 音乐播放器:一开始是右侧面板,后来改成顶部居中的灵动岛,最后定成贴右侧的竖向侧边条。收起时是一条窄条,点开向左展开成歌词面板;用的是自己写的弹簧引擎,不弹、不卡。
- 天气 / IP 胶囊:导航栏右侧显示城市、温度和天气图标,点开能看 IP、湿度、风速、7 天预报和网络状态。IP 定位放在服务端做,先用 ipapi 拿城市,再用国内 IP 库校正省份,避免把南宁的 IP 判成北京;天气数据来自 Open-Meteo。
- 旅行地图:
/travel,ECharts 画的离线地图,中国到市/区边界,国外只到国家;去过的城市是红色发光点,没去过的地方是灰色。地图数据 2.9MB,等浏览器空闲再加载,不让它和首屏抢带宽。 - 站点访问:不再依赖第三方统计,用访问记录表自己算 PV/UV;访客按 IP 哈希去重,历史数据按设备估算。
性能与 SEO
- 地图、字体、上传图片都设置了长缓存;上传图片走流式返回,Cloudflare 缓存后不会反复回源;
- 全站图片补了
lazy/async,首屏大图用高优先级加载; - 首页、文章、旅游页加了 canonical 和 JSON-LD(WebSite / Person / BlogPosting / BreadcrumbList / ItemList),sitemap 收录了
/travel; - 实验页
/lab/island设了noindex,不参与搜索。
部署
线上不是仓库示例里的 Caddy:实际是 Docker 容器 + 宿主机 Nginx + Cloudflare。应用只监听 127.0.0.1:3000,Nginx 做反向代理,Cloudflare 负责 DNS、TLS 和缓存。
发布流程也比“重新 build 一下”要谨慎:
- 在服务器 Linux 容器里构建(本机 Windows 干净构建有已知问题),先构建到独立目录;
- 冒烟测试新产物,确认首页、歌单、地图和接口正常;
- 停容器,备份当前
.next,换入新构建,chown后启动; - 线上验证首页 200、数据库/上传图正常,保留上一版
.next方便回滚。
SQLite 的数据库、上传图和音乐缓存都在 data/,不随 .next 回滚,所以每次发布前要单独备份数据库。
几个踩过的坑
- 不蒜子统计偶尔加载不出来,卡片一直显示“—”。后来改成用自己的
visits表统计,PV 和 UV 都从站内数据来; - IP 定位不能只看一家:同一段 IPv6,ipwho 判北京,ipapi 判南宁;现在服务端同时查 ipapi 和国内库,省份不一致时用省会兜底;
- 本机的
next dev和next start会争用同一个.next,构建 ID 可能被覆盖。生产构建统一放到服务器的独立目录; - 地图数据不要随首屏加载。2.9MB 的 JSON 等浏览器空闲再拉,Cloudflare 压缩后约 766KB,体验差别很大。
小结
个人博客不需要复杂架构。选一个自己能完全掌控的栈,比追新更重要。
但“能跑”只是开始。真正让它变成一个长期可用的站点,是后来那些不起眼的决定:给地图加懒加载,给图片加缓存,给数据库做备份,给每个接口想清楚失败时怎么办。
这个博客还会继续长。下一次可能是另一个小功能,也可能只是一次没有目的的折腾。