Nuxt 生态 精选推荐

Nuxt Content Module 内容管理系统

HTMLPAGE 团队
12 分钟阅读

完整讲解 Nuxt Content v2 的使用方法,包括 Markdown 文件管理、YAML frontmatter、组件插入、查询 API、性能优化等功能。

#Nuxt Content #Markdown #CMS #内容管理 #Frontmatter

Nuxt Content Module 内容管理系统

Nuxt Content 是构建内容驱动网站的完美方案。使用 Markdown 管理内容,享受强大的查询 API。

1. 基础设置

安装和配置

# 安装
npm install @nuxt/content

# 或使用 pnpm
pnpm add @nuxt/content
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/content'],
  
  content: {
    // 内容目录
    sources: {
      content: {
        driver: 'fs',
        base: './content'
      }
    },
    
    // Markdown 配置
    markdown: {
      // 高亮代码
      highlight: {
        theme: 'github-dark',
        preload: ['javascript', 'typescript', 'vue', 'python']
      },
      // 目录生成
      toc: {
        depth: 3
      }
    },
    
    // 数据库驱动 (生产推荐)
    database: {
      type: 'sqlite'
    }
  }
})

文件结构

content/
├── articles/
│   ├── vue-guide.md
│   ├── react-hooks.md
│   └── performance-tips.md
│
├── tutorials/
│   ├── setup.md
│   ├── basics.md
│   └── advanced.md
│
└── about.md

2. Markdown 和 Frontmatter

基础 Frontmatter

---
title: Vue 3 Composition API 完整指南
description: 深度讲解 Vue 3 的 Composition API
author: 张三
date: 2025-12-24
tags:
  - Vue
  - Composition API
  - 进阶
category: Frontend
image: /images/vue-composition.jpg
---

# {{ $doc.title }}

文章内容从这里开始...

高级 Frontmatter

---
# 基础信息
title: React Hooks 深度解析
description: 完整讲解 React Hooks 的使用和最佳实践
author: 李四
date: 2025-12-24

# 分类和标签
category: Frontend
tags:
  - React
  - Hooks
  - JavaScript

# 内容属性
language: zh-CN
published: true
featured: true

# SEO
slug: react-hooks-deep-dive
image: /images/react-hooks.jpg
imageAlt: React Hooks 教程配图

# 自定义字段
difficulty: intermediate  # beginner, intermediate, advanced
readingTime: 12          # 阅读时间 (分钟)
difficulty_score: 7     # 难度评分 1-10

# 相关内容
related:
  - vue-composition-api
  - custom-hooks-patterns

# 更新历史
updatedAt: 2025-12-24
versions:
  - v1.0: 初版发布
  - v1.1: 添加新示例
---

3. 查询内容

基础查询

// 获取所有文章
const { data: articles } = await useAsyncData('articles', () =>
  queryContent('articles').find()
)

// 获取单个文章
const route = useRoute()
const { data: article } = await useAsyncData(`article-${route.params.slug}`, () =>
  queryContent('articles').where({ _path: `/articles/${route.params.slug}` }).findOne()
)

// 获取特定目录
const { data: tutorials } = await useAsyncData('tutorials', () =>
  queryContent('tutorials').find()
)

高级查询

// 1. 按日期排序
const { data: articles } = await useAsyncData('articles', () =>
  queryContent('articles')
    .where({ published: true })
    .sort({ date: -1 })  // -1 倒序,1 正序
    .find()
)

// 2. 分页
const page = ref(1)
const pageSize = 10

const { data: articles } = await useAsyncData(`articles-page-${page.value}`, () =>
  queryContent('articles')
    .skip((page.value - 1) * pageSize)
    .limit(pageSize)
    .find()
)

// 3. 按标签过滤
const { data: articles } = await useAsyncData('vue-articles', () =>
  queryContent('articles')
    .where({ tags: { $contains: 'Vue' } })
    .find()
)

// 4. 按难度过滤
const { data: beginnerGuides } = await useAsyncData('beginner', () =>
  queryContent('tutorials')
    .where({ difficulty: 'beginner' })
    .find()
)

// 5. 全文搜索
const searchQuery = ref('')

const { data: results } = await useAsyncData(`search-${searchQuery.value}`, () =>
  queryContent()
    .where({
      $or: [
        { title: { $icontains: searchQuery.value } },
        { description: { $icontains: searchQuery.value } },
        { body: { $icontains: searchQuery.value } }
      ]
    })
    .find()
)

// 6. 条件组合
const { data: articles } = await useAsyncData('featured', () =>
  queryContent('articles')
    .where({
      $and: [
        { published: true },
        { featured: true },
        { date: { $gte: new Date('2025-01-01') } }
      ]
    })
    .sort({ date: -1 })
    .limit(5)
    .find()
)

4. 在页面中显示内容

单篇文章页面

<!-- pages/articles/[slug].vue -->
<script setup lang="ts">
const route = useRoute()

const { data: article } = await useAsyncData(`article-${route.params.slug}`, () =>
  queryContent('articles')
    .where({ _path: `/articles/${route.params.slug}` })
    .findOne()
)

// 生成目录
const toc = computed(() => article.value?.body?.toc || [])

// 获取相关文章
const { data: relatedArticles } = await useAsyncData(`related-${route.params.slug}`, () =>
  queryContent('articles')
    .where({
      $and: [
        { tags: { $intersects: article.value?.tags || [] } },
        { _path: { $ne: article.value?._path } }
      ]
    })
    .limit(3)
    .find()
)
</script>

<template>
  <article v-if="article" class="max-w-4xl mx-auto">
    <!-- 元信息 -->
    <header class="mb-8">
      <h1 class="text-4xl font-bold mb-4">{{ article.title }}</h1>
      <p class="text-gray-600 mb-4">{{ article.description }}</p>
      
      <div class="flex items-center gap-4 text-sm text-gray-500">
        <span>作者: {{ article.author }}</span>
        <span>日期: {{ new Date(article.date).toLocaleDateString() }}</span>
        <span>阅读时间: {{ article.readingTime || 5 }} 分钟</span>
      </div>
      
      <!-- 标签 -->
      <div v-if="article.tags" class="mt-4 flex gap-2">
        <NuxtLink
          v-for="tag in article.tags"
          :key="tag"
          :to="`/tags/${tag}`"
          class="inline-block px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm"
        >
          {{ tag }}
        </NuxtLink>
      </div>
    </header>
    
    <!-- 内容 -->
    <main class="prose prose-lg max-w-none mb-8">
      <ContentRenderer :value="article" />
    </main>
    
    <!-- 相关文章 -->
    <aside v-if="relatedArticles?.length" class="border-t pt-8">
      <h3 class="text-2xl font-bold mb-4">相关阅读</h3>
      <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
        <article v-for="item in relatedArticles" :key="item._id" class="border rounded-lg p-4">
          <h4 class="font-bold mb-2">
            <NuxtLink :to="item._path" class="text-blue-600 hover:underline">
              {{ item.title }}
            </NuxtLink>
          </h4>
          <p class="text-sm text-gray-600">{{ item.description }}</p>
        </article>
      </div>
    </aside>
  </article>
  
  <div v-else class="text-center py-12">
    <p class="text-gray-500">文章未找到</p>
  </div>
</template>

列表页面

<!-- pages/articles/index.vue -->
<script setup lang="ts">
const selectedTag = ref<string | null>(null)
const page = ref(1)
const pageSize = 12

// 获取所有文章
const query = queryContent('articles')
  .where({ published: true })
  .sort({ date: -1 })

// 按标签过滤
if (selectedTag.value) {
  query.where({ tags: { $contains: selectedTag.value } })
}

const { data: articles } = await useAsyncData(
  `articles-${page.value}-${selectedTag.value}`,
  () => query.skip((page.value - 1) * pageSize).limit(pageSize).find()
)

// 获取所有可用标签
const { data: allArticles } = await useAsyncData('all-articles', () =>
  queryContent('articles').find()
)

const allTags = computed(() => {
  const tags = new Set<string>()
  allArticles.value?.forEach(article => {
    article.tags?.forEach(tag => tags.add(tag))
  })
  return Array.from(tags).sort()
})
</script>

<template>
  <div class="max-w-6xl mx-auto">
    <h1 class="text-4xl font-bold mb-8">文章列表</h1>
    
    <!-- 标签过滤 -->
    <div class="mb-8">
      <button
        @click="selectedTag = null"
        :class="selectedTag === null ? 'bg-blue-600 text-white' : 'bg-gray-200'"
        class="px-4 py-2 rounded mr-2"
      >
        全部
      </button>
      <button
        v-for="tag in allTags"
        :key="tag"
        @click="selectedTag = tag"
        :class="selectedTag === tag ? 'bg-blue-600 text-white' : 'bg-gray-200'"
        class="px-4 py-2 rounded mr-2"
      >
        {{ tag }}
      </button>
    </div>
    
    <!-- 文章网格 -->
    <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
      <article v-for="article in articles" :key="article._id" class="border rounded-lg overflow-hidden hover:shadow-lg transition">
        <img v-if="article.image" :src="article.image" :alt="article.imageAlt" class="w-full h-48 object-cover">
        
        <div class="p-4">
          <h2 class="text-xl font-bold mb-2">
            <NuxtLink :to="article._path" class="text-blue-600 hover:underline">
              {{ article.title }}
            </NuxtLink>
          </h2>
          
          <p class="text-gray-600 text-sm mb-4">{{ article.description }}</p>
          
          <div class="flex justify-between items-center text-xs text-gray-500">
            <span>{{ new Date(article.date).toLocaleDateString() }}</span>
            <span>{{ article.readingTime || 5 }} 分钟阅读</span>
          </div>
        </div>
      </article>
    </div>
    
    <!-- 分页 -->
    <div class="flex justify-center gap-2">
      <button
        :disabled="page === 1"
        @click="page--"
        class="px-4 py-2 border rounded disabled:opacity-50"
      >
        上一页
      </button>
      <span class="px-4 py-2">第 {{ page }} 页</span>
      <button
        @click="page++"
        class="px-4 py-2 border rounded"
      >
        下一页
      </button>
    </div>
  </div>
</template>

5. 在 Markdown 中使用 Vue 组件

直接插入组件

---
title: 交互式示例
---

# {{ $doc.title }}

这是一个段落。

<Alert type="warning">
这是一个告警框组件!
</Alert>

另一个段落。

<CodeBlock lang="vue" :code="code" />

创建可复用组件

<!-- components/content/Alert.vue -->
<template>
  <div :class="`alert alert-${type}`">
    <slot />
  </div>
</template>

<script setup lang="ts">
defineProps<{
  type: 'info' | 'warning' | 'error' | 'success'
}>()
</script>

<style scoped>
.alert {
  padding: 1rem;
  border-radius: 0.5rem;
  margin: 1rem 0;
}

.alert-info {
  background-color: #dbeafe;
  color: #1e40af;
}

.alert-warning {
  background-color: #fef3c7;
  color: #92400e;
}

.alert-error {
  background-color: #fee2e2;
  color: #991b1b;
}

.alert-success {
  background-color: #dcfce7;
  color: #166534;
}
</style>
# 示例文章

<Alert type="info">
这是一个信息提示框
</Alert>

<Alert type="warning">
这是一个警告提示框
</Alert>

6. 性能优化

预生成静态内容

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // 预生成所有文章页面
    '/articles/**': { prerender: true },
    '/tags/**': { prerender: true },
    
    // 缓存首页 1 天
    '/': { cache: { maxAge: 60 * 60 * 24 } }
  }
})

增量静态再生成 (ISR)

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/articles/**': {
      swr: 3600  // 后台每小时重新生成
    }
  }
})

7. 搜索功能

客户端搜索

<script setup lang="ts">
const { data: articles } = await useAsyncData('all-articles', () =>
  queryContent('articles').find()
)

const searchQuery = ref('')

const searchResults = computed(() => {
  if (!searchQuery.value) return []
  
  const query = searchQuery.value.toLowerCase()
  return articles.value?.filter(article => {
    return (
      article.title?.toLowerCase().includes(query) ||
      article.description?.toLowerCase().includes(query) ||
      article.tags?.some(tag => tag.toLowerCase().includes(query))
    )
  }) || []
})
</script>

<template>
  <div>
    <input
      v-model="searchQuery"
      type="text"
      placeholder="搜索文章..."
      class="w-full px-4 py-2 border rounded"
    />
    
    <div class="mt-4">
      <div v-if="searchResults.length === 0" class="text-gray-500">
        没有找到结果
      </div>
      
      <div v-else>
        <NuxtLink
          v-for="article in searchResults"
          :key="article._id"
          :to="article._path"
          class="block p-4 border-b hover:bg-gray-50"
        >
          <h3 class="font-bold">{{ article.title }}</h3>
          <p class="text-sm text-gray-600">{{ article.description }}</p>
        </NuxtLink>
      </div>
    </div>
  </div>
</template>

8. 最佳实践

// ✅ 最佳实践清单

// 1. 使用 Frontmatter 元数据
// ✅ 好
---
title: 标题
date: 2025-12-24
published: true
tags: [tag1, tag2]
---

// 2. 合理的文件组织
// ✅ 好
content/
  ├── blog/
  ├── docs/
  └── pages/

// 3. 缓存和预渲染
// ✅ 好
routeRules: {
  '/articles/**': { prerender: true }
}

// 4. 优化查询
// ✅ 好: 只查询需要的字段
queryContent().where({ published: true })

// ❌ 避免: 查询所有内容再过滤
queryContent().find()

总结

Nuxt Content 的核心优势:

特性优势
Markdown 驱动简单、版本控制友好
强大查询 API灵活的内容过滤
Vue 集成在 Markdown 中使用组件
性能优化预渲染、缓存策略
SEO 友好天生支持静态生成

相关资源