Astro Sites skill for Codex

Static by default, islands only where they earn it. Codex builds Astro sites that ship no JavaScript unless an island needs it, picks client:visible or client:idle over client:load wherever possible, and types every content collection so bad frontmatter breaks the build. Personalised fragments become server islands on otherwise static pages, only truly dynamic routes render on demand, and older projects get walked through the Astro 6 content and Zod 4 changes.

  • Frontend
  • $astro-sites
  • 209 lines
  • 2 files
  • ~1,900 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- astro-sites

Installs to ~/.agents/skills/astro-sites/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $astro-sites, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds Astro 5 and 6 sites - content collections with the Content Layer (glob and file loaders, Zod schemas from astro/zod, render()), islands with the right client directive, server islands with server:defer, per-page prerendering versus on-demand rendering, actions, astro:env, images and adapters for Node, Vercel, Netlify and Cloudflare. Use when creating an Astro blog, docs or marketing site, adding a framework component, moving content into collections, upgrading from older content APIs, or deciding what renders statically versus on request.

Use it when

  • Building a blog, docs or marketing site in Astro
  • Adding React, Vue, Svelte or Solid components as islands
  • Deciding which pages prerender and which render per request
  • Migrating legacy content collections or upgrading Astro majors

Use something else when

How to check the result

astro check and astro build pass, pages without islands ship no JS files in dist, and an invalid frontmatter field fails the build with the field named.

The mistake it prevents

Putting client:load on every component out of habit, which turns a zero-JS static page into a full hydration bundle.

~/.agents/skills/astro-sites/SKILL.md

--- name: astro-sites description: Builds Astro 5 and 6 sites - content collections with the Content Layer (glob and file loaders, Zod schemas from astro/zod, render()), islands with… license: MIT ---

Done well means: pages ship zero JavaScript unless an island needs it, every island uses the laziest client directive that still works, content is typed through collection schemas so a bad frontmatter field fails the build, and only the routes that need per-request data render on demand.

Inspect first

  1. Read AGENTS.md, package.json (astro major; v6 requires Node 22.12+) and astro.config.mjs: output, adapter, integrations, site, env.
  2. Find the collections config: src/content.config.ts (Content Layer, v5+). A src/content/config.ts file means the legacy API, which Astro 6 removed.
  3. Check which UI frameworks are installed (@astrojs/react, vue, svelte, solid-js, preact). Do not add a second framework for one widget.

Content collections

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

const blog = defineCollection({
  loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }),
  schema: ({ image }) =>
    z.object({
      title: z.string().max(90),
      description: z.string().max(160),
      pubDate: z.coerce.date(),
      updated: z.coerce.date().optional(),
      draft: z.boolean().default(false),
      cover: image().optional(),
      author: reference("authors"),
      tags: z.array(z.string()).default([]),
    }),
});

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

export const collections = { blog, authors };
---
// src/pages/blog/[...id].astro
import { getCollection, getEntry, render } from "astro:content";
import Layout from "../../layouts/Post.astro";

export async function getStaticPaths() {
  const posts = await getCollection("blog", ({ data }) => import.meta.env.DEV || !data.draft);
  return posts.map((post) => ({ params: { id: post.id }, props: { post } }));
}
const { post } = Astro.props;
const author = await getEntry(post.data.author);
const { Content, headings } = await render(post);
---
<Layout title={post.data.title} description={post.data.description}>
  <p>By {author?.data.name}</p>
  <Content />
</Layout>
  • Entries have id (path-based for glob), not slug. Use render(entry), not entry.render().
  • Import z from astro/zod (Astro 6 uses Zod 4: z.email(), z.url()). Older projects import it from astro:content, which still works but is deprecated.
  • getCollection order is not guaranteed; sort explicitly (b.data.pubDate - a.data.pubDate).
  • Custom loaders can pull from a CMS or API at build time. For content that must be fresh per request, use live collections (src/live.config.ts, getLiveCollection), which require on-demand rendering.

Islands

Component needsDirective
No interactivitynone (renders to HTML, ships no JS)
Interactive immediately above the fold (nav menu, search box)client:load
Interactive but not urgent (chat widget, cookie banner)client:idle
Below the fold (comments, carousel)client:visible
Only on some viewports (mobile menu)client:media="(max-width: 768px)"
Cannot render on the server (uses window at import)client:only="react"
<SearchBox client:load placeholder="Search docs" />
<Comments client:visible postId={post.id} />
  • Props passed to islands are serialized: no functions, no class instances. Pass data, and define event handlers inside the island.
  • Islands do not share React context with each other. Share state through a small store (nanostores) or URL, not a provider wrapping the page.
  • Prefer a plain <script> in an .astro component for small behaviour (toggle, copy button); it is bundled and deduplicated without a framework runtime.

Static, on-demand and server islands

Default output: 'static' prerenders everything. Add an adapter to render some routes on demand:

---
// src/pages/account.astro
export const prerender = false;   // this route renders per request
const user = Astro.locals.user;
if (!user) return Astro.redirect("/login");
---

Server islands keep a page static and cached while deferring one personalised part:

<ProductDetails product={product} />
<UserCartSummary server:defer>
  <p slot="fallback">Loading your cart...</p>
</UserCartSummary>
Page contentStrategy
Same for everyone, changes on deployprerendered (default)
Same for everyone except a small personalised blockprerendered + server:defer island
Different per user or per request throughoutprerender = false
Mostly dynamic appoutput: 'server' and opt pages back in with prerender = true

Server islands and prerender = false both need an adapter. Props to a server island are encrypted into the URL; keep them small.

Actions, env and images

// src/actions/index.ts
import { defineAction, ActionError } from "astro:actions";
import { z } from "astro/zod";

export const server = {
  subscribe: defineAction({
    accept: "form",
    input: z.object({ email: z.email() }),
    handler: async ({ email }, ctx) => {
      if (await isRateLimited(ctx.clientAddress)) throw new ActionError({ code: "TOO_MANY_REQUESTS" });
      await addSubscriber(email);
      return { ok: true };
    },
  }),
};
  • Call from a form (<form method="POST" action={actions.subscribe}>) on an on-demand page, or from client code with const { data, error } = await actions.subscribe(formData).
  • Declare env vars in astro.config.mjs with env.schema and envField (context: 'server', access: 'secret'), then import from astro:env/server. Secrets never go in PUBLIC_* vars.
  • Use <Image /> / <Picture /> from astro:assets with local imports or image() schema fields; remote images need image.domains or image.remotePatterns in config. Always set alt.
  • Page transitions: <ClientRouter /> from astro:transitions (the old <ViewTransitions /> name is removed in v6).

Adapters

npx astro add node      # or vercel, netlify, cloudflare; needs network

@astrojs/node needs mode: 'standalone' (own server) or 'middleware' (inside Express/Fastify). Adding or switching an adapter edits config; deploying is outward-facing, so build locally and ask before any deploy command.

Upgrading to Astro 6

- [ ] Node 22.12+ locally and in CI
- [ ] npx @astrojs/upgrade                       (needs network)
- [ ] Move src/content/config.ts to src/content.config.ts with loaders; slug -> id,
      entry.render() -> render(entry), getEntryBySlug -> getEntry
- [ ] z from astro/zod; z.string().email() -> z.email(); check .default() types
- [ ] <ViewTransitions /> -> <ClientRouter />; Astro.glob -> import.meta.glob
- [ ] Bump adapters to their matching major; re-read Cloudflare adapter notes
- [ ] astro check && astro build; diff the dist/ route list against before

Verification

npx astro check            # types, collection schemas, .astro templates
npx astro build && npx astro preview
  • Inspect dist/: static pages exist as HTML; count JS files and confirm pages without islands ship none.
  • Lighthouse or DevTools Network on a key page: only the islands you intended hydrate.
  • Introduce an invalid frontmatter value in a scratch copy; the build must fail with the field name.

Output

Report collections and their schemas, pages and their render strategy (static, on-demand, server island), islands with directives, required env vars, and the adapter.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Astro Sites"
  short_description: "Static by default, islands only where they earn it"
  default_prompt: "$astro-sites Set up a typed blog collection in Astro with authors and tags"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem