Back to Blog
· 3 min read

Next.js 到 Astro

Web开发

人在无聊时会做什么?—— 重构博客 :)

不像项目需要考虑团队协作、不像工具需要考虑通用性,博客从内容到形式都可以完全按照自己的审美来。最早用 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

核心变化:

  1. Content Collections:Astro 6.0 的内容集合 API,用 schema 定义博客内容结构,类型安全
  2. 组件模式client:* 指令控制 React 组件的水合时机
  3. 布局封装: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-remotecontentlayer,而 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 的思路很对胃口:少做不必要的事,把必要的事做好