Nuxt Content 打造靜態部落格:從設定到部署
Nuxt Content 打造靜態部落格:從設定到部署
有了 Nuxt Content,你不需要資料庫、不需要後端 API,只需要 Markdown 檔案和 Nuxt,就能打造一個功能完整、效能優異的靜態部落格。這篇文章帶你從零開始,建立一個包含文章列表、標籤過濾、全文搜尋和 RSS feed 的完整部落格。
初始化專案
npx nuxi@latest init my-blog
cd my-blog
npx nuxi module add content
npm install目錄結構:
my-blog/
├── content/
│ ├── articles/
│ │ ├── hello-world.md
│ │ └── vue-tips.md
│ └── pages/
│ └── about.md
├── components/
├── pages/
│ ├── index.vue
│ ├── blog/
│ │ ├── index.vue
│ │ └── [slug].vue
│ └── tags/
│ └── [tag].vue
└── nuxt.config.ts設定 nuxt.config.ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/content'],
content: {
highlight: {
theme: {
default: 'github-light',
dark: 'github-dark'
},
langs: ['javascript', 'typescript', 'vue', 'python', 'bash', 'json', 'yaml']
},
markdown: {
toc: {
depth: 3,
searchDepth: 3
}
}
}
})撰寫 Markdown 文章
---
title: 我的第一篇文章
description: 這是文章的摘要,會顯示在列表頁
date: 2026-02-13
tags: [vue, nuxt, frontend]
image: /images/article-cover.jpg
draft: false
---
# 正文從這裡開始
這是文章內容,支援完整的 Markdown 語法。
## 程式碼高亮
```javascript
const message = 'Hello, Nuxt Content!'
console.log(message)支援自訂元件(MDC 語法)
::callout{type="info"} 這是一個提示框元件 ::
## 文章列表頁
```vue
<!-- pages/blog/index.vue -->
<template>
<div class="blog-index">
<h1>所有文章</h1>
<!-- 標籤過濾 -->
<div class="tag-filter">
<button
:class="{ active: !selectedTag }"
@click="selectedTag = null"
>
全部
</button>
<button
v-for="tag in allTags"
:key="tag"
:class="{ active: selectedTag === tag }"
@click="selectedTag = tag"
>
{{ tag }}
</button>
</div>
<!-- 搜尋框 -->
<input
v-model="searchQuery"
type="search"
placeholder="搜尋文章..."
class="search-input"
/>
<!-- 文章列表 -->
<div class="articles-grid">
<NuxtLink
v-for="article in filteredArticles"
:key="article._path"
:to="article._path"
class="article-card"
>
<img
v-if="article.image"
:src="article.image"
:alt="article.title"
/>
<div class="article-info">
<div class="tags">
<span v-for="tag in article.tags" :key="tag" class="tag">
{{ tag }}
</span>
</div>
<h2>{{ article.title }}</h2>
<p>{{ article.description }}</p>
<time :datetime="article.date">
{{ formatDate(article.date) }}
</time>
</div>
</NuxtLink>
</div>
</div>
</template>
<script setup lang="ts">
const selectedTag = ref<string | null>(null)
const searchQuery = ref('')
// 取得所有文章(排除草稿)
const { data: articles } = await useAsyncData('articles', () =>
queryContent('/articles')
.where({ draft: { $ne: true } })
.sort({ date: -1 })
.find()
)
// 取得所有標籤
const allTags = computed(() => {
const tagSet = new Set<string>()
articles.value?.forEach(article => {
article.tags?.forEach((tag: string) => tagSet.add(tag))
})
return Array.from(tagSet).sort()
})
// 過濾文章
const filteredArticles = computed(() => {
return articles.value?.filter(article => {
const matchesTag = !selectedTag.value || article.tags?.includes(selectedTag.value)
const matchesSearch = !searchQuery.value ||
article.title.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
article.description?.toLowerCase().includes(searchQuery.value.toLowerCase())
return matchesTag && matchesSearch
}) ?? []
})
function formatDate(dateStr: string) {
return new Date(dateStr).toLocaleDateString('zh-TW', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
// SEO
useSeoMeta({
title: '部落格文章',
description: '分享技術文章與開發心得'
})
</script>文章詳情頁
<!-- pages/blog/[slug].vue -->
<template>
<article class="article">
<!-- 封面圖 -->
<img
v-if="article.image"
:src="article.image"
:alt="article.title"
class="cover-image"
/>
<header class="article-header">
<div class="tags">
<NuxtLink
v-for="tag in article.tags"
:key="tag"
:to="`/tags/${tag}`"
class="tag"
>
{{ tag }}
</NuxtLink>
</div>
<h1>{{ article.title }}</h1>
<time :datetime="article.date">{{ formatDate(article.date) }}</time>
</header>
<!-- 目錄 -->
<nav v-if="article.body?.toc?.links?.length" class="toc">
<h2>目錄</h2>
<ul>
<li v-for="link in article.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>
<!-- 文章內容 -->
<div class="article-body">
<ContentRenderer :value="article" />
</div>
<!-- 上/下一篇 -->
<nav class="article-nav">
<NuxtLink v-if="prev" :to="prev._path" class="prev">
← {{ prev.title }}
</NuxtLink>
<NuxtLink v-if="next" :to="next._path" class="next">
{{ next.title }} →
</NuxtLink>
</nav>
</article>
</template>
<script setup lang="ts">
const route = useRoute()
const { data: article } = await useAsyncData(`article-${route.params.slug}`, () =>
queryContent(`/articles/${route.params.slug}`).findOne()
)
if (!article.value) {
throw createError({ statusCode: 404, message: '文章不存在' })
}
// 取得前後文章
const [{ data: prev }, { data: next }] = await Promise.all([
useAsyncData(`prev-${route.params.slug}`, () =>
queryContent('/articles')
.where({ draft: { $ne: true }, date: { $lt: article.value!.date } })
.sort({ date: -1 })
.limit(1)
.findOne()
),
useAsyncData(`next-${route.params.slug}`, () =>
queryContent('/articles')
.where({ draft: { $ne: true }, date: { $gt: article.value!.date } })
.sort({ date: 1 })
.limit(1)
.findOne()
)
])
// SEO
useSeoMeta({
title: article.value.title,
description: article.value.description,
ogImage: article.value.image
})
</script>生成靜態站台
# 預覽
npm run dev
# 建置靜態檔案
npm run generate
# 預覽靜態建置結果
npm run previewnuxt.config.ts 中設定靜態生成:
export default defineNuxtConfig({
nitro: {
prerender: {
routes: ['/sitemap.xml', '/rss.xml']
}
}
})部署到 Cloudflare Pages
# 建置並部署
npx wrangler pages deploy .output/public或透過 Git 連結 Cloudflare Pages:
- Build command:
npm run generate - Build output directory:
.output/public
總結
Nuxt Content 提供了一個優雅的方式來建立 Markdown 驅動的靜態部落格。它的 queryContent API 直覺好用,搭配 Nuxt 3 的 useAsyncData 和 SSG 功能,你可以打造一個零運行成本、高效能的個人部落格。內容版控在 Git,部署在全球 CDN,這就是現代靜態部落格的最佳形態。
分享這篇文章