Nuxt 生命週期詳解與 SEO 最佳實踐指南

什麼是 Nuxt?(What is Nuxt?)

Nuxt.js 是一個基於 Vue.js 的全端框架,提供了服務端渲染(SSR)、靜態站點生成(SSG)和單頁應用(SPA)等多種渲染模式。它通過預設的架構和豐富的模組生態系統,讓開發者能夠快速構建現代化的 Web 應用。

Nuxt 的核心優勢(Core Advantages)

  1. 服務端渲染(SSR):提升首屏加載速度和 SEO 表現
  2. 靜態站點生成(SSG):預渲染頁面,極致的性能表現
  3. 自動路由:基於文件系統的路由配置
  4. 模組化架構:豐富的官方和社區模組
  5. 開發體驗:熱重載、TypeScript 支持等

Nuxt 生命週期詳解(Nuxt Lifecycle Deep Dive)

1. 應用初始化階段(Application Initialization)

nuxt.config.ts 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// nuxt.config.ts
export default defineNuxtConfig({
// 應用配置
app: {
head: {
title: 'My Nuxt App',
meta: [
{ name: 'description', content: 'My amazing site.' }
]
}
},

// 模組配置
modules: ['@nuxtjs/tailwindcss'],

// 運行時配置
runtimeConfig: {
apiSecret: process.env.API_SECRET,
public: {
apiBase: process.env.API_BASE
}
}
})

插件執行順序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// plugins/1.client.ts - 僅客戶端
export default defineNuxtPlugin(() => {
console.log('Client-only plugin')
})

// plugins/2.server.ts - 僅服務端
export default defineNuxtPlugin(() => {
console.log('Server-only plugin')
})

// plugins/3.universal.ts - 通用插件
export default defineNuxtPlugin(() => {
console.log('Universal plugin')
})

2. 服務端渲染階段(Server-Side Rendering Phase)

服務端生命週期順序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 服務端插件執行
// plugins/*.server.ts

// 2. 中間件執行(服務端)
// middleware/auth.server.ts
export default defineNuxtRouteMiddleware((to, from) => {
// 服務端認證邏輯
console.log('Server middleware')
})

// 3. 頁面組件服務端生命週期
// pages/index.vue
export default defineComponent({
// 服務端專用
async setup() {
// 在服務端執行
const { data } = await useFetch('/api/posts')
return { posts: data.value }
}
})

服務端數據獲取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 服務端數據獲取最佳實踐
export default defineComponent({
async setup() {
// 1. useFetch - 自動處理 SSR
const { data: posts } = await useFetch('/api/posts')

// 2. useAsyncData - 更細緻的控制
const { data: user } = await useAsyncData('user', () =>
$fetch('/api/user')
)

// 3. $fetch - 直接 API 調用
const config = await $fetch('/api/config')

return { posts, user, config }
}
})

3. 客戶端水合階段(Client Hydration Phase)

客戶端生命週期

1
2
3
4
5
6
7
8
9
10
11
12
// 客戶端專用插件
// plugins/client-only.client.ts
export default defineNuxtPlugin(() => {
// 僅在客戶端執行
console.log('Client hydration started')

// 監聽路由變化
const router = useRouter()
router.beforeEach((to, from) => {
console.log('Route change:', from.path, '->', to.path)
})
})

客戶端中間件

1
2
3
4
5
6
7
8
// middleware/auth.client.ts
export default defineNuxtRouteMiddleware((to, from) => {
// 客戶端認證檢查
const user = useUser()
if (!user.value && to.path !== '/login') {
return navigateTo('/login')
}
})

4. 頁面組件生命週期(Page Component Lifecycle)

Vue 3 Composition API 生命週期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
<div>
<h1>{{ title }}</h1>
<div v-if="posts">{{ posts.length }} posts</div>
</div>
</template>

<script setup>
// 1. setup() - 組件初始化
const title = ref('My Blog')

// 2. onMounted() - 客戶端掛載後
onMounted(() => {
console.log('Component mounted on client')
})

// 3. onBeforeMount() - 掛載前
onBeforeMount(() => {
console.log('Before mount')
})

// 4. onBeforeUnmount() - 卸載前
onBeforeUnmount(() => {
console.log('Before unmount')
})

// 5. onUnmounted() - 卸載後
onUnmounted(() => {
console.log('Component unmounted')
})

// 6. 響應式數據變化
watch(title, (newTitle, oldTitle) => {
console.log('Title changed:', oldTitle, '->', newTitle)
})
</script>

Nuxt 專用生命週期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script setup>
// 1. useHead() - 動態設置 head
useHead({
title: 'Dynamic Page Title',
meta: [
{ name: 'description', content: 'Dynamic description' }
]
})

// 2. useSeoMeta() - SEO 優化
useSeoMeta({
title: 'SEO Optimized Title',
ogTitle: 'Open Graph Title',
description: 'SEO description',
ogDescription: 'Open Graph description',
ogImage: 'https://example.com/image.jpg',
twitterCard: 'summary_large_image',
})

// 3. 路由變化監聽
const route = useRoute()
watch(() => route.path, (newPath) => {
console.log('Route changed to:', newPath)
})
</script>

SEO 最佳實踐(SEO Best Practices)

1. 靜態元數據(Static Meta Data)

全局 SEO 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
title: 'My Website',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ name: 'description', content: 'My amazing website' },
{ name: 'keywords', content: 'vue, nuxt, javascript' },
// Open Graph
{ property: 'og:title', content: 'My Website' },
{ property: 'og:description', content: 'My amazing website' },
{ property: 'og:type', content: 'website' },
{ property: 'og:url', content: 'https://mywebsite.com' },
{ property: 'og:image', content: 'https://mywebsite.com/og-image.jpg' },
// Twitter Card
{ name: 'twitter:card', content: 'summary_large_image' },
{ name: 'twitter:title', content: 'My Website' },
{ name: 'twitter:description', content: 'My amazing website' },
{ name: 'twitter:image', content: 'https://mywebsite.com/twitter-image.jpg' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
{ rel: 'canonical', href: 'https://mywebsite.com' }
]
}
}
})

2. 動態 SEO 優化(Dynamic SEO Optimization)

頁面級別 SEO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
<div>
<h1>{{ post.title }}</h1>
<div v-html="post.content"></div>
</div>
</template>

<script setup>
// 獲取路由參數
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.id}`)

// 動態設置 SEO 元數據
useSeoMeta({
title: () => post.value?.title || 'Post Not Found',
description: () => post.value?.excerpt || 'No description available',
ogTitle: () => post.value?.title || 'Post Not Found',
ogDescription: () => post.value?.excerpt || 'No description available',
ogImage: () => post.value?.featuredImage || '/default-image.jpg',
twitterCard: 'summary_large_image',
twitterTitle: () => post.value?.title || 'Post Not Found',
twitterDescription: () => post.value?.excerpt || 'No description available',
twitterImage: () => post.value?.featuredImage || '/default-image.jpg',
})

// 結構化數據
useJsonld(() => ({
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.value?.title,
description: post.value?.excerpt,
author: {
'@type': 'Person',
name: post.value?.author?.name
},
datePublished: post.value?.publishedAt,
image: post.value?.featuredImage
}))
</script>

3. 確保 SSR 可讀性(Ensuring SSR Readability)

關鍵 SEO 檢查點

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 確保數據在服務端可用
export default defineComponent({
async setup() {
// ✅ 正確:在 setup 中獲取數據
const { data: posts } = await useFetch('/api/posts')

// ❌ 錯誤:在 onMounted 中獲取(客戶端專用)
// onMounted(async () => {
// const posts = await $fetch('/api/posts')
// })

return { posts }
}
})

條件渲染最佳實踐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<!-- ✅ 正確:使用 v-if 確保 SSR 可讀 -->
<div v-if="posts">
<h2>Posts ({{ posts.length }})</h2>
<ul>
<li v-for="post in posts" :key="post.id">
{{ post.title }}
</li>
</ul>
</div>

<!-- ✅ 正確:使用 ClientOnly 包裝客戶端專用組件 -->
<ClientOnly>
<div>This content is client-only</div>
</ClientOnly>
</div>
</template>

<script setup>
// 確保數據在服務端獲取
const { data: posts } = await useFetch('/api/posts')
</script>

4. 性能優化與 SEO(Performance Optimization & SEO)

圖片優化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<!-- ✅ 正確:使用 NuxtImg 自動優化 -->
<NuxtImg
src="/hero-image.jpg"
alt="Hero image"
width="1200"
height="600"
format="webp"
loading="lazy"
/>

<!-- ✅ 正確:使用 NuxtPicture 響應式圖片 -->
<NuxtPicture
src="/responsive-image.jpg"
alt="Responsive image"
:sizes="sm:100vw md:50vw lg:400px"
/>
</div>
</template>

預加載關鍵資源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
link: [
// 預加載關鍵 CSS
{ rel: 'preload', href: '/css/critical.css', as: 'style' },
// 預加載關鍵字體
{ rel: 'preload', href: '/fonts/main.woff2', as: 'font', type: 'font/woff2', crossorigin: '' },
// DNS 預解析
{ rel: 'dns-prefetch', href: 'https://api.example.com' }
]
}
}
})

常見問題與解決方案(Common Issues & Solutions)

1. 水合不匹配(Hydration Mismatch)

問題原因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- ❌ 錯誤:服務端和客戶端渲染結果不同 -->
<template>
<div>
<span v-if="isClient">Client Only</span>
<span v-else>Server Only</span>
</div>
</template>

<script setup>
const isClient = ref(false)
onMounted(() => {
isClient.value = true
})
</script>

解決方案

1
2
3
4
5
6
7
8
9
<!-- ✅ 正確:使用 ClientOnly 組件 -->
<template>
<div>
<span>Always visible</span>
<ClientOnly>
<span>Client Only Content</span>
</ClientOnly>
</div>
</template>

2. 動態路由 SEO 優化

靜態生成最佳實踐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 1. 生成靜態路由
export default defineNuxtConfig({
nitro: {
prerender: {
routes: ['/sitemap.xml', '/robots.txt']
}
}
})

// 2. 動態路由預渲染
// server/api/posts.ts
export default defineEventHandler(async () => {
const posts = await getPosts()
return posts
})

// 3. 在頁面中預渲染所有路由
// pages/posts/[id].vue
export default defineComponent({
async setup() {
const { data: posts } = await useFetch('/api/posts')

// 預渲染所有文章頁面
if (process.server) {
const router = useRouter()
for (const post of posts.value || []) {
await router.push(`/posts/${post.id}`)
}
}
}
})

3. 第三方腳本優化

安全的第三方腳本加載

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup>
// 使用 useScript 安全加載第三方腳本
const { $script } = useScript({
src: 'https://example.com/analytics.js',
defer: true,
strategy: 'lazyOnload'
})

// 或者使用 useHead 動態加載
useHead({
script: [
{
src: 'https://example.com/analytics.js',
defer: true,
'data-cfasync': 'false'
}
]
})
</script>

測試與驗證(Testing & Validation)

1. SEO 測試工具

本地 SEO 檢查

1
2
3
4
5
# 安裝 SEO 測試工具
npm install -g lighthouse

# 運行 Lighthouse 測試
lighthouse http://localhost:3000 --output html --output-path ./lighthouse-report.html

結構化數據測試

1
2
3
4
5
6
7
8
9
10
11
12
// 驗證結構化數據
const validateStructuredData = () => {
const scripts = document.querySelectorAll('script[type="application/ld+json"]')
scripts.forEach(script => {
try {
JSON.parse(script.textContent || '')
console.log('✅ Structured data is valid')
} catch (error) {
console.error('❌ Invalid structured data:', error)
}
})
}

2. 服務端渲染驗證

檢查 SSR 輸出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 驗證關鍵內容在服務端渲染
const validateSSR = () => {
const content = document.body.innerHTML

// 檢查關鍵 SEO 元素
const checks = [
{ selector: 'h1', message: 'H1 tag found' },
{ selector: 'meta[name="description"]', message: 'Meta description found' },
{ selector: 'title', message: 'Title tag found' }
]

checks.forEach(check => {
const element = document.querySelector(check.selector)
if (element) {
console.log(`✅ ${check.message}`)
} else {
console.error(`❌ Missing: ${check.message}`)
}
})
}

總結(Summary)

Nuxt.js 提供了強大的 SSR 能力和豐富的生命週期鉤子,讓開發者能夠構建既高性能又 SEO 友好的現代化 Web 應用。關鍵是要理解不同生命週期的執行時機,並確保關鍵內容在服務端渲染階段就已經準備好,這樣搜索引擎就能正確抓取和索引你的網站內容。

關鍵要點(Key Takeaways)

  1. 服務端優先:確保關鍵數據在服務端獲取
  2. 動態 SEO:使用 useSeoMetauseHead 動態設置元數據
  3. 性能優化:使用 NuxtImg 和預加載關鍵資源
  4. 測試驗證:定期使用 Lighthouse 等工具檢查 SEO 表現
  5. 結構化數據:添加 JSON-LD 結構化數據提升搜索結果展示

通過遵循這些最佳實踐,你的 Nuxt 應用將能夠在搜索引擎中獲得更好的排名和展示效果。