Skip to content

Common Patterns

Intercept requests before page rendering:

src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
const { locals, request, redirect, url } = context;
// Auth check
const token = request.headers.get('authorization');
if (!token && url.pathname.startsWith('/dashboard')) {
return redirect('/login');
}
// Set data for pages via locals
locals.user = await verifyToken(token);
return next();
});

Access in pages via Astro.locals:

---
const { user } = Astro.locals;
---
<p>Welcome, {user.name}!</p>

Type-safe server functions callable from any page:

src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';
export const server = {
subscribe: defineAction({
accept: 'form',
input: z.object({
email: z.string().email(),
name: z.string().min(2),
}),
handler: async ({ email, name }) => {
await db.subscribers.create({ email, name });
return { success: true, message: `Welcome, ${name}!` };
},
}),
};

Call from a page:

---
import { actions } from 'astro:actions';
if (Astro.request.method === 'POST') {
const result = await actions.subscribe(Astro.request);
}
---
<form method="POST" action={actions.subscribe}>
<input name="email" type="email" required />
<input name="name" required />
<button type="submit">Subscribe</button>
</form>
src/pages/api/users.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ request, url }) => {
const limit = url.searchParams.get('limit') ?? '10';
const users = await db.users.findMany({ take: Number(limit) });
return new Response(JSON.stringify(users), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
};
export const POST: APIRoute = async ({ request }) => {
const body = await request.json();
if (!body.email) {
return new Response(JSON.stringify({ error: 'Email required' }), {
status: 400,
});
}
const user = await db.users.create({ data: body });
return new Response(JSON.stringify(user), { status: 201 });
};

Store user data across requests:

astro.config.mjs
export default defineConfig({
session: { driver: 'fs' },
});
---
const session = await Astro.session;
// Read
const cart = await session.get('cart') ?? [];
// Write
await session.set('cart', [...cart, newItem]);
// Destroy
await session.destroy();
---

Smooth page transitions without full reload:

---
// In your layout
import { ViewTransitions } from 'astro:transitions';
---
<head>
<ViewTransitions />
</head>

Persist elements across navigations:

<audio id="player" transition:persist>
<source src="/music.mp3" />
</audio>
---
import { Image, getImage } from 'astro:assets';
import heroImg from '../assets/hero.jpg';
---
<!-- Component (recommended) -->
<Image src={heroImg} alt="Hero" width={800} />
<!-- Programmatic -->
{async () => {
const optimized = await getImage({ src: heroImg, width: 400 });
return <img src={optimized.src} {...optimized.attributes} />;
}}
src/pages/rss.xml.ts
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = await getCollection('blog');
return rss({
title: 'My Blog',
description: 'A blog about stuff',
site: context.site,
items: posts.map(post => ({
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/blog/${post.id}/`,
})),
});
}
.env
PUBLIC_API_URL=https://api.example.com
SECRET_KEY=super-secret
---
// Server-side (all vars available)
const secret = import.meta.env.SECRET_KEY;
// Client-side (only PUBLIC_ prefix)
const apiUrl = import.meta.env.PUBLIC_API_URL;
// Built-in
const isDev = import.meta.env.DEV;
const isProd = import.meta.env.PROD;
const mode = import.meta.env.MODE;
---
---
// Conditional component loading
const Component = (await import(`../components/${name}.astro`)).default;
---
<Component />
src/pages/404.astro
<html>
<body>
<h1>Page Not Found</h1>
<a href="/">Go home</a>
</body>
</html>
---
// src/pages/500.astro (SSR only)
---
<html>
<body>
<h1>Server Error</h1>
</body>
</html>
astro.config.mjs
export default defineConfig({
redirects: {
'/old-page': '/new-page',
'/blog/[...slug]': '/articles/[...slug]',
},
});

Or in middleware:

if (url.pathname === '/old') {
return redirect('/new', 301);
}
.github/workflows/deploy.yml
name: Deploy to Azure Static Web Apps
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://npm.pkg.github.com'
scope: '@your-org'
- run: npm ci
env:
NODE_AUTH_TOKEN: ${{ secrets.GH_PACKAGES_TOKEN }}
- run: npm run build
- uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
action: 'upload'
app_location: '/'
output_location: 'dist'
skip_app_build: true # Already built above

Key points:

  • skip_app_build: true — use when you’ve already run npm run build (needed for private registry auth)
  • registry-url + scope + NODE_AUTH_TOKEN — required for GitHub Packages private dependencies
  • output_location: 'dist' — Astro’s default build output directory

Optional configuration for Azure SWA routing and headers:

{
"navigationFallback": {
"rewrite": "/index.html"
},
"globalHeaders": {
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff"
}
}