Nuxt Content Management — Complete Guide to @nuxt/content
In this tutorial, you will learn about Nuxt Content Management. We cover key concepts, practical examples, and best practices to help you master this topic.
Learn Nuxt Content module — manage markdown files, query content with MongoDB-like syntax, render dynamic pages, and build a blog without a CMS.
In this lesson, you'll understand how to use the Nuxt Content module to create a file-based content management system for blogs, documentation, and landing pages.
What You'll Learn
How to install @nuxt/content, write content in markdown with frontmatter, query content using the Content Query Builder, render content with ContentDoc and ContentList, and build navigation from your content structure.
Why It Matters
A file-based CMS eliminates database costs, simplifies deployment, and integrates naturally with your Git workflow. Editors write markdown, developers control the presentation — no admin panel needed.
Real-World Use
A SaaS documentation site with 200+ articles uses @nuxt/content to generate searchable, versioned documentation from markdown files in a single Git Repository, with automatic navigation and full-text search.
flowchart TD
A[Markdown Files] --> B[Content Module]
B --> C[Query Builder]
C --> D[ContentDoc Component]
C --> E[ContentList Component]
C --> F[ContentNavigation]
D --> G[Rendered Pages]
E --> G
F --> H[Auto Navigation]
G --> I[Static Site]
style A fill:#00dc82,color:#fff
Installation
npm install @nuxt/content
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/content']
});
Create a content/ directory at the project root. This is where your markdown files live.
Writing Content
Create a markdown file with frontmatter:
---
title: "Getting Started with Nuxt"
description: "Learn how to set up your first Nuxt project"
published: true
date: 2026-06-01
author: "DodaTech"
---
## Introduction
Nuxt is a Vue framework that makes building web applications intuitive.
```vue
<template>
<h1>{{ title }}</h1>
</template>
Place this file at `content/blog/getting-started.md`. The file path becomes the URL path: `/blog/getting-started`.
## Querying Content with the Query Builder
Fetch content programmatically in any component or page:
```vue
<script setup>
const { data: articles } = await useAsyncData('articles', () => {
return queryContent('/blog')
.where({ published: true })
.sort({ date: -1 })
.limit(10)
.find();
});
</script>
<template>
<article v-for="article in articles" :key="article._path">
<h2>{{ article.title }}</h2>
<p>{{ article.description }}</p>
<NuxtLink :to="article._path">Read More</NuxtLink>
</article>
</template>
Expected output: A list of published blog articles sorted by date, each linking to its own page.
Using ContentDoc for Single Pages
Render a single markdown file:
<!-- pages/blog/[...slug].vue -->
<script setup>
const { path } = useRoute();
const { data: doc } = await useAsyncData('doc', () => {
return queryContent(path).findOne();
});
</script>
<template>
<main>
<ContentDoc />
</main>
</template>
Expected output: The markdown file renders as HTML with all components and anchor links working automatically.
Using ContentList for Multiple Items
Display a list of content items with the built-in component:
<template>
<ContentList path="/docs" v-slot="{ list }">
<ul>
<li v-for="doc in list" :key="doc._path">
<NuxtLink :to="doc._path">
{{ doc.title }}
</NuxtLink>
<span>{{ doc.description }}</span>
</li>
</ul>
</ContentList>
</template>
Expected output: An unordered list of all documents under /docs, each linking to its content page.
Navigation from Content Structure
Generate navigation automatically from your file hierarchy:
<script setup>
const { data: navigation } = await useAsyncData('nav', () => {
return fetchContentNavigation();
});
</script>
<template>
<nav>
<ul>
<li v-for="item in navigation" :key="item._path">
<NuxtLink :to="item._path">{{ item.title }}</NuxtLink>
<ul v-if="item.children">
<li v-for="child in item.children" :key="child._path">
<NuxtLink :to="child._path">{{ child.title }}</NuxtLink>
</li>
</ul>
</li>
</ul>
</nav>
</template>
Expected output: A nested navigation menu that matches your content directory structure, updating automatically when you add files.
Searching Content
Implement full-text search across your content:
<script setup>
const searchQuery = ref('');
const { data: results } = useAsyncData('search', () => {
if (!searchQuery.value) return [];
return queryContent('/')
.where({ published: true })
.search(searchQuery.value)
.limit(5)
.find();
}, { watch: [searchQuery] });
</script>
<template>
<input v-model="searchQuery" placeholder="Search documentation..." />
<ul v-if="results.length">
<li v-for="result in results" :key="result._path">
<NuxtLink :to="result._path">{{ result.title }}</NuxtLink>
</li>
</ul>
</template>
Expected output: A live search that filters content as the user types, using the Content module's built-in search capabilities.
Common Mistakes
Putting content files in the wrong directory: The Content module reads from the
content/directory at the project root, notpages/orassets/. Files outsidecontent/are invisible to the module.Forgetting to use async data wrappers: Always wrap query calls with
useAsyncDataoruseLazyAsyncDatato enable SSR support and prevent hydration mismatches.Querying with incorrect path syntax: Paths in
queryContent()start from thecontent/root. A file atcontent/blog/post.mduses path/blog/post. Omitting the leading slash causes empty results.Not handling missing content files: When
findOne()returns null for a missing file, the page crashes. Always add error handling:if (!data.value) throw createError({ statusCode: 404 }).Overwriting the catch-all route: Pages like
[...slug].vuemust handle only the paths they own. Checkpathto avoid catching unrelated routes from other modules.
Practice Questions
Where does @nuxt/content read files from? Answer: The
content/directory at the project root. File paths map directly to URL paths.What does
findOne()return when no file matches? Answer: It returnsnull. You should check for null and return a 404 error in that case.How do you sort query results? Answer: Use
.sort({ field: order })where order is1(ascending) or-1(descending). Multiple sort fields are supported.What is the difference between ContentDoc and ContentList? Answer: ContentDoc renders a single content file. ContentList iterates over multiple content items with a slot-based template.
Challenge
Build a documentation site with: nested sidebar navigation from fetchContentNavigation(), full-text search across all pages, a table of contents generated from headings, and previous/next page navigation at the bottom of each page.
Mini Project
Create a blog with @nuxt/content that includes: a homepage listing posts sorted by date, individual post pages with markdown rendering, a tag-based filtering system using frontmatter tags, an RSS feed using server/routes/feed.xml.ts, and a search page with live results.
FAQ
What's Next
Learn about Nuxt Styling and Theming to add CSS frameworks, global styles, and dark mode support to your Nuxt application.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro