Nuxt Content v3
Status: Production Ready Last Updated: 2025-01-10 Dependencies: None Latest Versions: @nuxt/content@^3.0.0, nuxt-studio@^0.1.0-alpha, zod@^4.1.12, valibot@^0.42.0, better-sqlite3@^11.0.0
Overview
Nuxt Content v3 is a powerful Git-based CMS for Nuxt projects that manages content through Markdown, YAML, JSON, and CSV files. It transforms content files into structured data with type-safe queries, automatic validation, and SQL-based storage for optimal performance.
What's New in v3
Major Improvements:
- Content Collections: Structured data organization with type-safe queries, automatic validation, and advanced query builder
- SQL-Based Storage: Production uses SQL (vs. large bundle sizes in v2) for optimized queries and universal compatibility (server/serverless/edge/static)
- Full TypeScript Integration: Automatic types for all collections and APIs
- Enhanced Performance: Ultra-fast data retrieval with adapter-based SQL system
- Nuxt Studio Integration: Self-hosted content editing in production with GitHub sync
When to Use This Skill
Use this skill when:
- Building blogs, documentation sites, or content-heavy applications
- Managing content with Markdown, YAML, JSON, or CSV files
- Implementing Git-based content workflows
- Creating type-safe content queries
- Deploying to Cloudflare (Pages/Workers) or Vercel
- Setting up production content editing with Nuxt Studio
- Building searchable content with full-text search
- Creating navigation systems from content structure
Quick Start (10 Minutes)
1. Install Nuxt Content
# Bun (recommended)
bun add @nuxt/content better-sqlite3
# npm
npm install @nuxt/content better-sqlite3
# pnpm
pnpm add @nuxt/content better-sqlite3
Why this matters:
@nuxt/contentis the core CMS modulebetter-sqlite3provides SQL storage for optimal performance- Zero configuration required for basic usage
2. Register Module
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/content']
})
CRITICAL:
- Module must be added to
modulesarray (notbuildModules) - No additional configuration needed for basic setup
3. Create First Collection
// content.config.ts
import { defineContentConfig, defineCollection } from '@nuxt/content'
import { z } from 'zod'
export default defineContentConfig({
collections: {
content: defineCollection({
type: 'page',
source: '**/*.md',
schema: z.object({
tags: z.array(z.string()).optional(),
date: z.date().optional()
})
})
}
})
Create content file:
<!-- content/index.md -->
---
title: Hello World
description: My first Nuxt Content page
tags: ['nuxt', 'content']
---
# Welcome to Nuxt Content v3
This is my first content-driven site!
4. Query and Render Content
<!-- pages/[...slug].vue -->
<script setup>
const route = useRoute()
const { data: page } = await useAsyncData(route.path, () =>
queryCollection('content').path(route.path).first()
)
</script>
<template>
<ContentRenderer v-if="page" :value="page" />
</template>
See Full Template: templates/blog-collection-setup.ts
Critical Rules
Always Do
- Define Collections in
content.config.tsbefore querying - Use ISO 8601 Date Format:
2024-01-15or2024-01-15T10:30:00Z - Restart Dev Server after changing
content.config.ts - Use
.only()to select specific fields (performance) - Place MDC Components in
components/content/directory - Use Zero-Padded Prefixes for numeric sorting:
01-,02- - Install Database Connector:
better-sqlite3required - Specify Language in code blocks for syntax highlighting
- Use
<!--more-->in content to separate excerpts - D1 Binding Must Be "DB" (case-sensitive) on Cloudflare
Never Do
- Don't Query Before Defining Collection in
content.config.ts - Don't Use Non-ISO Date Formats (e.g., "January 15, 2024")
- Don't Forget to Restart dev server after config changes
- Don't Query All Fields when you only need some (use
.only()) - Don't Place Components outside
components/content/ - Don't Use Single-Digit Prefixes (use
01-not1-) - Don't Skip Database Connector installation
- Don't Forget Language in code fences
- Don't Expect Excerpts without
<!--more-->divider - Don't Use Different Binding Names for D1 (must be "DB")
Content Collections
Defining Collections
Collections organize related content with shared configuration:
// content.config.ts
import { defineCollection, defineContentConfig } from '@nuxt/content'
import { z } from 'zod'
export default defineContentConfig({
collections: {
blog: defineCollection({
type: 'page',
source: 'blog/**/*.md',
schema: z.object({
title: z.string(),
date: z.date(),
tags: z.array(z.string()).default([])
})
}),
authors: defineCollection({
type: 'data',
source: 'authors/*.yml',
schema: z.object({
name: z.string(),
bio: z.string()
})
})
}
})
Collection Types
Page Type (type: 'page')
Use for: Content that maps to URLs
Features:
- Auto-generates paths from file structure
- Built-in fields:
path,title,description,body,navigation - Perfect for: blogs, docs, marketing pages
Path Mapping:
content/index.md → /
content/about.md → /about
content/blog/hello.md → /blog/hello
Data Type (type: 'data')
Use for: Structured data without URLs
Features:
- Complete schema control
- No automatic path generation
- Perfect for: authors, products, configs
Schema Validation
With Zod v4 (Recommended)
bun add -D zod@^4.1.12
# or: npm install -D zod@^4.1.12
import { z } from 'zod'
schema: z.object({
title: z.string(),
date: z.date(),
published: z.boolean().default(false),
tags: z.array(z.string()).optional(),
category: z.enum(['news', 'tutorial', 'update'])
})
With Valibot (Alternative)
bun add -D valibot@^0.42.0
# or: npm install -D valibot@^0.42.0
import * as v from 'valibot'
schema: v.object({
title: v.string(),
date: v.date(),
published: v.boolean(false),
tags: v.optional(v.array(v.string()))
})
Querying Content
Basic Queries
// Get all posts
const posts = await queryCollection('blog').all()
// Get single post by path
const post = await queryCollection('blog').path('/blog/hello').first()
// Get post by ID
const post = await queryCollection('blog').where('_id', '=', 'hello').first()
Field Selection
// Select specific fields (performance optimization)
const posts = await queryCollection('blog')
.only(['title', 'description', 'date', 'path'])
.all()
// Exclude fields
const posts = await queryCollection('blog')
.without(['body'])
.all()
Where Conditions
// Single condition
const posts = await queryCollection('blog')
.where('published', '=', true)
.all()
// Multiple conditions
const posts = await queryCollection('blog')
.where('published', '=', true)
.where('category', '=', 'tutorial')
.all()
// Operators: =, !=, <, <=, >, >=, in, not-in, like
const recent = await queryCollection('blog')
.where('date', '>', new Date('2024-01-01'))
.all()
const tagged = await queryCollection('blog')
.where('tags', 'in', ['nuxt', 'vue'])
.all()
Ordering and Pagination
// Sort by field
const posts = await queryCollection('blog')
.sort('date', 'DESC')
.all()
// Pagination
const posts = await queryCollection('blog')
.sort('date', 'DESC')
.limit(10)
.offset(0)
.all()
Counting
// Count results
const total = await queryCollection('blog')
.where('published', '=', true)
.count()
#