| name | astro |
| description | Astro framework patterns for SSR/SSG, middleware, and TypeScript. Use when working with Astro pages, API routes, middleware configuration, or debugging rendering issues. Trigger phrases include "Astro", "prerender", "middleware", "SSR", "SSG", "static site". |
Astro Framework Skill
Patterns and gotchas for Astro development, with focus on authentication, middleware, and rendering modes.
Rendering Modes
SSG vs SSR for Protected Content
Critical: In Astro 5.x with output: "static", pre-rendered pages bypass middleware completely.
User Request → Vercel CDN → Pre-rendered HTML (cached)
↳ Middleware SKIPPED for static content
| Rendering Mode | Middleware Runs? | Use Case |
|---|
SSG (prerender = true) | No - build time only | Public content, marketing |
SSR (prerender = false) | Yes - every request | Protected content, auth |
Protecting Content Pages
---
export const prerender = false;
const auth = Astro.locals.auth();
if (!auth?.userId) {
return Astro.redirect('/sign-in');
}
---
API Routes Always Need SSR
export const prerender = false;
export const GET: APIRoute = async ({ locals }) => {
const auth = locals.auth?.();
if (!auth?.userId) {
return new Response("Unauthorized", { status: 401 });
}
};
Middleware Patterns
Basic Route Protection
import { clerkMiddleware, createRouteMatcher } from "@clerk/astro/server";
const isPublicRoute = createRouteMatcher([
"/",
"/sign-in(.*)",
"/sign-up(.*)",
"/api/webhooks/(.*)",
]);
export const onRequest = clerkMiddleware((auth, context) => {
const { userId } = auth();
if (isPublicRoute(context.request)) {
return;
}
if (!userId) {
return auth().redirectToSignIn();
}
});
Member Status Validation
Check member status in addition to authentication:
import { checkMemberAccess, ACCESS_LEVELS } from "@lib/auth";
async function checkAccess(userId, context) {
const member = await memberQueries.findByClerkId(userId);
if (!member) {
return context.redirect("/pending-approval");
}
const access = checkMemberAccess(member, ACCESS_LEVELS.MEMBER);
if (!access.hasAccess && access.redirectTo) {
return context.redirect(access.redirectTo);
}
return undefined;
}
TypeScript Gotchas
Unused Variable in Template Strings
Astro's TypeScript preprocessor may report variables as "unused" even when used in template literals:
---
const slug = entries[0]?.slug.split("/").pop();
return Astro.redirect(`/learn/${workshop}/${slug}`);
---
---
return Astro.redirect(`/learn/${workshop}/${entries[0]?.slug.split("/").pop() ?? "default"}`);
---
Path Brackets in Shell Commands
File paths with brackets need quoting in shell:
git add src/pages/learn/[workshop]/index.astro
git add 'src/pages/learn/[workshop]/index.astro'
Content Collections
Dynamic Routes with SSR
When using SSR, you can't use getStaticPaths(). Use direct lookups instead:
export const prerender = false;
export function getStaticPaths() { ... }
export const prerender = false;
const { workshop, slug } = Astro.params;
const entry = await getEntry("workshops", `${workshop}/${slug}`);
if (!entry) {
return Astro.redirect("/404");
}
Cache Headers
Prevent Caching of Status Pages
Status pages (pending, expired, error) should never be cached:
---
Astro.response.headers.set("Cache-Control", "no-store, no-cache, must-revalidate");
Astro.response.headers.set("Pragma", "no-cache");
---
Configuration Reference
astro.config.mjs
export default defineConfig({
output: "static",
output: "server",
adapter: vercel(),
});
Per-Page Rendering
export const prerender = true;
export const prerender = false;
Common Issues
Middleware Not Running
Symptoms: Auth checks bypassed, expired users see content
Cause: Page is pre-rendered (SSG default)
Fix: Add export const prerender = false;
404 on Dynamic Routes
Symptoms: /learn/workshop/module returns 404
Cause: Dynamic params not handled in SSR mode
Fix: Check Astro.params and handle missing values:
const { workshop, slug } = Astro.params;
if (!workshop || !slug) {
return Astro.redirect("/404");
}
Build Fails with Type Errors
Symptoms: ts(6133): variable is declared but never read
Cause: Astro preprocessor quirk with template strings
Fix: Inline expressions or suppress with // @ts-ignore
Last updated: December 2025