← 返回首页

用 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、内容优先——恰好是对症的。

网站搭好之后,剩下的就是好好写文章了。与诸君共勉。