关于 astro
面向 Astro 开发者的参考技能,涵盖组件、内容集合与 Islands 架构,并提供性能、安全和故障排查建议。
主要功能
- 01Astro 组件与客户端指令
- 02类型安全的内容集合
- 03Markdown 与 MDX 支持
- 04性能和安全配置指导
- 05常见问题排查
适用场景
- 01构建内容型网站
- 02配置 Islands 延迟水合
- 03验证博客内容结构
- 04排查 Astro 构建问题
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill astro安装后在 Claude Code 中输入/astro确认已加载。
附带文件quick-ref/content.mdquick-ref/islands.md
详细介绍
Astro 核心知识
深入了解:使用 technology 为
astro的mcp__documentation__fetch_docs获取全面文档。
组件结构
---
// Component script (runs at build time)
import Header from '../components/Header.astro';
import ReactCounter from '../components/Counter.tsx';
const { title } = Astro.props;
const posts = await Astro.glob('./posts/*.md');
---
<html>
<head><title>{title}</title></head>
<body>
<Header />
<main>
<slot />
</main>
<ReactCounter client:load />
</body>
</html>
<style>
main { max-width: 800px; }
</style>
客户端指令(岛屿)
| 指令 | 行为 |
|---|---|
client:load | 立即注水 |
client:idle | 空闲时注水 |
client:visible | 可见时注水 |
client:media | 媒体查询匹配时注水 |
client:only | 跳过 SSR,仅在客户端运行 |
内容集合
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
date: z.date(),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
---
import { getCollection } from 'astro:content';
const posts = await getCollection('blog', ({ data }) => !data.draft);
---
主要特性
- 默认不包含 JS(仅发送 HTML)
- 可同时使用 React、Vue 和 Svelte
- 内容集合提供类型安全
- 内置 Markdown/MDX 支持
生产就绪性
安全配置
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
security: {
checkOrigin: true, // CSRF protection for SSR
},
vite: {
define: {
// Never expose secrets to client
'import.meta.env.SECRET_KEY': 'undefined',
},
},
});
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
const response = await next();
// Security headers
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
);
return response;
});
内容验证
// src/content/config.ts
import { defineCollection, z, reference } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: ({ image }) =>
z.object({
title: z.string().max(100),
description: z.string().max(200),
date: z.date(),
author: reference('authors'),
cover: image().refine((img) => img.width >= 800, {
message: 'Cover image must be at least 800px wide',
}),
tags: z.array(z.string()).max(5),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
性能
// astro.config.mjs
import { defineConfig } from 'astro/config';
import compress from 'astro-compress';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://example.com',
integrations: [
sitemap(),
compress({
CSS: true,
HTML: true,
Image: true,
JavaScript: true,
SVG: true,
}),
],
build: {
inlineStylesheets: 'auto',
},
prefetch: {
prefetchAll: true,
defaultStrategy: 'viewport',
},
});
---
// Image optimization
import { Image, getImage } from 'astro:assets';
import heroImage from '../assets/hero.png';
const optimizedBackground = await getImage({ src: heroImage, format: 'webp' });
---
<Image
src={heroImage}
alt="Hero"
widths={[400, 800, 1200]}
sizes="(max-width: 800px) 100vw, 800px"
loading="eager"
/>
<ReactWidget client:visible />
<ViewTransitions />
错误处理
---
// src/pages/404.astro
import Layout from '../layouts/Layout.astro';
---
<Layout title="Page Not Found">
<div class="error-page">
<h1>404</h1>
<p>Page not found</p>
<a href="/">Go home</a>
</div>
</Layout>
---
// src/pages/500.astro
import Layout from '../layouts/Layout.astro';
---
<Layout title="Server Error">
<div class="error-page">
<h1>500</h1>
<p>Something went wrong</p>
<a href="/">Go home</a>
</div>
</Layout>
// src/pages/api/data.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ request }) => {
try {
const data = await fetchData();
return new Response(JSON.stringify(data), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('API error:', error);
return new Response(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
};
测试
// tests/e2e/blog.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Blog', () => {
test('lists published posts', async ({ page }) => {
await page.goto('/blog');
const posts = page.locator('article');
await expect(posts).toHaveCount(await posts.count());
await expect(posts.first()).toBeVisible();
});
test('navigates to post', async ({ page }) => {
await page.goto('/blog');
await page.click('article a');
await expect(page.locator('h1')).toBeVisible();
await expect(page).toHaveURL(/\/blog\/.+/);
});
});
// Component testing with container queries
test('island hydrates on visibility', async ({ page }) => {
await page.goto('/');
const counter = page.locator('[data-testid="counter"]');
await expect(counter).not.toBeVisible();
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await expect(counter).toBeVisible();
});
部署配置
# Vercel - vercel.json
{
"buildCommand": "astro build",
"outputDirectory": "dist",
"framework": "astro"
}
# Netlify - netlify.toml
[build]
command = "astro build"
publish = "dist"
# Docker
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
监控指标
| 指标 | 目标 |
|---|---|
| Lighthouse 评分 | > 95 |
| 首次内容绘制 | < 1s |
| 可交互时间 | < 1.5s |
| 总阻塞时间 | < 50ms |
| Bundle 大小(JS) | < 50KB |
检查清单
- 在中间件中设置安全标头
- 为 SSR 启用 checkOrigin
- 使用 Zod schema 定义内容集合
- 使用 astro:assets 优化图像
- 延迟注水(client:visible/idle)
- 启用 View Transitions
- 设置 404/500 错误页面
- 生成站点地图
- 压缩资源
- 使用 Playwright 进行 E2E 测试
- 在流水线中运行 Lighthouse CI
不适合使用此技能的情况
此技能适用于 Astro(以内容为中心、采用岛屿架构)。请勿用于:
- Next.js(React 元框架):改用
nextjs-app-router技能 - Nuxt(Vue 元框架):改用
nuxt3技能 - SvelteKit(Svelte 元框架):改用
sveltekit技能 - Remix(React 元框架):改用
remix技能 - 纯 React 应用:改用
frontend-react技能 - 纯 Vue 应用:改用
frontend-vue技能 - 纯 Svelte 应用:改用
frontend-svelte技能 - Gatsby:Astro 是一种现代替代方案,但迁移方式有所不同
反模式
| 反模式 | 错误原因 | 正确做法 |
|---|---|---|
| 到处使用 client:load | 违背零 JS 理念,导致 Bundle 过大 | 使用 client:idle 或 client:visible 延迟注水 |
| 不使用内容集合 | 内容未经验证,也不具备类型安全 | 在 src/content/config.ts 中定义集合 |
| 不必要地混用框架 | 增加 Bundle 大小和复杂性 | 每个项目使用一个框架,或使用 Astro 组件 |
| 忽视图像优化 | 性能差,资源体积大 | 使用 astro:assets 中的 |
| 不为图像设置 alt 文本 | 影响无障碍访问,并损害 SEO | 始终提供有意义的 alt 文本 |
| 对所有内容使用 client:only | 没有 SSR,SEO 表现差 | 仅对只能在浏览器中运行的组件使用 client:only |
| 在组件中硬编码数据 | 难以维护,无法集成 CMS | 使用内容集合或通过 API 获取数据 |
| 不使用 ViewTransitions | 导航体验生硬 | 在布局中添加 |
快速故障排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| "Cannot use import.meta.env in client" | 访问了仅限服务端的环境变量 | 添加 PUBLIC_ 前缀以便在客户端访问 |
| 孤岛未激活水合 | 客户端指令错误 | 检查是否设置了 client:load/idle/visible 指令 |
| 找不到内容集合 | 未定义 Schema 或路径错误 | 在 src/content/config.ts 中定义,并检查 src/content/{collection} |
| 图片未优化 | 使用了 | 从 astro:assets 导入并使用 |
| "getCollection is not defined" | 导入错误 | 从 'astro:content' 导入 |
| 构建因类型错误失败 | 内容 Schema 不匹配 | 检查 frontmatter 是否符合 Zod Schema |
| 未显示 404 页面 | 缺少 src/pages/404.astro | 在 src/pages/ 中创建 404.astro |
| CSS 未限定作用域 | 组件中缺少 | 在 Astro 组件中添加 <style> 块 |
参考文档
评论
支持 Markdown