home / skills / fawzymohamed / devops / nuxt-content
This skill helps you work with Nuxt Content in Nuxt 4, enabling seamless querying, rendering, and navigation of markdown content.
npx playbooks add skill fawzymohamed/devops --skill nuxt-contentReview the files below or copy the command above to add this skill to your agents.
---
name: nuxt-content
description: Expert knowledge for @nuxt/content module in Nuxt 4. Activate when working with content directory, markdown files, frontmatter, or ContentRenderer.
---
# Nuxt Content Expertise (Nuxt 4)
## Activation Triggers
- Creating/editing files in `content/` directory
- Working with markdown frontmatter
- Using `queryContent()` composable
- Rendering content with `<ContentRenderer>`
- Building navigation from content
## Nuxt 4 Specifics
Nuxt 4 uses the `app/` directory structure:
```
project/
├── app/
│ ├── components/
│ ├── pages/
│ └── ...
├── content/ # Content stays at root level
│ └── ...
└── nuxt.config.ts
```
## Content Directory Structure
```
content/
├── 1.phase-1-sdlc/
│ ├── _dir.yml # Directory metadata (optional)
│ ├── 1.sdlc-models/
│ │ ├── _dir.yml
│ │ ├── 1.waterfall-model.md
│ │ ├── 2.agile-methodology.md
│ │ └── 3.scrum-framework.md
│ └── 2.sdlc-phases/
│ └── ...
└── 2.phase-2-foundations/
└── ...
```
**Naming Convention**:
- Numeric prefixes (1., 2.) control ordering
- Prefixes are stripped from URLs
- Use kebab-case for slugs
## Frontmatter Schema
```yaml
---
title: "Lesson Title"
description: "Brief description for SEO and previews"
estimatedMinutes: 15
difficulty: beginner | intermediate | advanced
learningObjectives:
- "Objective 1"
- "Objective 2"
quiz:
passingScore: 70
questions:
- question: "Question text"
type: single | multiple | true-false
options: ["A", "B", "C", "D"]
correctAnswer: "A"
explanation: "Why this is correct"
---
# Content starts here
```
## Nuxt Config
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
compatibilityVersion: 4,
modules: ['@nuxt/content', '@nuxt/ui'],
content: {
highlight: {
theme: 'github-dark',
langs: ['bash', 'typescript', 'javascript', 'python', 'yaml', 'dockerfile', 'json', 'sql']
},
markdown: {
toc: {
depth: 3,
searchDepth: 3
}
}
}
})
```
## Querying Content
### Get Single Document
```typescript
const route = useRoute()
// Using path from route
const { data: lesson } = await useAsyncData(
`lesson-${route.path}`,
() => queryContent(route.path).findOne()
)
// Explicit path
const { data: lesson } = await useAsyncData('waterfall', () =>
queryContent('phase-1-sdlc/sdlc-models/waterfall-model').findOne()
)
```
### Get All Documents in Directory
```typescript
const { data: lessons } = await useAsyncData('sdlc-lessons', () =>
queryContent('phase-1-sdlc/sdlc-models')
.where({ _extension: 'md' })
.sort({ _path: 1 })
.find()
)
```
### Get Navigation Tree
```typescript
const { data: navigation } = await useAsyncData('navigation', () =>
fetchContentNavigation()
)
// Or for specific path
const { data: phaseNav } = await useAsyncData('phase-nav', () =>
fetchContentNavigation(queryContent('phase-1-sdlc'))
)
```
### Previous/Next Navigation
```typescript
const { data: surround } = await useAsyncData('surround', () =>
queryContent()
.only(['_path', 'title'])
.sort({ _path: 1 })
.findSurround(route.path)
)
const [prev, next] = surround.value || [null, null]
```
### Query with Filters
```typescript
// By difficulty
const { data: beginnerLessons } = await useAsyncData('beginner', () =>
queryContent()
.where({ difficulty: 'beginner' })
.find()
)
// By field existence
const { data: withQuiz } = await useAsyncData('with-quiz', () =>
queryContent()
.where({ 'quiz': { $exists: true } })
.find()
)
// Count documents
const count = await queryContent('phase-1-sdlc').count()
```
## Rendering Content
### Basic Rendering
```vue
<template>
<div v-if="lesson" class="prose prose-invert">
<ContentRenderer :value="lesson" />
</div>
</template>
```
### With ContentDoc Component
```vue
<template>
<ContentDoc :path="path">
<template #default="{ doc }">
<article>
<h1>{{ doc.title }}</h1>
<div class="prose prose-invert">
<ContentRenderer :value="doc" />
</div>
</article>
</template>
<template #not-found>
<div>Lesson not found</div>
</template>
<template #empty>
<div>No content available</div>
</template>
</ContentDoc>
</template>
```
### Table of Contents
```vue
<template>
<nav v-if="lesson?.body?.toc?.links">
<ul>
<li v-for="link in lesson.body.toc.links" :key="link.id">
<a :href="`#${link.id}`">{{ link.text }}</a>
<ul v-if="link.children">
<li v-for="child in link.children" :key="child.id">
<a :href="`#${child.id}`">{{ child.text }}</a>
</li>
</ul>
</li>
</ul>
</nav>
</template>
```
## Prose Styling
Use Tailwind Typography for content styling:
```vue
<div class="prose prose-invert prose-lg max-w-none">
<ContentRenderer :value="lesson" />
</div>
```
Customize prose in Tailwind config if needed:
```javascript
// tailwind.config.js
module.exports = {
theme: {
extend: {
typography: {
invert: {
css: {
'--tw-prose-body': 'var(--color-gray-300)',
'--tw-prose-headings': 'var(--color-gray-100)',
// ... more customizations
}
}
}
}
}
}
```
## Common Patterns
### Loading State
```vue
<template>
<div v-if="pending">
<USkeleton class="h-8 w-64 mb-4" />
<USkeleton class="h-4 w-full mb-2" />
<USkeleton class="h-4 w-3/4" />
</div>
<div v-else-if="error">
<p>Error loading content</p>
</div>
<div v-else-if="lesson">
<ContentRenderer :value="lesson" />
</div>
</template>
```
### Dynamic Routes
```
app/pages/[phase]/[topic]/[subtopic].vue
```
```typescript
const route = useRoute()
const { phase, topic, subtopic } = route.params as {
phase: string
topic: string
subtopic: string
}
const contentPath = `${phase}/${topic}/${subtopic}`
```
## Key Differences from Nuxt 3
1. App directory is `app/` not root
2. Use `compatibilityVersion: 4` in config
3. Content module works the same way
4. Query syntax unchanged
5. ContentRenderer unchanged
## Gotchas
- Use `_path` not `path` for internal content paths
- Numeric prefixes are stripped from URLs (1.topic becomes /topic)
- Use `_dir.yml` for directory-level metadata
- Always use `useAsyncData` for SSR compatibility
- The `$exists` filter checks if a field exists
This skill provides expert guidance for using the @nuxt/content module in Nuxt 4, focused on content directory layout, markdown frontmatter, querying, and rendering with ContentRenderer. It covers Nuxt 4 specifics like the app/ directory, config options, and common patterns for navigation, TOC, and SSR-safe data loading. Practical tips and examples help build ordered content, dynamic routes, and polished prose styling with Tailwind Typography.
It inspects content stored in the root-level content/ directory, parses markdown files and frontmatter, and exposes a composable API (queryContent, fetchContentNavigation, useAsyncData) to query documents and build trees. ContentRenderer and ContentDoc components render parsed content, body.toc supports table-of-contents links, and directory metadata can be provided via _dir.yml files. Configuration in nuxt.config.ts controls highlighting and markdown toc behavior.
How do numeric prefixes affect URLs?
Numeric prefixes control ordering but are stripped from the generated URLs, so files like 1.topic.md become /topic.
What should I use for SSR-safe content loading?
Use useAsyncData with queryContent or fetchContentNavigation to ensure server-side rendering and caching behave correctly.