Vue 基础体系 · 第 68/70 篇。示例基于 Vue 3、Composition API、TypeScript 与现代 Vite 工具链;版本敏感能力会单独标注。
Vue 与 Nuxt SEO:Meta、结构化数据、Canonical、站点地图和渲染
搜索引擎优化(Search Engine Optimization,SEO)在 Vue 应用中并不是“给页面加几个 <meta> 标签”这么简单。搜索引擎需要先发现 URL,再获取页面内容,判断页面是否可抓取、是否适合建立索引,最后才可能在相关查询中展示页面。
一个页面能否被正确理解,通常同时依赖以下几层:
- Meta 信息:页面标题、摘要、robots 指令、社交分享信息等。
- 结构化数据:使用 JSON-LD 等格式,明确告诉搜索引擎页面代表什么实体。
- Canonical:在多个 URL 指向相同或近似内容时,声明首选 URL。
- 站点地图:向搜索引擎提供可发现的 URL 集合及其更新时间等信息。
- 渲染方式:决定搜索引擎首次获取 HTML 时能看到多少真实内容。
这些机制解决的问题不同,不能互相替代:
title不能替代结构化数据;- 结构化数据不能替代正文内容;
- canonical 不能保证搜索引擎一定选择该 URL;
- sitemap 不能保证 URL 一定被索引;
- SSR 能让内容出现在首个 HTML 中,但不能修复错误的 canonical 或无效的 JSON-LD。
下文使用 Vue 3、Composition API、TypeScript 和 Nuxt 3 的常见写法说明。Nuxt 的部分 API 会随版本变化,实际项目应以当前 Nuxt 文档和所安装版本的类型定义为准。
一、先建立 SEO 的因果模型
设一个 URL 为 。搜索引擎最终是否可能展示它,可以抽象成几个条件:
其中:
Discoverable:搜索引擎能否发现这个 URL;Crawlable:抓取时是否被 robots.txt、网络错误或访问控制阻断;Indexable:页面是否允许建立索引,例如没有noindex;Useful:页面是否有足够、独立且符合查询意图的内容。
这不是搜索引擎公开的精确算法,而是用于工程分析的因果模型。几个技术机制分别影响不同变量:
| 机制 | 主要影响 |
|---|---|
| 站点地图、内部链接 | Discoverable |
| SSR、预渲染、客户端渲染 | 抓取时能否获得页面内容 |
robots.txt、HTTP 状态码、noindex |
Crawlable、Indexable |
| canonical | 多个候选 URL 的归一化与重复内容处理 |
| 正文、标题、结构化数据 | 页面主题理解与展示资格 |
因此,不能通过“增加 SEO 标签”直接推导出“排名提高”。SEO 标签只是机器读取页面信号的一部分。
二、Vue 与 Nuxt 中的 Meta 是什么
2.1 Meta 包含哪些信息
HTML 的 <head> 中可以放置影响页面识别和抓取的元素:
<title>Vue SSR 教程</title>
<meta
name="description"
content="介绍如何使用 Nuxt 为 Vue 应用生成可抓取的 HTML。"
/>
<meta name="robots" content="index,follow" />
<link rel="canonical" href="https://example.com/guides/vue-ssr" />
常见字段的职责不同:
<title>:页面标题,是搜索结果标题的重要候选来源,也是浏览器标签页标题。description:页面摘要候选来源,不是严格意义上的排名权重开关。robots:向爬虫声明是否允许索引、是否跟踪链接等。canonical:声明当前内容的首选 URL。- Open Graph 和 Twitter Card:主要服务于社交平台分享,不等同于搜索排名信号。
例如:
<meta property="og:title" content="Vue SSR 教程" />
<meta property="og:description" content="介绍 Nuxt SSR 的工作方式。" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://example.com/guides/vue-ssr" />
<meta property="og:image" content="https://example.com/images/vue-ssr.png" />
og:url 和 canonical 经常使用相同 URL,但它们属于不同协议体系:
- canonical 面向搜索引擎的重复内容归一化;
og:url面向社交平台分享卡片的对象标识。
2.2 Nuxt 中使用 useSeoMeta
Nuxt 提供了 useSeoMeta,用于以类型安全的方式声明常见 SEO Meta:
<script setup lang="ts">
useSeoMeta({
title: 'Vue SSR 教程',
description: '介绍如何使用 Nuxt 为 Vue 应用生成可抓取的 HTML。',
robots: 'index,follow',
ogTitle: 'Vue SSR 教程',
ogDescription: '介绍如何使用 Nuxt 为 Vue 应用生成可抓取的 HTML。',
ogType: 'article',
twitterCard: 'summary_large_image'
})
</script>
<template>
<main>
<h1>Vue SSR 教程</h1>
<p>这里是页面正文。</p>
</main>
</template>
Nuxt 会将这些声明交给 Unhead 管理,并在服务端渲染和客户端导航时同步更新 <head>。
这个过程不是把代码执行结果“永久写死”在模板中,而是一个响应式 head 状态:
- 组件执行
useSeoMeta; - Nuxt/Unhead 收集当前页面的 head 声明;
- SSR 阶段将结果写入响应 HTML;
- 客户端路由切换后,重新计算并更新文档的
<head>; - 组件卸载时,属于该组件的 head 条目被移除或恢复。
2.3 useHead 用于更通用的 head 元素
当需要声明 link、script 或不在 useSeoMeta 便捷字段中的元素时,使用 useHead:
<script setup lang="ts">
useHead({
link: [
{
rel: 'canonical',
href: 'https://example.com/guides/vue-ssr'
}
],
meta: [
{
name: 'author',
content: 'WR BLOG'
}
]
})
</script>
在同一个页面中,多个组件都可能声明 head。最终结果由 Unhead 合并。实际项目应避免不同层级组件为同一个页面字段声明互相冲突的 title、description 或 canonical。
三、静态 Meta 与动态 Meta 的生命周期
3.1 静态页面
静态页面可以直接在组件顶层声明:
<script setup lang="ts">
useSeoMeta({
title: '关于我们',
description: '了解团队背景、技术方向和联系方式。'
})
</script>
在 script setup 中,顶层代码会在组件实例建立时执行。对于 SSR,组件会在服务端执行一次;对于客户端首次加载和后续路由切换,还会在浏览器中建立对应实例。
3.2 动态页面
动态页面必须在数据获取完成后再生成 Meta。下面是一个完整的 Nuxt 页面示例:
<!-- pages/articles/[slug].vue -->
<script setup lang="ts">
interface Article {
slug: string
title: string
excerpt: string
content: string
publishedAt: string
updatedAt?: string
coverUrl?: string
}
const route = useRoute()
const config = useRuntimeConfig()
const requestUrl = useRequestURL()
const { data: article, error } = await useFetch<Article>(
`/api/articles/${encodeURIComponent(String(route.params.slug))}`,
{
key: `article:${String(route.params.slug)}`
}
)
if (error.value || !article.value) {
throw createError({
statusCode: 404,
statusMessage: 'Article Not Found'
})
}
const currentArticle = article.value
const canonicalUrl = new URL(
`/articles/${encodeURIComponent(currentArticle.slug)}`,
config.public.siteUrl || requestUrl.origin
).toString()
useSeoMeta({
title: currentArticle.title,
description: currentArticle.excerpt,
ogTitle: currentArticle.title,
ogDescription: currentArticle.excerpt,
ogType: 'article',
ogUrl: canonicalUrl,
ogImage: currentArticle.coverUrl,
articlePublishedTime: currentArticle.publishedAt,
articleModifiedTime: currentArticle.updatedAt,
robots: 'index,follow'
})
useHead({
link: [
{
rel: 'canonical',
href: canonicalUrl
}
]
})
</script>
<template>
<main>
<article>
<h1>{{ currentArticle.title }}</h1>
<p>{{ currentArticle.excerpt }}</p>
<div>{{ currentArticle.content }}</div>
</article>
</main>
</template>
这个例子中的执行顺序很重要:
- 读取路由参数
slug; - 服务端调用
useFetch获取文章; - 请求失败或资源不存在时抛出 404;
- 只有获取到文章后,才计算标题、摘要和 canonical;
- Nuxt 将这些信息写入 SSR 返回的 HTML。
如果把 useSeoMeta 放在异步数据之前,并使用 article.value?.title,服务端可能首先生成空标题,或者客户端更新后才出现正确标题。这会造成首个 HTML 和客户端状态不一致,甚至导致分享抓取器看到错误信息。
3.3 useServerSeoMeta
对于只需要服务端输出、不需要在客户端响应式更新的 Meta,可以使用 useServerSeoMeta。它适合固定或由服务端数据决定的 SEO 信息:
<script setup lang="ts">
useServerSeoMeta({
robots: 'index,follow'
})
</script>
它的意图是减少客户端不必要的 head 更新。不能把它理解成“比 useSeoMeta 更容易被搜索引擎识别”;搜索引擎最终看到的是输出的 HTML,而不是 API 名称。
四、页面标题、描述和 robots 的边界
4.1 title 不是正文的替代品
页面标题应描述页面的主要主题:
Vue 与 Nuxt SEO:Meta、结构化数据、Canonical、站点地图和渲染
下面的标题虽然包含关键词,但语义质量较差:
Vue SEO Nuxt SEO Meta SEO Canonical SEO 教程 最佳实践
标题必须和正文一致。如果标题声称页面介绍 Nuxt SSR,而正文只有一段“欢迎访问”,即使技术上存在 <title>,页面仍缺乏可用内容。
4.2 description 不是强制摘要
description 是摘要建议,不是命令。搜索引擎可能根据查询词从正文中生成另一段摘要。因此:
<meta name="description" content="..." />
不能推导出搜索结果一定显示该内容。
4.3 robots 的语义与风险
常见值:
<meta name="robots" content="noindex,follow" />
表示不希望该页面进入索引,但允许继续跟踪其中的链接。生产环境中最危险的错误之一,是把测试环境配置带到线上:
<meta name="robots" content="noindex,nofollow" />
如果页面在 SSR 中正确输出这段标签,搜索引擎可能遵循它。诊断时应同时检查:
curl -s https://example.com/articles/vue-ssr | grep -iE 'title|description|robots|canonical'
预期应能在原始 HTML 中看到正确标题、描述和 canonical,而不是只能在浏览器开发者工具中看到客户端执行后的结果。
五、结构化数据:让页面的实体关系可机器识别
5.1 什么是结构化数据
结构化数据是按照约定格式描述页面实体的数据。搜索引擎看到普通正文时,需要自行推断:
这是一篇文章吗?
作者是谁?
发布时间是什么?
这是产品、组织还是事件?
结构化数据则可以明确表达:
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Vue SSR 教程",
"datePublished": "2024-01-01T00:00:00Z",
"author": {
"@type": "Person",
"name": "Alice"
}
}
这里的 @type 决定实体类型,字段含义由 Schema.org 词汇定义。结构化数据不是给用户展示的正文,而是页面语义的机器可读表示。
JSON-LD 通常放在:
<script type="application/ld+json">
{ ... }
</script>
JSON-LD 相比把属性散落到 HTML 元素上的方式,更容易在 Nuxt 的页面逻辑中集中生成。
5.2 文章页面的 JSON-LD
下面将结构化数据加入前面的文章页面:
<script setup lang="ts">
interface Article {
slug: string
title: string
excerpt: string
content: string
publishedAt: string
updatedAt?: string
authorName: string
coverUrl?: string
}
const route = useRoute()
const config = useRuntimeConfig()
const requestUrl = useRequestURL()
const { data: article, error } = await useFetch<Article>(
`/api/articles/${encodeURIComponent(String(route.params.slug))}`
)
if (error.value || !article.value) {
throw createError({
statusCode: 404,
statusMessage: 'Article Not Found'
})
}
const item = article.value
const siteUrl = config.public.siteUrl || requestUrl.origin
const canonicalUrl = new URL(
`/articles/${encodeURIComponent(item.slug)}`,
siteUrl
).toString()
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
'@id': `${canonicalUrl}#article`,
headline: item.title,
description: item.excerpt,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': canonicalUrl
},
datePublished: item.publishedAt,
...(item.updatedAt ? { dateModified: item.updatedAt } : {}),
author: {
'@type': 'Person',
name: item.authorName
},
...(item.coverUrl ? { image: [item.coverUrl] } : {})
}
useSeoMeta({
title: item.title,
description: item.excerpt,
ogTitle: item.title,
ogDescription: item.excerpt,
ogType: 'article',
ogUrl: canonicalUrl
})
useHead({
link: [
{
rel: 'canonical',
href: canonicalUrl
}
],
script: [
{
type: 'application/ld+json',
children: JSON.stringify(jsonLd)
}
]
})
</script>
数据流为:
文章 API
↓
useFetch 获取 Article
↓
生成 canonicalUrl
├─→ Meta
├─→ canonical link
└─→ Article JSON-LD
如果 API 返回的 datePublished 不是合法 ISO 8601 日期,或者 coverUrl 是不可访问的相对路径,结构化数据即使语法是合法 JSON,也可能语义无效。
5.3 JSON-LD 的安全边界
结构化数据通常来自 CMS 或数据库,因此不能假设字段永远安全。直接把未经处理的字符串拼进原始 <script> 可能引发脚本上下文逃逸,例如内容包含:
</script><script>...</script>
使用 Nuxt/Unhead 的 children: JSON.stringify(jsonLd) 比手工拼接 HTML 安全得多,但仍应:
- 对 CMS 输入做长度和格式校验;
- 对 URL 使用 URL 解析器验证;
- 对日期做严格校验;
- 不把用户输入当作 HTML 片段注入;
- 如果自行生成 HTML,额外转义
<、>、&等字符。
结构化数据必须描述页面上真实可见且有依据的信息。页面没有作者,却在 JSON-LD 中伪造作者;正文不是产品,却标成 Product;这些做法可能导致验证失败或失去搜索展示资格。
5.4 结构化数据不能制造搜索结果增强展示
JSON-LD 只是提供“有机会被理解和使用”的信号,不保证一定出现富结果。完整条件至少包括:
前三项由站点较大程度控制,最后一项不由页面代码决定。因此应使用搜索引擎提供的结构化数据测试工具验证语法和字段,同时检查实体是否与页面正文一致。
六、Canonical:重复 URL 的首选地址
6.1 Canonical 的定义
Canonical 是:
<link
rel="canonical"
href="https://example.com/articles/vue-ssr"
/>
它表示当前页面内容的首选 URL。假设以下地址返回相同文章:
/articles/vue-ssr
/articles/vue-ssr?utm_source=newsletter
/articles/vue-ssr?ref=home
可以把它们归一到:
https://example.com/articles/vue-ssr
Canonical 解决的是“多个 URL 对应哪些主要内容”的声明问题,而不是访问控制问题。
6.2 Canonical 不是重定向
两者行为完全不同:
| 机制 | 浏览器地址栏 | 页面是否仍被访问 | 主要用途 |
|---|---|---|---|
| 301/308 重定向 | 改变 | 原 URL 不继续展示 | URL 迁移、强制归一 |
| canonical | 不改变 | 当前页面正常返回 | 声明首选索引 URL |
例如,给 /articles/vue-ssr?ref=home 添加 canonical,并不会让浏览器跳转到无参数 URL。若业务要求所有带追踪参数的请求都重定向,需要在服务端实现重定向;canonical 不能承担这个功能。
6.3 Canonical 生成规则
一个稳健的 canonical 生成过程可以写成:
- 取站点的可信公开 origin;
- 取当前页面的 pathname;
- 删除不影响内容身份的查询参数;
- 根据站点规则统一尾斜杠;
- 生成绝对 HTTPS URL;
- 不把 fragment 放入 canonical。
示例:
function buildCanonicalUrl(
siteUrl: string,
pathname: string,
ignoredQueryKeys: string[] = []
): string {
const url = new URL(pathname, siteUrl)
for (const key of ignoredQueryKeys) {
url.searchParams.delete(key)
}
url.hash = ''
return url.toString()
}
const canonicalUrl = buildCanonicalUrl(
'https://example.com',
'/articles/vue-ssr?utm_source=mail&ref=home',
['utm_source', 'ref']
)
// https://example.com/articles/vue-ssr
注意不要无条件删除所有查询参数。以下页面可能由查询参数决定内容:
/products?page=2
/search?q=vue
/calendar?month=2025-03
如果 ?page=2 是独立可访问页面,错误地把所有分页页面 canonical 到第一页,会向搜索引擎发送错误信号。
6.4 Nuxt 中从请求 URL 生成 canonical 的风险
useRequestURL() 可以获得当前请求 URL,但生产环境必须考虑反向代理:
const requestUrl = useRequestURL()
console.log(requestUrl.origin)
如果应用直接信任客户端传入的 Host 或代理头,攻击者可能构造错误的 canonical 域名。更可靠的做法是通过公开运行时配置指定站点地址:
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
siteUrl: process.env.NUXT_PUBLIC_SITE_URL || 'http://localhost:3000'
}
}
})
生产环境:
NUXT_PUBLIC_SITE_URL=https://example.com
然后:
const config = useRuntimeConfig()
const canonicalUrl = new URL(
`/articles/${article.slug}`,
config.public.siteUrl
).toString()
这也避免了本地开发地址、内部容器域名或错误代理协议进入页面 HTML。
6.5 常见 canonical 错误
错误一:所有页面都指向首页
<link rel="canonical" href="https://example.com/" />
如果每篇文章都这样声明,搜索引擎会认为文章页面的首选 URL 是首页。正确做法是每个内容页面指向自身的规范 URL,或者指向真正的内容主版本。
错误二:canonical 指向不可访问地址
如果 canonical 返回 404、需要登录或被 noindex,该信号与页面内容互相冲突。
错误三:HTTP 与 HTTPS 不一致
站点已经强制 HTTPS,却生成:
<link rel="canonical" href="http://example.com/articles/vue-ssr" />
应统一使用公开访问的 HTTPS URL。
错误四:语言版本互相 canonical
如果 /zh/articles/vue-ssr 和 /en/articles/vue-ssr 是不同语言内容,它们不应简单地互相 canonical。通常每种语言页面使用自己的 self-canonical,并根据站点的国际化策略补充 hreflang。
七、站点地图:可发现 URL 的机器清单
7.1 站点地图的作用
站点地图通常是 XML 文件:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com/articles/vue-ssr</loc>
<lastmod>2025-01-10T12:00:00Z</lastmod>
</url>
</urlset>
它向爬虫提供一组 URL。站点地图适合:
- 新站点或内部链接较少的站点;
- 内容量较大的站点;
- 文章、产品等动态数据页面;
- 需要向爬虫提示更新时间的站点。
站点地图不是 URL 白名单,也不是索引保证。放进 sitemap 的 URL 仍然可能因为 404、noindex、低质量内容或重复内容而不被索引。
站点地图中的 URL 还应满足:
这是工程上的一致性约束:站点地图应列出公开、成功返回、允许索引并且自身为规范版本的 URL。
7.2 Nuxt 中生成动态 sitemap
Nuxt 使用 Nitro 服务器目录提供服务端路由。下面的示例生成一个动态 sitemap.xml:
// server/routes/sitemap.xml.ts
interface SitemapArticle {
slug: string
updatedAt?: string
}
function escapeXml(value: string): string {
return value
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''')
}
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig(event)
const siteUrl = String(config.public.siteUrl).replace(/\/+$/, '')
const articles = await $fetch<SitemapArticle[]>(
`${config.contentApiBase}/articles/sitemap`
)
const urls = articles
.map((article) => {
const loc = new URL(
`/articles/${encodeURIComponent(article.slug)}`,
`${siteUrl}/`
).toString()
const lastmod = article.updatedAt
? `<lastmod>${escapeXml(article.updatedAt)}</lastmod>`
: ''
return [
' <url>',
` <loc>${escapeXml(loc)}</loc>`,
` ${lastmod}`,
' </url>'
].join('\n')
})
.join('\n')
const xml = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
urls,
'</urlset>'
].join('\n')
setHeader(event, 'Content-Type', 'application/xml; charset=UTF-8')
return xml
})
同时配置运行时参数:
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
contentApiBase: process.env.CONTENT_API_BASE || 'http://localhost:8080',
public: {
siteUrl: process.env.NUXT_PUBLIC_SITE_URL || 'http://localhost:3000'
}
}
})
请求:
curl -i http://localhost:3000/sitemap.xml
预期结果包括:
HTTP/1.1 200 OK
content-type: application/xml; charset=UTF-8
以及 XML 中的绝对 URL。
这里的 XML 转义不是装饰性代码。若 slug 或其他数据包含 &:
/articles/vue?mode=ssr&lang=zh
直接拼接会使 XML 结构失效;& 才是合法 XML 表示。
7.3 站点地图的数据一致性
站点地图的数据源应和页面数据源保持一致。典型故障路径如下:
CMS 删除文章
↓
文章页面返回 404
↓
sitemap 缓存仍保留旧 URL
↓
爬虫反复抓取失效地址
解决方式不是在页面返回 404 时继续输出旧内容,而是让 sitemap 查询使用同一发布状态,并设置合理缓存策略。若需要缓存,必须明确:
- sitemap 的刷新周期;
- CMS 发布后多久可见;
- 旧 URL 删除后的失效时间;
- API 故障时是返回旧 sitemap 还是 5xx。
对大型站点,应拆分多个 sitemap 并生成 sitemap index;具体 URL 数量限制和文件大小应遵循搜索引擎与 sitemap 协议的当前要求,而不是在应用中硬编码一个未经验证的数字。
7.4 robots.txt 与 sitemap 的关系
可以在 server/routes/robots.txt.ts 中输出:
// server/routes/robots.txt.ts
export default defineEventHandler((event) => {
const config = useRuntimeConfig(event)
const siteUrl = String(config.public.siteUrl).replace(/\/+$/, '')
setHeader(event, 'Content-Type', 'text/plain; charset=UTF-8')
return [
'User-agent: *',
'Allow: /',
`Sitemap: ${siteUrl}/sitemap.xml`,
''
].join('\n')
})
robots.txt 主要表达抓取规则,Sitemap 行告诉爬虫站点地图位置。它不是访问控制系统,也不能保护敏感数据。需要保护的数据应通过认证、授权和网络控制实现。
八、渲染方式决定首个 HTML 中有什么
8.1 CSR:客户端渲染
CSR(Client-Side Rendering)通常返回一个很小的 HTML 壳:
<div id="app"></div>
<script type="module" src="/assets/app.js"></script>
浏览器下载 JavaScript 后,Vue 才请求数据并渲染正文。
CSR 的问题不是“搜索引擎绝对不能执行 JavaScript”,而是:
- 爬虫需要额外执行 JavaScript;
- 数据请求和脚本执行增加抓取延迟;
- 执行失败时正文可能为空;
- 页面初始 HTML 中没有动态 title、正文和 JSON-LD;
- 社交分享爬虫通常不会像完整浏览器一样执行应用。
8.2 SSR:服务端渲染
SSR(Server-Side Rendering)在每次请求时由服务端生成当前页面 HTML:
HTTP 请求
↓
Nuxt 服务端路由
↓
获取页面数据
↓
执行 Vue 组件
↓
生成正文和 head
↓
返回完整 HTML
↓
浏览器 hydration
其中 hydration(激活)是客户端 Vue 接管服务端 HTML 的过程。SSR 并不意味着页面之后不需要 JavaScript;它只是让首次响应已经包含可见内容。
Nuxt 的 SSR 页面应确保:
- 服务端能访问页面所需数据;
- API 失败时返回正确 HTTP 状态码;
- title、description、canonical 在服务端就能确定;
- 服务端与客户端使用相同的初始数据;
- 不能依赖
window、document等仅浏览器对象生成首屏关键内容。
8.3 SSG/预渲染
SSG(Static Site Generation)或预渲染在构建阶段生成 HTML 文件。部署后请求静态文件,不需要每次请求都运行 Vue 服务端渲染。
Nuxt 可以通过预渲染或部署平台的静态输出实现这种模式。例如概念性配置:
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true },
'/about': { prerender: true },
'/articles/**': { swr: 3600 }
}
})
这里需要区分:
prerender: true:构建或预渲染阶段生成静态页面;swr:允许 Nitro 按缓存策略提供内容,具体行为依赖部署目标和 Nuxt/Nitro 版本;ssr: false:关闭服务端渲染,使页面退化为 CSR,不应为了“优化前端部署”而无条件使用。
路由规则属于版本和部署目标敏感能力。Cloudflare、Node、静态托管对缓存和服务器能力的支持不同,必须在目标平台验证实际输出。
8.4 三种渲染方式的选择条件
| 页面特征 | 更适合的方式 |
|---|---|
| 内容公开、需要搜索抓取、实时性高 | SSR |
| 内容公开、更新可预测、构建可接受 | SSG/预渲染 |
| 登录后应用、个性化控制台、搜索无价值 | CSR |
| 公开页面与登录功能混合 | 公开路由 SSR/SSG,交互部分客户端化 |
真正需要优化的是“首个可抓取响应”,而不是单纯追求某种渲染标签。
九、Nuxt SSR 的完整页面流程和错误路径
以下页面把数据获取、404、Meta、canonical 和 JSON-LD 连接起来:
<!-- pages/products/[id].vue -->
<script setup lang="ts">
interface Product {
id: string
name: string
description: string
price: number
currency: string
available: boolean
updatedAt: string
}
const route = useRoute()
const config = useRuntimeConfig()
const id = String(route.params.id)
const { data: product, error } = await useFetch<Product>(
`/api/products/${encodeURIComponent(id)}`,
{
key: `product:${id}`
}
)
if (error.value || !product.value) {
throw createError({
statusCode: 404,
statusMessage: 'Product Not Found'
})
}
const item = product.value
const canonicalUrl = new URL(
`/products/${encodeURIComponent(item.id)}`,
config.public.siteUrl
).toString()
useSeoMeta({
title: `${item.name} | WR BLOG`,
description: item.description,
robots: item.available ? 'index,follow' : 'noindex,follow',
ogTitle: item.name,
ogDescription: item.description,
ogType: 'product',
ogUrl: canonicalUrl
})
useHead({
link: [
{
rel: 'canonical',
href: canonicalUrl
}
],
script: [
{
type: 'application/ld+json',
children: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Product',
'@id': `${canonicalUrl}#product`,
name: item.name,
description: item.description,
offers: {
'@type': 'Offer',
url: canonicalUrl,
price: item.price,
priceCurrency: item.currency,
availability: item.available
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock'
}
})
}
]
})
</script>
<template>
<main>
<h1>{{ item.name }}</h1>
<p>{{ item.description }}</p>
<p>{{ item.price }} {{ item.currency }}</p>
</main>
</template>
请求一个不存在的 ID 时,期望是:
curl -i https://example.com/products/not-found
返回:
HTTP/1.1 404 Not Found
而不是:
HTTP/1.1 200 OK
然后页面正文显示“商品不存在”。
后者是典型的 soft 404:用户看起来看到了错误页,但 HTTP 状态仍是 200,可能使爬虫误以为这是有效页面。Nuxt 中通过 throw createError 抛出服务端错误,才能让状态码和页面内容保持一致。
如果商品已下架但仍希望保留页面历史,可以选择:
- 返回 200,并保留真实的历史内容;
- 返回 404/410,表示资源已不存在;
- 重定向到明确的替代商品;
- 对确实不应建立索引但仍需用户访问的页面使用
noindex。
不能仅凭“页面暂时不可售”就机械地把所有商品设成 noindex,应根据业务语义决定。
十、Hydration 不一致如何破坏 SEO 和页面稳定性
服务端和客户端必须对同一初始状态生成相同结构。下面的代码存在风险:
<script setup lang="ts">
const title = import.meta.client
? document.title
: '默认标题'
useSeoMeta({
title
})
</script>
服务端得到“默认标题”,客户端得到浏览器当前标题,两个执行环境的结果不同,可能引起 head 更新或 hydration 警告。
另一个常见问题是使用随机值:
<script setup lang="ts">
const id = Math.random()
useHead({
meta: [
{
name: 'page-id',
content: String(id)
}
]
})
</script>
服务端和客户端的随机数不可能稳定相同。首屏关键数据应来自:
- 路由参数;
- 服务端接口;
- Nuxt 的 payload;
- 明确的运行时配置;
- 可确定的纯函数。
浏览器专属行为可以放在 onMounted 中,但不要让它决定搜索引擎需要的标题、canonical、正文或结构化数据。
十一、Vue 原生 SSR 与 Nuxt 的关系
Vue 官方 SSR 指南描述了 Vue SSR 的基本模型:服务端使用 createSSRApp 创建应用,并通过服务器渲染器将组件树转换为 HTML;客户端再使用同样的应用状态进行 hydration。
概念代码如下:
// server-entry.ts
import { createSSRApp } from 'vue'
import { renderToString } from '@vue/server-renderer'
import App from './App.vue'
export async function render(url: string) {
const app = createSSRApp(App)
const html = await renderToString(app)
return html
}
真实生产应用还需要处理:
- 路由匹配;
- 服务端数据预取;
- 状态序列化;
- 客户端 hydration;
- head 收集与注入;
- 404 和 500 状态码;
- 静态资源;
- 缓存;
- 安全响应头。
Nuxt 在 Vue SSR 之上提供了文件系统路由、Nitro 服务端、数据获取约定、运行时配置、head 管理和部署适配。因此,使用 Nuxt 时一般不需要手工搭建完整的 renderToString 管线,但仍然必须理解 SSR 的状态一致性和请求生命周期。
十二、页面数据、Meta、sitemap 必须来自一致的数据模型
SEO 故障经常不是模板问题,而是三个数据出口互相不一致:
文章数据库
├─→ 页面正文
├─→ Meta / JSON-LD
└─→ sitemap.xml
假设数据库中的文章 slug 为:
vue-nuxt-seo
但页面使用了旧 slug:
vue-seo
可能出现:
- 页面 URL 返回 404;
- sitemap 继续输出旧 URL;
- JSON-LD 的
mainEntityOfPage指向另一个地址; - canonical 指向页面之外的 URL。
应把 URL 规则集中到可复用函数中:
// shared/utils/urls.ts
export function articlePath(slug: string): string {
return `/articles/${encodeURIComponent(slug)}`
}
export function absoluteUrl(siteUrl: string, path: string): string {
return new URL(path, `${siteUrl.replace(/\/+$/, '')}/`).toString()
}
页面和 sitemap 都使用它:
const path = articlePath(item.slug)
const url = absoluteUrl(config.public.siteUrl, path)
这不是为了抽象而抽象,而是为了让三个输出对同一实体使用同一 URL 规范。
十三、诊断:不要只看浏览器开发者工具
13.1 检查原始 HTML
curl -L -sS https://example.com/articles/vue-ssr -o /tmp/page.html
grep -iE \
'<title>|meta name="description"|meta name="robots"|rel="canonical"|application/ld\+json' \
/tmp/page.html
需要检查:
- 页面标题是否正确;
- description 是否为空或重复;
- canonical 是否是绝对 HTTPS URL;
- 是否错误输出
noindex; - JSON-LD 是否存在;
- 正文是否在原始 HTML 中出现。
如果浏览器 Elements 面板中有这些内容,但 curl 原始 HTML 没有,说明它们可能是客户端 JavaScript 执行后才添加的。对于 SSR 页面,这通常是需要进一步调查的信号。
13.2 检查状态码和重定向
curl -I -L https://example.com/articles/vue-ssr
观察:
- 最终是否为 200;
- 不存在页面是否为 404;
- HTTP 是否重定向到 HTTPS;
www与非www是否一致;- 是否出现重定向链;
- canonical 是否和最终公开 URL 一致。
13.3 检查 sitemap 和 robots
curl -i https://example.com/sitemap.xml
curl -i https://example.com/robots.txt
常见失败表现:
sitemap.xml返回 HTML 错误页;- Content-Type 是
text/html而不是 XML; <loc>使用内部域名;- sitemap 收录了
noindex或 404 页面; - robots.txt 阻断了公开页面;
- robots.txt 阻断 sitemap 所在路径,导致爬虫无法获取。
13.4 检查结构化数据
要分别验证:
- JSON 是否可解析;
- Schema.org 类型和字段是否有效;
- URL、日期、价格等值是否符合格式;
- JSON-LD 与页面可见内容是否一致。
浏览器控制台中可以做基本语法检查:
const node = document.querySelector(
'script[type="application/ld+json"]'
)
JSON.parse(node?.textContent ?? '')
这只能验证 JSON 语法,不能验证结构化数据的语义,也不能保证搜索结果出现富结果。
十四、常见误解和失败方案
14.1 “SPA 一定不能做 SEO”
错误。SPA 可以通过预渲染、SSR 或搜索引擎的 JavaScript 渲染获得内容,但纯 CSR 的抓取链路更依赖脚本执行,失败面更多。对于公开内容页面,SSR 或预渲染通常更容易控制首个 HTML。
14.2 “加了 sitemap 就会被收录”
错误。sitemap 只是发现和提示机制。URL 仍需要可访问、内容有价值、状态码正确、没有 noindex,并且搜索引擎自行决定是否索引。
14.3 “canonical 会把用户跳转到主 URL”
错误。canonical 不改变浏览器地址和 HTTP 响应。需要改变访问地址时使用 301 或 308。
14.4 “JSON-LD 能替代正文”
错误。结构化数据描述页面,但不能替代用户可见内容。页面声称是 Article,正文仍应真实呈现文章标题、内容、作者和日期。
14.5 “把所有 query 参数删除就能避免重复”
错误。追踪参数通常不改变内容,但搜索、分页、筛选参数可能改变内容。canonical 规则必须基于内容身份,而不是简单地删除全部参数。
14.6 “开发环境能看到 Meta,生产环境就没问题”
错误。生产环境可能有:
- CDN 缓存旧 HTML;
- 代理改变了 host 或协议;
- 环境变量缺失;
- 服务端 API 无法访问;
- 静态预渲染没有包含动态路由;
- 部署平台不支持某些 Nitro 缓存规则。
因此要对部署后的公开 URL 直接使用 curl 检查,而不是只检查本地开发服务器。
十五、生产取舍:SSR、预渲染和动态内容
15.1 SSR 的成本
SSR 请求通常需要:
请求
↓
服务端执行路由和组件
↓
调用内容 API
↓
生成 HTML
如果每次请求都实时查询 CMS,内容 API 故障会直接影响页面响应和 SEO Meta。可以使用缓存或 stale-while-revalidate,但必须明确缓存失效和发布延迟。
15.2 预渲染的成本
预渲染将成本提前到构建阶段:
构建
↓
枚举路由
↓
获取数据
↓
生成 HTML 文件
↓
部署静态文件
它降低了运行时复杂度,但动态新增页面可能要等下一次构建。若文章数量很大,构建时间和路由枚举会成为瓶颈。
15.3 混合渲染
Nuxt 的价值之一是允许按路由选择策略:
- 首页预渲染;
- 文章页 SSR 或增量缓存;
- 后台页面 CSR;
- API 路由由 Nitro 处理;
- 需要实时数据的页面禁用静态缓存。
选择标准不是“SSR 一定高级”,而是页面的内容更新频率、公开性、数据依赖和部署平台能力。
十六、版本敏感能力的边界
以下能力需要特别注意版本和部署环境:
useSeoMeta、useServerSeoMeta的字段类型由 Nuxt 与 Unhead 版本共同决定;routeRules的缓存和预渲染行为依赖 Nuxt/Nitro 版本及部署适配器;@nuxtjs/sitemap等 sitemap 模块不是 Nuxt 核心,配置项以对应模块版本为准;- 不同托管平台对 ISR、SWR、边缘运行时和服务端路由的支持不同;
- Vue SSR 的 hydration、状态序列化和客户端接管规则必须与当前 Vue 版本一致。
如果项目采用 Nuxt 模块生成 sitemap,应锁定模块版本并验证生成结果;如果采用上文的 Nitro 路由,则需要自行负责 XML 格式、缓存和数据一致性。
十七、交付前验证模型
可以把一个公开文章页面的交付条件写成下面的检查式:
逐项解释:
HTMLContainsContent:原始响应中有标题和主要正文;ValidHead:title、description、robots 等字段符合页面语义;CanonicalSelfConsistent:canonical 是可访问的规范地址;JsonLdMatchesContent:结构化数据与用户可见内容一致;SitemapConsistent:sitemap 中的 URL 确实返回 200 且允许索引;StatusCorrect:不存在的页面返回 404,重定向和错误状态符合业务语义。
对应的最小验证命令:
URL="https://example.com/articles/vue-ssr"
curl -L -sS "$URL" -o /tmp/page.html
curl -I -L "$URL"
curl -i https://example.com/sitemap.xml
curl -i https://example.com/robots.txt
grep -iE \
'<title>|description|robots|canonical|application/ld\+json' \
/tmp/page.html
这组命令不能代替搜索引擎验证工具,但可以在应用交付前发现大量确定性错误:服务端没有输出 Meta、canonical 域名错误、sitemap 返回 500、404 页面返回 200,以及结构化数据根本不存在。
Vue 负责组件和响应式 UI,Nuxt 负责将路由、数据、服务端渲染、head 管理和部署运行时连接起来。SEO 的关键不在于某一个 API,而在于让页面内容、Meta、结构化数据、canonical、sitemap、HTTP 状态和渲染结果共同描述同一个真实资源。只有这些信号在数据和 URL 层面保持一致,搜索引擎才有足够条件正确发现、抓取、理解并处理页面。
系列导航与关联阅读
- 系列入口:Vue 完整学习路线:从响应式与组件到工程化、SSR 和生产交付
- 上一篇:Vue SSR 水合诊断:不一致来源、客户端边界和调试方法
- 下一篇:Vue SSR 缓存:页面、数据、边缘缓存、个性化和失效
官方资料
本文依据 Vue、Vite 与生态项目官方文档重新梳理;正文与示例由 WR BLOG 编写。

评论
0 条讨论