---
title: "Type-Safe Content Pipelines with Zod & Astro"
description: "How to construct schema-validated Markdown and JSON pipelines that catch data anomalies, invalid links, and frontmatter drift at compile-time."
publishDate: "2024-05-12"
category: "TypeScript"
tags: ["typescript", "zod", "astro", "content-collections"]
canonicalUrl: "https://sahillangoo.in/blog/type-safe-content-pipelines/"
---

# Type-Safe Content Pipelines with Zod & Astro

> How to construct schema-validated Markdown and JSON pipelines that catch data anomalies, invalid links, and frontmatter drift at compile-time.

- **Published**: 2024-05-12
- **Category**: TypeScript
- **Tags**: typescript, zod, astro, content-collections
- **Reading Time**: 6 min read

Static site generation is only as reliable as the data feeding into it. In traditional Markdown and CMS workflows, data models suffer from continuous "schema drift":

- An author formats a date as `12/05/2024` instead of ISO `2024-05-12`.
- A tag is misspelled as `['typesript']` instead of `['typescript']`.
- An optional `featuredImage` URL points to a dead asset path.
- A required `author` field is omitted entirely.

In unstructured pipelines, these subtle mistakes slip silently into production, causing runtime JavaScript crashes, distorted CSS grids, or broken metadata cards.

By combining **Astro Content Collections** with **Zod**, we turn content into a compiled, type-safe data pipeline.

---

## 1. Defining Strict Zod Schemas

In Astro 5+, collection loaders allow you to validate both local Markdown files and remote API payloads using declarative Zod schemas in `src/content.config.ts`:

```typescript
// src/content.config.ts
import { defineCollection } from 'astro:content';
import { z } from 'astro/zod';
import { glob } from 'astro/loaders';

export const collections = {
  projects: defineCollection({
    loader: glob({ pattern: '**/[^_]*.md', base: './src/content/projects' }),
    schema: z.object({
      title: z.string().min(3),
      description: z.string().max(160),
      summary: z.string().optional(),
      category: z.enum(['web-app', 'open-source', 'cli-tool', 'systems', 'design-engineering']),
      tags: z.array(z.string()).min(1),
      featured: z.boolean().default(false),
      liveUrl: z.string().url().optional(),
      githubUrl: z.string().url().optional(),
      year: z.number().int().min(2020).max(2030),
      role: z.string().default('Lead Engineer'),
      publishDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Must be YYYY-MM-DD format'),
    }),
  }),
};
```

---

## 2. Compile-Time Failure with Precise Line Coordinates

When an author commits a markdown file with an invalid category enum (`category: 'mobile-app'`), running `pnpm check` or `pnpm build` fails immediately:

```
[content] Failed to validate content file: src/content/projects/new-tool.md
[ZodError]: [
  {
    "code": "invalid_enum_value",
    "options": ["web-app", "open-source", "cli-tool", "systems", "design-engineering"],
    "path": ["category"],
    "message": "Invalid enum value. Expected 'web-app' | 'open-source' | 'cli-tool' | 'systems' | 'design-engineering', received 'mobile-app'"
  }
]
```

The error stops the CI/CD deployment pipeline before bad data ever reaches Cloudflare Pages or Vercel.

---

## 3. End-to-End Type Safety in UI Templates

Because Astro compiles these schemas into internal TypeScript declarations, your Astro components enjoy full autocomplete and type inference without manual interface casting:

```astro
---
// src/components/portfolio/ProjectCard.astro
import type { CollectionEntry } from 'astro:content';

export interface Props {
  project: CollectionEntry<'projects'>;
  featured?: boolean;
}

const { project, featured = false } = Astro.props;
const { data, id } = project;

// TypeScript knows 'data.category' is strictly the 5-item union
const isCli = data.category === 'cli-tool';
---

<article
  class={`rounded-xl border border-base-300 bg-base-200 p-6 transition-all duration-200 hover:-translate-y-0.5 ${featured ? 'border-accent/40' : ''}`}
>
  <span class="text-accent font-mono text-xs uppercase">{data.category}</span>
  <h3 class="text-xl font-bold">{data.title}</h3>
  <p class="text-base-content/75 text-sm">{data.description}</p>

  <div class="mt-4 flex gap-2">
    {data.tags.map((tag) => (
      <span class="bg-base-300 rounded px-2 py-0.5 font-mono text-xs">#{tag}</span>
    ))}
  </div>
</article>
```

---

## 4. Key Production Benefits

1. **Zero Runtime Null Checks**: You never have to write defensive checks like `if (project.data.tags && project.data.tags.length > 0)` because Zod guarantees the structure at build time.
2. **Deterministic SEO Metadata**: Every page automatically receives valid dates, character-capped titles, and validated URLs for Schema.org JSON-LD generation.
3. **Effortless Refactoring**: If you rename a field in `src/content.config.ts`, TypeScript immediately highlights every component that needs updating.
