Hello! Welcome back to the course.
In our last lesson, we built a scalable, paginated archive for your blog. Now, when a user clicks on a post title from that list, they need to be taken to a page that displays the full article. This is where dynamic page templates come in.
Today, your goal is to create dynamic page templates to render individual blog posts and service pages. We'll leverage Astro's file-based routing and Content Collections to generate a unique, static page for every entry in your collections. This is a fundamental pattern for building any content-driven site, and you'll use it for your blog, service descriptions, and case studies on the accounting website.
1. Understanding Dynamic Routes and getStaticPaths
So far, we've created static pages like about.astro and a paginated route blog/[page].astro. Now, we need a route that can handle any possible blog post, like /blog/my-first-post or /blog/tax-tips-2025.
Astro handles this with dynamic routes. You create a file with a special bracket syntax, for example, src/pages/blog/[slug].astro. The [slug] part is a placeholder. At build time, Astro runs a special function within this file called getStaticPaths to determine all the possible values for slug and generates a static HTML page for each one.
Your experience with JavaScript array methods like .map() will be very useful here, as getStaticPaths is all about transforming an array of content entries into an array of page routes.
The following video provides an excellent walkthrough of creating a dynamic route, implementing getStaticPaths, and rendering the content. It's the core of what we're doing today.
Astro 3.0 Crash Course (View Transitions, Image, Content Collections, and More!)
The video 'Astro 3.0 Crash Course' by James Q Quick demonstrates exactly how to build dynamic pages from a content collection. It clearly explains the role of getStaticPaths and how to render the final content.
Please watch the segment from 31:03 to 35:32. Focus on these three key concepts: The purpose of the getStaticPaths function. The structure of the object returned for each post (params and props). How the slug from the content collection is used to define the page's URL via the params object.
As you saw in the video, getStaticPaths must return an array of objects, where each object defines a page to be generated. The structure looks like this:
[
{
params: { slug: 'post-1-slug' }, // Determines the URL: /blog/post-1-slug
props: { post: post1Object } // Data passed to the page component
},
{
params: { slug: 'post-2-slug' }, // Determines the URL: /blog/post-2-slug
props: { post: post2Object } // Data passed to the page component
},
// ...and so on for every post
]
2. Building the Blog Post Template
Let's put this theory into practice and build the template for a single blog post.
1. Create the Dynamic Route File:
Inside your src/pages/ directory, create a new folder named blog. Inside src/pages/blog/, create a new file named [slug].astro.
Your file structure should now look something like this:
src/
├── components/
├── content/
│ └── blog/
│ ├── post-1.mdx
│ └── post-2.mdx
├── layouts/
└── pages/
└── blog/
├── [page].astro // Your paginated list from the last lesson
└── [slug].astro // The new template for a single post
2. Implement the Page Template:
Now, open src/pages/blog/[slug].astro and add the following code. We'll break it down right after.
---
import { type CollectionEntry, getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import FormattedDate from '../../components/FormattedDate.astro'; // Let's assume you create this component
// 1. Generate routes for all blog posts
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
// 2. Get the post for the current page from props
const { post } = Astro.props;
type Props = { post: CollectionEntry<'blog'> };
// 3. Get the <Content /> component to render the post body
const { Content } = await post.render();
---
<BaseLayout title={post.data.title} description={post.data.description}>
<article>
<div class="post-header">
<h1>{post.data.title}</h1>
<FormattedDate date={post.data.pubDate} />
<p><em>By {post.data.author}</em></p>
</div>
<div class="prose">
<Content />
</div>
</article>
</BaseLayout>
<style>
.post-header {
text-align: center;
margin-bottom: 2rem;
}
.post-header h1 {
font-size: 3rem;
margin-bottom: 0.5rem;
}
/* The 'prose' class can be used with @tailwindcss/typography for nice default styling */
.prose {
max-width: 65ch;
margin: auto;
line-height: 1.6;
}
</style>
Code Breakdown:
getStaticPaths: We fetch all entries from theblogcollection and map over them. For eachpost, we return an object telling Astro to create a page at/blog/{post.slug}and pass the entirepostobject as a prop to that page.Astro.props: We retrieve thepostobject thatgetStaticPathsprovided for this specific page.post.render(): This is a key method on content collection entries. It's an async function that processes the Markdown/MDX body and returns a<Content />component. This component, when rendered, will be the HTML of your article.- Template: We use our
BaseLayoutand structure the page. We pull metadata likepost.data.titledirectly from the frontmatter. The magic happens when we render<Content />, which injects the main body of our article.
For a clearer textual reference on this process, the following article is very helpful.
Getting Started With Astro's Content Collection API
The article 'Getting Started With Astro's Content Collection API' by Shawn.so provides a concise, code-first explanation of creating dynamic pages.
Read the section 'Rendering The Post Page'. Notice the similar structure: creating the file, implementing getStaticPaths, and using Astro.props to render the content. This reinforces the pattern we just implemented.
Test your understanding!
In our [slug].astro template, the code currently renders the post title, date, and author. Let's say your blog's frontmatter also includes an image field for a hero image (e.g., image: '/images/blog/my-post-hero.jpg').
How would you modify the template to display this image between the header and the main content? You can use a standard <img> tag for this.
Show answer
You would access post.data.image and add an <img> tag inside the <article> element. It's also good practice to check if the image exists before trying to render it.
---
// ... (rest of the frontmatter script)
---
<BaseLayout title={post.data.title} description={post.data.description}>
<article>
<div class="post-header">
<h1>{post.data.title}</h1>
<FormattedDate date={post.data.pubDate} />
<p><em>By {post.data.author}</em></p>
</div>
{/* Add the hero image here */}
{post.data.image && (
<img src={post.data.image} alt="" class="hero-image" />
)}
<div class="prose">
<Content />
</div>
</article>
</BaseLayout>
<style>
/* ... (existing styles) */
.hero-image {
width: 100%;
max-height: 400px;
object-fit: cover;
margin-bottom: 2rem;
border-radius: 8px;
}
</style>
This demonstrates how you can easily use any piece of frontmatter data to build out your page template. Note that for a production site, you'd want to use Astro's <Image /> component for optimization, which we'll cover later.
3. Applying the Pattern to Service Pages
The beauty of this dynamic routing pattern is its reusability. Your goal is to build a site with services, case studies, and a blog. The process for creating pages for each is nearly identical.
Let's outline how you would create dynamic pages for your accounting firm's services.
- Ensure a
servicescollection exists. In yoursrc/content/config.ts, you would define aservicescollection with its own schema (e.g.,title,summary,icon). - Add content. Create a
src/content/services/folder and add MDX files liketax-planning.mdx,bookkeeping.mdx, etc. - Create a new dynamic route. Create the file
src/pages/services/[slug].astro. - Implement the template. The code inside would be a direct adaptation of the blog post template:
// src/pages/services/[slug].astro
---
import { type CollectionEntry, getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
export async function getStaticPaths() {
const services = await getCollection('services'); // <-- Use the 'services' collection
return services.map(service => ({
params: { slug: service.slug },
props: { service }, // <-- Pass 'service' as a prop
}));
}
const { service } = Astro.props;
const { Content } = await service.render();
---
<BaseLayout title={service.data.title}>
<section class="service-page">
<h1>{service.data.title}</h1>
<!-- You might have a different layout here -->
<p class="summary">{service.data.summary}</p>
<hr />
<div class="prose">
<Content />
</div>
<!-- Maybe a call-to-action component -->
</section>
</BaseLayout>
<style>
/* Styles specific to a service page */
.service-page {
/* ... */
}
</style>
As you can see, the core logic (getStaticPaths, Astro.props, render()) remains the same. Only the collection name and the surrounding HTML structure change. This powerful, repeatable pattern is key to building a maintainable and scalable website.
Conclusion
Excellent work today! You have now mastered one of Astro's most powerful features: generating dynamic, static pages from content collections. This is the foundation for displaying all the detailed content on your accounting website.
Key Takeaways:
- Dynamic routes like
[slug].astroallow you to generate pages programmatically. - The
getStaticPathsfunction is the engine that drives this, defining which pages to build at build time. getStaticPathsmaps a content collection to an array of{ params, props }objects, which define the URL and data for each page.- An individual content entry's body is rendered using the
<Content />component obtained from the asyncpost.render()method. - This dynamic page pattern is highly reusable and can be applied to any content type, such as blog posts, services, or case studies.
Up Next:
You've created a template for blog posts and another for services. You might have noticed some repetition in their structure. In our next lesson, we will implement nested layouts for different content types. This will allow you to create a specific layout for all blog posts (e.g., one that always includes author info) and a different one for all services, all while inheriting from the main BaseLayout to keep your code DRY (Don't Repeat Yourself).