mcpField
A
astro

astro

github.com/claude-dev-suite开发4639 次安装3 天前更新
代码仓库

关于 astro

面向 Astro 开发者的参考技能,涵盖组件、内容集合与 Islands 架构,并提供性能、安全和故障排查建议。

主要功能

  1. 01Astro 组件与客户端指令
  2. 02类型安全的内容集合
  3. 03Markdown 与 MDX 支持
  4. 04性能和安全配置指导
  5. 05常见问题排查

适用场景

  1. 01构建内容型网站
  2. 02配置 Islands 延迟水合
  3. 03验证博客内容结构
  4. 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 中的 README 配图
不为图像设置 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}
图片未优化使用了 README 配图 而非 README 配图从 astro:assets 导入并使用 README 配图
"getCollection is not defined"导入错误从 'astro:content' 导入
构建因类型错误失败内容 Schema 不匹配检查 frontmatter 是否符合 Zod Schema
未显示 404 页面缺少 src/pages/404.astro在 src/pages/ 中创建 404.astro
CSS 未限定作用域组件中缺少 在 Astro 组件中添加 <style> 块

参考文档

评论

支持 Markdown