2026年7月3日 · 6 分钟阅读

用 Astro + Markdown + Vercel 搭建个人博客

记录从空项目开始搭建个人技术博客的完整过程:内容集合、静态生成、标签归档、RSS、SEO 和 Vercel 部署。

这篇文章记录我从零搭建个人技术博客的完整过程。这个博客的目标不是做一个复杂的内容管理系统,而是做一个适合长期写作、容易维护、部署成本低的个人技术站。

最终方案是:

  • 使用 Astro 作为静态站框架。
  • 使用 Markdown 作为文章内容来源。
  • 使用 Astro Content Collections 管理文章元信息。
  • 使用 Vercel 自动构建和部署。
  • 支持文章列表、详情页、标签、归档、RSS、SEO 和站点地图。

博客首页效果

上图是博客首页效果:顶部是基础导航,首屏突出个人技术博客定位,下方展示最新文章。

为什么选择 Astro

个人博客最核心的需求是阅读体验、SEO、性能和维护成本。相比完整的前后端应用,静态站更适合这个场景。

Astro 的优势主要有三点:

  1. 默认输出静态 HTML,页面加载快,也方便被搜索引擎抓取。
  2. 对 Markdown 支持很好,适合把文章放在 Git 仓库里管理。
  3. 项目结构简单,不需要数据库、登录系统和后台服务。

这也让博客的维护方式变得很直接:新增文章就是新增一个 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());
}

这个函数做了两件事:

  1. 过滤掉 draft: true 的草稿文章。
  2. 按发布时间倒序排列文章。

这样可以保证所有页面使用同一套发布规则,减少后期维护成本。

实现文章页

文章详情页使用 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 页面效果

RSS 本质上仍然是给阅读器订阅的 XML,但增加样式后,用户直接点击导航里的 RSS 也能看懂它的用途。

SEO 方面,我封装了统一的 SEO 组件,负责生成:

  • 页面标题。
  • 页面描述。
  • canonical 链接。
  • Open Graph 标题、描述和图片。
  • Twitter card 信息。

这些信息对搜索引擎收录、社交平台分享和面试展示都有帮助。

部署到 Vercel

Vercel 对 Astro 支持很好,部署流程很简单:

  1. 把项目推送到 GitHub。
  2. 在 Vercel 导入这个仓库。
  3. Framework Preset 选择 Astro。
  4. Build Command 使用 npm run build
  5. Output Directory 使用 dist
  6. 点击 Deploy。

Vercel 部署配置

之后每次向主分支提交代码,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 版本,因此项目里加了 .nvmrcengines 字段,减少环境差异带来的问题。

项目收获

这个项目虽然不复杂,但覆盖了一个完整内容站的核心链路:

  • 内容建模。
  • 静态生成。
  • 动态路由。
  • SEO。
  • RSS。
  • 标签和归档。
  • 自动部署。