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.

Why it matters: Marketing sites live and die on Lighthouse, Core Web Vitals, and ad-landing-page speed. Starting from zero JS means your baseline is already fast — you add interactivity surgically instead of paying a framework tax site-wide.

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
DirectiveLoads JS…
client:loadimmediately on page load
client:idlewhen the main thread is free
client:visiblewhen the element scrolls into view (great for below-the-fold)
client:mediawhen a media query matches (e.g. mobile-only menu)
client:onlyskip SSR, render only in the browser
Why it matters: A SaaS homepage can have a fully interactive ROI calculator or demo embed while the surrounding 95% of the page ships as static HTML. You pay JS cost per-island, not per-page.

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>
Why it matters: You can call a client's API, database, or WordPress REST endpoint directly in the component — with secrets — and only the resulting HTML ships. No exposed keys, no client fetch waterfalls.

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>
Why it matters: Style leakage across a large marketing site is a real maintenance cost. Scoped-by-default keeps client sites tidy without a CSS framework (which fits your token-based system).

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;
Why it matters: This is exactly how you turn N WordPress posts into N pre-rendered static pages at build time — the engine behind your /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" },
  },
});
Why it matters: Directly relevant to your Phase 4 blog-URL migration (old slug-at-root → /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.

Why it matters: Many SaaS clients go multi-region. Built-in i18n saves you from bolting on a routing library later.

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
ModeUse for
StaticMarketing 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.
HybridStatic site + a few dynamic routes/endpoints. Best of both for client sites.
Why it matters: You rarely need a fully dynamic site. Keep the marketing site static (cheap, fast, resilient) and reach for SSR only on the handful of routes that truly need it.

Adapters

An adapter compiles your project for a host. @astrojs/cloudflare targets Cloudflare Pages/Workers; others exist for Vercel, Netlify, Node.

Why it matters: You're on Cloudflare Pages. The Cloudflare adapter is what unlocks SSR routes, on-demand endpoints, and middleware on that platform when a client needs them.

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);
Why it matters: This is the clean, modern way to wrap a headless WordPress backend. You get Zod schema validation (catches a renamed ACF field at build time, not in production), incremental caching between builds, and one consistent API across every client's CMS — even if one's on WordPress and another's on Contentful.

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() }),
});
Why it matters: Not every client needs a CMS for everything. Repo-based content (with schema validation + Git history) is perfect for things the dev team owns: case studies, docs, marketing copy you don't want a marketer breaking.

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>
Why it matters: Lets a client's marketing team write in Markdown while still dropping in your branded components (CTAs, callouts, pricing tables) — no raw HTML, no broken layouts.

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>
Why it matters: The holy grail for SaaS marketing pages: a fully CDN-cached, instantly-fast page that still shows live data (current pricing, A/B variant, logged-in upsell, seat counts) without making the whole page dynamic.

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" />
Why it matters: Images are the #1 cause of bad CWV on marketing sites. This handles format, sizing, and CLS automatically — the single biggest Lighthouse win you get for free.

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.

Why it matters: You currently self-host via Fontsource @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.

NeedTool
Sitemap@astrojs/sitemap — auto-generates sitemap-index.xml at build
RSS feed@astrojs/rss — generate /rss.xml from a collection
Meta / OG / canonicalRender in your <head>; pass per-page via props (or a community astro-seo component)
Structured dataInline JSON-LD <script type="application/ld+json"> — Organization, BlogPosting, BreadcrumbList
robots.txtStatic 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,
})} />
Why it matters: This is your bread and butter — and you've already wired most of it (Organization/WebSite, BlogPosting, BreadcrumbList). The pattern: a <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.

Why it matters: Branded, per-page OG images measurably lift social CTR — a nice premium deliverable for content-heavy SaaS clients.

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" },
});
Why it matters: AI assistants and answer engines increasingly fetch pages to summarize/cite them. Serving clean Markdown (no nav, no scripts) means they ingest your client's actual content accurately — better representation in AI answers, and far cheaper for agents to parse than full HTML.

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" } });
};
Why it matters: A low-effort, forward-looking add-on you can sell to every SaaS client: "AI-discoverability." Cheap to generate from content you already have, and it positions the agency as ahead of the curve.

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.

Why it matters: Clients will ask "do I want AI to train on / cite my site?" Knowing the levers (and that they live at the Cloudflare layer, not just in Astro) makes you the advisor.

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 });
Why it matters: This is how you build the Markdown twins, 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 }; },
  }),
};
Why it matters: A cleaner alternative for contact/lead forms than a hand-rolled fetch. Validation is shared client↔server, and it degrades gracefully without JS — exactly what a lead-gen form on a marketing site needs. (Your current form POSTs directly to the WP Gravity Forms endpoint; Actions would be the upgrade path if you want server-side validation/spam-handling in Astro first.)

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
};
Why it matters: Where you'd add security headers (CSP), geo/locale redirects, simple auth gates on a preview environment, or per-request edge logic — only relevant once a route runs on-demand.

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" }),
}}
Why it matters: Your two env vars (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.

IntegrationGives you
@astrojs/sitemapAuto sitemap
@astrojs/rssRSS feeds
@astrojs/mdxComponents in Markdown
@astrojs/partytownMove 3rd-party scripts (GA, marketing tags) to a web worker — off the main thread
@astrojs/cloudflareSSR/edge adapter for Cloudflare
UI framework (react/vue/svelte/solid)Use those components as islands
@astrojs/dbBuilt-in SQL (libSQL) for content/data
Why it matters: Partytown alone is a frequent client win — marketing teams pile on tag-manager scripts that tank performance; moving them to a worker recovers your CWV score without removing the tags.

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
Why it matters: Gives a static marketing site the smooth, polished feel of an app — a tangible "premium" upgrade for clients — with almost no JS and no SPA complexity.

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>"
Why it matters: Your whole headless model depends on this. The site is static, so new/edited WordPress content only appears after a rebuild — the deploy hook is the trigger.

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);
Why it matters: This closes the loop: a marketer hits "Publish" in WordPress → WP pings Cloudflare → Astro rebuilds → new post is live in a couple of minutes. The client experiences it as a normal CMS; you get a static, fast, cheap site. Watch out for: rapid edits firing many builds — debounce, or only trigger on publish/unpublish, not every autosave.

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`);
    },
}}
Why it matters: Useful for build-time automation — submitting the sitemap to search engines, warming a cache, posting a "deploy finished" Slack message, or generating that 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.

Why it matters: Knowing the escalation path means you can quote "static + deploy hook" confidently now, and have an answer when a client's blog hits thousands of posts and builds slow down.

Performance levers

The quick wins, in order

LeverWhat it does
Zero-JS defaultShip no JS unless an island needs it
client:visibleDefer below-the-fold island hydration
<Image>WebP/AVIF, responsive srcset, no CLS
PrefetchPreload links on hover/viewport (prefetch config + data-astro-prefetch)
Partytown3rd-party tags off the main thread
Static + edge cacheNear-zero TTFB from Cloudflare's CDN
Fonts API / preloadReduce font-driven CLS & FOUT

Agency cheat-sheet

"Client wants X → reach for Y."

Client needAstro feature
Blazing-fast marketing siteStatic output + edge cache + <Image>
Editors manage content in WordPressContent Layer loader + deploy hook on publish
Live data on a fast pageServer Islands (server:defer)
Lead-gen / contact formActions (validation) → WP/Gravity Forms endpoint
Strong SEOsitemap + RSS + JSON-LD + canonical via a <Seo> component
Be visible/citable to AIMarkdown twins + llms.txt + crawler rules
Marketing tags killing performancePartytown
Multi-region / multi-languageBuilt-in i18n routing
App-like polishView transitions (<ClientRouter />)
Migrating an old site's URLsconfig redirects (301s)
Per-request logic / headers / authSSR route + middleware
Multiple component frameworksIslands — 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.