简介

Astro已经迅速成为内容驱动型网站最受欢迎的框架之一。随着Astro 5的发布,框架引入了强大的Content Layer API、增强的Content Collections和一流的View Transitions支持——使其成为构建现代博客的理想选择。

在本教程中,你将使用Astro 5从零开始构建一个完整的博客。完成后你将拥有:

  • 由Content Collections驱动的类型安全内容系统
  • 带有frontmatter验证的Markdown/MDX博客文章
  • 响应式设计的精美布局
  • 使用View Transitions API实现的流畅页面过渡
  • 在Vercel或Netlify上的生产部署
  • 最棒的是,Astro默认不向客户端发送任何JavaScript,实现了令人难以置信的快速页面加载。

    前置条件

    开始之前,请确保你有:

  • Node.js 18.17.1或更高版本(推荐LTS)
  • npmpnpmyarn包管理器
  • 代码编辑器(推荐VS Code配合Astro扩展
  • 基本的HTML、CSS和JavaScript知识
  • 用于部署的VercelNetlify账户
  • 项目初始化

    创建一个新的Astro 5项目:

    npm create astro@latest my-astro-blog
    

    在提示时选择:

  • How would you like to start your new project? → Empty
  • Do you plan to write TypeScript? → Yes
  • How strict should TypeScript be? → Strict
  • Install dependencies? → Yes
  • Initialize a git repository? → Yes
  • 进入项目并启动开发服务器:

    cd my-astro-blog
    

    npm run dev

    网站现在运行在http://localhost:4321

    项目结构

    我们将构建以下结构:

    my-astro-blog/
    

    ├── src/

    │ ├── content/

    │ │ ├── posts/

    │ │ │ ├── getting-started-with-astro.md

    │ │ │ ├── why-content-collections.md

    │ │ │ └── view-transitions-guide.md

    │ │ └── config.ts

    │ ├── layouts/

    │ │ ├── BaseLayout.astro

    │ │ └── PostLayout.astro

    │ ├── components/

    │ │ ├── Header.astro

    │ │ ├── Footer.astro

    │ │ ├── PostCard.astro

    │ │ └── TableOfContents.astro

    │ ├── pages/

    │ │ ├── index.astro

    │ │ ├── blog/

    │ │ │ ├── index.astro

    │ │ │ └── [...slug].astro

    │ │ └── 404.astro

    │ └── styles/

    │ └── global.css

    ├── public/

    │ └── favicon.svg

    ├── astro.config.mjs

    ├── package.json

    └── tsconfig.json

    Content Collections Schema

    Content Collections是Astro内置的内容管理和验证机制。在Astro 5中,Content Layer API允许你从任何来源定义集合——本地文件、API或数据库。

    创建内容配置文件:

    // src/content/config.ts
    

    import { defineCollection, z } from 'astro:content';

    import { glob } from 'astro/loaders';

    const posts = defineCollection({

    // 使用glob加载器加载本地Markdown文件

    loader: glob({ pattern: '*/.{md,mdx}', base: './src/content/posts' }),

    schema: z.object({

    title: z.string().max(100),

    description: z.string().max(300),

    author: z.string().default('Blog Author'),

    publishedAt: z.coerce.date(),

    updatedAt: z.coerce.date().optional(),

    heroImage: z.string().optional(),

    tags: z.array(z.string()).default([]),

    draft: z.boolean().default(false),

    readTime: z.number().optional(),

    }),

    });

    export const collections = { posts };

    核心概念

  • defineCollection() — 创建带验证的类型化集合
  • glob()加载器 — Astro 5新的Content Layer加载器,从目录读取文件
  • z(Zod) — 提供Schema验证;frontmatter不匹配时Astro会抛出构建错误
  • z.coerce.date() — 自动将日期字符串转换为Date对象
  • 为什么这很重要

    没有Content Collections,frontmatter中的拼写错误会悄悄地破坏你的网站。有了它,你可以获得:

  • 类型安全 — TypeScript了解每篇文章的结构
  • 构建时验证 — 在部署前捕获错误
  • 自动生成的类型 — 编辑器中完整的IntelliSense支持
  • 创建文章

    创建第一篇博客文章:

    ---
    

    title: "Astro 5入门指南"

    description: "使用Astro 5构建网站的新手指南——专为内容驱动型站点设计的Web框架。"

    author: "Jane Developer"

    publishedAt: 2026-02-01

    tags: ["astro", "web", "tutorial"]

    heroImage: "/images/astro-hero.jpg"

    readTime: 5

    draft: false

    ---

    # Astro 5入门指南

    Astro是一个专为内容驱动型网站设计的现代Web框架。与React或Vue等框架向客户端发送JavaScript包不同,Astro默认将所有内容渲染为静态HTML。

    为什么选择Astro?

    1. 默认零JS — 页面瞬间加载

    2. 岛屿架构 — 仅在需要的地方添加交互性

    3. Content Collections — 类型安全的内容管理

    4. 框架无关 — 自由使用React、Vue、Svelte或不使用任何框架

    布局和组件

    基础布局

    创建所有页面共享的布局:

    ---
    

    // src/layouts/BaseLayout.astro

    import { ViewTransitions } from 'astro:transitions';

    interface Props {

    title: string;

    description?: string;

    }

    const { title, description = '使用Astro 5构建的现代博客' } = Astro.props;

    ---

    <!doctype html>

    <html lang="zh">

    <head>

    <meta charset="UTF-8" />

    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <meta name="description" content={description} />

    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />

    <title>{title} | My Astro Blog</title>

    <ViewTransitions />

    </head>

    <body>

    <Header />

    <main>

    <slot />

    </main>

    <Footer />

    </body>

    </html>

    <style is:global>

    :root {

    --color-bg: #0d1117;

    --color-text: #c9d1d9;

    --color-primary: #58a6ff;

    --color-surface: #161b22;

    --color-border: #30363d;

    --max-width: 768px;

    }

    body {

    font-family: system-ui, -apple-system, sans-serif;

    background: var(--color-bg);

    color: var(--color-text);

    line-height: 1.7;

    }

    main {

    max-width: var(--max-width);

    margin: 0 auto;

    padding: 2rem 1rem;

    }

    </style>

    文章卡片组件

    ---
    

    // src/components/PostCard.astro

    interface Props {

    title: string;

    description: string;

    publishedAt: Date;

    tags: string[];

    slug: string;

    readTime?: number;

    }

    const { title, description, publishedAt, tags, slug, readTime } = Astro.props;

    const formattedDate = publishedAt.toLocaleDateString('zh-CN', {

    year: 'numeric',

    month: 'long',

    day: 'numeric',

    });

    ---

    <article class="post-card">

    <a href={/blog/${slug}}>

    <h2>{title}</h2>

    <p class="meta">

    <time datetime={publishedAt.toISOString()}>{formattedDate}</time>

    {readTime && <span> · {readTime}分钟阅读</span>}

    </p>

    <p class="description">{description}</p>

    <div class="tags">

    {tags.map(tag => <span class="tag">#{tag}</span>)}

    </div>

    </a>

    </article>

    <style>

    .post-card {

    padding: 1.5rem;

    border: 1px solid var(--color-border);

    border-radius: 8px;

    background: var(--color-surface);

    transition: border-color 0.2s;

    }

    .post-card:hover {

    border-color: var(--color-primary);

    }

    </style>

    博客索引页面

    ---
    

    // src/pages/blog/index.astro

    import BaseLayout from '../../layouts/BaseLayout.astro';

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

    import { getCollection } from 'astro:content';

    const posts = await getCollection('posts', ({ data }) => !data.draft);

    // 按日期排序,最新的在前

    const sortedPosts = posts.sort(

    (a, b) => b.data.publishedAt.getTime() - a.data.publishedAt.getTime()

    );

    ---

    <BaseLayout title="博客">

    <h1>博客文章</h1>

    <p class="subtitle">关于Web开发、Astro等技术的思考。</p>

    <div class="posts-grid">

    {sortedPosts.map(post => (

    <PostCard

    title={post.data.title}

    description={post.data.description}

    publishedAt={post.data.publishedAt}

    tags={post.data.tags}

    slug={post.id}

    readTime={post.data.readTime}

    />

    ))}

    </div>

    </BaseLayout>

    动态文章页面

    ---
    

    // src/pages/blog/[...slug].astro

    import PostLayout from '../../layouts/PostLayout.astro';

    import { getCollection, render } from 'astro:content';

    export async function getStaticPaths() {

    const posts = await getCollection('posts', ({ data }) => !data.draft);

    return posts.map(post => ({

    params: { slug: post.id },

    props: { post },

    }));

    }

    const { post } = Astro.props;

    const { Content } = await render(post);

    ---

    <PostLayout post={post}>

    <Content />

    </PostLayout>

    注意在Astro 5中,我们使用从astro:content导入的独立render()函数,而不是旧的post.render()方法。

    View Transitions

    Astro内置了对View Transitions API的支持,无需任何客户端框架即可实现流畅的SPA式页面过渡。

    启用View Transitions

    只需在BaseLayout.astro中添加(上面的布局代码中已经包含)。就这样,所有页面导航现在都有了流畅的淡入淡出动画!

    自定义过渡动画

    使用transition:animate为每个元素自定义过渡效果:

    <article class="post-card" transition:animate="slide">
    

    <!-- 卡片内容 -->

    </article>

    Astro提供的内置动画:

    | 动画 | 描述 |

    |------|------|

    | fade | 淡入淡出(默认) |

    | slide | 从侧面滑入 |

    | none | 禁用过渡 |

    | initial | 使用CSS而非Astro默认值 |

    使用transition:name实现持久元素

    创建元素在页面间平滑变形的效果:

    <!-- PostCard.astro中 -->
    

    <h2 transition:name={title-${slug}}>{title}</h2>

    <!-- PostLayout.astro中 -->

    <h1 transition:name={title-${post.id}}>{title}</h1>

    当点击文章卡片时,标题会从卡片位置平滑动画到文章标题位置。这在零JavaScript开销的情况下创造了令人愉悦的应用式体验。

    过渡期间的脚本处理

    View Transitions会在不完全重新加载的情况下交换页面内容,因此脚本需要特殊处理:

    <script>
    

    // 初始加载和每次过渡后都会运行

    document.addEventListener('astro:page-load', () => {

    console.log('页面已加载或已过渡!');

    // 初始化组件、绑定事件监听器等

    });

    </script>

    添加RSS订阅

    安装RSS集成:

    npx astro add @astrojs/rss
    

    创建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('posts', ({ data }) => !data.draft);

    return rss({

    title: 'My Astro Blog',

    description: '一个关于Web开发的博客',

    site: context.site!,

    items: posts.map(post => ({

    title: post.data.title,

    pubDate: post.data.publishedAt,

    description: post.data.description,

    link: /blog/${post.id}/,

    })),

    });

    }

    部署

    部署到Vercel

    安装Vercel适配器:

    npx astro add vercel
    

    // astro.config.mjs
    

    import { defineConfig } from 'astro/config';

    import vercel from '@astrojs/vercel';

    export default defineConfig({

    site: 'https://my-astro-blog.vercel.app',

    output: 'static',

    adapter: vercel(),

    });

    部署:

    npm i -g vercel
    

    vercel

    或者将GitHub仓库连接到Vercel,实现每次推送自动部署。

    部署到Netlify

    npx astro add netlify
    

    // astro.config.mjs
    

    import { defineConfig } from 'astro/config';

    import netlify from '@astrojs/netlify';

    export default defineConfig({

    site: 'https://my-astro-blog.netlify.app',

    output: 'static',

    adapter: netlify(),

    });

    部署:

    npm i -g netlify-cli
    

    npm run build

    netlify deploy --prod --dir=dist

    构建输出

    对于静态博客,Astro生成纯HTML/CSS文件:

    $ npm run build
    
    

    generating static routes

    ▶ src/pages/index.astro → /index.html (+12ms)

    ▶ src/pages/blog/index.astro → /blog/index.html (+8ms)

    ▶ src/pages/blog/[...slug].astro → /blog/getting-started-with-astro/index.html (+5ms)

    ▶ src/pages/rss.xml.ts → /rss.xml (+3ms)

    Completed in 1.2s

    dist/ 42.5 kB

    整个博客只有42.5 kB——没有JavaScript包,没有hydration开销。

    性能优化

    Astro默认就很快,但以下技巧可以让它更快:

    图片优化

    使用Astro内置的组件:

    ---
    

    import { Image } from 'astro:assets';

    import heroImage from '../assets/hero.jpg';

    ---

    <Image

    src={heroImage}

    alt="博客封面图"

    width={800}

    height={400}

    format="avif"

    />

    这会自动生成现代格式的优化图片,并带有正确的srcset属性。

    预取

    当启用View Transitions时,Astro会在链接进入视口时自动预取。你可以自定义:

    // astro.config.mjs
    

    export default defineConfig({

    prefetch: {

    prefetchAll: true,

    defaultStrategy: 'hover',

    },

    });

    总结

    你已经使用Astro 5构建了一个现代化的高速博客:

  • Content Collections — 类型安全的Schema和新的Content Layer API
  • Markdown文章 — 带frontmatter验证
  • 响应式布局 — 作用域CSS
  • View Transitions — 流畅的页面导航
  • RSS订阅 — 供订阅者使用
  • 生产部署 — Vercel或Netlify
  • Astro 5默认零JavaScript发送的理念使其成为内容密集型网站的完美选择。Content Collections系统确保随着博客增长内容始终有效,View Transitions为用户提供优质的浏览体验。

    下一步

  • 添加MDX支持npx astro add mdx)在文章中使用交互组件
  • 集成Tailwind CSSnpx astro add tailwind)使用原子化CSS
  • 使用Pagefind添加搜索功能——静态搜索库
  • 使用Astro内置的i18n国际化路由
  • 添加CMSDecap CMSTina CMS进行可视化编辑

本教程的完整源代码可在GitHub上获取。Happy blogging! 🚀