个人博客搭建全记录:从 Fork 到上线
前言
写这篇文章的时候,这个博客已经上线运行了一段时间。回头看整个过程 —— 从 Fork 一个开源主题,到逐项修改配置、接入统计和评论、部署到自己的服务器 —— 踩了不少坑,也学到了很多东西。与其让这些经验烂在脑子里,不如整理出来,给也想自己搭博客的朋友一个参考。
本文不是教程,是复盘。会提到具体的技术选型和操作步骤,但更想说的是每个阶段踩过的坑和当时的决策逻辑。
起点:为什么要自己搭
其实本来有一个基于 Halo 的个人主站,日常写文章、发动态都在那边。但 Halo 毕竟是个 CMS,重、依赖 Java 运行时、一些定制不够灵活。想有一个更轻、更可控、能当静态站部署的地方,专门放那些需要精心排版的长文。
需求很明确:
- 静态优先:最好能生成纯 HTML,放 Nginx 就跑,不需要 Node 进程常驻
- Markdown 写作:所有内容用
.md文件管,方便 Git 版本控制 - 组件丰富:文章里能嵌入各种视觉组件(提示框、卡片、时间线、代码沙盒等)
- 自己掌控部署:不和任何平台绑定,随时搬家
在这个前提下,Nuxt Content 几乎是唯一的完美选项 —— 它是 Nuxt 的内容模块,写 Markdown 就像写 Vue 组件一样,支持自定义 MDC 组件(也就是你在这个博客里看到的 ::alert、::card-list 这些),还能一键生成静态站。
选主题:Fork 了 Clarity
确定用 Nuxt Content 之后,在 GitHub 上找到了 L33Z22L11/blog-v3—— 也就是这个博客的「上游」。Clarity 主题的设计风格参考了 Stellar(一个 Hexo 主题),走的简洁、专注阅读的路线,和我的需求高度吻合。
Fork 之后的第一件事:把原作者的配置全部改成自己的。这一步看着简单,实际改了十几个文件:
| 文件 | 改了什么 |
|---|---|
blog.config.ts | 标题、副标题、作者名、头像、邮箱、域名、建站日期 |
app/app.config.ts | 导航菜单、页脚社交链接、公告、GitHub 卡片、备案号 |
content/posts/ | 删除原作者文章,换上自己的 |
app/feeds.ts | 删除原作者友链,换上自己的友链列表 |
redirects.json | 清空原作者的重定向规则 |
一个重要的教训:原作者的 README 里明确写了「严禁将项目内我(原作者)的文章以你的名义重新发布」。Fork 后第一步就应该把 content/posts/ 里的所有文章删干净,再开始写自己的。不要留着「当模板参考」—— 万一不小心推送到了公开仓库,就是版权问题。
自定义:不只是改配置
换完基本信息之后,开始动刀更多定制:
导航和侧栏
Clarity 默认只有「文章」「友链」「归档」三个入口。我逐步加到了 10 个 —— 即刻、标签、友圈、番剧、项目、游戏、关于。每加一个页面就配一个侧栏组件(公告卡片、一言、GitHub 卡片、最新评论等)。
这个过程教会我一件事:Nuxt 的自动导入和约定路由太省心了。新建 app/pages/tags.vue,它自动就是 /tags 页面。新建 app/components/content/ 下的 .vue 文件,Markdown 里就能直接用。
统计和评论
这部分主要是接入外部服务。用了 Umami(自建统计分析)、Cloudflare Web Analytics(辅助统计)和 Twikoo(评论系统)。都是浏览器端加载的 —— 把 JS 脚本和对应的 ID/Token 填进 blog.config.ts 的 scripts 数组,构建时不需要它们可达。所以即使博客是纯静态站,这些动态功能一样能用 —— 评论和统计的 JS 是用户浏览器自己去请求的,不经过服务器。
友链系统
这可能是自己实现得最满意的一个功能。不想依赖外部 Friend-Circle-Lite 后端,就在 server/api/friend-circle.get.ts 里写了一个构建时 RSS 聚合器—— 读取 app/feeds.ts 里每个友链的订阅源地址,逐一抓取并解析 Atom/RSS,合并成友圈数据。
这样做的好处:
- 零外部依赖,不用部署任何额外后端
- 每次
bash deploy.sh刷新数据 - 走同源接口,无跨域问题
组件库
这也是折腾最多的地方。Clarity 自带了不少组件(::alert、::quote、::pic、::chat、::timeline、::card-list、::tab、::folding 等),写文章时已经够用。但后来在写教程类文章时发现 —— 多层嵌套列表太长了,折叠框缩进容易出 bug,有些视觉表达没合适的组件。试着移植过 Stellar 主题的组件,踩了 MDC 语法和缩进的一堆坑。
最终的教训是:不要从零手写组件然后猜样式。要么从同架构的 fork 里移植,要么从真实渲染页面截取 DOM 结构当设计稿。
部署:从 Vercel 到 1Panel
最初想直接推 Vercel,但考虑到自己有服务器、想完全掌控,最终还是选了自托管。
服务器是雨云(RainYun)的 Debian 机器,面板用的 1Panel。部署流程简化后就是:本地 git push → 服务器 bash deploy.sh(拉代码 → 构建 → 拷贝到 nginx 目录)。
几个关键配置:
- 网站类型:1Panel 创建时选「静态网站」,root 指到
.output/public - 伪静态:
try_files $uri $uri.html $uri/ /404.html(否则文章页刷新 404) - HTTPS:1Panel 里一键申请 Let's Encrypt + 开启强制跳转
- deploy.sh:封装了
git pull && npx nuxi generate && rm -rf 目标目录/* && cp
踩坑复盘
整个搭建过程踩过的坑,挑几个印象最深的:
坑 1:服务器 Node 版本太低
Nuxt Content v3 需要 Node 22.5+ 才支持 node:sqlite。服务器初始是 Node 22.0,导致内容数据库读不出来,首页一篇文章都没有。排查了很久才发现是 SQLite 连接器的问题。升级到 Node 24 后解决。
坑 2:MDC 组件缩进
::tab、::timeline、::folding 这些块级组件的闭合标记 :: 必须顶格,不能和内容对齐缩进。一旦缩进错了,MDC 解析器不会报错,只是默默地把你后面所有内容都吞掉。这是最难排查的一类 bug,因为看起来语法完全正确。
坑 3:行内组件用单冒号
::mark 双冒号在 MDC 里被当成块级组件处理,不会渲染。行内组件(mark、kbd、wavy 等)必须用单冒号 :mark[text]{color=red}。
坑 4:GitHub 图在服务器上超时
首页即刻卡片引用了 raw.githubusercontent.com 的冰山图截图。本地好好的,服务器一构建就卡住 —— 因为雨云在国内,GitHub 的 raw 域名经常不可达。后来把图片移到了自己的图床才稳定。
坑 5:Prerender 外链超时
构建时会爬取所有外链(友链、图片、参考链接),有些友链站返回 404 或超时就把整个构建中断了。后来在 nuxt.config.ts 加了两个配置:
nitro: { prerender: { failOnError: false } }, // 单页失败不中断
linkChecker: { skipInspections: ['no-error-response'] }, // 跳过外链存活检查
写在最后
从一个 Fork 到现在的样子,这个博客经历了十几个小时的高强度改造。但它给我带来的不仅是「有了一个博客」—— 更重要的是对自己部署链路的完整掌控,以及对 Nuxt + Nuxt Content 这个技术栈的深入理解。
如果你也想搭一个类似的博客,希望这篇复盘能帮你少走一些弯路。
博客不只是发表文章的地方,也是记录自己成长的地方。这个博客本身,就是最好的第一篇作品。
评论区
评论加载中...