Nuxt Content and MDC: Writing Pages Like Components
How MDC transforms static content into interactive, component-driven pages — and why I use it to build every page on this site.
Why static markdown isn't always enough
Markdown is great for writing. It's fast, readable, and portable. But most web pages aren't just paragraphs and headings — they have heroes, grids, CTAs, and branded sections that no amount of ## syntax can express cleanly.
The classic solution is to write those pages in Vue or HTML, which works fine until your content team needs to edit them, or you realize the content is scattered across component files with no clear ownership.
That's the problem MDC solves.
What is MDC?
MDC stands for Markdown Components. It's a syntax extension on top of standard Markdown, built into Nuxt Content v3, that lets you embed Vue components directly inside .md files.
A simple example:
::u-page-hero
---
title: Hello, world.
description: A component, rendered from markdown.
---
::
The ::u-page-hero block maps directly to the UPageHero Nuxt UI component. Props are written in frontmatter-style YAML inside ---. Any content between the opening and closing :: becomes the default slot.
You can nest components, pass complex props, add classes, and compose entire page layouts — all from a .md file.
How I use it on this site
Every page on this site — the homepage, the blog index, the service pages — is a Markdown file in content/. None of them are .vue files.
Here's a simplified version of the homepage content:
::page-hero
---
title: Web Development redefined.
description: Modern, performant applications that bring aesthetics and functionality into harmony.
---
:::hero-singularity
::
::page-section
---
title: What I do for you
---
:::u-page-grid
::::u-page-card
---
title: Frontend Development
description: Performant, accessible interfaces with Vue 3 and Nuxt.
---
::::
:::
::
The page-hero and page-section blocks are thin wrappers I wrote in components/content/ — they layer ContainerCorners and other project-specific chrome on top of the standard Nuxt UI components. MDC resolves them automatically because they live in that directory.
Why this approach works
Separation of concerns. Layout lives in Vue components. Content lives in Markdown. Neither bleeds into the other.
No CMS lock-in. The content is plain files in the repo. Any team member can edit them, git tracks every change, and deployment is just a push.
Full component power. Props, slots, nested components, and auto-imports are all on the table. If it works in a Vue template, it works in MDC.
Type safety. Nuxt Content v3 adds collection schemas through content.config.ts. Define z.string() fields for date and tags, and query results are fully typed across the app.
The one thing to watch out for
MDC converts component names to PascalCase for resolution. ::u-page-hero resolves to UPageHero, ::page-section resolves to PageSection. This means the components must be auto-importable — either globally registered or placed in components/content/ for content-specific ones.
If a component isn't found, Nuxt Content silently renders it as a raw HTML element. The fix is straightforward: check components/content/ first and verify the naming maps correctly.
Wrapping up
MDC is one of those features that sounds like a niche edge case until you actually use it. Once you've built a full page layout from a .md file and seen how maintainable it becomes, scattering content across .vue files feels like a step backwards.
If you're using Nuxt Content and haven't explored MDC yet — start small. Pick one page, convert it to MDC, and see how the structure clarifies itself.
