From Next.js to Astro
What do people do when bored? Rebuilding the blog :)
Unlike projects that require team collaboration, or tools that need to be general-purpose, a blog can follow my own taste completely, from content to form. I started with Hexo, then Hugo, and later felt static site generators weren’t “modern” enough, so I switched to Next.js.
This time I migrated from Next.js to Astro.
Why migrate
As a full React framework, Next.js supports SSR, SSG, API Routes, Image optimization… but my blog only needs SSG. 90% of the features are wasted.
// Next.js 的博客可能长这样
export async function getStaticProps() {
const posts = await getAllPosts();
return { props: { posts } };
}
The features are there, but configuration and optimization are a bit troublesome, build speed is slow for a blog, and deployment setup is relatively complex (honestly, I’m just lazy).
Astro’s positioning is clear: a framework for content-heavy websites.
---
const posts = await getCollection('blog');
---
{posts.map(post => (
<article>
<h2>{post.data.title}</h2>
<Content />
</article>
))}
That’s what a blog should be. Less configuration, fast builds, pure static output.
Architecture changes
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
Core changes:
- Content Collections: Astro 6.0’s content collections API, using schemas to define blog content structure, type-safe
- Component patterns:
client:*directives control when React components hydrate - Layout encapsulation: Layout as a standalone
.astrofile, not a Higher Order Component
Content organization
Blog content lives in src/content/blog/, with each post as a separate .md file:
// 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()),
}),
});
No more frontmatter default value configuration; schema validation happens in one step.
Markdown Processing
With Next.js, Markdown rendering relied on next-mdx-remote or contentlayer, but Astro has native MDX support:
---
import { render } from 'astro:content';
const { post } = Astro.props;
const { Content } = await render(post);
---
<Content />
Markdown content renders directly, with no extra remote processing needed.
Mermaid Diagram Support
Technical writing inevitably needs diagrams, and Mermaid is the most convenient option. But getting the rendering right took real effort and went through several iterations.
First iteration: remark plugin + client-side rendering.
The initial approach used a remark plugin at build time to convert mermaid code blocks into HTML comments, then rendered them with JS on page load:
// 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;
}
});
};
}
The client uses TreeWalker to find comment nodes and replace them with SVG.
Problem: It relies on DOM manipulation. TreeWalker can miss nodes, and when rendering fails users see blank space or raw source. I remember several times when special characters in diagrams caused parsing errors, and occasionally both the diagram and source code would show up at once.
Second version: astro-mermaid.
Later I switched to the astro-mermaid integration package, which outputs <pre class="mermaid"> instead of comments, keeping the source visible when rendering fails.
The configuration is also cleaner:
// astro.config.mjs
import mermaid from 'astro-mermaid';
export default defineConfig({
integrations: [
mermaid({
theme: 'base',
mermaidConfig: { securityLevel: 'loose' }
}),
],
});
No more hand-written remark plugins, and no more TreeWalker code in pages.
Styling system
The blog has gone through quite a few styling approaches: Styled Components, CSS Modules, Tailwind… and finally settled on 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 handle theme colors, Tailwind handles utility classes. Each does its own job.
Responsive design was handled simply with utilities, since I rarely read it on a phone anyway.
<main class="px-4 sm:px-6 max-w-3xl mx-auto">
No media queries. Simple and crude.
Internationalization
The blog briefly supported switching between Chinese and English.
const chinesePosts = posts.filter(
(post) => (post.data.lang || 'zh') !== 'en'
);
The idea was that each article would have .md and .en.md versions, with routing distinguished by URL prefix. Later I felt maintaining two sets of content was too much trouble, and since the blog’s main readers are Chinese users (actually just me), I reverted to keeping only the Chinese version.
Internationalization is a double-edged sword. Multilingual content looks professional, but the maintenance cost is linear. If there’s no real demand, there’s no need to do it. After all, there are plenty of AI-powered online translation plugins now.
Deployment
Deploying a static blog is simple. Both Vercel and Netlify support Astro with native integration.
The Node version is locked to 22.12.0 or higher, which is a requirement for Astro 6.0.
The build output is purely static files, deployable to any CDN. GitHub Pages and Cloudflare Pages, which I had experimented with before, both work. I just use Vercel now for convenience.
After migrating to Astro, the blog’s:
- Build speed: from ~1min to ~15s
- Code volume: reduced by about 40% (removed Next.js configuration)
- Maintenance cost: significantly lower
Astro’s philosophy really suits my taste: Do less of what doesn’t matter, and do what matters well.