home / skills / fawzymohamed / devops / nuxt-content

nuxt-content skill

/.claude/skills/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-content

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
6.4 KB
---
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

Overview

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.

How this skill works

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.

When to use it

  • Building documentation, lessons, or knowledge bases from markdown files
  • Rendering markdown pages with structured frontmatter (title, description, difficulty, quiz)
  • Generating navigation trees and previous/next flows for sequential content
  • Implementing dynamic routes that map to nested content paths
  • Filtering or counting content by frontmatter fields (difficulty, existence of quiz)

Best practices

  • Keep content/ at project root and use numeric prefixes + kebab-case for ordering and slugs
  • Use useAsyncData with queryContent for SSR compatibility and caching
  • Prefer _path for internal queries and sorting, not path
  • Provide descriptive frontmatter (title, description, estimatedMinutes) for SEO and listings
  • Use _dir.yml for directory-level metadata and to control navigation behavior

Example use cases

  • Course site: store lessons in phase/topic directories, query by difficulty and build lesson lists
  • Documentation site: auto-generate sidebar navigation with fetchContentNavigation()
  • Blog with structured posts: use frontmatter for reading time, tags, and SEO previews
  • Interactive lesson flow: find surrounding docs via findSurround() to render prev/next links
  • Content-driven pages: render markdown with ContentRenderer and styled prose via Tailwind Typography

FAQ

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.