Dynamic Pages from Data
Next.js App Router uses generateStaticParams() to enumerate dynamic routes at build time. Next Pages Router uses getStaticPaths(). Astro uses getStaticPaths() in a [slug].astro file — same name as Pages Router, but you return an array of objects with a params key (optionally a props key too) and access them via Astro.props and Astro.params in the frontmatter.
Needs a full Astro project to run. Use
npm create astro@latestto scaffold one.
App Router generateStaticParams vs Astro getStaticPaths
Section titled “App Router generateStaticParams vs Astro getStaticPaths”// app/blog/[slug]/page.tsx (Next.js App Router)import { allPosts } from 'contentlayer/generated';
export async function generateStaticParams() { return allPosts.map(post => ({ slug: post.slug }));}
export default function PostPage({ params }) { const post = allPosts.find(p => p.slug === params.slug); return ( <article> <h1>{post.title}</h1> <p>{post.description}</p> </article> );}---// src/pages/blog/[slug].astro (Astro)import { getCollection, render } from 'astro:content';
export async function getStaticPaths() { const posts = await getCollection('blog'); return posts.map(post => ({ params: { slug: post.slug }, props: { post }, }));}
const { post } = Astro.props;const { Content } = await render(post);---<article> <h1>{post.data.title}</h1> <Content /></article>In Astro, getStaticPaths and the page template live in the same file. The props key lets you pass the full collection entry directly — no second lookup needed.
Pages Router getStaticPaths vs Astro
Section titled “Pages Router getStaticPaths vs Astro”// pages/blog/[slug].tsx (Next.js Pages Router)export async function getStaticPaths() { const posts = await fetchAllPosts(); // or contentlayer return { paths: posts.map(p => ({ params: { slug: p.slug } })), fallback: false, };}
export async function getStaticProps({ params }) { const post = await fetchPost(params.slug); return { props: { post } };}
export default function PostPage({ post }) { return ( <article> <h1>{post.title}</h1> <div dangerouslySetInnerHTML={{ __html: post.contentHtml }} /> </article> );}---// src/pages/blog/[slug].astro (Astro)import { getCollection, render } from 'astro:content';
export async function getStaticPaths() { const posts = await getCollection('blog'); // params defines the URL; props pass data to the page return posts.map(post => ({ params: { slug: post.slug }, props: { post }, }));}
// Both params and props land here — no second getStaticProps neededconst { post } = Astro.props;const { Content } = await render(post);---<article> <h1>{post.data.title}</h1> <Content /></article>Next Pages Router splits path enumeration (getStaticPaths) from data fetching (getStaticProps). Astro merges them: pass props inside getStaticPaths and skip the second round-trip.
Complete dynamic route page
Section titled “Complete dynamic route page”---import { getCollection, render } from 'astro:content';import BaseLayout from '../../layouts/BaseLayout.astro';
export async function getStaticPaths() { const posts = await getCollection('blog', ({ data }) => !data.draft); return posts.map((post) => ({ params: { slug: post.slug }, props: { post }, }));}
const { post } = Astro.props;const { Content, headings } = await render(post);const { title, date, tags = [] } = post.data;---<BaseLayout title={title}> <article> <header> <h1>{title}</h1> <time datetime={date.toISOString()}> {date.toLocaleDateString('en-US', { dateStyle: 'long' })} </time> {tags.length > 0 && ( <ul class="tags"> {tags.map((tag) => <li>{tag}</li>)} </ul> )} </header> <Content /> </article></BaseLayout>