Skip to content

Nuxt Content Management — Complete Guide to @nuxt/content

DodaTech Updated 2026-06-28 5 min read

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.

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

  1. Putting content files in the wrong directory: The Content module reads from the content/ directory at the project root, not pages/ or assets/. Files outside content/ are invisible to the module.

  2. Forgetting to use async data wrappers: Always wrap query calls with useAsyncData or useLazyAsyncData to enable SSR support and prevent hydration mismatches.

  3. Querying with incorrect path syntax: Paths in queryContent() start from the content/ root. A file at content/blog/post.md uses path /blog/post. Omitting the leading slash causes empty results.

  4. 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 }).

  5. Overwriting the catch-all route: Pages like [...slug].vue must handle only the paths they own. Check path to avoid catching unrelated routes from other modules.

Practice Questions

  1. Where does @nuxt/content read files from? Answer: The content/ directory at the project root. File paths map directly to URL paths.

  2. What does findOne() return when no file matches? Answer: It returns null. You should check for null and return a 404 error in that case.

  3. How do you sort query results? Answer: Use .sort({ field: order }) where order is 1 (ascending) or -1 (descending). Multiple sort fields are supported.

  4. 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

Can I use Vue components inside markdown content?

: Yes. Define components in the components/content/ directory. They become available as custom markdown tags. Use them directly in markdown files without registration.

Does @nuxt/content support images?

: Yes. Use standard markdown image syntax or the <img> tag. Place images in the public/ directory or use the assets/ directory with path aliases.

How does the module handle drafts?

: Use frontmatter fields like draft: true. Filter them out in queries with .where({ draft: { $ne: true } }). This gives you full control over draft visibility.

What is the difference between `find()` and `findOne()`?

: find() returns an array of all matching documents. findOne() returns a single document or null. Use find() for lists, findOne() for single pages.

Does the content module work with SSG?

: Yes. All content is fetched at build time and pre-rendered as static HTML. No server is needed for content delivery.

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