Astro 操作指南
事实确认截至 2026-07。
Astro 框架开发的综合模式:群岛架构、内容集合、渲染策略、视图过渡和多平台部署。
渲染策略决策树
Which rendering strategy?
│
├─ Is content mostly static (blog, docs, marketing)?
│ ├─ YES → Does it change less than daily?
│ │ ├─ YES → SSG (output: 'static')
│ │ │ Fastest TTFB, CDN-cacheable, zero runtime cost
│ │ └─ NO → Hybrid (output: 'hybrid')
│ │ Default static + opt-in SSR per route
│ └─ NO → Does every page need personalization?
│ ├─ YES → SSR (output: 'server')
│ │ Dynamic per-request, auth-aware, real-time data
│ └─ NO → Hybrid (output: 'hybrid')
│ Static shell + server islands for dynamic parts
│
├─ Does the app need real-time interactivity (dashboard, SPA)?
│ ├─ YES → Is it a full SPA with client-side routing?
│ │ ├─ YES → Consider React/Vue SPA instead, or Astro + client:only
│ │ └─ NO → Hybrid + islands architecture
│ │ Interactive islands in static pages
│ └─ NO → SSG (output: 'static')
│
├─ Build time concerns (>10k pages)?
│ ├─ YES → Hybrid with on-demand rendering
│ │ Prerender popular pages, SSR the long tail
│ └─ NO → SSG handles it fine
│
└─ Need edge computing (low latency globally)?
├─ YES → SSR + Cloudflare/Vercel Edge adapter
└─ NO → SSR + Node adapter or SSG
配置
// astro.config.mjs
import { defineConfig } from 'astro/config';
// SSG (default) - all pages prerendered at build time
export default defineConfig({
output: 'static',
});
// SSR - all pages rendered on request
export default defineConfig({
output: 'server',
adapter: cloudflare(), // or vercel(), netlify(), node()
});
// Hybrid - static default, opt-in SSR per page
export default defineConfig({
output: 'hybrid',
adapter: cloudflare(),
});
---
// In hybrid mode, opt OUT of prerendering for specific pages:
export const prerender = false;
// In SSR mode, opt IN to prerendering:
export const prerender = true;
---
群岛架构快速参考
| 指令 | 水合时机 | 发送 JS | 使用场景 |
|---|---|---|---|
client:load | 页面加载后立即执行 | 完整包 | 首屏交互(导航栏、Hero CTA) |
client:idle | 页面空闲后 (requestIdleCallback) | 完整包 | 非首屏交互(评论表单、聊天) |
client:visible | 滚动到视口时 | 完整包 | 远离首屏的组件(页脚小部件、轮播图) |
client:media | 媒体查询匹配时 | 完整包 | 仅移动端导航、响应式组件 |
client:only="react" | 立即执行,跳过 SSR | 完整包 | 无法 SSR 的组件(Canvas、WebGL) |
| (无) | 从不 - 仅静态 HTML | 零 JS | 静态内容、卡片、标题 |
---
import NavBar from '../components/NavBar.tsx';
import CommentForm from '../components/CommentForm.tsx';
import ImageCarousel from '../components/ImageCarousel.svelte';
import MobileMenu from '../components/MobileMenu.vue';
import ThreeScene from '../components/ThreeScene.tsx';
---
<!-- Loads immediately - critical interactivity -->
<NavBar client:load />
<!-- Loads after page is idle - non-critical -->
<CommentForm client:idle />
<!-- Loads when scrolled into view - lazy -->
<ImageCarousel client:visible />
<!-- Loads only on mobile -->
<MobileMenu client:media="(max-width: 768px)" />
<!-- Client-only, no SSR (WebGL can't run on server) -->
<ThreeScene client:only="react" />
内容集合快速入门
定义 Schema
// src/content.config.ts (Astro 5) or src/content/config.ts (Astro 4)
import { defineCollection, z, reference } 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(),
description: z.string().max(160),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
author: reference('authors'), // Reference another collection
}),
});
const authors = defineCollection({
loader: glob({ pattern: '**/*.json', base: './src/content/authors' }),
schema: z.object({
name: z.string(),
avatar: z.string(),
bio: z.string(),
socials: z.object({
twitter: z.string().optional(),
github: z.string().optional(),
}).optional(),
}),
});
export const collections = { blog, authors };
查询集合
---
import { getCollection, getEntry } from 'astro:content';
// Get all non-draft blog posts, sorted by date
const posts = (await getCollection('blog', ({ data }) => !data.draft))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
// Get a single entry
const post = await getEntry('blog', 'my-first-post');
// Resolve a reference
const author = await getEntry(post.data.author);
// Render content
const { Content, headings } = await post.render();
---
<Content />
内容集合 vs 外部 CMS
| 标准 | 内容集合 | 外部 CMS (Payload 等) |
|---|---|---|
| 内容类型 | 本地 Markdown/MDX、文档、博客 | 关系数据模型 |
| 作者 | 开发者 (版本控制) | 编辑者 (管理界面、多用户认证) |
| 验证 | 通过 Zod 在构建时实现类型安全 | CMS 端 Schema + API 合约 |
| 更新频率 | 随站点一起部署 | 独立于部署 |
| API 需求 | 无 (构建时查询) | REST/GraphQL (供其他消费者使用) |
| 工作流程 | Git PR、简单审阅 | 编辑工作流、草稿、角色 |
经验法则:从内容集合开始;仅当非开发人员需要在无需部署的情况下发布内容,或者内容确实是关系型时,才使用 CMS。
项目结构参考
project-root/
├── astro.config.mjs # Astro configuration
├── tsconfig.json # TypeScript config (extends astro/tsconfigs)
├── package.json
├── public/ # Static assets (copied as-is)
│ ├── favicon.svg
│ ├── robots.txt
│ └── og-image.png
├── src/
│ ├── pages/ # File-based routing
│ │ ├── index.astro # → /
│ │ ├── about.astro # → /about
│ │ ├── blog/
│ │ │ ├── index.astro # → /blog
│ │ │ └── [slug].astro # → /blog/:slug (dynamic)
│ │ ├── api/
│ │ │ └── search.ts # → /api/search (API endpoint)
│ │ └── [...slug].astro # → catch-all/404
│ ├── layouts/
│ │ ├── BaseLayout.astro # HTML shell, <head>, global styles
│ │ └── BlogPost.astro # Blog post layout
│ ├── components/
│ │ ├── Header.astro # Static Astro component
│ │ ├── Footer.astro
│ │ ├── NavBar.tsx # React island
│ │ └── Counter.svelte # Svelte island
│ ├── content/ # Content collections source files
│ │ ├── blog/
│ │ │ ├── post-one.md
│ │ │ └── post-two.mdx
│ │ └── authors/
│ │ └── jane.json
│ ├── content.config.ts # Collection schemas (Astro 5)
│ ├── middleware.ts # Request/response middleware
│ ├── styles/
│ │ └── global.css
│ └── lib/ # Shared utilities
│ ├── utils.ts
│ └── constants.ts
└── .env # Environment variables
视图过渡快速参考
---
// src/layouts/BaseLayout.astro
import { ViewTransitions } from 'astro:transitions';
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<slot />
</body>
</html>
过渡指令
<!-- Persist element across pages (keeps state, avoids re-render) -->
<audio transition:persist id="player">
<source src="/music.mp3" />
</audio>
<!-- Named transition for animation pairing -->
<img transition:name="hero" src={post.heroImage} />
<!-- Custom animation -->
<div transition:animate="slide">Content</div>
<div transition:animate="fade">Content</div>
<div transition:animate="none">No animation</div>
<!-- Persist with name (for multiple persistent elements) -->
<video transition:persist="media-player" />
生命周期事件
<script>
document.addEventListener('astro:before-preparation', (e) => {
// Before new page is fetched - cancel navigation, show loading
});
document.addEventListener('astro:after-preparation', (e) => {
// New page fetched, before swap
});
document.addEventListener('astro:before-swap', (e) => {
// Customize DOM swap behavior
});
document.addEventListener('astro:after-swap', () => {
// DOM updated - reinitialize scripts
});
document.addEventListener('astro:page-load', () => {
// Page fully loaded (fires on initial + every navigation)
// Use this instead of DOMContentLoaded with View Transitions
});
</script>
前进/后退处理
// astro.config.mjs
export default defineConfig({
prefetch: {
prefetchAll: true, // Prefetch all links on hover
defaultStrategy: 'hover', // 'hover' | 'tap' | 'viewport' | 'load'
},
});
<!-- Per-link prefetch control -->
<a href="/about" data-astro-prefetch>Prefetch on hover (default)</a>
<a href="/blog" data-astro-prefetch="viewport">Prefetch when visible</a>
<a href="/contact" data-astro-prefetch="load">Prefetch immediately</a>
<a href="/external" data-astro-prefetch="false">No prefetch</a>
部署决策树
Where to deploy?
│
├─ Need edge computing + Cloudflare ecosystem (KV, D1, R2)?
│ └─ Cloudflare Pages/Workers
│ Adapter: @astrojs/cloudflare
│ Best for: Global edge, Workers bindings, cost-effective
│
├─ Need serverless + Vercel ecosystem (ISR, analytics)?
│ └─ Vercel
│ Adapter: @astrojs/vercel
│ Best for: Next.js migration, image optimization, ISR
│
├─ Need serverless + Netlify ecosystem (forms, identity)?
│ └─ Netlify
│ Adapter: @astrojs/netlify
│ Best for: JAMstack, built-in forms, split testing
│
├─ Need full server control (Docker, custom runtime)?
│ └─ Node.js (standalone or Express/Fastify)
│ Adapter: @astrojs/node
│ Best for: Self-hosted, WebSocket, long-running processes
│
└─ Pure static site (no SSR needed)?
└─ Any static host (GitHub Pages, S3, Cloudflare Pages)
No adapter needed, output: 'static'
Best for: Blogs, docs, marketing sites
适配器安装
# Cloudflare
npx astro add cloudflare
# Vercel
npx astro add vercel
# Netlify
npx astro add netlify
# Node.js
npx astro add node
常见陷阱
| 陷阱 | 原因 | 修复 |
|---|---|---|
| 水合不匹配错误 | 服务器 HTML 与客户端渲染不同(日期、随机 ID、浏览器 API) | 对浏览器相关的组件使用 client:only,或确保确定性渲染 |
客户端中 import.meta.env 未定义 | 只有 PUBLIC_ 前缀的变量会暴露给客户端代码 | 重命名为 PUBLIC_MY_VAR 或通过 props 从服务器传递 |
| SSG 中动态路由 404 | getStaticPaths() 未返回所有可能的参数 | 确保 getStaticPaths() 返回每个有效的路径,或切换到混合/SSR |
| 图片未优化 | 使用 <img> 而不是 Astro 的 <Image /> 组件 | 从 astro:assets 导入:import { Image } from 'astro:assets' 并对 src 使用本地导入 |
| 无适配器的 SSR 失败 | output: 'server' 或 'hybrid' 需要部署适配器 | 安装适配器:npx astro add cloudflare(或 vercel、netlify、node) |
| MDX 组件未渲染 | 自定义组件未传递给 MDX 内容 | 通过 <Content components={{ MyComponent }} /> 传递组件,或使用 astro.config.mjs 的 MDX 配置 |
| 内容集合 Schema 更改未反映 | 类型生成已缓存,.astro 类型过期 | 运行 astro sync 重新生成类型,重启开发服务器 |
在 Astro 组件上使用 client:* | 客户端指令仅适用于框架组件(React、Vue、Svelte) | Astro 组件是纯静态的;将交互部分提取到框架组件中 |
document/window 未定义 | 服务器端代码无法访问浏览器全局变量 | 使用 if (typeof window !== 'undefined') 进行保护,或将其移至 client:only |
| 组件间样式泄露 | 使用全局 CSS 而不是作用域样式 | 使用 <style>(在 .astro 中默认作用域)或有意使用 <style is:global> |
| 视图过渡破坏脚本 | DOMContentLoaded 在视图过渡中只触发一次 | 改用 astro:page-load 事件,该事件在每次导航时都会触发 |
| 生产环境中缺少环境变量 | 未加载 .env 或平台环境变量未配置 | 在 astro.config.mjs 中使用 envField 进行验证;在平台仪表盘中设置变量 |
生产安全清单
对于每个生产部署,请处理:
- CSP 头 - 配置严格的
Content-Security-Policy(请参阅references/deployment.md中的中间件模式) - 远程图片限制 - 强制执行明确的
image.domains/remotePatterns允许列表;切勿从用户输入中派生图片 URL(SSRF 风险) - 主机头验证 - 在中间件中验证请求主机是否与预期域匹配(仅 SSR/混合)
- 密钥管理 - 在 Cloudflare 上,使用 Workers Bindings (
wrangler secret put),而不是将环境变量嵌入代码中;其他地方使用平台密钥存储 - 仅 HTTPS - 确保所有外部资源(脚本、图片、字体)通过 HTTPS 加载
- 输入验证 - 在 SSR 上下文中清理所有用户输入(查询参数、表单体、Cookie)
参考文件
| 文件 | 内容 | 行数 |
|---|---|---|
references/content-collections.md | Schema patterns, Zod types, querying, MDX, content layer API, migrations | ~500 |
| `references/islands-rendering |
…
[已截断供翻译]