Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 30

Building Dynamic Case Study Pages

Recommended resources

Hello! Welcome back.

In our last lesson, you did an excellent job creating a TestimonialsSection that dynamically queries and renders content. You mastered the core workflow of defining a content collection, authoring content in Markdown, and using getCollection to display it. This is a huge step towards building the maintainable, content-driven site you're aiming for.

Today, we're going to build on that foundation to create a complete content section. Your goal is to build a dynamic listing page for case studies and the individual pages for each case study. This will involve two key parts: a central page that lists all case studies, and then unique, detailed pages for each one, generated automatically from your content files. This introduces you to one of Astro's most powerful features: dynamic routing.

1. Defining the case-studies Collection

Just like with testimonials, our first step is to create a well-structured home for our case study content. We'll define a new content collection with a schema that ensures every case study has a consistent set of information.

Action: Define the Case Studies Schema

  1. Open your src/content/config.ts file.
  2. Alongside your testimonialsCollection, let's add one for case-studies. We'll include fields for a title, client name, a summary for the listing page, an optional cover image, and a publication date.
// src/content/config.ts
import { z, defineCollection } from 'astro:content';

const testimonialsCollection = defineCollection({
  // ... your existing testimonials collection
});

// Define a new 'case-studies' collection
const caseStudiesCollection = defineCollection({
  type: 'content',
  schema: ({ image }) => z.object({
    title: z.string(),
    client: z.string(),
    summary: z.string(),
    publishDate: z.coerce.date(), // 'coerce' transforms the string from frontmatter into a Date object
    coverImage: image().optional(), // Uses Astro's built-in image optimization
  }),
});

// Export all your collections
export const collections = {
  'testimonials': testimonialsCollection,
  'case-studies': caseStudiesCollection,
};

Action: Create Case Study Content

Now, let's create the actual content files.

  1. Create a new directory: src/content/case-studies/.
  2. Inside, create two new Markdown files. The file names will become the URL slugs (e.g., streamlining-startup-finances.md becomes /case-studies/streamlining-startup-finances).

File: src/content/case-studies/streamlining-startup-finances.md

---
title: "Streamlining Finances for a High-Growth Tech Startup"
client: "Innovate Inc."
summary: "How we implemented a scalable accounting system that supported a 300% growth in revenue without increasing overhead."
publishDate: "2024-05-15"
coverImage: "./images/startup-office.jpg" # Place an image in src/content/case-studies/images/
---

### The Challenge
Innovate Inc. was experiencing rapid growth, but their accounting system, based on manual spreadsheets, couldn't keep up. This led to reporting delays and a lack of real-time financial visibility.

### Our Solution
We conducted a full audit of their financial processes and implemented a cloud-based accounting platform...

*(...continue with more details of the case study...)*

File: src/content/case-studies/manufacturing-cost-reduction.md

---
title: "Manufacturing Firm Achieves 15% Cost Reduction"
client: "Precision Parts Co."
summary: "A deep dive into supply chain and production costs revealed key areas for optimization, leading to a significant boost in profit margins."
publishDate: "2024-04-22"
---

### The Challenge
Precision Parts Co. faced shrinking profit margins due to rising material costs and inefficient inventory management. They needed to find savings without compromising quality.

### Our Solution
Our team analyzed their entire production pipeline...

*(...continue with more details...)*

Remember to restart your dev server (npm run dev) for Astro to pick up the new collection.


2. Building the Case Study Listing Page

With our content ready, we'll create a single page to list all available case studies. In Astro, page routes are created by the files in the src/pages directory. To create a page at yoursite.com/case-studies, we simply create a file at src/pages/case-studies.astro.

This process is very similar to how you built the TestimonialsSection, but now it will live on its own dedicated page.

Astro Web Framework Crash Course

This section of the 'Astro Web Framework Crash Course' video from freeCodeCamp.org is a perfect guide. It demonstrates how to create a blog listing page by querying a content collection and mapping over the results to display them. You can apply the exact same logic for our case studies.

Watch from 00:34:19 to 00:40:58. Pay close attention to how getCollection('posts') is used, and how the results are mapped to render a list of post components. Notice how the link for each post is constructed using the post.slug.

Action: Create the Listing Page

  1. Create the new page file: src/pages/case-studies.astro.
  2. Add the following code. We will query the case-studies collection and use the Card component we built in a previous lesson to display each one.
---
// src/pages/case-studies.astro
import { getCollection } from 'astro:content';
import Layout from '../layouts/Layout.astro';
import Card from '../components/Card.astro'; // Assuming you have this from a previous lesson

// 1. Fetch all case studies and sort them by date
const allStudies = await getCollection('case-studies');
const sortedStudies = allStudies.sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf());
---
<Layout title="Case Studies | Premier Accounting">
  <main class="container mx-auto px-4 py-16">
    <h1 class="text-4xl font-bold text-center mb-4">Our Success Stories</h1>
    <p class="text-xl text-center text-gray-600 mb-12">See how we've helped businesses like yours achieve their financial goals.</p>

    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
      <!-- 2. Map over the sorted studies and render a Card for each -->
      {sortedStudies.map(study => (
        <Card
          title={study.data.title}
          href={`/case-studies/${study.slug}/`}
          body={study.data.summary}
          headingTag="h2"
        />
      ))}
    </div>
  </main>
</Layout>

Navigate to http://localhost:4321/case-studies in your browser. You should now see a page listing your case studies. The links won't work yet, but we're about to fix that!


3. Creating Individual Pages with Dynamic Routing

This is where the magic happens. Instead of creating a separate .astro file for every single case study, we'll create one template file that Astro can use to generate all of them. This is called a dynamic route.

The key is a special file naming convention: [parameter].astro. The brackets signal to Astro that this part of the URL is a variable. We'll also use a function called getStaticPaths to tell Astro exactly which pages to generate at build time based on our content.

Astro Web Framework Crash Course

This next section of the same video is crucial. It explains exactly how to create dynamic pages using a [slug].astro file and the getStaticPaths function. This is the core concept you need to create your individual case study pages.

Watch from 00:40:58 to 00:46:38. Focus on understanding the role of getStaticPaths. See how it queries the collection and returns an array of objects containing params (for the URL slug) and props (to pass the full post data to the page).

Action: Create the Dynamic Route Template

  1. First, create a new folder inside pages: src/pages/case-studies/.

  2. Inside this new folder, create the dynamic route file: src/pages/case-studies/[slug].astro.

  3. Add the following code to the file. This is the most important part of today's lesson.

---
// src/pages/case-studies/[slug].astro
import { getCollection, type CollectionEntry } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
import { Image } from 'astro:assets';

// Define the shape of the data passed from getStaticPaths
interface Props {
  study: CollectionEntry<'case-studies'>;
}

// 1. This function runs at build time to tell Astro which pages to generate.
export async function getStaticPaths() {
  const allStudies = await getCollection('case-studies');
  return allStudies.map(study => ({
    params: { slug: study.slug }, // `slug` matches the file name `[slug].astro`
    props: { study }, // Pass the full case study object to the page
  }));
}

// 2. The `study` prop is now available for the page to use.
const { study } = Astro.props;
const { Content } = await study.render(); // This renders the Markdown body to HTML
---
<Layout title={`${study.data.title} | Case Study`}>
  <main class="container mx-auto px-4 py-16">
    <article class="prose lg:prose-xl mx-auto">
      {study.data.coverImage && (
        <Image 
            src={study.data.coverImage} 
            alt={`Cover image for ${study.data.title}`} 
            class="rounded-lg mb-8"
        />
      )}
      <h1>{study.data.title}</h1>
      <p class="text-lg text-gray-600"><strong>Client:</strong> {study.data.client}</p>
      
      <!-- 3. The full content of your .md file is rendered here -->
      <Content />
    </article>
  </main>
</Layout>

Let's break down getStaticPaths:

  • It fetches all entries from the case-studies collection.
  • It then uses .map() to return an array. Each item in the array is an object that defines one page to be built.
  • params: { slug: study.slug }: This tells Astro the URL for the page. Since our file is [slug].astro, the slug property is used to fill in that part of the URL.
  • props: { study }: This passes the entire study object to the page template, so we can access its title, date, and content.

Now, go back to your /case-studies listing page and click on one of the links. It should take you to a fully rendered page for that specific case study, complete with its title and the full Markdown content.

Test your understanding!

Imagine your client wants to hide certain case studies that are still being written. You add an isDraft: z.boolean().optional() field to your schema. How would you modify getStaticPaths in [slug].astro to prevent pages from being generated for any case study where isDraft is true?

Show answer

You would use the .filter() array method before .map() inside getStaticPaths to remove the draft entries from the list of pages to be generated.

export async function getStaticPaths() {
  const allStudies = await getCollection('case-studies');
  
  const publishedStudies = allStudies.filter(study => !study.data.isDraft);

  return publishedStudies.map(study => ({
    params: { slug: study.slug },
    props: { study },
  }));
}

This ensures that Astro only generates static pages for the content you want to be public, which is great for performance and managing content workflows.


Conclusion

Fantastic work today! You have now implemented one of the most powerful and scalable patterns in modern web development. By combining content collections with dynamic routing, you can now manage hundreds of case studies, blog posts, or service pages just by adding or editing simple Markdown files.

Key Takeaways:

  • File-Based Routing: You created a listing page at /case-studies by creating the file src/pages/case-studies.astro.
  • Dynamic Routing: You created a template for all individual case study pages using a single file: src/pages/case-studies/[slug].astro.
  • getStaticPaths: You learned how to use this essential Astro function to programmatically define which pages to build at compile time, based on your content.
  • Passing Props: You passed the complete data for each case study from getStaticPaths to the page template using the props object, making the content available for rendering.
  • Rendering Content: You used study.render() and the <Content /> component to display the full body of your Markdown files on the page.

Up Next:

You've built a base layout, a listing page, and detail pages. You might notice that different page types could benefit from slightly different structures. In our next lesson, we will implement nested layouts for different content types, allowing you to create, for example, a specific BlogLayout that inherits from your main Layout but adds blog-specific elements. This will further enhance the structure and maintainability of your project.