Astro 现代化博客搭建全记录 - 我的博客
我的博客

Astro 现代化博客搭建全记录

面向前端开发者的实战指南:从零到上线,打造一个快到离谱的个人博客

| 3.0k 字 | 阅读时间 8 分钟

面向前端开发者的实战指南:从零到上线,打造一个快到离谱的个人博客

作者按:这篇文章不是翻译官方文档。我搭建博客过程中踩的坑、做的选择、为什么不选 Next.js/Nuxt,全写在这里。你照着做,半天出站。


一、为什么选 Astro

1.1 三个框架的终极选择

对比项AstroNext.js (App Router)Nuxt 3
默认渲染SSG(零 JS)SSR/SSG(有水合)SSR/SSG(有水合)
博客场景 JS 体积0KB~80KB+~60KB+
Lighthouse 性能分100(常态)85-9585-95
内容管理Content Collections(原生)需自己搭需自己搭
学习曲线极低中高
UI 框架任意(React/Vue/Svelte)React onlyVue only

博客的核心需求是什么?内容展示、SEO、速度。 不是交互、不是动态渲染。

Astro 的哲学:默认零 JS,按需加载。博客页面生成纯 HTML,Lighthouse 直接满分,SEO 天然友好。

我的选择逻辑

  • ❌ Next.js:React 生态强,但博客场景杀鸡用牛刀,水合开销浪费
  • ❌ Nuxt 3:我熟 Vue,但博客不需要 SSR 的灵活性,SSG 才是正解
  • ✅ Astro:专为内容站设计,零 JS 默认、Content Collections 原生支持、还支持 Vue 组件(需要交互时按需引入)

1.2 Astro 的核心概念(30 秒版)

  • Islands Architecture:页面是静态 HTML,交互组件是”岛”,互不干扰
  • Content Collections:类型安全的 Markdown/MDX 内容管理,带 schema 校验
  • View Transitions:原生页面切换动画,零配置
  • Astro Components.astro 文件,类似 Vue SFC 但只输出 HTML

二、项目初始化

2.1 创建项目

npm create astro@latest my-blog

# 交互选择:
# ✔ Template: Blog
# ✔ TypeScript: Strict
# ✔ Install dependencies: Yes
# ✔ Initialize git: Yes

直接用 Blog 模板,别从 Empty 开始造轮子。模板自带 Content Collections、RSS、基础布局,改比写快。

2.2 项目结构

my-blog/
├── public/              # 静态资源(不经过构建)
   ├── favicon.svg
   └── fonts/           # 自定义字体放这里
├── src/
   ├── components/      # 可复用组件
   ├── Header.astro
   ├── Footer.astro
   ├── BlogCard.astro
   └── ThemeToggle.tsx   # 交互组件用 React/Vue
   ├── content/
   ├── config.ts    # Content Collections schema 定义
   └── blog/        # 博客文章 Markdown/MDX
       ├── first-post.md
       └── second-post.mdx
   ├── layouts/
   ├── BaseLayout.astro    # 全局基础布局
   └── BlogPost.astro      # 文章详情布局
   ├── pages/
   ├── index.astro         # 首页
   ├── about.astro
   ├── blog/
   ├── index.astro     # 文章列表
   └── [...slug].astro # 动态路由:/blog/:slug
   └── tags/
       ├── index.astro
       └── [tag].astro
   └── styles/
       └── global.css
├── astro.config.mjs
├── tailwind.config.mjs
└── package.json

2.3 依赖安装

# 核心依赖
npm install @astrojs/tailwind @astrojs/mdx @astrojs/sitemap

# 可选:需要交互组件时
npm install @astrojs/react react react-dom
# 或
npm install @astrojs/vue vue

# 工具
npm install reading-time gray-matter

三、Content Collections:博客的核心

这是 Astro 最香的功能,没有之一。类型安全的 Markdown 内容管理,带 Zod schema 校验。

3.1 定义 Schema

// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    heroImage: z.string().optional(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
  }),
});

export const collections = { blog };

为什么比 gray-matter 好一百倍? 写 frontmatter 写错字段,构建时直接报错,不用等到上线才发现。

3.2 文章 Frontmatter

---
title: "用 Trae 从零搭建 AI 对话组件"
description: "手把手教你用 Trae IDE + Vue3 实现一个支持流式输出的 AI 对话界面"
pubDate: 2026-05-20
tags: ["AI", "Vue3", "Trae", "实战"]
heroImage: "/blog/ai-chat-component/cover.jpg"
draft: false
---

正文内容从这里开始...

3.3 查询与渲染

---
// src/pages/blog/[...slug].astro
import { type CollectionEntry, getCollection } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog', ({ data }) => !data.draft);
  return posts.map((post) => ({
    params: { slug: post.slug },
    props: post,
  }));
}

type Props = CollectionEntry<'blog'>;

const post = Astro.props;
const { Content } = await post.render();
---

<BlogPost title={post.data.title} date={post.data.pubDate}>
  <Content />
</BlogPost>

3.4 过滤草稿

// 只在生产构建时过滤草稿
const posts = await getCollection('blog', ({ data }) => {
  return import.meta.env.PROD ? !data.draft : true;
});

四、样式方案:Tailwind CSS v4

4.1 集成配置

// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://your-domain.com',
  integrations: [
    tailwind(),
    mdx(),
    sitemap(),
  ],
});
/* src/styles/global.css */
@import "tailwindcss";

/* 自定义主题变量 */
@theme {
  --color-primary: #7c3aed;       /* 紫色,你的偏好 */
  --color-primary-light: #a78bfa;
  --color-primary-dark: #5b21b6;
  --font-sans: "Inter", "Noto Sans SC", system-ui, sans-serif;
  --font-mono: "JetBrains Mono", "Fira Code", monospace;
}

4.2 暗色模式

// src/components/ThemeToggle.tsx
import { useState, useEffect } from 'react';

export default function ThemeToggle() {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  useEffect(() => {
    const saved = localStorage.getItem('theme') as 'light' | 'dark';
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    const initial = saved || (prefersDark ? 'dark' : 'light');
    setTheme(initial);
    document.documentElement.classList.toggle('dark', initial === 'dark');
  }, []);

  const toggle = () => {
    const next = theme === 'light' ? 'dark' : 'light';
    setTheme(next);
    localStorage.setItem('theme', next);
    document.documentElement.classList.toggle('dark', next === 'dark');
  };

  return (
    <button onClick={toggle} aria-label="切换主题" className="p-2 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
      {theme === 'light' ? '🌙' : '☀️'}
    </button>
  );
}

在布局中引入(client:load 是 Islands 关键语法):

---
import ThemeToggle from '../components/ThemeToggle';
---

<header class="flex items-center justify-between px-6 py-4">
  <nav>...</nav>
  <ThemeToggle client:load />
</header>

client: 指令是 Astro 的精髓:

  • client:load:页面加载时立即水合
  • client:visible:进入视口时水合
  • client:idle:浏览器空闲时水合
  • client:media="(max-width: 768px)":匹配媒体查询时水合

只有加了 client: 的组件才会发送 JS 到客户端,其余全是纯 HTML。


五、文章列表与详情页

5.1 首页文章卡片

---
// src/components/BlogCard.astro
import type { CollectionEntry } from 'astro:content';
import { FormattedDate } from './FormattedDate';

interface Props {
  post: CollectionEntry<'blog'>;
}

const { post } = Astro.props;
---

<article class="group rounded-xl border border-gray-200 dark:border-gray-700 p-5 hover:border-primary/50 hover:shadow-lg transition-all">
  <a href={`/blog/${post.slug}`} class="block">
    <h3 class="text-lg font-semibold group-hover:text-primary transition-colors">
      {post.data.title}
    </h3>
    <p class="mt-2 text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
      {post.data.description}
    </p>
    <div class="mt-3 flex items-center gap-3 text-xs text-gray-500">
      <FormattedDate date={post.data.pubDate} />
      <span class="flex gap-1">
        {post.data.tags.map((tag) => (
          <span class="rounded-full bg-primary/10 px-2 py-0.5 text-primary">
            {tag}
          </span>
        ))}
      </span>
    </div>
  </a>
</article>

5.2 文章列表页

---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import BlogCard from '../../components/BlogCard.astro';

const posts = (await getCollection('blog', ({ data }) => !data.draft))
  .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---

<BaseLayout title="博客">
  <section class="max-w-3xl mx-auto px-4 py-12">
    <h1 class="text-3xl font-bold mb-8">文章</h1>
    <div class="grid gap-6">
      {posts.map((post) => (
        <BlogCard post={post} />
      ))}
    </div>
  </section>
</BaseLayout>

5.3 阅读时间

// src/utils/reading-time.ts
import readingTime from 'reading-time';

export function getReadingTime(content: string): string {
  const stats = readingTime(content, { wordsPerMinute: 300 });
  return stats.text; // "3 min read"
}

在文章详情页中使用:

---
const { Content } = await post.render();
const readingTime = getReadingTime(post.body || '');
---

<span>{readingTime}</span>

六、SEO 优化

6.1 基础 Meta 标签

---
// src/components/SEO.astro
interface Props {
  title: string;
  description: string;
  image?: string;
  article?: boolean;
  pubDate?: Date;
}

const { title, description, image = '/og-default.jpg', article = false, pubDate } = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---

<!-- Primary Meta -->
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />

<!-- Open Graph -->
<meta property="og:type" content={article ? 'article' : 'website'} />
<meta property="og:url" content={canonicalURL} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(image, Astro.site)} />
{pubDate && <meta property="article:published_time" content={pubDate.toISOString()} />}

<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={new URL(image, Astro.site)} />

6.2 Sitemap & RSS

Sitemap 已通过 @astrojs/sitemap 集成,构建自动生成。

RSS 配置:

// src/pages/rss.xml.ts
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
import type { APIContext } from 'astro';

export async function GET(context: APIContext) {
  const posts = await getCollection('blog', ({ data }) => !data.draft);
  return rss({
    title: 'Your Blog Name',
    description: '前端开发 & AI 实战',
    site: context.site!,
    items: posts
      .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
      .map((post) => ({
        title: post.data.title,
        pubDate: post.data.pubDate,
        description: post.data.description,
        link: `/blog/${post.slug}/`,
      })),
  });
}

6.3 Structured Data(JSON-LD)

---
// 在 BlogPost.astro 的 <head> 中
const jsonLd = {
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": post.data.title,
  "description": post.data.description,
  "datePublished": post.data.pubDate.toISOString(),
  "dateModified": post.data.updatedDate?.toISOString(),
  "image": post.data.heroImage,
  "author": {
    "@type": "Person",
    "name": "wantasy",
  },
};
---

<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />

七、MDX 增强

MDX 让你在 Markdown 里直接用组件,写技术文章神器。

7.1 常用自定义组件

---
// src/components/Callout.astro
interface Props {
  type?: 'info' | 'warning' | 'tip' | 'danger';
}

const { type = 'info' } = Astro.props;
const styles = {
  info: 'bg-blue-50 dark:bg-blue-900/20 border-blue-500 text-blue-800 dark:text-blue-200',
  warning: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500 text-yellow-800 dark:text-yellow-200',
  tip: 'bg-green-50 dark:bg-green-900/20 border-green-500 text-green-800 dark:text-green-200',
  danger: 'bg-red-50 dark:bg-red-900/20 border-red-500 text-red-800 dark:text-red-200',
};
const icons = { info: '💡', warning: '⚠️', tip: '✅', danger: '🚫' };
---

<div class={`border-l-4 rounded-r-lg p-4 my-4 ${styles[type]}`}>
  <span class="mr-2">{icons[type]}</span>
  <slot />
</div>

7.2 在 MDX 中使用

import Callout from '../components/Callout.astro';

## 安装依赖

<Callout type="warning">
  Tailwind CSS v4 和 v3 的配置方式完全不同,别混用!
</Callout>

npm install @astrojs/tailwind


---

## 八、性能优化

### 8.1 图片优化

Astro 内置 `<Image />` 组件,自动生成多种尺寸 + WebP/AVIF:

```astro
---
import { Image } from 'astro:assets';
import coverImage from '../../assets/blog/cover.jpg';
---

<Image
  src={coverImage}
  alt="文章封面"
  width={800}
  height={400}
  loading="lazy"
  decoding="async"
/>

8.2 字体优化

---
// src/layouts/BaseLayout.astro
---

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Noto+Sans+SC:wght@400;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
  rel="stylesheet"
/>

8.3 View Transitions

// astro.config.mjs
export default defineConfig({
  prefetch: {
    prefetchAll: true,      // 预取所有链接
    defaultStrategy: 'viewport',  // 进入视口时预取
  },
  // ...
});
---
// 在 BaseLayout.astro 的 <head> 中
---

<!-- 启用页面切换动画 -->
<ViewTransitions />

效果:页面切换时自动添加淡入动画,浏览器前进后退也能正确恢复滚动位置。零配置,开箱即用。


九、部署到 Vercel

9.1 配置适配器

npm install @astrojs/vercel
// astro.config.mjs
import vercel from '@astrojs/vercel';

export default defineConfig({
  output: 'static',     // 纯静态部署用 static
  adapter: vercel(),    // 需要 SSR 功能时才用
  site: 'https://your-domain.com',
  // ...
});

博客场景用 output: 'static' 就够了,不需要 SSR。

9.2 部署

方式一:Vercel CLI(推荐)

npm i -g vercel
vercel

# 首次需要登录,按提示操作
# 后续更新直接 vercel --prod

方式二:Git 自动部署

  1. 推送代码到 GitHub
  2. Vercel 导入项目
  3. Framework Preset 选 Astro
  4. 自动构建部署

每次 push 到 main 分支自动重新构建。

9.3 自定义域名

在 Vercel Dashboard → Settings → Domains 添加,DNS 添加 CNAME 记录指向 cname.vercel-dns.com


十、写作工作流

10.1 我的发文流程

1. 构思 src/content/blog/ 下创建 draft-xxx.md(draft: true
2. 写作 本地 dev server 实时预览
3. 校对 draft: false,检查 frontmatter 完整性
4. 构建 npm run build,本地预览 dist/
5. 部署 vercel --prod

10.2 本地预览

# 开发
npm run dev          # http://localhost:4321

# 构建预览
npm run build
npm run preview      # http://localhost:4321(预览构建产物)

10.3 文章模板

---
title: ""
description: ""
pubDate: 2026-05-2x
tags: []
heroImage: ""
draft: true
---

## 前言

(为什么写这篇,解决什么问题)

## 正文

### 小标题1

### 小标题2

## 总结

(核心结论,一两句话)

踩坑记录

坑1:Content Collections 路径问题

src/content/ 下的文件夹名必须和 config.ts 中的 collection 名一致。config.ts 定义了 blog,文件夹就必须叫 blog,叫 posts 会报错。

坑2:Tailwind v4 和 Astro 集成

@astrojs/tailwind 目前默认支持 v3。用 v4 的话,需要手动配置:

// astro.config.mjs - Tailwind v4 方案
// 不用 @astrojs/tailwind,改用 Vite 插件
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  vite: {
    plugins: [tailwindcss()],
  },
});

然后在 global.css 中用 @import "tailwindcss" 代替旧的三行指令。

坑3:MDX 中使用 Astro 组件

MDX 文件里 import .astro 组件完全没问题,但 props 传递要严格按 schema 来,类型不匹配不会在编辑器报错,只在构建时报错。

坑4:图片路径

public/ 下的图片用绝对路径 /img/xxx.jpgsrc/assets/ 下的图片用 import + <Image /> 组件。别混用,混用容易 404。


总结

Astro 搭博客的核心逻辑就三件事:

  1. Content Collections 管内容 → 类型安全,构建时校验
  2. Islands Architecture 管交互 → 默认零 JS,按需加载
  3. SSG 输出纯 HTML → 极致性能,SEO 天然友好

搭建一个 Lighthouse 满分的博客,用 Astro 是最短路径。不是因为它功能最多,而是因为它恰好只做了博客需要的事


本文基于 Astro 5.x + Tailwind CSS v4,2026 年 5 月实测可用。

评论

相关文章