Skills

Install

$ npx ai-agents-skills add --skill astro-best-practices
Domain v1.0

Astro Best Practices

Quality patterns for Astro island architecture, SEO strategy, and Core Web Vitals. Complements the astro skill (which covers syntax and directives) — this skill covers architecture decisions.

When to Use

  • Reviewing an Astro site for quality or hydration decisions
  • Deciding page-type SEO strategy
  • Evaluating whether an island is justified
  • Auditing Core Web Vitals or JS bundle size

Don’t use for:

  • Client directive syntax (use astro)
  • SEO meta tag implementation details (use web-seo)
  • Performance tooling and measurement (use web-performance)

Critical Patterns

✅ REQUIRED [CRITICAL]: Zero JS Is the Default

Hydration is a cost, not a feature. Ask “why does this need browser JS?” before every directive.

<!-- ❌ WRONG — client:load as default "just in case" -->
<Nav client:load />
<Footer client:load />
<Sidebar client:load />

<!-- ✅ CORRECT — only hydrate what requires interactivity -->
<SearchBox client:load />
<Nav />
<Footer />

✅ REQUIRED: Hydration Directive Ladder

Choose the least aggressive directive that meets the requirement.

client:load   → above-fold, requires immediate interaction (search, nav toggle)
client:visible → below-fold interactive (comment section, map)
client:idle   → non-critical enhancement (analytics widget, lazy chat)
client:media  → viewport-conditional (mobile-only menu)
<!-- ❌ WRONG — client:load on below-fold, non-critical component -->
<NewsletterForm client:load />

<!-- ✅ CORRECT — defer until visible -->
<NewsletterForm client:visible />

✅ REQUIRED: SEO Per Page Type

Each page type needs a distinct SEO strategy. Never use the same meta description across pages.

<!-- ✅ Blog post: Article schema + unique description -->
<title>{post.title} | Blog</title>
<meta name="description" content={post.excerpt} />
<meta property="og:type" content="article" />
<script type="application/ld+json">{JSON.stringify(articleSchema)}</script>

<!-- ✅ Paginated list: canonical + rel="next/prev" -->
<link rel="canonical" href={canonicalUrl} />
{page.url.next && <link rel="next" href={page.url.next} />}

✅ REQUIRED: Content Collection Typing

Every collection needs a Zod schema in src/content/config.ts.

// ❌ WRONG — untyped frontmatter, runtime errors on missing fields
const posts = await getCollection('blog');

// ✅ CORRECT — schema validated at build time
const blogSchema = z.object({
  title: z.string(),
  pubDate: z.date(),
  description: z.string().max(160),
  image: z.string().optional(),
});

❌ NEVER: SSR Without Purpose

prerender: false only when the page uses Astro.locals, request cookies, or personalization. Never SSR a page that could be static.

// ❌ WRONG — SSR on a page that never uses request data
export const prerender = false; // on /about page

// ✅ CORRECT — SSR only for pages that need it
export const prerender = false; // on /dashboard (uses Astro.locals.user)

❌ NEVER: React for Static Content

Static logo, nav links, footers, and text blocks ship zero JS as .astro components.

<!-- ❌ WRONG — 15kb React runtime for a static header -->
<Header client:load />

<!-- ✅ CORRECT — .astro component ships zero JS -->
<Header />

✅ REQUIRED: Core Web Vitals Discipline

LCP, CLS, and INP each have specific patterns.

<!-- ✅ LCP: hero image loads immediately with highest priority -->
<img src={hero.src} alt={hero.alt} loading="eager" fetchpriority="high"
     width={hero.width} height={hero.height} />

<!-- ✅ CLS: explicit dimensions on all media prevent layout shift -->
<img src={avatar} alt="User" width="48" height="48" />

Symptom → Solution

SymptomCauseFix
JS bundle > 50kb on static pageclient:load on non-interactive componentRemove directive; use .astro
LCP > 2.5sHero image missing priority hintsAdd fetchpriority="high" + loading="eager"
CLS on hydrationIsland changes size when hydratedSet explicit width/height on island wrapper
Missing OG image on social shareog:image not in LayoutAdd og:image to base Layout component
Build fails on SSR pageMissing adapterAdd SSR adapter in astro.config.mjs
Duplicate content on paginated pagesMissing canonical/rel linksAdd canonical + rel=“next/prev” per page

Decision Tree

Should this component be hydrated?
  → Does it need event listeners or browser APIs?
  → No → Use .astro component (zero JS)
  → Yes → Choose directive from ladder

Which directive?
  → Above-fold, immediate interaction → client:load
  → Below-fold interactive → client:visible
  → Non-critical enhancement → client:idle
  → Viewport-conditional → client:media

SEO for static blog post?
  → Unique title + description + JSON-LD Article schema

SEO for product page?
  → JSON-LD Product schema + canonical on paginated variants

SEO for paginated list?
  → Canonical on each page + rel="next/prev"

Content collection with 5+ entries?
  → Define Zod schema in src/content/config.ts

Page needs user session or request data?
  → SSR (prerender: false) + adapter
  → Otherwise keep as static

Astro vs full SPA decision?
  → Mostly static with islands → Astro
  → Constant mutation + WebSockets + shared client state → SPA framework

Example

Blog post layout with correct SEO, one justified island, and LCP-optimized hero.

---
// src/layouts/BlogPost.astro
const { post } = Astro.props;
const articleSchema = { "@type": "Article", headline: post.title };
---
<html lang="en">
  <head>
    <title>{post.title} | Blog</title>
    <meta name="description" content={post.description} />
    <meta property="og:type" content="article" />
    <script type="application/ld+json" set:html={JSON.stringify(articleSchema)} />
  </head>
  <body>
    <!-- LCP: hero image gets highest fetch priority -->
    <img src={post.hero} alt={post.heroAlt}
         loading="eager" fetchpriority="high"
         width="1200" height="630" />

    <slot />

    <!-- Island justified: requires scroll position + IntersectionObserver -->
    <TableOfContents client:visible headings={post.headings} />
  </body>
</html>

Edge Cases

Third-party React components: If a library only ships React components, use client:visible at minimum — never client:load unless the component is above-fold and interactive on page load.

Dynamic routes with SSR: getStaticPaths cannot be used with prerender: false. If you need both dynamic routes and SSR, use path params via Astro.params in SSR mode.

Content collections vs Markdown imports: Collections are the correct pattern for 5+ content files. Direct imports are fine for 1–3 standalone documents (changelog, about page).

og:image sizing: Minimum 1200×630px for Twitter/Facebook. Generate with @astrojs/og or a static pre-generated image — never a hot-resize URL that adds latency.