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.tools
Building resilient distributed systems with circuit breakers, retries with full-jitter exponential backoff, retry budgets (per-request 3-attempt + per-client 10% ratio per Google SRE), deadline propagation, and the cascading-failure math (4 layers × 3 retries = 64x amplification). Grounded in Resilience4j, Microsoft Cloud Patterns, AWS Architecture Blog (Marc Brooker), and Google SRE Book.
testing
Designing HTTP cache headers that work correctly across browsers, CDNs, and shared proxies — `Cache-Control` directives per RFC 9111, `stale-while-revalidate` and `stale-if-error` per RFC 5861, the Vary header for varying responses, and surrogate keys for tag-based purging. Grounded in IETF RFCs and Cloudflare/Fastly docs.
development
Use when designing or fixing a Content Security Policy on a real site, choosing between nonce-based and hash-based CSP, adding strict-dynamic, debugging "Refused to execute inline script" errors, deploying CSP in report-only mode first, configuring report-to / report-uri, or auditing an existing policy for unsafe-inline / unsafe-eval / wildcards. Triggers: "CSP blocks legitimate inline script", strict-dynamic, nonce-{RANDOM}, sha256-{HASH}, object-src none, base-uri none, frame-ancestors, Trusted Types, X-Content-Security-Policy obsolete, report-only vs enforced. NOT for general HTTP security headers (HSTS, COOP/COEP), Trusted Types deep dive, CORS configuration, or building a WAF.
tools
Choosing and operating an HTTP API versioning strategy that doesn't break clients — Stripe's date-based pinned versions, the Deprecation/Sunset header pair (RFC 9745 + RFC 8594), URI vs header vs media-type approaches, and the version-transformer pattern. Grounded in Stripe's published architecture and IETF RFCs.