Files
skillhub-077-astro/SKILL.md
T
2026-07-13 21:36:10 +08:00

10 KiB
Raw Blame History

name, description, category, risk, source, date_added, author, tags, tools
name description category risk source date_added author tags tools
astro 使用 Astro 构建以内容为中心的网站 —— 默认零 JavaScript、岛屿架构、多框架组件,支持 Markdown/MDX。 frontend safe community 2026-03-18 suhaibjanjua
astro
ssg
ssr
islands
content
markdown
mdx
performance
claude
cursor
gemini

Astro Web 框架

概述

Astro 是一个专为内容密集型网站设计的 Web 框架 —— 博客、文档、作品集、营销站点和电子商务。其核心创新在于岛屿架构:默认情况下,Astro 不向浏览器发送任何 JavaScript。交互式组件会被选择性地水合为独立的"岛屿"。Astro 支持在同一项目中同时使用 React、Vue、Svelte、Solid 及其他 UI 框架,让你可以为每个组件选择最合适的工具。

何时使用本技能

  • 当构建博客、文档站点、营销页面或作品集时使用
  • 当性能和 Core Web Vitals 是最高优先级时使用
  • 当项目包含大量 Markdown 或 MDX 文件时使用
  • 当用户询问 .astro 文件、Astro.props、内容集合或 client: 指令时使用

使用方法

第 1 步:项目设置

npm create astro@latest my-site
cd my-site
npm install
npm run dev

按需添加集成:

npx astro add tailwind        # Tailwind CSS
npx astro add react           # React 组件支持
npx astro add mdx             # MDX 支持
npx astro add sitemap         # 自动生成 sitemap.xml
npx astro add vercel          # Vercel SSR 适配器

项目结构:

src/
  pages/          ← 基于文件的路由(.astro、.md、.mdx
  layouts/        ← 可复用的页面外壳
  components/     ← UI 组件(.astro、.tsx、.vue 等)
  content/        ← 类型安全的内容集合(Markdown/MDX
  styles/         ← 全局 CSS
public/           ← 静态资源(原样复制)
astro.config.mjs  ← 框架配置

第 2 步:Astro 组件语法

.astro 文件顶部有一个代码围栏(仅服务端运行),下方是模板:

---
// src/components/Card.astro
// 此代码块仅在服务端运行 —— 绝不在浏览器中执行
interface Props {
  title: string;
  href: string;
  description: string;
}

const { title, href, description } = Astro.props;
---

<article class="card">
  <h2><a href={href}>{title}</a></h2>
  <p>{description}</p>
</article>

<style>
  /* 自动作用于当前组件 */
  .card { border: 1px solid #eee; padding: 1rem; }
</style>

第 3 步:基于文件的页面与路由

src/pages/index.astro          → /
src/pages/about.astro          → /about
src/pages/blog/[slug].astro    → /blog/:slug(动态路由)
src/pages/blog/[...path].astro → /blog/*(通配路由)

getStaticPaths 的动态路由:

---
// src/pages/blog/[slug].astro
export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

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

<h1>{post.data.title}</h1>
<Content />

第 4 步:内容集合

内容集合让你可以对 Markdown 和 MDX 文件进行类型安全访问:

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

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

export const collections = { blog };
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';

const posts = (await getCollection('blog'))
  .filter(p => !p.data.draft)
  .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
---

<ul>
  {posts.map(post => (
    <li>
      <a href={`/blog/${post.slug}`}>{post.data.title}</a>
      <time>{post.data.date.toLocaleDateString()}</time>
    </li>
  ))}
</ul>

第 5 步:岛屿 —— 选择性水合

默认情况下,UI 框架组件会渲染为不含 JavaScript 的静态 HTML。使用 client: 指令进行水合:

---
import Counter from '../components/Counter.tsx';  // React 组件
import VideoPlayer from '../components/VideoPlayer.svelte';
---

<!-- 静态 HTML —— 不向浏览器发送 JavaScript -->
<Counter initialCount={0} />

<!-- 页面加载后立即水合 -->
<Counter initialCount={0} client:load />

<!-- 当组件滚动到可视区域时水合 -->
<VideoPlayer src="/demo.mp4" client:visible />

<!-- 仅在浏览器空闲时水合 -->
<Analytics client:idle />

<!-- 仅在特定媒体查询匹配时水合 -->
<MobileMenu client:media="(max-width: 768px)" />

第 6 步:布局

---
// src/layouts/BaseLayout.astro
interface Props {
  title: string;
  description?: string;
}
const { title, description = '我的 Astro 站点' } = Astro.props;
---

<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <title>{title}</title>
    <meta name="description" content={description} />
  </head>
  <body>
    <nav>...</nav>
    <main>
      <slot />  <!-- 页面内容在此渲染 -->
    </main>
    <footer>...</footer>
  </body>
</html>
---
// src/pages/about.astro
import BaseLayout from '../layouts/BaseLayout.astro';
---

<BaseLayout title="关于我们">
  <h1>关于我们</h1>
  <p>欢迎来到我们的公司...</p>
</BaseLayout>

第 7 步:SSR 模式(按需渲染)

通过设置适配器启用 SSR,以支持动态页面:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';

export default defineConfig({
  output: 'hybrid',  // 'static' | 'server' | 'hybrid'
  adapter: vercel(),
});

使用 export const prerender = false 将单个页面切换为 SSR 模式。

示例

示例 1:带 RSS 订阅的博客

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

export async function GET(context) {
  const posts = await getCollection('blog');
  return rss({
    title: '我的博客',
    description: '最新文章',
    site: context.site,
    items: posts.map(post => ({
      title: post.data.title,
      pubDate: post.data.date,
      link: `/blog/${post.slug}/`,
    })),
  });
}

示例 2API 端点(SSR

// src/pages/api/subscribe.ts
import type { APIRoute } from 'astro';

export const POST: APIRoute = async ({ request }) => {
  const { email } = await request.json();

  if (!email) {
    return new Response(JSON.stringify({ error: '需要提供邮箱' }), {
      status: 400,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  await addToNewsletter(email);
  return new Response(JSON.stringify({ success: true }), { status: 200 });
};

示例 3React 组件作为岛屿

// src/components/SearchBox.tsx
import { useState } from 'react';

export default function SearchBox() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  async function search(e: React.FormEvent) {
    e.preventDefault();
    const data = await fetch(`/api/search?q=${query}`).then(r => r.json());
    setResults(data);
  }

  return (
    <form onSubmit={search}>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <button type="submit">搜索</button>
      <ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>
    </form>
  );
}
---
import SearchBox from '../components/SearchBox.tsx';
---
<!-- 立即水合 —— 这个岛屿是交互式的 -->
<SearchBox client:load />

最佳实践

  • 尽可能将大多数组件保留为静态 .astro 文件 —— 仅对必须交互的部分进行水合
  • 对所有 Markdown/MDX 内容使用内容集合 —— 可获得类型安全和自动校验
  • 对首屏以下的组件优先使用 client:visible 而非 client:load,以减少初始 JavaScript 体积
  • 使用 import.meta.env 管理环境变量 —— 公开变量以 PUBLIC_ 为前缀
  • 使用 astro:transitions 中的 <ViewTransitions /> 实现流畅的页面导航,无需完整的 SPA
  • 不要在每个组件上都使用 client:load —— 这会抵消 Astro 的性能优势
  • 不要在面向客户端的模板中使用的 .astro 文件围栏中放入密钥
  • 在静态模式下不要跳过动态路由的 getStaticPaths —— 否则构建将失败

安全注意事项

  • .astro 文件中的围栏代码仅在服务端运行,绝不会暴露给浏览器。
  • 仅对非敏感值使用 import.meta.env.PUBLIC_*。私有环境变量(无 PUBLIC_ 前缀)绝不会发送给客户端。
  • 使用 SSR 模式时,在数据库查询或 API 调用之前验证所有 Astro.request 输入。
  • 使用 set:html 渲染用户提供的内容前必须先进行转义处理 —— 它会绕过自动转义。

常见陷阱

  • 问题: React/Vue 组件的 JavaScript 在浏览器中不运行 解决方法: 添加 client: 指令(client:loadclient:visible 等)—— 不加指令时,组件仅渲染为静态 HTML。

  • 问题: 开发过程中内容更新后 getStaticPaths 的数据未更新 解决方法: Astro 的开发服务器会监听内容文件 —— 如果对 content/config.ts 的修改未生效,请重启服务器。

  • 问题: Astro.props 的类型为 any —— 无自动补全 解决方法: 在围栏中定义 Props 接口或类型,Astro 会自动推断类型。

  • 问题: .astro 组件中的 CSS 泄漏到其他组件中 解决方法: .astro<style> 标签中的样式会自动作用域隔离。仅在有意针对子元素时使用 :global()

相关技能

  • @sveltekit —— 当需要带有响应式 UI 的全栈框架时使用(相对于 Astro 的内容专注定位)
  • @nextjs-app-router-patterns —— 当需要以 React 为首的全栈框架时使用
  • @tailwind-patterns —— 使用 Tailwind CSS 为 Astro 站点添加样式
  • @progressive-web-app —— 为 Astro 站点添加 PWA 功能

限制说明

  • 仅当任务明确符合上述范围时才使用本技能。
  • 请勿将本技能的输出视为替代特定环境的验证、测试或专家审查。
  • 如果缺少必要的输入、权限、安全边界或成功标准,请停下来询问澄清。