Next.js 到 Astro
人在无聊时会做什么?—— 重构博客 :)
不像项目需要考虑团队协作、不像工具需要考虑通用性,博客从内容到形式都可以完全按照自己的审美来。最早用 Hexo,后来用 Hugo,再后来觉得静态站点生成器不够”现代”,切到了 Next.js。
这一次是从 Next.js 迁移到 Astro。
为什么迁移
Next.js 作为一个完整的 React 框架,支持 SSR、SSG、API Routes、Image 优化……但我的博客只需要 SSG。90% 的功能都浪费了。
// Next.js 的博客可能长这样
export async function getStaticProps() {
const posts = await getAllPosts();
return { props: { posts } };
}
功能是有的,但配置优化有点麻烦,构建速度对博客来说偏慢,部署配置也相对复杂(其实还是懒)。
Astro 的定位很清晰——内容密集型网站的框架。
---
const posts = await getCollection('blog');
---
{posts.map(post => (
<article>
<h2>{post.data.title}</h2>
<Content />
</article>
))}
这才是博客该有的样子。配置少、构建快、输出纯静态。
架构变化
Next.js
src/
├── app/
│ ├── blog/
│ │ ├── page.js
│ │ └── [slug]/
│ │ └── page.js
│ ├── page.js
│ └── layout.js
├── components/
├── content/
└── lib/
**Astro **
src/
├── pages/
│ ├── blog/
│ │ ├── index.astro
│ │ ├── [slug].astro
│ │ └── page/
│ │ └── [...page].astro
│ ├── index.astro
│ └── projects.astro
├── layouts/
│ └── BlogLayout.astro
├── components/
│ ├── Mermaid.jsx
│ └── ...
├── content/
│ ├── config.ts
│ └── blog/
│ ├── 2021.md
│ └── ...
└── lib/
└── remark-mermaid.ts
核心变化:
- Content Collections:Astro 6.0 的内容集合 API,用 schema 定义博客内容结构,类型安全
- 组件模式:
client:*指令控制 React 组件的水合时机 - 布局封装:Layout 作为独立的
.astro文件,而不是 Higher Order Component
内容组织
博客内容放在 src/content/blog/,每篇文章是独立的 .md 文件:
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
date: z.date(),
categories: z.array(z.string()),
tags: z.array(z.string()),
}),
});
不再需要 frontmatter 的默认值配置,schema 验证一步到位。
Markdown 处理
用 Next.js 的时候,Markdown 渲染依赖 next-mdx-remote 或 contentlayer,而 Astro 原生支持 MDX:
---
import { render } from 'astro:content';
const { post } = Astro.props;
const { Content } = await render(post);
---
<Content />
Markdown 内容直接渲染,不需要额外的远程处理。
Mermaid 图表支持
技术博客难免要画图,Mermaid 是最方便的选择,但在渲染方式上着实费了一番功夫,经过了几版迭代。
第一版:remark 插件 + 客户端渲染
最初的方案是在构建时用 remark 插件把 mermaid 代码块转换成 HTML 注释,浏览器加载时再用 JS 渲染:
// src/lib/remark-mermaid.ts
export function remarkMermaid() {
return (tree) => {
visit(tree, 'code', (node) => {
if (node.lang === 'mermaid') {
node.type = 'html';
node.value = `<!-- mermaid:start -->${node.value}<!-- mermaid:end -->`;
delete node.lang;
}
});
};
}
客户端通过 TreeWalker 找到注释节点,替换成 SVG。
问题:依赖 DOM 操作,TreeWalker 可能漏掉节点,渲染失败时用户看到空白或源码,这里记得好几次在图表中有特殊字符时出现解析异常的问题,时有发生图表和源码同时显示。
第二版:astro-mermaid
后来换成了 astro-mermaid 集成包,输出 <pre class="mermaid"> 代替注释,渲染失败时保留源码可见。
配置也简洁了:
// astro.config.mjs
import mermaid from 'astro-mermaid';
export default defineConfig({
integrations: [
mermaid({
theme: 'base',
mermaidConfig: { securityLevel: 'loose' }
}),
],
});
不再需要手写的 remark 插件,也不再需要在页面里写 TreeWalker。
样式系统
博客的样式用过不少方案:Styled Components、CSS Modules、Tailwind……最后定格在 Tailwind + CSS Variables。
/* BlogLayout.astro */
:root {
--color-bg: #faf9f7;
--color-text: #1a1a1a;
--color-accent: #b45309;
}
.prose {
font-family: 'Source Sans 3', sans-serif;
line-height: 1.8;
}
.prose h1, .prose h2 {
font-family: 'Cormorant Garamond', serif;
}
CSS Variables 负责主题色,Tailwind 负责工具类,两者配合各司其职。
响应式设计只是简单用 Utilities 处理了一下,毕竟不怎么在手机上看。
<main class="px-4 sm:px-6 max-w-3xl mx-auto">
没有媒体查询,简单粗暴。
国际化
博客短暂支持过中英文切换。
const chinesePosts = posts.filter(
(post) => (post.data.lang || 'zh') !== 'en'
);
思路是每个文章有 .md 和 .en.md 两个版本,路由根据 URL 前缀区分。后来觉得维护两套内容太麻烦,而且博客的主要读者是中文用户(其实就我自己看),就回退到了只保留中文版本。
国际化是双刃剑。多语言内容看起来专业,但维护成本是线性的。如果不是真的有需求,也没必要做,毕竟现在 AI 在线翻译插件一大堆。
部署
静态博客的部署很简单。Vercel 和 Netlify 都支持 Astro,原生接入。
# .nvmrc
22
Node 版本锁定在 22.12.0 以上,这是 Astro 6.0 的要求。
构建产物是纯静态文件,可以部署到任何 CDN。之前折腾过的 GitHub Pages、Cloudflare Pages 都可以,只是现在图省事用了 Vercel。
迁移到 Astro 后,博客的:
- 构建速度:从 ~1min 到 ~15s
- 代码量:减少约 40%(移除 Next.js 配置)
- 维护成本:明显降低
Astro 的思路很对胃口:少做不必要的事,把必要的事做好。