Learn-skills.dev web-meta-framework-astro

Astro content-first framework - islands architecture, content collections, file-based routing, SSR/SSG hybrid rendering, View Transitions, server islands, multi-framework component support

install
source · Clone the upstream repo
git clone https://github.com/NeverSight/learn-skills.dev
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/agents-inc/skills/web-meta-framework-astro" ~/.claude/skills/neversight-learn-skills-dev-web-meta-framework-astro && rm -rf "$T"
manifest: data/skills-md/agents-inc/skills/web-meta-framework-astro/SKILL.md
source content

Astro Framework Patterns

Quick Guide: Astro renders pages to static HTML by default with zero client-side JavaScript. Use

.astro
components for all static content, add
client:*
directives only on interactive framework components (React/Vue/Svelte). Use content collections for type-safe structured content. Choose between static (default) and on-demand (SSR) rendering per-page with
export const prerender
.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,

import type
, named constants)

(You MUST keep pages static by default - only add

export const prerender = false
when the page genuinely needs request-time data)

(You MUST use

client:*
directives on framework components that need interactivity - without a directive, components render to static HTML with zero JavaScript)

(You MUST define content collections in

src/content.config.ts
with Zod schemas for type-safe frontmatter)

(You MUST use

<ClientRouter />
from
astro:transitions
for View Transitions - the old
<ViewTransitions />
component is removed in Astro 6)

(You MUST install a server adapter (@astrojs/node, @astrojs/vercel, etc.) before using on-demand rendering)

(You MUST use

getStaticPaths()
for dynamic routes in static mode - it is not needed for on-demand (SSR) routes)

</critical_requirements>


Auto-detection: Astro, .astro files, astro.config, islands architecture, client:load, client:visible, client:idle, client:only, client:media, server:defer, content collections, defineCollection, defineLiveCollection, getCollection, getLiveCollection, getEntry, getLiveEntry, render, astro:content, astro:transitions, ClientRouter, getStaticPaths, Astro.props, Astro.params, Astro.cookies, Astro.redirect, prerender, astro add, @astrojs/react, @astrojs/vue, @astrojs/svelte, Starlight

When to use:

  • Building content-driven websites (blogs, docs, marketing, portfolios)
  • Sites where most pages are static with selective interactivity (islands)
  • Projects using content collections for structured Markdown/MDX/YAML content
  • Multi-framework projects mixing React, Vue, Svelte, or Solid components
  • Sites needing hybrid rendering (static pages + some server-rendered pages)

When NOT to use:

  • Highly interactive web applications (dashboards, real-time collaboration) - use a full-stack SSR framework or SPA
  • Apps where every page requires user authentication and dynamic data - use a full-stack SSR framework
  • Projects that need React Server Components or Server Actions - use a React SSR framework

Key patterns covered:

  • Astro component syntax (.astro files, frontmatter, template expressions, slots)
  • Islands architecture (client directives, server islands, selective hydration)
  • Content collections (schemas, querying, rendering, references, live collections)
  • File-based routing (static routes, dynamic routes, rest parameters, pagination)
  • Rendering modes (static, on-demand/SSR, hybrid with prerender control)
  • View Transitions (ClientRouter, transition directives, persist state)
  • Framework integrations (React, Vue, Svelte, Solid islands)

Detailed Resources:

  • For decision frameworks and anti-patterns, see reference.md

Core patterns:


<philosophy>

Philosophy

Astro is a content-first web framework that ships zero JavaScript by default. It pioneered the islands architecture where most of the page is fast static HTML, with small interactive "islands" of JavaScript hydrated only where needed.

Core principles:

  1. Content-first - Optimized for content-driven sites (blogs, docs, marketing, e-commerce)
  2. Zero JS by default - Components render to static HTML unless explicitly hydrated
  3. Islands architecture - Interactive components hydrate independently, reducing JavaScript payloads
  4. UI-agnostic - Use React, Vue, Svelte, Solid, Preact, or plain Astro components
  5. File-based routing -
    src/pages/
    directory structure maps directly to URLs
  6. Type-safe content - Content collections with Zod schemas enforce structure and provide TypeScript types
  7. Hybrid rendering - Mix static (SSG) and on-demand (SSR) pages in the same project

When to use Astro:

  • Content-driven websites (blogs, documentation, portfolios, marketing)
  • Sites with mostly static content and occasional interactivity
  • Documentation sites (Starlight integration)
  • E-commerce product pages with interactive carts
  • Multi-framework projects where teams use different UI libraries

When NOT to use Astro:

  • Fully interactive web applications (use a full-stack SSR framework or SPA)
  • Real-time collaborative apps (use a dedicated SPA with WebSocket support)
  • Projects requiring React Server Components or Server Actions (use a React SSR framework)
</philosophy>
<patterns>

Core Patterns

Pattern 1: Astro Component Structure

Astro components (

.astro
files) have two parts: a frontmatter script block (between
---
fences) and an HTML template.

Component Anatomy

---
// Component Script (frontmatter) - runs on the server only
import Layout from "../layouts/Layout.astro";
import { getCollection } from "astro:content";

// Props accessed via Astro.props
interface Props {
  title: string;
  description?: string;
}

const { title, description = "Default description" } = Astro.props;

// Server-side data fetching
const posts = await getCollection("blog");
---

<!-- Component Template - HTML with expressions -->
<Layout title={title}>
  <h1>{title}</h1>
  <p>{description}</p>

  <ul>
    {posts.map((post) => (
      <li>
        <a href={`/blog/${post.id}`}>{post.data.title}</a>
      </li>
    ))}
  </ul>
</Layout>

<style>
  /* Scoped to this component by default */
  h1 {
    color: navy;
    font-size: 2rem;
  }
</style>

Why good: Frontmatter runs server-only (no JavaScript shipped), type-safe props with interface, scoped styles prevent leakage, expressions use JSX-like syntax


Pattern 2: Slots for Composition

Slots allow parent components to inject content into child component templates.

Default and Named Slots

---
// src/components/Card.astro
interface Props {
  title: string;
}

const { title } = Astro.props;
---

<article class="card">
  <header>
    <slot name="header">
      <h2>{title}</h2>
    </slot>
  </header>

  <div class="body">
    <slot />  <!-- Default slot -->
  </div>

  <footer>
    <slot name="footer">
      <p>Default footer</p>
    </slot>
  </footer>
</article>
---
// Usage in a page
import Card from "../components/Card.astro";
---

<Card title="My Card">
  <span slot="header"><h2>Custom Header</h2></span>

  <p>This goes in the default slot.</p>

  <div slot="footer">
    <a href="/more">Read more</a>
  </div>
</Card>

Why good: Named slots provide flexible composition, fallback content renders when no slot content is provided, matches Web Component slot semantics


Pattern 3: Layouts

Layouts are Astro components that wrap page content with shared UI (header, footer, navigation).

Base Layout with Metadata

---
// src/layouts/BaseLayout.astro
interface Props {
  title: string;
  description?: string;
}

const { title, description = "My Astro Site" } = Astro.props;
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content={description} />
    <title>{title}</title>
  </head>
  <body>
    <nav>
      <a href="/">Home</a>
      <a href="/blog">Blog</a>
      <a href="/about">About</a>
    </nav>

    <main>
      <slot />
    </main>

    <footer>
      <p>&copy; {new Date().getFullYear()} My Site</p>
    </footer>
  </body>
</html>
---
// src/pages/about.astro
import BaseLayout from "../layouts/BaseLayout.astro";
---

<BaseLayout title="About Us" description="Learn about our team">
  <h1>About Us</h1>
  <p>We build great things with Astro.</p>
</BaseLayout>

Why good: Shared layout reduces duplication, metadata props ensure SEO per-page, slot injects page content


Pattern 4: Islands Architecture (Client Directives)

By default, framework components (React, Vue, Svelte) render to static HTML. Add a

client:*
directive to hydrate them as interactive islands.

Client Directive Reference

DirectiveWhen it hydratesUse for
client:load
Immediately on page loadCritical interactive UI
client:idle
When browser is idleLower-priority interactivity
client:visible
When component scrolls into viewportBelow-the-fold content
client:media
When media query matchesResponsive interactivity
client:only
Skips server render, client-onlyBrowser-dependent components
---
// src/pages/index.astro
import Header from "../components/Header.astro"; // Static - no JS
import SearchBar from "../components/SearchBar"; // React component
import Newsletter from "../components/Newsletter"; // React component
import Comments from "../components/Comments"; // React component
import Analytics from "../components/Analytics"; // React component
---

<Header />

<!-- Hydrate immediately - user interacts with search right away -->
<SearchBar client:load />

<!-- Hydrate when browser is idle - not urgent -->
<Newsletter client:idle />

<!-- Hydrate only when scrolled into view -->
<Comments client:visible />

<!-- Hydrate only on mobile screens -->
<Analytics client:media="(max-width: 768px)" />

Why good: Only interactive components ship JavaScript, hydration is deferred to optimal timing, static components render as zero-JS HTML

Server Islands

Server islands defer rendering of dynamic content on the server, allowing static shell to cache while dynamic parts render per-request.

---
// src/pages/product.astro
import ProductInfo from "../components/ProductInfo.astro"; // Static
import UserReviews from "../components/UserReviews.astro"; // Dynamic
---

<!-- Static product info - cached -->
<ProductInfo product={product} />

<!-- Server island - rendered per-request, placeholder shown while loading -->
<UserReviews server:defer>
  <div slot="fallback">Loading reviews...</div>
</UserReviews>

Why good: Static page shell caches and serves instantly, dynamic portions render per-request without blocking the page, fallback content prevents layout shift


Pattern 5: Content Collections

Content collections provide type-safe management of structured content (blog posts, docs, product data).

Defining Collections

// src/content.config.ts
import { defineCollection } from "astro:content";
import { glob, file } from "astro/loaders";
import { z } from "astro/zod";

const blog = defineCollection({
  loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    heroImage: z.string().optional(),
    draft: z.boolean().default(false),
    tags: z.array(z.string()).default([]),
  }),
});

const authors = defineCollection({
  loader: file("src/data/authors.json"),
  schema: z.object({
    name: z.string(),
    bio: z.string(),
    avatar: z.string(),
  }),
});

export const collections = { blog, authors };

Querying and Rendering Collections

---
// src/pages/blog/index.astro
import { getCollection } from "astro:content";
import BaseLayout from "../../layouts/BaseLayout.astro";

const allPosts = await getCollection("blog", ({ data }) => {
  return data.draft !== true; // Filter out drafts
});

// Sort by date descending
const sortedPosts = allPosts.sort(
  (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
---

<BaseLayout title="Blog">
  <h1>Blog</h1>
  <ul>
    {sortedPosts.map((post) => (
      <li>
        <a href={`/blog/${post.id}`}>
          <h2>{post.data.title}</h2>
          <time datetime={post.data.pubDate.toISOString()}>
            {post.data.pubDate.toLocaleDateString()}
          </time>
        </a>
      </li>
    ))}
  </ul>
</BaseLayout>
---
// src/pages/blog/[id].astro
import { getCollection, getEntry, render } from "astro:content";
import BaseLayout from "../../layouts/BaseLayout.astro";

export async function getStaticPaths() {
  const posts = await getCollection("blog");
  return posts.map((post) => ({
    params: { id: post.id },
    props: { post },
  }));
}

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

<BaseLayout title={post.data.title}>
  <article>
    <h1>{post.data.title}</h1>
    <time datetime={post.data.pubDate.toISOString()}>
      {post.data.pubDate.toLocaleDateString()}
    </time>
    <Content />
  </article>
</BaseLayout>

Why good: Zod schemas validate frontmatter at build time, TypeScript types are auto-generated, querying with filters is type-safe,

render()
converts Markdown to a component

Live Collections (Astro 6+)

For data that changes frequently and needs to be fresh on every request, use

defineLiveCollection
in a separate
src/live.config.ts
file (not
src/content.config.ts
). Query with
getLiveCollection()
and
getLiveEntry()
instead of
getCollection()
. Requires SSR (
prerender = false
). See examples/content.md for full examples.


Pattern 6: File-Based Routing

The

src/pages/
directory structure directly maps to URL paths.

Static Routes

src/pages/
├── index.astro          → /
├── about.astro          → /about
├── blog/
│   ├── index.astro      → /blog
│   └── [id].astro       → /blog/:id (dynamic)
├── docs/
│   └── [...slug].astro  → /docs/* (rest parameter)
└── _helpers.ts          → excluded (underscore prefix)

Dynamic Routes with getStaticPaths

---
// src/pages/tags/[tag].astro
import { getCollection } from "astro:content";

export async function getStaticPaths() {
  const posts = await getCollection("blog");
  const tags = [...new Set(posts.flatMap((post) => post.data.tags))];

  return tags.map((tag) => ({
    params: { tag },
    props: {
      posts: posts.filter((post) => post.data.tags.includes(tag)),
    },
  }));
}

const { tag } = Astro.params;
const { posts } = Astro.props;
---

<h1>Posts tagged: {tag}</h1>
<ul>
  {posts.map((post) => (
    <li><a href={`/blog/${post.id}`}>{post.data.title}</a></li>
  ))}
</ul>

Rest Parameters

---
// src/pages/docs/[...slug].astro
export async function getStaticPaths() {
  return [
    { params: { slug: "getting-started" } },
    { params: { slug: "guides/installation" } },
    { params: { slug: "api/reference" } },
    { params: { slug: undefined } }, // matches /docs
  ];
}

const { slug } = Astro.params;
---

<h1>Docs: {slug ?? "Index"}</h1>

Why good: File structure mirrors URL structure, dynamic params are type-safe via getStaticPaths, rest parameters handle arbitrary depth


Pattern 7: On-Demand Rendering (SSR)

Opt individual pages into server-side rendering while keeping the rest static.

Per-Page SSR

---
// src/pages/dashboard.astro
export const prerender = false; // Render on every request

const cookie = Astro.cookies.get("session");
if (!cookie) {
  return Astro.redirect("/login");
}

const user = await getUserFromSession(cookie.value);
---

<h1>Welcome, {user.name}</h1>

Server Output Mode

// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";

export default defineConfig({
  output: "server", // All pages server-rendered by default
  adapter: node({ mode: "standalone" }),
});
---
// src/pages/about.astro
export const prerender = true; // Opt this page back to static
---

<h1>About Us</h1>

API Endpoints

// src/pages/api/search.ts
export const prerender = false;

import type { APIRoute } from "astro";

export const GET: APIRoute = async ({ url }) => {
  const query = url.searchParams.get("q");
  if (!query) {
    return new Response(JSON.stringify({ error: "Missing query" }), {
      status: 400,
      headers: { "Content-Type": "application/json" },
    });
  }

  const results = await searchDatabase(query);
  return new Response(JSON.stringify(results), {
    headers: { "Content-Type": "application/json" },
  });
};

Why good: Static by default for performance, opt into SSR per-page as needed, API endpoints support full request/response control


Pattern 8: View Transitions

Astro provides smooth page transitions using the browser's View Transition API via the

<ClientRouter />
component.

Enabling View Transitions

---
// src/layouts/BaseLayout.astro
import { ClientRouter } from "astro:transitions";
---

<html lang="en">
  <head>
    <ClientRouter />
  </head>
  <body>
    <slot />
  </body>
</html>

Transition Directives

---
// src/pages/blog/[id].astro
---

<!-- Name pairs elements across pages for smooth morphing -->
<img
  src={post.data.heroImage}
  transition:name={`hero-${post.id}`}
  transition:animate="slide"
/>

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

<!-- Persist interactive elements across navigations -->
<audio controls transition:persist>
  <source src="/music.mp3" type="audio/mp3" />
</audio>

Why good: Smooth transitions between pages without SPA framework, paired elements morph naturally, persistent elements maintain state across navigation

</patterns>

<red_flags>

RED FLAGS

High Priority Issues:

  • Adding
    client:load
    to every component
    - Defeats islands architecture; only hydrate components that need interactivity
  • Using framework components for static content - Use
    .astro
    components for zero-JS static HTML
  • Missing
    getStaticPaths()
    on dynamic routes in static mode
    - Build will fail
  • Using
    <ViewTransitions />
    - Removed in Astro 6; use
    <ClientRouter />
    from
    astro:transitions
  • No server adapter with
    prerender = false
    - On-demand rendering requires an adapter

Medium Priority Issues:

  • Fetching data in
    <script>
    tags instead of frontmatter
    - Frontmatter runs server-side; use it for data fetching
  • Using
    client:only
    when
    client:load
    would work
    -
    client:only
    skips SSR, hurting SEO
  • Missing Zod schema on content collections - Loses type safety and build-time validation
  • Using
    output: 'server'
    for mostly static sites
    - Default static mode with per-page SSR opt-in is more performant

Gotchas & Edge Cases:

  • Styles in
    .astro
    are scoped by default
    - Use
    <style is:global>
    or
    :global()
    selector for global styles
  • client:visible
    uses IntersectionObserver
    - Component won't hydrate if always off-screen
  • Astro.redirect()
    only works in on-demand rendered pages
    - Static pages cannot redirect at request time
  • Multiple framework islands share no state - Each island is independent; use nanostores for cross-island communication
  • transition:persist
    requires matching
    transition:name
    - Elements must have the same name on both pages
  • Astro 6 requires Node.js 22.12.0+ - Earlier Node versions are not supported
  • Astro.glob()
    removed in Astro 6
    - Use
    import.meta.glob()
    instead
  • z
    from
    astro:content
    removed in Astro 6
    - Import
    z
    from
    astro/zod
    instead
  • Astro 6 uses Zod 4 - Some Zod 3 patterns changed (e.g.,
    z.string().email()
    becomes
    z.email()
    )
  • getStaticPaths()
    params must be strings
    - Number params are no longer allowed in Astro 6
  • Live collections use
    src/live.config.ts
    - Not
    src/content.config.ts
    (separate config file)
  • import.meta.env
    values are inlined at build time in Astro 6
    - Use
    process.env
    for runtime secrets in live collections and SSR code

For complete anti-patterns with code examples, see reference.md.

</red_flags>


<integration>

Integration Guide

Astro is the top-level framework. It handles routing, rendering, and content management. UI framework components (React, Vue, Svelte) are used as islands within Astro pages.

Adding framework support:

# Add framework support via CLI
npx astro add react
npx astro add vue

# Add multiple integrations at once
npx astro add react sitemap

Key integrations:

  • UI Frameworks - React, Vue, Svelte, Solid used as interactive islands within Astro pages
  • MDX - Via
    @astrojs/mdx
    for components in Markdown
  • Starlight - Astro's documentation theme, built on content collections

Deployment adapters:

  • @astrojs/vercel - Vercel deployment with edge/serverless
  • @astrojs/netlify - Netlify Functions/Edge
  • @astrojs/cloudflare - Cloudflare Workers/Pages
  • @astrojs/node - Self-hosted Node.js server

Does NOT replace:

  • Full-stack SSR frameworks for highly interactive, full-stack React applications
  • SPA frameworks for real-time, fully client-rendered apps
</integration>

<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md

(You MUST keep pages static by default - only add

export const prerender = false
when the page genuinely needs request-time data)

(You MUST use

client:*
directives on framework components that need interactivity - without a directive, components render to static HTML with zero JavaScript)

(You MUST define content collections in

src/content.config.ts
with Zod schemas for type-safe frontmatter)

(You MUST use

<ClientRouter />
from
astro:transitions
for View Transitions - the old
<ViewTransitions />
component is removed in Astro 6)

(You MUST install a server adapter (@astrojs/node, @astrojs/vercel, etc.) before using on-demand rendering)

(You MUST use

getStaticPaths()
for dynamic routes in static mode - it is not needed for on-demand (SSR) routes)

Failure to follow these rules will ship unnecessary JavaScript, break builds, cause missing pages, or produce type errors.

</critical_reminders>