博客前端重构:从 Astro 跑路到 Next.js

博客前端重构:从 Astro 跑路到 Next.js

2026年2月2日
Next.js

博客前端重构:从 Astro 跑路到 Next.js

起因

前段时间把博客从 Astro 迁移到了 Next.js,折腾了好一阵子,趁还没忘干净赶紧记录一下。

其实 Astro 用得好好的,为啥要换?主要是我自己作的。

博客一开始就是个简单的内容站,Astro 的岛屿架构确实香——页面默认纯静态 HTML,需要交互的地方才加载 JS,性能拉满。但后来我往里面塞了越来越多东西:登录注册、Markdown 在线编辑器、小说大纲设计器……交互组件越来越多,几乎每个页面都得挂 client:load,岛屿架构的优势基本没了。

另外当时选的 UI 框架是 SolidJS。SolidJS 本身没毛病,响应式做得比 React 优雅多了,性能也好。但问题是生态太小众了,很多 React 生态里现成的轮子没法用,得自己造或者找平替。时间长了就有点累。

想了想,既然交互这么多,干脆换成 Next.js + React 算了。React 生态成熟,想用什么库基本都有,Next.js 的 App Router 对 SSR 支持也很完善。

换了啥

简单列一下迁移前后的变化:

之前之后
框架Astro 5.8Next.js 16.1
UISolidJSReact 19
样式Tailwind + DaisyUI没换,照搬
图标lucide-solidlucide-react
Markdown 渲染自己写的 WASMreact-markdown

样式层基本是无缝迁移,Tailwind 的 class 直接复制过去就能用,这点省了不少事。

目录结构的变化

Astro 那边的结构是这样的:

blog-web/src/
├── pages/              # astro 页面
│   ├── index.astro
│   ├── blog/[slug].astro
│   └── ...
├── components/
│   ├── Header.astro    # 静态组件
│   ├── Footer.astro
│   └── solid/          # 交互组件单独放
│       ├── BlogPosts.tsx
│       ├── AuthForm.tsx
│       └── ...

Astro 里静态组件和交互组件是分开的,.astro 写静态的,SolidJS 的 .tsxsolid/ 目录下,页面里用 client:load 激活。

换到 Next.js 之后:

frontend/
├── app/                # App Router
│   ├── layout.tsx
│   ├── page.tsx
│   ├── blog/[slug]/page.tsx
│   └── ...
├── components/         # 组件全放一起
│   ├── Header.tsx
│   ├── Footer.tsx
│   ├── BlogPosts.tsx
│   └── ...

不用再分「这个是静态组件」「那个是交互组件」了。Next.js 里想让组件在客户端跑就加个 "use client",否则默认服务端渲染,逻辑清晰很多。

踩的坑

1. SolidJS 转 React 的语法差异

这块是工作量最大的。虽然都是 JSX,但写法区别还挺多。

信号变状态:

// SolidJS - 信号要调用才能拿到值
const [count, setCount] = createSignal(0);
<span>{count()}</span>

// React - 直接用
const [count, setCount] = useState(0);
<span>{count}</span>

副作用:

// SolidJS - 自动追踪依赖,不用手动写
createEffect(() => {
  console.log(count());
});

// React - 得手动声明依赖数组
useEffect(() => {
  console.log(count);
}, [count]);

列表渲染:

// SolidJS
<For each={items()}>{(item) => <div>{item.name}</div>}</For>

// React
{items.map((item) => <div key={item.id}>{item.name}</div>)}

看着好像差不多,但十几个组件一个个改过来还是挺费神的。主要是怕漏改,SolidJS 的信号调用忘了去掉括号页面就白屏。

2. 主题切换闪一下的问题

我用的 DaisyUI,主题是通过 data-theme 属性切换的。用户选了深色主题,下次打开页面,因为服务端不知道用户偏好,先渲染了浅色,等客户端 JS 跑起来再切成深色——于是就闪了一下。

解决办法是在 <head> 里塞一段同步脚本,页面渲染之前就把主题设好:

const themeScript = `
  try {
    const savedTheme = localStorage.getItem("theme");
    if (savedTheme) {
      document.documentElement.setAttribute("data-theme", savedTheme);
    } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
      document.documentElement.setAttribute("data-theme", "dark");
    } else {
      document.documentElement.setAttribute("data-theme", "light");
    }
  } catch (e) {
    document.documentElement.setAttribute("data-theme", "light");
  }
`;

然后 <html> 上加个 suppressHydrationWarning,不然 React 会警告服务端和客户端渲染结果不一致。

3. API 地址要分环境

这个坑卡了我一阵子。

博客是 Docker 部署的,前后端分开的容器。服务端渲染的时候,Next.js 容器要访问后端 API,这时候走 Docker 内网就行(比如 http://backend:8080)。但客户端浏览器发请求,得走公网域名(https://api.xxx.com)。

所以 axios 封装的时候得判断一下环境:

if (typeof window === 'undefined') {
  // 服务端,走内网
  apiUrl = process.env.INTERNAL_API_BASE_URL;
} else {
  // 客户端,走公网
  apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
}

之前没注意这个,服务端渲染直接用公网地址,绕了一大圈,慢得要死。

4. Markdown 渲染换方案

原来我自己用 Rust 写了个 Markdown 渲染器,编译成 WASM 在浏览器跑。性能是挺好的,但维护起来麻烦,而且换到 React 之后还得再封装一层。

想了想算了,直接换成 react-markdown,配合 rehype-highlight 做代码高亮,remark-gfm 支持 GitHub 风格的表格和删除线。生态成熟,插件丰富,博客这点内容渲染性能根本不是瓶颈。

5. SEO 相关

Astro 那边我是手写了个 SEO.astro 组件,手动拼 meta 标签,挺土的。

Next.js 内置了 Metadata API,直接导出一个 metadata 对象就行:

export const metadata: Metadata = {
  title: "页面标题",
  description: "页面描述",
  openGraph: { ... },
};

动态页面(比如博客文章详情)还能用 generateMetadata 函数根据参数生成。比手拼舒服多了。

顺便还给文章页加了 JSON-LD 结构化数据,对搜索引擎收录友好一点。

6. Sitemap 和 Robots

Astro 用 @astrojs/sitemap 插件自动生成。Next.js 里改成在 app/ 下建 sitemap.tsrobots.ts 文件,编程式生成。好处是可以动态拉取所有文章的 slug,不用每次手动更新。

迁移完的感受

折腾完之后确实舒服多了:

  • 不用在 Astro 和 SolidJS 两套语法之间来回切了
  • 想找个什么组件库基本都有 React 版本
  • 服务端组件直接 async/await 取数据,客户端组件用 useEffect,职责分得很清楚
  • SEO 那块不用自己操心了

要说缺点的话,打包产物比 Astro 大一些,毕竟 React 运行时不是白给的。不过对于我这种已经重度交互的站来说,这点差异无所谓了。

给想迁移的朋友一点建议

  • 样式层(Tailwind、DaisyUI)基本无缝迁移,不用担心
  • SolidJS 转 React 主要是响应式模型不同,信号改状态,别忘了去掉调用括号
  • SSR 场景下注意区分服务端和客户端的 API 地址
  • 主题切换记得处理闪烁问题
  • 不要一口气全改,先把框架搭好,再一个页面一个页面迁移,出问题好排查

就这些,希望对有类似需求的朋友有点帮助。