Static Site Generation (SSG) Patterns
Build-time rendering, getStaticPaths, and static data fetching
Core Patterns
- When to Read This
- Project Detection
- getStaticPaths for Dynamic Routes
- Pagination
When to Read This
- Building static websites (blogs, documentation, marketing)
- Using getStaticPaths for dynamic routes
- Fetching data at build time
- No adapter installed (output: ‘static’)
Project Detection
// astro.config.mjs
export default defineConfig({
output: "static", // or omit (default)
// NO adapter = SSG-only
});
If you see an adapter (node, vercel, netlify), this is NOT SSG-only. See ssr-patterns.md or hybrid-strategies.md.
getStaticPaths for Dynamic Routes
Basic Dynamic Route
---
// src/pages/blog/[slug].astro
export async function getStaticPaths() {
const posts = await getPosts(); // Fetch at build time
return posts.map((post) => ({
params: { slug: post.slug },
props: { post }, // Pass data to component
}));
}
interface Props {
post: Post;
}
const { post } = Astro.props;
---
<article>
<h1>{post.title}</h1>
<div set:html={post.content} />
</article>
Multiple Parameters
---
// src/pages/[lang]/[category]/[slug].astro
export async function getStaticPaths() {
const languages = ['en', 'es', 'fr'];
const categories = await getCategories();
const posts = await getPosts();
const paths = [];
for (const lang of languages) {
for (const category of categories) {
const categoryPosts = posts.filter(p => p.category === category);
for (const post of categoryPosts) {
paths.push({
params: {
lang,
category,
slug: post.slug,
},
props: { post },
});
}
}
}
return paths;
}
---
Fetching from API
---
export async function getStaticPaths() {
const response = await fetch('https://api.example.com/posts');
const posts = await response.json();
return posts.map((post) => ({
params: { id: post.id.toString() },
props: { post },
}));
}
---
Fetching from Database
---
import { db } from '../lib/db';
export async function getStaticPaths() {
const products = await db.product.findMany();
return products.map((product) => ({
params: { id: product.id },
props: { product },
}));
}
---
Pagination
Built-in Pagination
---
// src/pages/blog/[...page].astro
export async function getStaticPaths({ paginate }) {
const posts = await getPosts();
return paginate(posts, { pageSize: 10 });
}
const { page } = Astro.props;
---
<div>
{page.data.map((post) => (
<article>
<h2>{post.title}</h2>
</article>
))}
<nav>
{page.url.prev && <a href={page.url.prev}>Previous</a>}
<span>Page {page.currentPage} of {page.lastPage}</span>
{page.url.next && <a href={page.url.next}>Next</a>}
</nav>
</div>
Custom Pagination Logic
---
export async function getStaticPaths() {
const posts = await getPosts();
const pageSize = 10;
const pageCount = Math.ceil(posts.length / pageSize);
return Array.from({ length: pageCount }, (_, i) => ({
params: { page: (i + 1).toString() },
props: {
posts: posts.slice(i * pageSize, (i + 1) * pageSize),
currentPage: i + 1,
totalPages: pageCount,
},
}));
}
---
Build-Time Data Fetching
Top-Level Fetch
---
// Runs at build time (SSG)
const posts = await fetch('https://api.example.com/posts')
.then(res => res.json());
const stats = await calculateStats(posts);
---
<div>
<h1>Blog Statistics</h1>
<p>Total posts: {stats.total}</p>
<p>Published: {new Date().toISOString()}</p>
</div>
Parallel Data Fetching
---
const [posts, authors, categories] = await Promise.all([
fetch('/api/posts').then(r => r.json()),
fetch('/api/authors').then(r => r.json()),
fetch('/api/categories').then(r => r.json()),
]);
---
With Error Handling
---
let posts = [];
let error = null;
try {
const response = await fetch('https://api.example.com/posts');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
posts = await response.json();
} catch (e) {
error = e.message;
console.error('Failed to fetch posts:', e);
}
---
{error ? (
<div class="error">Failed to load posts</div>
) : (
<ul>
{posts.map(post => <li>{post.title}</li>)}
</ul>
)}
Static API Routes
Generate JSON Endpoints
// src/pages/api/posts.json.ts
import type { APIRoute } from "astro";
export const GET: APIRoute = async () => {
const posts = await getPosts();
return new Response(JSON.stringify(posts), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
};
Dynamic API Routes
// src/pages/api/posts/[id].json.ts
export async function getStaticPaths() {
const posts = await getPosts();
return posts.map((post) => ({
params: { id: post.id },
}));
}
export const GET: APIRoute = async ({ params }) => {
const post = await getPostById(params.id);
if (!post) {
return new Response(JSON.stringify({ error: "Not found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify(post), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
Environment Variables
---
// All env vars available at build time
const apiKey = import.meta.env.API_KEY;
const publicUrl = import.meta.env.PUBLIC_URL;
// PUBLIC_ vars are also available client-side
---
<script>
const url = import.meta.env.PUBLIC_URL; // Available
// const key = import.meta.env.API_KEY; // ERROR: Not available client-side
</script>
// src/env.d.ts
interface ImportMetaEnv {
readonly API_KEY: string;
readonly PUBLIC_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
Image Optimization
---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---
<!-- Optimized at build time -->
<Image src={heroImage} alt="Hero" width={800} height={600} />
<!-- Remote images -->
<Image
src="https://example.com/image.jpg"
alt="Remote"
width={800}
height={600}
/>
Incremental Regeneration (ISR)
True ISR is NOT available in pure SSG. For ISR-like behavior:
- Hybrid mode with adapter
- On-demand regeneration with server endpoints
- Partial prerendering (Astro 4+)
See hybrid-strategies.md for ISR patterns.
Best Practices
Cache External Data
---
import { cache } from '../lib/cache';
const posts = await cache.get('posts', async () => {
const response = await fetch('https://api.example.com/posts');
return response.json();
}, { ttl: 3600 }); // Cache for 1 hour during build
---
Generate Sitemaps
// src/pages/sitemap.xml.ts
export const GET: APIRoute = async () => {
const posts = await getPosts();
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://example.com/</loc></url>
${posts
.map(
(post) => `
<url>
<loc>https://example.com/blog/${post.slug}</loc>
<lastmod>${post.updatedAt}</lastmod>
</url>
`,
)
.join("")}
</urlset>`;
return new Response(sitemap, {
headers: { "Content-Type": "application/xml" },
});
};
Generate RSS Feeds
// src/pages/rss.xml.ts
import rss from "@astrojs/rss";
export const GET: APIRoute = async (context) => {
const posts = await getPosts();
return rss({
title: "My Blog",
description: "A blog about things",
site: context.site,
items: posts.map((post) => ({
title: post.title,
pubDate: post.date,
description: post.excerpt,
link: `/blog/${post.slug}/`,
})),
});
};
Common Pitfalls
Trying to Use SSR Features
---
// ❌ ERROR in SSG-only: No adapter installed
export const prerender = false; // Won't work
// ❌ ERROR: Astro.locals not available in SSG
const user = Astro.locals.user;
// ❌ ERROR: Request methods not supported
if (Astro.request.method === 'POST') { /* ... */ }
---
Missing getStaticPaths
---
// src/pages/blog/[slug].astro
// ❌ ERROR: getStaticPaths required for dynamic routes in SSG
const { slug } = Astro.params;
const post = await getPostBySlug(slug); // Won't work without getStaticPaths
---
Internationalization (i18n) Routing
Astro 3.5+ includes a built-in i18n routing API. Earlier versions require manual routing or astro-i18next.
Configure i18n in astro.config.mjs
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
i18n: {
defaultLocale: 'en',
locales: ['en', 'es', 'fr'],
routing: {
prefixDefaultLocale: false, // /about (not /en/about)
},
},
});
Locale-Aware Pages Structure
src/pages/
en/
about.astro → /en/about (or /about if prefixDefaultLocale: false)
es/
about.astro → /es/about
fr/
about.astro → /fr/about
Access Current Locale and Build Links
---
// src/pages/es/about.astro
import { getRelativeLocaleUrl } from 'astro:i18n';
const currentLocale = Astro.currentLocale; // 'es'
const enUrl = getRelativeLocaleUrl('en', 'about'); // '/about'
const frUrl = getRelativeLocaleUrl('fr', 'about'); // '/fr/about'
---
<nav>
<a href={enUrl}>English</a>
<a href={frUrl}>Français</a>
</nav>
hreflang Tags for SEO
---
import { getRelativeLocaleUrl } from 'astro:i18n';
const locales = ['en', 'es', 'fr'];
---
<head>
{locales.map(locale => (
<link
rel="alternate"
hreflang={locale}
href={getRelativeLocaleUrl(locale, Astro.url.pathname)}
/>
))}
<link rel="alternate" hreflang="x-default"
href={getRelativeLocaleUrl('en', Astro.url.pathname)} />
</head>
Translations with JSON Files
src/
i18n/
en.json
es.json
fr.json
// en.json
{ "hero.title": "Welcome", "hero.cta": "Get Started" }
// es.json
{ "hero.title": "Bienvenido", "hero.cta": "Comenzar" }
---
const t = await import(`../i18n/${Astro.currentLocale}.json`);
---
<h1>{t['hero.title']}</h1>
<a href="/get-started">{t['hero.cta']}</a>