skills/astro-islands-architect/SKILL.md
Use when building content-heavy sites with Astro, deciding between SSG/SSR/hybrid, choosing client directives (idle/visible/load/media), structuring content collections with type-safe schemas, integrating React/Vue/Solid/Svelte components in the same project, or migrating from Next.js for marketing/docs. Triggers: client:idle vs client:load tradeoffs, content collections schema with zod, image optimization via @astrojs/image, view transitions, server islands, MDX layouts. NOT for SPA-style apps that need full interactivity (use Next/Remix), Astro internals/plugin authoring, or non-content sites where islands provide no benefit.
npx skillsauth add curiositech/windags-skills astro-islands-architectInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Astro is "ship HTML by default, hydrate only what needs JS." For marketing, docs, and content-heavy sites, this produces dramatically smaller bundles than React/Next.js. The whole game is choosing which components are static and which are islands.
---
// src/pages/blog/[slug].astro — frontmatter runs at build/SSR time
import { getCollection, getEntry } from 'astro:content';
import Layout from '../../layouts/Default.astro';
import Newsletter from '../../components/Newsletter.tsx';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((p) => ({ params: { slug: p.slug }, props: { post: p } }));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<Layout title={post.data.title}>
<h1>{post.data.title}</h1>
<article><Content /></article>
{/* Island — only this component ships JS */}
<Newsletter client:visible />
</Layout>
The --- fences are server code. Inside <...>, the default is static HTML. Components with client:* directives become islands.
| Directive | When it hydrates | Use for |
|-----------|-------------------|---------|
| client:load | Page load | Above-the-fold interactive (cart, search). |
| client:idle | When the browser is idle | Non-critical interactive (chat widget). |
| client:visible | When scrolled into viewport | Below-the-fold (newsletter signup, comments). |
| client:media="(min-width: 768px)" | When the media query matches | Desktop-only widgets. |
| client:only="react" | Skip SSR; client-only render | Components that need browser APIs at mount. |
client:idle is the right default for most non-critical islands. client:load is the heavy hammer; reserve it.
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content', // markdown/MDX files
schema: z.object({
title: z.string().max(120),
description: z.string().max(160),
date: z.coerce.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
cover: z.object({ src: z.string(), alt: z.string() }).optional(),
}),
});
export const collections = { blog };
Now getCollection('blog') is fully typed. Frontmatter mistakes fail the build, not production.
const posts = (await getCollection('blog'))
.filter((p) => !p.data.draft)
.sort((a, b) => +b.data.date - +a.data.date);
npx astro add react vue svelte
---
import ReactSearch from '../components/Search.tsx';
import VueChart from '../components/Chart.vue';
import SvelteToggle from '../components/Toggle.svelte';
---
<ReactSearch client:idle />
<VueChart client:visible />
<SvelteToggle client:load />
Each framework's runtime is bundled separately and loaded only on pages that use it. For a docs site that's mostly static, this can mean shipping zero React JS to most pages.
<Layout>
<Header />
<PersonalizedRecommendations server:defer>
<p slot="fallback">Loading recommendations...</p>
</PersonalizedRecommendations>
<Footer />
</Layout>
server:defer renders the rest of the page immediately and streams the deferred component's HTML in via a separate request. Useful for personalized content that would otherwise force the whole page to be SSR.
---
import { Image } from 'astro:assets';
import cover from '../assets/cover.jpg';
---
<Image src={cover} alt="Cover photo" widths={[400, 800, 1200]} sizes="(max-width: 768px) 400px, 1200px" />
Astro generates srcset, runs Sharp for resizing, and ships only what's needed. Place images under src/assets/ for full optimization; public/ is unprocessed.
---
import { ViewTransitions } from 'astro:transitions';
---
<html>
<head>
<ViewTransitions />
</head>
...
</html>
<a href="/about" transition:name="hero">About</a>
Cross-page transitions without a SPA. The browser does most of the work; Astro coordinates element matching.
---
title: My post
date: 2026-04-30
---
import Callout from '../../components/Callout.astro';
# {frontmatter.title}
<Callout type="warn">
This is a warning rendered server-side. No JS.
</Callout>
Components imported in MDX run at build/SSR time unless they're islands.
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'hybrid',
adapter: cloudflare(),
});
---
// src/pages/api/search.ts — runs on the edge
export const prerender = false;
export async function GET({ request }) {
const url = new URL(request.url);
const q = url.searchParams.get('q') ?? '';
const results = await search(q);
return new Response(JSON.stringify(results), { headers: { 'content-type': 'application/json' } });
}
prerender = false opts a route into SSR; the rest of the site stays static.
client:load everywhereSymptom: Lighthouse JS bundle bloats; Astro's hydration story disappears.
Diagnosis: Engineers default to client:load because it's the most familiar.
Fix: Default to client:idle or client:visible. Reserve client:load for above-the-fold interactive.
Symptom: Loading spinners on every page; SEO hurt.
Diagnosis: Data fetching moved to React components instead of the Astro frontmatter.
Fix: Fetch in the --- fences, render server-side, pass data as props to islands.
Symptom: Build succeeds; production shows undefined in titles.
Diagnosis: No content collection schema; typo in frontmatter went unnoticed.
Fix: Use defineCollection with a zod schema. Frontmatter mistakes fail the build.
public/ and src/assets/ for imagesSymptom: Some images optimized, others ship at original size.
Diagnosis: public/ is served as-is; only src/assets/ runs through Sharp.
Fix: Move images to src/assets/. Use <Image> everywhere. public/ only for OG images and favicons.
Symptom: Constant battle with hydration; islands grow until the entire page is interactive. Diagnosis: Wrong framework for the job. Astro shines for content; SPAs need Next/Remix. Fix: Reach for Next/Remix when most pages are interactive. Use Astro for marketing + docs.
client:onlySymptom: Layout shift (CLS) on every page load; "blink" before content appears.
Diagnosis: Skipping SSR means the browser sees an empty placeholder until JS runs.
Fix: Use client:only only when the component truly can't render server-side. Provide a placeholder that approximates the final size.
client:idle or client:visible; client:load justified per use.defineCollection.src/assets/ and use the <Image> component.@astrojs/react, etc.) added only for components that exist.vite-build-optimizer for chunk sizing, HMR, plugin lifecycle issues.data-ai
license: Apache-2.0 NOT for unrelated tasks outside this domain.
development
Use when designing caching strategies (cache-aside, write-through, write-behind), implementing distributed locks, building rate limiters, leaderboards, real-time streams (XADD/consumer groups), pub/sub, or tuning eviction policies. Triggers: thundering-herd on cache miss, dogpile on key expiry, Redlock vs SET-NX-PX choice, sliding-window rate limiter, hot-key on a single cluster slot, big-key blowup, MULTI/EXEC across slots, KEYS in production. NOT for Redis Cluster operations/admin (different domain), embedded KV (SQLite, leveldb), in-process LRU caches, or Memcached.
tools
Drawing the `'use client'` boundary correctly in React Server Components apps (Next.js App Router, RSC frameworks) — leaf-pushing, slot composition, serialization rules, and environment poisoning prevention. Grounded in react.dev and Next.js 16 docs.
development
Use when designing rate limiting for an API, choosing between token bucket / sliding window / leaky bucket / fixed window, implementing it in Redis, deciding edge (Cloudflare/Upstash) vs origin enforcement, sizing per-user vs per-IP vs per-endpoint quotas, returning the right 429 response with Retry-After, or fixing the boundary-burst bug in fixed-window limiters. Triggers: 429 too many requests, INCR + EXPIRE, ZADD + ZREMRANGEBYSCORE + ZCARD, X-RateLimit-Remaining header, Cloudflare WAF rate limiting rules, Upstash @upstash/ratelimit, leaky bucket shaping vs policing, distributed rate limiter consistency. NOT for DDoS mitigation specifically (different scale), CAPTCHA / bot management, full WAF design, or per-user quota billing.