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 友好 | 天生支持静态生成 |


