Day 13: Build Case Study Pages

Day 13 was about turning yesterday’s structured case study content into real, polished pages: a listing page, a detail template, and the supporting components, styling, and tests to keep everything reliable and readable.

Update

  • Shipped a new case studies listing page at /case-studies
  • Shipped statically generated case study detail pages at /case-studies/[slug]
  • Built a reusable CaseStudyCard component with visual accents per project type
  • Expanded the case study content schema to include clientName (plus optional projectSource)
  • Hardened URL validation for external links and added dedicated tests
  • Added unit tests + fast-check property tests to validate page structure and data consistency
  • Updated Lighthouse CI config to include the new case study routes

What Shipped

1. /case-studies Listing Page

The new listing page (src/pages/case-studies/index.astro) loads all case studies via getCaseStudies() and renders them in a responsive grid. It includes:

  • A hero section with clear positioning and an intro
  • A case study grid powered by the new CaseStudyCard
  • A bottom CTA block linking to /assessment and /book
  • Reduced-motion support and keyboard focus styles

2. /case-studies/[slug] Detail Pages

The detail template (src/pages/case-studies/[slug].astro) uses getStaticPaths() to generate one page per case study. Each page includes:

  • Breadcrumb-style back navigation (/case-studies)
  • A hero block with project title, client type label, and optional metadata (e.g. “collaborating since…”)
  • Structured sections for Overview, Scope, Challenge, Constraints & Context, Solution, Technologies, and Outcomes
  • Optional testimonial block with attribution
  • A CTA section at the end (book / assessment)

All external links coming from content frontmatter are passed through validateUrlOrFallback() to avoid unsafe href values.

3. Case Study Card Component

src/components/case-studies/CaseStudyCard.astro renders the minimum “scan” info needed on the listing page:

  • Project title, client type label, short description
  • A few technology tags (with a +N indicator when there are more)
  • Accent colors and hover states keyed off clientType

Accessibility details were treated as first-class: aria-label on the card link, :focus-visible styles, and touch targets ≥ 44px.

4. Content Model Improvements

The case study schema was updated (src/content/config.ts + src/content/caseStudies.ts) to support richer storytelling:

  • Added required clientName
  • Added optional projectSource (useful for attribution like referral marketplaces)

The existing case study markdown files were updated to match the schema (src/content/case-studies/*.md).

5. Safer URL Handling + Better Tests

We expanded src/utils/urlValidation.ts so it can be used safely across the site:

  • Allows only http:, https:, and mailto: URLs (plus absolute same-origin paths starting with /)
  • Rejects javascript: and data: protocols
  • Adds validateUrlOrFallback() for safe defaults in templates
  • Tightens validateBookingUrl() to validate allowed domains

This is covered by a dedicated test suite (src/utils/urlValidation.test.ts).

6. Test Coverage (Unit + Property Tests)

Because astro:content imports aren’t test-friendly, a small helper (src/__tests__/case-studies-test-helper.ts) loads markdown files directly with gray-matter and validates frontmatter via Zod.

On top of that, we added:

  • Page source tests for /case-studies and /case-studies/[slug]
  • Component tests for CaseStudyCard
  • Fast-check property tests ensuring listing/detail completeness and consistent structure

Try It