Build · Reference
Astro Feature Quick Reference
A practical map of Astro's core features — framed for agency work on SaaS marketing sites. Each entry says what it is, why it matters for client sites, and how it looks in code. Targets Astro 5/6.
Mental model
The two ideas everything else hangs off of.
Zero JS by default
Astro renders components to plain HTML & CSS at build time and ships no client JavaScript unless you ask for it. Pages are static HTML out of the box.
Islands architecture client:*
Interactive widgets ("islands") hydrate independently. You drop in a React/Vue/Svelte/Solid component and choose when it loads with a client directive — everything else stays static HTML.
<Hero /> // static HTML, 0 KB JS
<PricingCalculator client:visible /> // hydrates when scrolled into view
<Navbar client:idle /> // hydrates after page is interactive
<ChatWidget client:only="react" /> // client-rendered only | Directive | Loads JS… |
|---|---|
| client:load | immediately on page load |
| client:idle | when the main thread is free |
| client:visible | when the element scrolls into view (great for below-the-fold) |
| client:media | when a media query matches (e.g. mobile-only menu) |
| client:only | skip SSR, render only in the browser |
Components & templating
The .astro file and how data flows.
.astro components
A component has a JS/TS "frontmatter" fence (runs at build/request time on the server) and an HTML-like template below it. Frontmatter never ships to the browser.
---
// runs on the server only
const { title } = Astro.props;
const posts = await fetch(WP_API_URL + "/posts").then(r => r.json());
---
<section>
<h1>{title}</h1>
{posts.map(p => <article>{p.title.rendered}</article>)}
</section>
<style> h1 { color: var(--color-accent); } </style> Scoped styles & slots
<style> blocks are automatically scoped to the component (use is:global to opt out). <slot /> handles composition / layout children, including named slots.
<Layout>
<h1 slot="header">About</h1>
<p>Default slot content…</p>
</Layout> Routing
File-based, with dynamic params for CMS-driven pages.
File-based & dynamic routes
Files in src/pages/ become routes. [slug].astro is a dynamic segment; [...path].astro is a catch-all (rest). In static mode you enumerate pages with getStaticPaths().
// src/pages/blog/[slug].astro
export async function getStaticPaths() {
const posts = await getAllPosts();
return posts.map(post => ({
params: { slug: post.slug },
props: { post }, // passed to the page
}));
}
const { post } = Astro.props; /blog/[slug]/ routes.Redirects & rewrites
Declare redirects in astro.config.mjs; they compile to your host's redirect rules. Astro.rewrite() serves a different route without changing the URL.
export default defineConfig({
redirects: {
"/old-post-slug": "/blog/old-post-slug",
"/services/wp": { status: 301, destination: "/services" },
},
}); /blog/[slug]/). Keeps link equity and avoids 404s at launch.i18n routing built-in
Configure locales and Astro handles locale-prefixed routes, default-locale fallback, and helper functions for building localized URLs.
Rendering modes
Static, on-demand, or per-route hybrid — set globally, override per page.
Static (SSG), Server (SSR) & hybrid
Default is fully static. Add an adapter and set output to render on-demand. Crucially, you can flip individual routes with export const prerender.
// a mostly-static site with one dynamic route
// src/pages/dashboard/status.astro
export const prerender = false; // this page renders per-request | Mode | Use for |
|---|---|
| Static | Marketing pages, blog, docs — the 95% case. Cached at the edge, near-zero TTFB. |
| On-demand (SSR) | Personalized pages, auth gates, live data, form handlers, preview mode. |
| Hybrid | Static site + a few dynamic routes/endpoints. Best of both for client sites. |
Adapters
An adapter compiles your project for a host. @astrojs/cloudflare targets Cloudflare Pages/Workers; others exist for Vercel, Netlify, Node.
Content Layer API Astro 5+
The single most important feature for agency CMS work.
Loaders — pull content from anywhere into a typed collection
The Content Layer lets you define loaders that fetch content from any source (WordPress REST, headless CMS, an API, local files) at build time, cache it, and expose it as a type-safe collection with a unified query API.
// src/content.config.ts
import { defineCollection, z } from "astro:content";
const posts = defineCollection({
loader: async () => {
const data = await getAllPosts(); // your WP fetch
return data.map(p => ({ id: p.slug, ...p }));
},
schema: z.object({
title: z.string(),
date: z.coerce.date(),
excerpt: z.string().optional(),
}),
});
export const collections = { posts }; // anywhere in a page
import { getCollection, getEntry } from "astro:content";
const all = await getCollection("posts");
const one = await getEntry("posts", Astro.params.slug); Content Collections
Type-safe local content with Zod schemas.
Glob & file loaders for local Markdown/MDX/JSON
For content that lives in the repo (case studies, legal pages, changelog), the built-in glob() and file() loaders turn folders of Markdown/JSON into validated collections.
import { glob } from "astro/loaders";
const caseStudies = defineCollection({
loader: glob({ pattern: "**/*.mdx", base: "./src/content/case-studies" }),
schema: z.object({ client: z.string(), logo: z.string(), result: z.string() }),
}); Markdown & MDX
Authoring with components, frontmatter, and built-in syntax highlighting.
MDX, Shiki, and the remark/rehype pipeline
Markdown gets frontmatter and Shiki syntax highlighting out of the box. @astrojs/mdx lets authors embed Astro/React components inside Markdown. You can extend parsing with any remark/rehype plugin (reading time, auto-linked headings, TOC).
---
title: "How we cut their LCP in half"
---
import Callout from "../../components/Callout.astro";
<Callout>Real component, inside Markdown.</Callout> Server Islands Astro 5+
Cache the static shell, defer the dynamic bits
Mark a component server:defer and Astro renders the rest of the page as cacheable static HTML, then streams that one component in on-demand from the server, with an optional fallback placeholder.
<PricingTable /> // static, edge-cached
<LiveSeatCount server:defer>
<span slot="fallback">Loading…</span>
</LiveSeatCount> Images & assets astro:assets
<Image> and <Picture> — automatic optimization
Built-in image service converts to modern formats (WebP/AVIF), generates responsive srcsets, sets width/height to prevent layout shift, and lazy-loads. Works on local and (configured) remote images.
---
import { Image } from "astro:assets";
import hero from "../assets/hero.jpg";
---
<Image src={hero} alt="Dashboard" widths={[400, 800, 1200]}
sizes="(max-width: 800px) 100vw, 800px" /> Fonts Astro 5.7+
Built-in Fonts API
The experimental.fonts / astro:assets font API self-hosts fonts from Google, Fontsource, or local files, auto-generates optimized @font-face with fallback metrics to reduce layout shift, and preloads.
@imports (Inter + Instrument Serif). The native Fonts API can replace that with automatic preloading and fallback-metric tuning — worth evaluating to shave font-driven CLS.SEO toolkit
Astro doesn't impose an SEO solution — it gives you the primitives. Here's the full kit.
Sitemap, RSS, canonical & meta
Official integrations cover the mechanical bits; the rest is just rendering tags in your layout.
| Need | Tool |
|---|---|
| Sitemap | @astrojs/sitemap — auto-generates sitemap-index.xml at build |
| RSS feed | @astrojs/rss — generate /rss.xml from a collection |
| Meta / OG / canonical | Render in your <head>; pass per-page via props (or a community astro-seo component) |
| Structured data | Inline JSON-LD <script type="application/ld+json"> — Organization, BlogPosting, BreadcrumbList |
| robots.txt | Static file in public/, or generate via endpoint |
// JSON-LD straight into the template (set:html avoids escaping)
<script type="application/ld+json" set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title, datePublished: post.date,
})} /> <Seo> component in the layout fed by per-page props, so every client page is consistent and nothing ships untagged.Dynamic OG images
Generate per-page social share images at build with an endpoint + a library like satori (HTML/CSS → SVG → PNG), or use a runtime image service.
AI / agent readability
The new frontier you flagged — making sites legible to LLMs and AI crawlers.
Markdown content negotiation & .md twins
The emerging pattern: serve a clean Markdown representation of each page to AI agents. This happens two ways — (1) content negotiation, where a request with Accept: text/markdown (or a tool/agent UA) gets Markdown instead of HTML; (2) a parallel /page.md URL for every /page. Cloudflare's AI/agent features (and their proxy layer) can do this negotiation at the edge, stripping nav/chrome and returning just the content as Markdown.
// DIY: an Astro endpoint that emits a Markdown twin
// src/pages/blog/[slug].md.ts
export async function getStaticPaths() { /* same slugs as the HTML route */ }
export const GET = ({ props }) => new Response(props.markdownBody, {
headers: { "Content-Type": "text/markdown; charset=utf-8" },
}); llms.txt
A proposed convention: a root /llms.txt (and optional /llms-full.txt) file in Markdown that gives LLMs a curated map of your site — key pages, descriptions, links. Generate it from a content collection via an endpoint.
// src/pages/llms.txt.ts
export const GET = async () => {
const posts = await getCollection("posts");
const body = `# Red Bridge Internet\n\n## Blog\n` +
posts.map(p => `- [${p.data.title}](/blog/${p.id}/): ${p.data.excerpt}`).join("\n");
return new Response(body, { headers: { "Content-Type": "text/plain" } });
}; Crawler controls
Use robots.txt and Cloudflare's bot/AI-crawler rules to decide which AI crawlers (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, etc.) may access content. Cloudflare can allow, block, or meter AI crawlers at the edge.
Endpoints & Actions
API endpoints (route handlers)
Any .ts / .js file in pages/ exporting GET/POST/etc. is an API route. Static-mode endpoints generate files at build (e.g. a JSON feed); SSR endpoints run per-request.
// src/pages/api/health.ts
export const GET = () => Response.json({ ok: true }); llms.txt, JSON feeds, webhook receivers, and form proxies above — all without a separate backend.Actions Astro 5+
Type-safe server functions you can call from client code or wire to a form, with built-in Zod input validation, error handling, and progressive enhancement.
// src/actions/index.ts
import { defineAction } from "astro:actions";
import { z } from "astro:schema";
export const server = {
contact: defineAction({
accept: "form",
input: z.object({ email: z.string().email(), message: z.string().min(1) }),
handler: async (data) => { /* forward to WP / Gravity Forms */ return { ok: true }; },
}),
}; Middleware
src/middleware.ts
Runs before every request (in SSR/on-demand routes). Inspect/modify the request and response, set headers, do auth checks, redirects, A/B routing, locals injection.
export const onRequest = (context, next) => {
context.locals.startTime = performance.now();
return next(); // can also short-circuit with a redirect/Response
}; Typed env & config
astro:env Astro 5+
Declare environment variables with a schema (type, public vs. secret, server vs. client). Astro validates them at build and gives you typed, autocompleted imports — and stops you from leaking a server secret to the client.
// astro.config.mjs
env: { schema: {
WP_API_URL: envField.string({ context: "server", access: "secret" }),
PUBLIC_WP_SUBMIT_URL: envField.string({ context: "client", access: "public" }),
}} WP_API_URL server-only, PUBLIC_WP_SUBMIT_URL public) are the textbook case. Schema-validated env catches a missing Cloudflare dashboard variable at build instead of with a blank page in production.Integrations
The official integration ecosystem
Add capabilities via npx astro add. Each registers config, build hooks, and injected routes.
| Integration | Gives you |
|---|---|
| @astrojs/sitemap | Auto sitemap |
| @astrojs/rss | RSS feeds |
| @astrojs/mdx | Components in Markdown |
| @astrojs/partytown | Move 3rd-party scripts (GA, marketing tags) to a web worker — off the main thread |
| @astrojs/cloudflare | SSR/edge adapter for Cloudflare |
| UI framework (react/vue/svelte/solid) | Use those components as islands |
| @astrojs/db | Built-in SQL (libSQL) for content/data |
View transitions <ClientRouter />
SPA-like navigation on a static site
Drop <ClientRouter /> in your <head> and Astro intercepts navigations, animates between pages with the View Transitions API, and persists elements (audio, video, nav state) across pages — with graceful fallback where unsupported.
import { ClientRouter } from "astro:transitions";
<head><ClientRouter /></head>
<img transition:name="hero" /> // morphs between pages Deploy & rebuild hooks
The "rebuild when a WordPress post changes" workflow you asked about — this is the agency operations layer.
Cloudflare Pages Deploy Hooks
Cloudflare Pages gives each project a unique Deploy Hook — a secret URL. POST to it and Cloudflare runs a fresh build/deploy. (This is a Pages feature, not Astro itself — but it's how a static Astro site stays in sync with a CMS.)
curl -X POST "https://api.cloudflare.com/client/v4/pages/webhooks/deploy_hooks/<TOKEN>" Trigger from WordPress on content change
Fire the deploy hook from WP whenever a post is published/updated. Hook into save_post / transition_post_status (or use a plugin), and POST to the Cloudflare URL.
// in the rbi-api plugin or theme functions.php
add_action("transition_post_status", function ($new, $old, $post) {
if ($new === "publish" || $old === "publish") {
wp_remote_post(CF_DEPLOY_HOOK_URL); // trigger rebuild
}
}, 10, 3); Astro build lifecycle hooks Integration API
Distinct from deploy hooks: Astro integrations expose build lifecycle hooks (astro:config:setup, astro:build:start, astro:build:done, etc.) for running code during the build — generating files, injecting routes, post-processing output.
{ name: "ping-on-done", hooks: {
"astro:build:done": async ({ pages }) => {
console.log(`Built ${pages.length} pages`);
},
}} llms.txt file.On-demand revalidation alternatives
If full rebuilds get too slow as a client's content grows, the alternatives are: (1) make the blog routes SSR + edge-cache them and purge the cache on WP change instead of rebuilding; or (2) use Server Islands for the freshest bits. Full static rebuild is simplest and best until scale forces the change.
Performance levers
The quick wins, in order
| Lever | What it does |
|---|---|
| Zero-JS default | Ship no JS unless an island needs it |
| client:visible | Defer below-the-fold island hydration |
| <Image> | WebP/AVIF, responsive srcset, no CLS |
| Prefetch | Preload links on hover/viewport (prefetch config + data-astro-prefetch) |
| Partytown | 3rd-party tags off the main thread |
| Static + edge cache | Near-zero TTFB from Cloudflare's CDN |
| Fonts API / preload | Reduce font-driven CLS & FOUT |
Agency cheat-sheet
"Client wants X → reach for Y."
| Client need | Astro feature |
|---|---|
| Blazing-fast marketing site | Static output + edge cache + <Image> |
| Editors manage content in WordPress | Content Layer loader + deploy hook on publish |
| Live data on a fast page | Server Islands (server:defer) |
| Lead-gen / contact form | Actions (validation) → WP/Gravity Forms endpoint |
| Strong SEO | sitemap + RSS + JSON-LD + canonical via a <Seo> component |
| Be visible/citable to AI | Markdown twins + llms.txt + crawler rules |
| Marketing tags killing performance | Partytown |
| Multi-region / multi-language | Built-in i18n routing |
| App-like polish | View transitions (<ClientRouter />) |
| Migrating an old site's URLs | config redirects (301s) |
| Per-request logic / headers / auth | SSR route + middleware |
| Multiple component frameworks | Islands — mix React/Vue/Svelte per widget |
Astro feature quick reference · prepared for Red Bridge Internet · targets Astro 5/6. Version-tagged features (Astro 5+, etc.) note when something landed — confirm exact availability against the official docs before quoting it to a client. Deploy hooks, Markdown content negotiation, and AI-crawler controls live partly at the Cloudflare layer, not in Astro itself.