skip to content

[ tech ]_

Building a typed content layer for Next.js (without Contentlayer)

20 jun 2026 · 1 min read

nextjstypescriptarchitecture

Every portfolio site eventually faces the same question: where does the content live? The fashionable answers — Contentlayer, a headless CMS, some MDX bundler — all couple your content model to someone else's roadmap. Contentlayer is effectively unmaintained. CMSes add a network dependency to a site that could be fully static. I wanted a third option: own the thin layer.

The contract comes first

The trick is to define the interface before the implementation:

export interface ArticleRepository {
  getAll(filter?: ArticleFilter): Promise<Article[]>;
  getBySlug(slug: string): Promise<Article | null>;
  getSlugs(): Promise<string[]>;
}

Pages import repositories.articles and nothing else. They don't know about frontmatter, file paths, or fs. That single decision means the storage engine is swappable — today it's MDX files parsed with gray-matter, tomorrow it can be Postgres rows behind the same interface.

Zod is the gatekeeper

Every frontmatter block is validated at build time:

const parsed = articleFrontmatterSchema.safeParse(data);
if (!parsed.success) {
  throw new Error(`Invalid frontmatter in ${file}: …`);
}

A typo in a date or a missing summary fails next build with a precise error. Malformed content cannot reach production, and the Zod types double as the future database row types.

What this costs

About two hundred lines, written once. What it buys: no framework churn, a data model I can explain in an interview, and a migration path where the UI never changes. Sometimes the senior move is writing less clever code.