Documentation Index
Fetch the complete documentation index at: https://mintlify.com/remix-run/react-router/llms.txt
Use this file to discover all available pages before exploring further.
Pre-rendering (SSG)
Learn how to pre-render static pages at build time for optimal performance and SEO.
Overview
Pre-rendering (Static Site Generation) generates HTML pages at build time, providing:
- Instant page loads from CDN
- Perfect SEO with fully-rendered HTML
- Lower server costs
- Better performance at scale
Configuring Pre-rendering
Define pages to pre-render in your config:
// react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
async prerender() {
return [
"/",
"/about",
"/contact",
"/products",
];
},
} satisfies Config;
Dynamic Path Pre-rendering
Generate paths from your data:
// react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
async prerender() {
const posts = await db.post.findMany();
const products = await db.product.findMany();
return [
"/",
"/blog",
...posts.map((post) => `/blog/${post.slug}`),
"/products",
...products.map((product) => `/products/${product.id}`),
];
},
} satisfies Config;
Pre-rendering with Loaders
Loaders run at build time for pre-rendered routes:
// app/routes/blog.$slug.tsx
import type { Route } from "./+types/blog.$slug";
export async function loader({ params }: Route.LoaderArgs) {
const post = await getPost(params.slug);
if (!post) {
throw new Response("Not Found", { status: 404 });
}
return { post };
}
export default function BlogPost({ loaderData }: Route.ComponentProps) {
return (
<article>
<h1>{loaderData.post.title}</h1>
<time>{loaderData.post.publishedAt}</time>
<div dangerouslySetInnerHTML={{ __html: loaderData.post.content }} />
</article>
);
}
Advanced Pre-render Configuration
Customize pre-rendering behavior:
// react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
async prerender({ getStaticPaths }) {
const routes = await getStaticPaths();
return {
// List of paths to pre-render
paths: [
"/",
"/about",
...routes,
],
// Number of concurrent renders
concurrency: 10,
// Retry failed renders
retryCount: 3,
retryDelay: 500,
// Follow redirects up to this many times
maxRedirects: 5,
// Timeout for each render
timeout: 10000,
};
},
} satisfies Config;
Fetching Data for Pre-rendering
Query your database or API at build time:
// react-router.config.ts
import { db } from "./app/db.server";
import type { Config } from "@react-router/dev/config";
export default {
async prerender() {
// Fetch all published posts
const posts = await db.post.findMany({
where: { published: true },
select: { slug: true },
});
// Fetch all active categories
const categories = await db.category.findMany({
where: { active: true },
select: { slug: true },
});
return [
// Static pages
"/",
"/about",
"/blog",
// Dynamic blog posts
...posts.map((post) => `/blog/${post.slug}`),
// Category pages
...categories.map((cat) => `/blog/category/${cat.slug}`),
];
},
} satisfies Config;
Partial Pre-rendering
Mix pre-rendered and dynamic pages:
// react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
// Pre-render only these routes
async prerender() {
return [
"/", // Home page
"/about", // About page
"/pricing", // Pricing page
];
},
// Other routes render on-demand
} satisfies Config;
Build-Time Environment
Access build-time environment variables:
// app/routes/blog._index.tsx
import type { Route } from "./+types/blog._index";
export async function loader({}: Route.LoaderArgs) {
// This runs at build time for pre-rendered pages
const posts = await fetch(
`${process.env.CMS_API_URL}/posts`,
{
headers: {
Authorization: `Bearer ${process.env.CMS_API_KEY}`,
},
}
).then((r) => r.json());
return { posts };
}
Incremental Static Regeneration
Rebuild specific pages on-demand:
// app/routes/blog.$slug.tsx
import type { Route } from "./+types/blog.$slug";
export async function loader({ params }: Route.LoaderArgs) {
const post = await getPost(params.slug);
if (!post) {
throw new Response("Not Found", { status: 404 });
}
return json(
{ post },
{
headers: {
// Revalidate after 1 hour
"Cache-Control": "public, max-age=3600, s-maxage=3600",
},
}
);
}
Sitemap Generation
Generate a sitemap from pre-rendered paths:
// react-router.config.ts
import type { Config } from "@react-router/dev/config";
import { writeFileSync } from "fs";
export default {
async prerender() {
const posts = await db.post.findMany({
select: { slug: true, updatedAt: true },
});
const paths = [
{ path: "/", priority: 1.0 },
{ path: "/about", priority: 0.8 },
...posts.map((post) => ({
path: `/blog/${post.slug}`,
lastmod: post.updatedAt.toISOString(),
priority: 0.6,
})),
];
// Generate sitemap.xml
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${paths
.map(
(p) => ` <url>
<loc>https://example.com${p.path}</loc>
${p.lastmod ? `<lastmod>${p.lastmod}</lastmod>` : ""}
<priority>${p.priority}</priority>
</url>`
)
.join("\n")}
</urlset>`;
writeFileSync("public/sitemap.xml", sitemap);
return paths.map((p) => p.path);
},
} satisfies Config;
Create RSS feeds during pre-rendering:
// react-router.config.ts
import { writeFileSync } from "fs";
export default {
async prerender() {
const posts = await db.post.findMany({
where: { published: true },
orderBy: { publishedAt: "desc" },
take: 20,
});
// Generate RSS feed
const rss = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>My Blog</title>
<link>https://example.com</link>
<description>Blog posts</description>
${posts
.map(
(post) => `
<item>
<title>${post.title}</title>
<link>https://example.com/blog/${post.slug}</link>
<description>${post.excerpt}</description>
<pubDate>${post.publishedAt.toUTCString()}</pubDate>
</item>`
)
.join("")}
</channel>
</rss>`;
writeFileSync("public/rss.xml", rss);
return posts.map((post) => `/blog/${post.slug}`);
},
} satisfies Config;
Build Optimization
Optimize pre-rendering performance:
// react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
async prerender() {
// Fetch data once, use for multiple pages
const [posts, products, categories] = await Promise.all([
db.post.findMany(),
db.product.findMany(),
db.category.findMany(),
]);
const paths = [
"/",
...posts.map((p) => `/blog/${p.slug}`),
...products.map((p) => `/products/${p.id}`),
...categories.map((c) => `/category/${c.slug}`),
];
return {
paths,
concurrency: 20, // Render 20 pages in parallel
};
},
} satisfies Config;
Deployment
Deploy pre-rendered sites to static hosts:
# Build with pre-rendering
npm run build
# Deploy build/client to:
# - Netlify
# - Vercel
# - Cloudflare Pages
# - AWS S3 + CloudFront
# - GitHub Pages
Handling 404s
Pre-render a custom 404 page:
// react-router.config.ts
export default {
async prerender() {
return [
"/",
"/404", // Pre-render 404 page
// ... other pages
];
},
} satisfies Config;
Configure your host:
# netlify.toml
[[redirects]]
from = "/*"
to = "/404.html"
status = 404
Best Practices
- Pre-render static content - Marketing pages, blog posts, documentation
- Skip user-specific pages - Don’t pre-render dashboards or personalized content
- Use concurrency - Speed up builds by rendering pages in parallel
- Generate sitemaps - Help search engines discover your pages
- Optimize images - Compress and resize images at build time
- Cache headers - Set long cache times for pre-rendered HTML
- Monitor build times - Keep builds fast as your site grows
- Validate paths - Ensure all pre-rendered paths return 200 status