Hello! Welcome back.
In our last lesson, you built the interactive frame for your site: a fully responsive and accessible header using React. This was a fantastic exercise in creating and hydrating "islands of interactivity."
Now, we'll turn our attention back to Astro's strength: generating static, content-rich pages. We'll populate your site with its core offerings by creating dynamic pages for each of your accounting services. This will bring your project goal of a "content-driven" website to life.
By the end of this lesson, you will be able to create dynamic pages for individual services by fetching data from the services content collection. This process combines two powerful Astro features you've encountered before: Content Collections for organizing type-safe data and Dynamic Routing for generating pages programmatically.
1. Defining the "Services" Content Collection
First, we need a structured way to manage the content for your services. Instead of creating static .astro pages for each one, we'll use a content collection. This approach provides type-safety, makes your content easy to query, and is highly scalable—perfect for your goal of a maintainable, production-ready website.
Let's set up the collection.
Action 1: Create the Content Files
- In your project, create a new folder inside
src/contentand name itservices. - Inside
src/content/services, create two new files. Let's call themtax-advisory.mdxandbookkeeping.mdx.
Paste the following content into tax-advisory.mdx:
---
title: 'Tax Advisory & Planning'
summary: 'Strategic tax planning to minimize liabilities and maximize savings for individuals and businesses.'
icon: 'tabler:receipt-tax' # Example icon name from an icon library
---
Our expert tax advisory services are designed to help you navigate the complex world of taxation. We go beyond simple compliance to provide proactive strategies that align with your financial goals.
- Corporate and personal tax planning
- GST/HST compliance
- Tax dispute resolution
- International tax consulting
And this content into bookkeeping.mdx:
---
title: 'Full-Cycle Bookkeeping'
summary: 'Accurate and timely bookkeeping services to give you a clear picture of your financial health.'
icon: 'tabler:calculator' # Example icon name
---
We provide meticulous bookkeeping services to keep your financial records in perfect order. Focus on running your business while we handle the numbers.
- Accounts payable and receivable
- Bank and credit card reconciliation
- Financial statement preparation
- Payroll services
Action 2: Define the Collection Schema
Now, we need to tell Astro about this new collection and what kind of data to expect in the frontmatter.
Open your src/content/config.ts file. You likely have a blog collection defined here from an earlier module. We will add a new services collection alongside it.
Update the file to look like this:
import { defineCollection, z } from 'astro:content';
const blogCollection = defineCollection({
// ... your existing blog schema
});
// Add the new services collection
const servicesCollection = defineCollection({
type: 'content', // 'content' for MD/MDX, 'data' for JSON/YAML
schema: z.object({
title: z.string(),
summary: z.string(),
icon: z.string().optional(), // We'll make the icon optional for now
}),
});
export const collections = {
blog: blogCollection,
services: servicesCollection, // Register the new collection
};
By defining this schema with Zod, you get incredible benefits:
- Type Safety: Astro will throw an error during development if a service's frontmatter is missing a
titleor if it's not a string. - Autocomplete: Your code editor will now know the exact shape of your service data, providing autocompletion for
entry.data.title,entry.data.summary, etc.
2. Generating Pages with Dynamic Routes
With our content ready, we need to tell Astro how to create a unique page for each service. We won't create src/pages/services/tax-advisory.astro manually. Instead, we'll create a single template file that can generate all service pages. This is called a dynamic route.
The official Astro documentation provides the definitive guide on this topic. Reading it will give you a solid foundation for the code we're about to write.
Content collections - Astro Docs
We'll be building a static site, so we need to tell Astro what pages to generate at build time. This documentation explains the core mechanism for this: getStaticPaths.
Read the sections 'Generating Routes from Content' and 'Building for static output (default)'. Focus on understanding the role of getStaticPaths() and how it uses getCollection() to create a page for each collection entry.
3. Implementing the Dynamic Service Page
Now that you understand the theory behind getStaticPaths, let's put it into practice.
Action: Create the Dynamic Route File
- Create a new folder inside
src/pagesnamedservices. - Inside
src/pages/services, create a new file named[slug].astro.
The square brackets [] are special syntax that tells Astro this is a dynamic route. The slug is a parameter that will correspond to the filename of our content entries (e.g., tax-advisory or bookkeeping).
Paste the following code into src/pages/services/[slug].astro:
---
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro'; // Adjust path if needed
// 1. Generate a path for each service
export async function getStaticPaths() {
const services = await getCollection('services');
return services.map(entry => ({
params: { slug: entry.slug },
props: { entry },
}));
}
// 2. Get the specific service entry for this page
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<Layout title={entry.data.title}>
<article class="prose prose-lg mx-auto p-8">
<h1>{entry.data.title}</h1>
<p class="lead">{entry.data.summary}</p>
<hr />
<Content />
</article>
</Layout>
(Note: I've added some basic Tailwind CSS typography classes (prose, prose-lg) for styling. You can adjust these later.)
To see this process in action, the following video demonstrates building a dynamic route for a blog. The principle is identical to what we're doing for our services.
Typesafe Markdown With Astro Content Collections
This clip provides a great visual walkthrough of writing a getStaticPaths function and using the resulting props to render a page.
Watch from 08:27 to 11:25. Pay close attention to how getStaticPaths is written and how Astro.props is used to receive the entry data. Notice the use of await entry.render() to get the <Content /> component.
4. Deconstructing the Dynamic Page Template
Let's break down the [slug].astro file into two main parts.
Part 1: getStaticPaths()
This function runs at build time. Its job is to return an array of all the pages that need to be generated from this template.
getCollection('services'): We fetch all entries from our newly createdservicescollection.services.map(entry => ({...})): We loop over each service entry.params: { slug: entry.slug }: This is the crucial part for routing. For ourtax-advisory.mdxfile,entry.slugwill be"tax-advisory". This tells Astro to generate a page at/services/tax-advisory.props: { entry }: We pass the entire data object for the current service as a prop to the page template itself. This is how the template knows what content to display.
Part 2: The Page Template
This is the rest of the file, which acts like a regular Astro component.
const { entry } = Astro.props;: Here, we receive theentryobject that we passed in viapropsfromgetStaticPaths.const { Content } = await entry.render();: This special function processes the body of the MDX file and returns it as a renderable<Content />component.<Layout title={entry.data.title}>: We use our main layout and pass the service title up to it, which is great for SEO and browser tabs.<h1>{entry.data.title}</h1>: We render the title from the frontmatter.<Content />: We render the main body of our MDX file.
Now, you can run your dev server (npm run dev) and navigate to http://localhost:4321/services/tax-advisory and http://localhost:4321/services/bookkeeping. You should see two distinct pages, each rendered from the same template but with different data!
Test your understanding!
In your src/pages/services/[slug].astro file, what is the purpose of the props: { entry } line inside the object returned by getStaticPaths? What would happen if you removed it?
Show answer
The props: { entry } line passes the entire content collection entry for a specific service (including its frontmatter data and body) as a prop to the page template that will render the final HTML.
If you removed it, the getStaticPaths function would still successfully generate the page URLs, but the page template itself would have no data to display. The line const { entry } = Astro.props; would fail because Astro.props would be an empty object. This would result in a build error because the template code tries to access properties of entry (like entry.data.title), which would be undefined.
Conclusion
Excellent work! You have just implemented one of the most powerful and common patterns in modern static site generation. By combining Content Collections with Dynamic Routes, you've created a system that is scalable, maintainable, and type-safe.
Key Takeaways:
- Data and Presentation Separation: Your service content (MDX files) is now completely separate from its presentation (the
[slug].astrotemplate). - Single Source of Truth: The
src/content/servicesdirectory is the single source of truth for all your service offerings. Adding a new service is as simple as adding a new MDX file. - Static Generation at Scale: You used
getStaticPathsto programmatically generate HTML pages at build time, ensuring maximum performance for your users. - Data Flow: You mastered the flow of data from
getCollection->getStaticPaths->Astro.props-> page template.
Up Next:
Now that you can create detailed pages for your services, the next step is to create summary views of your content. In the next lesson, you will design and build reusable card components for testimonials and case study summaries. This will allow you to create engaging sections on your homepage and other landing pages that link to your dynamic content.