2026年7月3日 · 6 分钟阅读
用 Astro + Markdown + Vercel 搭建个人博客
记录从空项目开始搭建个人技术博客的完整过程:内容集合、静态生成、标签归档、RSS、SEO 和 Vercel 部署。
这篇文章记录我从零搭建个人技术博客的完整过程。这个博客的目标不是做一个复杂的内容管理系统,而是做一个适合长期写作、容易维护、部署成本低的个人技术站。
最终方案是:
- 使用 Astro 作为静态站框架。
- 使用 Markdown 作为文章内容来源。
- 使用 Astro Content Collections 管理文章元信息。
- 使用 Vercel 自动构建和部署。
- 支持文章列表、详情页、标签、归档、RSS、SEO 和站点地图。
上图是博客首页效果:顶部是基础导航,首屏突出个人技术博客定位,下方展示最新文章。
为什么选择 Astro
个人博客最核心的需求是阅读体验、SEO、性能和维护成本。相比完整的前后端应用,静态站更适合这个场景。
Astro 的优势主要有三点:
- 默认输出静态 HTML,页面加载快,也方便被搜索引擎抓取。
- 对 Markdown 支持很好,适合把文章放在 Git 仓库里管理。
- 项目结构简单,不需要数据库、登录系统和后台服务。
这也让博客的维护方式变得很直接:新增文章就是新增一个 Markdown 文件,提交代码后由 Vercel 自动部署。
初始化项目
项目使用 Astro + TypeScript。核心脚本如下:
{
"scripts": {
"dev": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview"
}
}
其中 astro check 用于在构建前做类型检查,astro build 负责生成静态站点。这个组合可以尽早发现内容字段、页面 props 或组件类型上的问题。
使用 Markdown 管理文章
文章统一放在 src/content/blog/ 目录下,每篇文章都是一个 Markdown 文件:
---
title: "文章标题"
description: "文章摘要"
date: 2026-07-03
tags: ["Astro", "工程实践"]
draft: false
---
这里是正文内容。
这些 frontmatter 字段负责描述文章的元信息:
title:文章标题。description:摘要,用于列表页、SEO 和 RSS。date:发布时间。tags:主题标签。draft:是否为草稿。cover:可选封面图。
为了让 Astro 能识别这些文章,我配置了 Content Collection:
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const blog = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }),
schema: z.object({
title: z.string().default("未命名文章"),
description: z.string().default(""),
date: z.coerce.date().default(new Date("1970-01-01")),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
cover: z.string().optional()
})
});
export const collections = { blog };
这里使用 zod 定义文章字段,可以在构建时校验文章内容。如果字段格式写错,构建阶段就会提醒,而不是上线后才发现页面异常。
获取已发布文章
博客里很多页面都需要读取文章,例如首页、文章列表、标签页、归档页和 RSS。为了避免重复逻辑,我把文章读取封装成工具函数:
import { getCollection } from "astro:content";
export async function getPublishedPosts() {
const posts = await getCollection("blog", ({ data }) => !data.draft);
return posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
}
这个函数做了两件事:
- 过滤掉
draft: true的草稿文章。 - 按发布时间倒序排列文章。
这样可以保证所有页面使用同一套发布规则,减少后期维护成本。
实现文章页
文章详情页使用 Astro 的动态路由:
src/pages/blog/[...slug].astro
构建时通过 getStaticPaths 生成每篇文章的静态路径:
export async function getStaticPaths() {
const posts = await getPublishedPosts();
return posts.map((post, index) => ({
params: { slug: post.id },
props: {
post,
previous: posts[index + 1],
next: posts[index - 1]
}
}));
}
这里还顺便计算了上一篇和下一篇文章。文章页不仅展示正文,还展示发布时间、阅读时间、标签和文章导航,让阅读体验更完整。
实现标签和归档
标签不需要单独维护配置文件,而是从所有文章的 tags 字段自动聚合:
export function getAllTags(posts) {
const tagCounts = new Map();
for (const post of posts) {
for (const tag of post.data.tags) {
tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
}
}
return [...tagCounts.entries()].map(([name, count]) => ({
name,
count,
href: `/tags/${encodeURIComponent(name)}`
}));
}
这样新增文章时只需要在 frontmatter 里写标签,/tags 和 /tags/某个标签 会自动更新。
归档页则按年份分组,把所有文章组织成时间线。它适合回顾内容积累,也能让面试官快速看到项目不是一个静态 Demo,而是为长期维护设计的。
标签页的核心是自动聚合:文章越多,主题分布越清晰,读者也能更快找到同类内容。
增加 RSS 和 SEO
RSS 的作用是让读者可以用 RSS 阅读器订阅博客。当博客发布新文章时,阅读器会自动收到更新。
我使用 @astrojs/rss 生成 /rss.xml:
import rss from "@astrojs/rss";
import { getPublishedPosts } from "@lib/posts";
import { site } from "@lib/site";
export async function GET() {
const posts = await getPublishedPosts();
return rss({
title: site.name,
description: site.description,
site: site.url,
stylesheet: "/rss-styles.xsl",
items: posts.map((post) => ({
title: post.data.title,
description: post.data.description,
pubDate: post.data.date,
link: `/blog/${post.id}`
}))
});
}
为了避免浏览器直接打开 RSS 时显示成空白 XML,我额外加了一个 rss-styles.xsl,让 RSS 在浏览器里也能以可读页面展示。
RSS 本质上仍然是给阅读器订阅的 XML,但增加样式后,用户直接点击导航里的 RSS 也能看懂它的用途。
SEO 方面,我封装了统一的 SEO 组件,负责生成:
- 页面标题。
- 页面描述。
- canonical 链接。
- Open Graph 标题、描述和图片。
- Twitter card 信息。
这些信息对搜索引擎收录、社交平台分享和面试展示都有帮助。
部署到 Vercel
Vercel 对 Astro 支持很好,部署流程很简单:
- 把项目推送到 GitHub。
- 在 Vercel 导入这个仓库。
- Framework Preset 选择 Astro。
- Build Command 使用
npm run build。 - Output Directory 使用
dist。 - 点击 Deploy。
之后每次向主分支提交代码,Vercel 都会自动重新构建和发布。
上线前需要把站点域名改成真实地址:
export default defineConfig({
site: "https://your-domain.com"
});
同时也要更新站点配置里的 url,否则 RSS、sitemap 和 Open Graph 里的链接仍然会指向占位域名。
遇到的问题
搭建过程中遇到过几个小问题:
第一,Astro 5 的内容集合推荐显式使用 glob loader。这样可以更清楚地告诉 Astro Markdown 文件在哪里,避免内容集合没有被正确识别。
第二,中文标签需要注意 URL 编码。页面链接应该使用 encodeURIComponent(tag),但静态路由参数保留原始标签名更稳,避免双重编码导致匹配不到。
第三,RSS 本身是 XML,不是普通 HTML 页面。浏览器直接打开可能看起来不友好,所以增加 XSL 样式文件可以改善这个体验。
第四,本地 Node 版本不能太低。Astro 5 需要较新的 Node 版本,因此项目里加了 .nvmrc 和 engines 字段,减少环境差异带来的问题。
项目收获
这个项目虽然不复杂,但覆盖了一个完整内容站的核心链路:
- 内容建模。
- 静态生成。
- 动态路由。
- SEO。
- RSS。
- 标签和归档。
- 自动部署。