用 Astro 搭建博客:从零到容器化部署
这个博客最近刚从 Next.js 重构为 Astro,正好借这篇文章记录一下:Astro 是什么、为什么它适合搭博客,以及完整的搭建过程。
Astro 是什么
Astro 是一个面向内容站点的 Web 框架,官方口号是 “The web framework for content-driven websites”。博客、文档站、营销页、作品集——凡是“内容为主、交互为辅”的站点,都是它的主场。
它有几个非常鲜明的特点:
1. 默认零 JavaScript
这是 Astro 与 Next.js、Nuxt 等框架最本质的区别。Astro 默认在构建时把页面渲染成纯 HTML,浏览器拿到的就是一个静态页面,没有任何框架运行时。
对比一下:用 Next.js 搭博客,即使页面不需要交互,浏览器仍然要下载并执行 React 运行时;而 Astro 构建出来的页面,HTML 就是全部。
只有真正需要交互的地方,才用“岛屿(Islands)架构”按需注入 JavaScript。博客这种 99% 都是内容的站点,最终产物几乎就是零 JS。
2. 文件即路由
src/pages/ 下的文件结构就是路由结构:
src/pages/
├── index.astro → /
├── about.astro → /about/
└── posts/[slug].astro → /posts/<slug>/
不需要专门注册路由,文件放对位置就行。
3. Content Collections:为 Markdown 而生
这是我最喜欢的特性。Astro 内置了类型安全的内容管理,用 glob loader 指定文章目录,用 Zod schema 声明 frontmatter 的结构:
// src/content.config.ts
import { defineCollection } from "astro:content";
import { glob } from "astro/loaders";
import { z } from "astro/zod";
const posts = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./posts" }),
schema: z.object({
title: z.string(),
date: z.coerce.date(),
excerpt: z.string().optional(),
}),
});
export const collections = { posts };
frontmatter 写错字段名、日期格式不合法,构建时直接报错——文章多了以后这个保障非常值钱。
搭建一个博客
初始化项目
pnpm create astro@latest my-blog
cd my-blog
pnpm dev
浏览器打开 http://localhost:4321 就能看到默认页面。然后再把 Tailwind 加上:
pnpm add tailwindcss @tailwindcss/vite
// astro.config.mjs
import { defineConfig } from "astro/config";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
site: "https://example.com",
trailingSlash: "always", // /posts/foo/ 而不是 /posts/foo
vite: { plugins: [tailwindcss()] },
});
目录结构
一个典型的 Astro 博客长这样:
my-blog/
├── astro.config.mjs # 站点配置
├── posts/ # Markdown 文章
│ └── hello.md
├── src/
│ ├── content.config.ts # 内容集合定义
│ ├── layouts/
│ │ └── BaseLayout.astro
│ ├── components/
│ │ ├── Header.astro
│ │ └── Footer.astro
│ └── pages/
│ ├── index.astro # 首页(文章列表)
│ └── posts/
│ └── [slug].astro # 文章详情页
└── public/ # 静态资源
写一篇文章
在 posts/ 下新建 Markdown 文件,带上 frontmatter:
---
title: 你好,Astro
date: 2026-11-01
excerpt: 我的第一篇 Astro 博客。
---
正文支持 GFM:表格、任务列表、删除线、脚注都是内置的。
首页:列出所有文章
.astro 文件由两部分组成:顶部 --- 之间是构建时执行的组件脚本,下面是类 JSX 的模板语法。
---
// src/pages/index.astro
import BaseLayout from "../layouts/BaseLayout.astro";
import { getCollection } from "astro:content";
// 取出全部文章并按日期倒序
const posts = (await getCollection("posts"))
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
---
<BaseLayout title="我的博客">
{posts.map((post) => (
<article>
<a href={`/posts/${post.id}/`}>
<h2>{post.data.title}</h2>
</a>
<time>{post.data.date.toISOString().split("T")[0]}</time>
{post.data.excerpt && <p>{post.data.excerpt}</p>}
</article>
))}
</BaseLayout>
文章页:动态路由
[slug].astro 用 getStaticPaths 告诉 Astro 有哪些文章,构建时逐一生成静态页面:
---
// src/pages/posts/[slug].astro
import { getCollection, render } from "astro:content";
export async function getStaticPaths() {
const posts = await getCollection("posts");
return posts.map((post) => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
---
<h1>{post.data.title}</h1>
<div class="prose">
<Content />
</div>
<Content /> 就是 Markdown 渲染出来的 HTML,配合 Tailwind 的 typography 插件(prose 类),排版开箱即用。
代码高亮:Shiki 双主题
Astro 内置 Shiki 高亮,在配置里声明明暗两套主题:
// astro.config.mjs
export default defineConfig({
markdown: {
shikiConfig: {
themes: { light: "github-light", dark: "github-dark" },
},
},
});
再用几行 CSS 让代码块跟随站点的深色模式切换:
.dark .astro-code,
.dark .astro-code span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
}
深色模式:十几行搞定
不需要任何第三方库。在 <head> 里放一段内联脚本,在页面渲染前根据 localStorage 或系统偏好设置 dark 类,避免闪烁:
<script is:inline>
(function () {
var stored = localStorage.getItem("theme");
var dark =
stored === "dark" ||
(stored !== "light" &&
window.matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.classList.toggle("dark", dark);
})();
</script>
再加一个切换按钮,点击时切换类并写入 localStorage 即可。原来 Next.js 版需要 next-themes + React 组件才能做到的事,在 Astro 里就是纯 HTML + 原生 JS。
构建与部署
pnpm build # 输出到 out/,每个路由一个目录 + index.html
产物是彻头彻尾的静态文件,部署方式随意:
- 对象存储:阿里云 OSS / 腾讯云 COS / S3 开静态托管,把
out/传上去 - 服务器:rsync 到 Nginx 目录,或者干脆容器化
本站目前是 Docker 多阶段构建:Node 镜像里 pnpm build,产物复制进 nginx:alpine,最终镜像不到 100MB:
FROM node:22-alpine AS build
WORKDIR /app
RUN npm install -g pnpm@10
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/out /usr/share/nginx/html
迁移后的实际收益
最后晒一下这次重构的账单:
| 指标 | Next.js 版 | Astro 版 |
|---|---|---|
| 运行时依赖 | 约 20 个 | 6 个 |
| 客户端 JS | React + next-themes 等 | 0 |
| 代码高亮 | 引入了 CSS 但没生效 | Shiki 双主题,构建时生成 |
| 构建速度 | 较慢 | Rust 编译器,4 个页面几秒完成 |
当然,Astro 不是银弹。如果你的站点有大量复杂交互(管理后台、实时协作),Next.js 这类框架仍然更合适。但对于博客这种“写 Markdown、发文章”的场景,Astro 的哲学——默认不发 JS、内容优先——恰好是对症的。
网站搭好之后,剩下的就是好好写文章了。与诸君共勉。