用 Astro 搭一个纯静态博客
这篇文章记录站点本身是怎么搭起来的。如果你也想搞一个,照着走一遍大概半小时。
环境要求
只需要 Node.js 22 以上版本:
node -v # v22.12.0 或更高
目录结构
搭好之后长这样:
个人博客/
├── astro.config.mjs # 站点配置:域名、集成、Markdown 高亮
├── package.json
├── public/ # 原样拷贝到产物里的文件(favicon、robots.txt)
└── src/
├── consts.ts # 站点名、导航、页脚链接都在这
├── content.config.ts # 内容模型:文章有哪些字段
├── components/ # 可复用的小组件
├── layouts/ # 页面骨架
├── styles/ # 全局样式
├── pages/ # 每个文件对应一个网址
└── content/
├── posts/ # 文章,一篇一个 .md
└── works/ # 作品,一个项目一个 .md
关键约定是 src/pages/ 下的文件路径就是网址。比如 src/pages/about.astro 对应 /about/,src/pages/posts/index.astro 对应 /posts/。
内容模型
文章有哪些字段,由 src/content.config.ts 里的 schema 决定:
const posts = defineCollection({
loader: glob({ base: './src/content/posts', pattern: '**/*.{md,mdx}' }),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
tags: z.array(z.string()).default([]),
cover: z.optional(image()),
draft: z.boolean().default(false),
}),
});
这样做的好处是:frontmatter 写错了会在构建时报错,而不是等到页面上出现一个空白标题才发现。比如把 pubDate 拼成 pubdate,构建直接失败并告诉你是哪一篇。
一篇文章是怎么变成网页的
流程比想象的简单:
src/pages/posts/[...slug].astro里的getStaticPaths()把posts/下每个 md 文件变成一个路径- 页面拿到文件内容,
render()把 Markdown 正文转成 HTML,同时给出目录用的标题列表 PostLayout.astro套上页头页脚,输出成一个静态 HTML 文件
整个过程在构建时完成,访问者拿到的就是最终的 HTML。
发一篇文章
新建 src/content/posts/文章名.md,头部写上:
---
title: 文章标题
description: 一句话摘要,会显示在列表页
pubDate: 2026-09-24
tags: ['标签一', '标签二']
---
正文从这开始写。
保存,刷新浏览器就能看到。文件名会成为网址的一部分,所以建议用英文或拼音,比如 astro-blog-setup.md 对应 /posts/astro-blog-setup/。
构建与部署
npm run build # 产物输出到 dist/
npm run preview # 用本地服务器预览构建结果
dist/ 里就是完整的静态站点,把它放到任何一台能跑 Nginx 的服务器上都能直接访问,不需要 Node 运行时。
几个设计上的取舍
不加载网络字体。 中文站点用 webfont 动辄几 MB,首屏会明显闪一下。这里全部使用系统字体栈,标题走衬线、正文走无衬线,零请求、零闪烁。
不做深色模式。 这个站点的定位是「像纸一样的阅读体验」,暖白底配深色字是它的一部分。强行加一套深色配色反而会让设计意图变得模糊。
导航只有五项。 每多一项,读者做选择的成本就高一分。文章、作品、标签、关于、友链 —— 够了。