这是 wzk.icu 重构后的第一篇文章,记录一下这次重构的过程和选型理由。

为什么要重构

原来的站点是一个手写的 HTML + CSS 页面,有点好看,但维护起来很痛苦:

  • 想写博客?没有,只能在 CSDN / 掘金发,流量都贡献给了别人的平台
  • 想加新内容?直接改 HTML,每次都得小心不破坏样式
  • SEO?几乎没有,meta 标签就是随便写的
  • 版本管理?代码在 Git 里,但内容和代码完全混在一起

目标很明确:能方便地用 Markdown 写文章,有完整的 SEO 基础设施,风格统一,可以长期维护。

为什么选 Astro

考虑过几个方案:

方案优点为什么没选
Next.js生态大、功能强太重,个人站不需要 SSR,Vercel 免费额度有限
Hugo极快、纯静态模板语言学习成本高,不如 JSX/组件化直观
VitePress专注文档不适合做门户页,定制性差
AstroSSG、组件化、内容集合✅ 正好

Astro 的核心优势:

  • Island Architecture:默认零 JS,只有需要交互的组件才加载 JS
  • Content Collections:内置的 Markdown 内容管理,有 Schema 校验
  • 框架无关:可以用 React/Vue/Svelte 写组件,但这个站用纯 .astro 就够了

遇到的坑

Bun 替代 npm

Node 18 版本太低,npm create astro 直接报错。换用 Bun:

bun create astro@latest

Bun 比 npm 快很多,包安装速度体感有 3-5 倍的差距。但要注意所有命令都得用 bun run,不能混用 npm。

Astro 6 的 Content Collections 新 API

网上大多数教程还是 Astro 4/5 的写法,Astro 6 有几处 breaking change:

配置文件位置变了:

旧:src/content/config.ts
新:src/content.config.ts(移到根目录)

Loader 写法变了:

// 旧写法
const blog = defineCollection({ type: 'content', schema: z.object({...}) });

// 新写法(Astro 6)
import { glob } from 'astro/loaders';
const blog = defineCollection({
  loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
  schema: z.object({...}),
});

文章渲染变了:

<!-- 旧:需要异步调用 render() -->
const { Content } = await post.render();
<Content />

<!-- 新:直接用 rendered.html -->
<div set:html={post.rendered.html} />

路由参数变了:

// 旧:post.slug
// 新:post.id
params: { slug: post.id }

这几个坑在迁移时全踩了,花了不少时间 debug。

Fixed Header 遮挡内容

Header 设置了 position: fixed,但页面 main 没有对应的 padding-top,内容被遮住一截。

解法:每个页面的 mainpadding-top: 56px(与 Header 高度一致)。

SEO 基础设施

这次重构把 SEO 当成一等需求,而不是事后补上:

  • SeoHead 组件:统一管理 <title>、meta description、OG、Twitter Card、canonical
  • JSON-LD:Person + WebSite + Article 结构化数据,让搜索引擎和 AI 正确识别内容
  • Sitemap@astrojs/sitemap 自动生成
  • robots.txt:动态生成,指向 sitemap
  • RSS Feed:博客 + 项目合并订阅
  • llms.txt:专门为 AI 搜索引擎写的站点说明(类似 robots.txt,但面向 LLM)

结果

构建后是纯静态 HTML,托管在 Vercel 上,加载速度极快,维护成本低。写文章只需要在 src/content/blog/ 下新建一个 Markdown 文件,推送到 GitHub,Vercel 自动构建部署。

这才是个人站应该有的形态。