博客从 2013 年开到现在,经历了 GitHub Pages、WordPress、自建 Django、Hexo 一连串折腾。这次换到 Astro,一天内完成,全程用 Claude Code 辅助。
为什么换 Astro
Hexo 用了 10 年,问题也攒了 10 年。373 篇全量构建要 30 秒以上。想改样式得进 themes/next/ 的 ejs,一改就和主题升级冲突。主流插件多年没更新。db.json 有 19 MB,public/ 100 MB。ejs 模板加前端 JS 的组合也不好 debug。
Astro 的吸引点有几个。默认纯静态 HTML,需要交互再加一个 island。Content Collections 能对 frontmatter 做类型检查,还能自动生成 TS 类型。Vite 的增量构建几百毫秒就完事。组件层不锁框架,React、Vue、Svelte 都能塞。
基础设置
技术栈:
Astro 6.1.9 + @astrojs/mdx 5.0.4 + @astrojs/sitemap + @astrojs/rss
Node 22 (engines 锁死)
pnpm 9
TypeScript 严格模式
项目结构:
src/
├── content/
│ ├── posts/ # 所有文章(tech + life 合并)
│ ├── products.yaml # 产品清单
│ └── ...
├── layouts/
│ ├── BaseLayout.astro
│ └── PostLayout.astro
├── pages/
│ ├── [...slug].astro # 老 URL 保留的 catchall
│ ├── blog/index.astro # 全部文章归档
│ ├── about.astro
│ └── rss.xml.ts
├── config/site.ts # 站点配置 + 隐身模式 env gate
└── lib/postUrl.ts
内容迁移
写了一个脚本把 Hexo 的 373 篇 markdown 一次性搬到 Astro 的 content collection,统一 frontmatter、图片路径和日期格式。过程中记几条。
Hexo 允许 created: 代替 date:,我多年两个字段混用。脚本起初只读 date:,没有 date: 的文章全被塞了默认日期挤到 2017 年度。修复办法是 date ?? created ?? 默认 三级兜底。
Shiki 的代码语言标签比 Hexo 严格。我过去乱写 react、mysql、django、vuejs 不报错,Astro 全报。用 sed 统一换成 jsx、sql、python、vue。
老图片 84 MB 不想进 git。legacy-hexo/source/uploads/ 保留在仓库,public/uploads/ 进 .gitignore,构建时 prebuild 钩子复制过去。
有篇 address-favorites.md frontmatter 里嵌了双引号,YAML 解析失败。改成单引号外包、内部不转义就好。
URL 保留(SEO)
老文章的 URL 不能变,否则 Google 索引全部失效。Astro 的做法是在 frontmatter 加 legacyUrl:
---
title: Docker 使用手册
legacyUrl: /docker/
---
然后在 src/pages/[...slug].astro 里写 catchall:
export async function getStaticPaths() {
const posts = await getCollection('posts',
({ data }) => !data.draft && !!data.legacyUrl)
return posts.map((entry) => {
const slug = entry.data.legacyUrl.replace(/^\/+|\/+$/g, '')
return { params: { slug }, props: { entry } }
})
}
带 legacyUrl 的文章都在原路径渲染。新文章走 /blog/<slug>/。
Astro 默认不加尾斜杠,Hexo 加。Netlify 里用 301 统一:
# netlify.toml
[[redirects]]
from = "/:slug"
to = "/:slug/"
status = 301
force = false
外加 astro.config.mjs 设 trailingSlash: 'always' 保持一致。
评论系统
从 Disqus、Giscus、Utterances、Remark42、Waline、Cusdis、Artalk、Twikoo 里选了 Giscus。理由是零后端(评论存在 GitHub Discussions 里),国内访问走 giscus.app CDN 不经过 GitHub 主站所以不被墙,11.6k 星活跃维护,Markdown 和 reactions 都支持。
一个问题:我的代码仓库是私有的(stealth 模式),但 Giscus 要求评论仓库公开,否则只有 collaborator 能评论。我新建了空仓库 haoflynet-comments,放个 README 说明用途,打开 Discussions,装 Giscus App。评论就沉淀在这里,代码仓库不暴露。
SEO 加固
Astro 给了基础(静态预渲染、sitemap、canonical),中高层需要自己补:
| 项 | 位置 | 作用 |
|---|---|---|
robots.txt | public/robots.txt | 允许抓取 + sitemap 指向 |
JSON-LD BlogPosting | PostLayout.astro | 搜索结果富摘要 |
JSON-LD BreadcrumbList | 同上 | 面包屑结构化数据 |
og:type=article + article:published_time | BaseLayout 动态 | 社交卡片 |
| 默认 og:image 兜底 | 同上 | 分享不裸奔 |
robots=noindex(stealth 时) | 条件渲染 | 防止 dev/预览被索引 |
Google Fonts 在国内被墙,LCP 直接拖 3 到 5 秒。换成 fonts.loli.net 镜像,一行改完:
<link href="https://fonts.loli.net/css2?family=Inter..." rel="stylesheet" />
部署
Netlify 配置:
# netlify.toml
[build]
command = "pnpm build"
publish = "dist"
[build.environment]
NODE_VERSION = "22"
PNPM_VERSION = "9"
自定义域名 haofly.net 和 ICP 备案(渝ICP备14004550号-1)都保留。
Secret 防护四层:gitleaks pre-commit 在 commit 前阻断;.env 进 gitignore 放本地 token;Netlify 默认的部署产物扫描;Giscus 等 PUBLIC_ env var 严格校验,生产没设就 fail。
分类换成标签
Hexo 时代的分类体系(backend/frontend/tools/devops/database/languages/platform/ai/life)用了多年,但每篇只打一个分类,tags 几乎没用。Astro 迁移时改成纯标签。分类映射到中文主标签(后端、前端、工具、运维、数据库、生活),再根据文件名关键字自动推断二级标签(Python、Django、Docker、MySQL、Redis、React 等)。一个脚本处理完 373 篇,230 篇只有 1 个标签,143 篇拿到 2 个以上。标签云从 9 个「类别」变成 70 多个可导航标签,长尾更可查。
所用工具
| 用途 | 工具 |
|---|---|
| AI 辅助 | Claude Code(全程) |
| Netlify 管理 | @netlify/mcp MCP server |
| Secret 扫描 | gitleaks |
| 字体 | fonts.loli.net 镜像 |
| 评论 | Giscus |
| 监控 | Google Analytics 4 |
评论 · Comments
评论由 Giscus 提供,需用 GitHub 账号登录;留言会同步到这个仓库的 Discussions 里。