Nuxt应用动态meta标签页面源码显示undefined解决方案
Nuxt动态路由页面静态源码meta值为undefined的修复方案
问题现象
- 博客详情动态路由页面,客户端渲染完成后
metadataObject可正常拿到接口返回值,浏览器元素检查面板中meta标签显示正常 - 点击浏览器「查看页面源码」时,所有meta标签属性值均为
undefined,无法满足SEO爬虫抓取要求
问题根因
之前的推测只对了一半,核心问题有两个:
- 显式配置了
fetchOnServer: false,直接导致服务端渲染阶段完全不执行fetch钩子,服务端生成静态HTML时metadataObject还是初始空对象,自然读不到任何属性,返回给爬虫的静态源码里meta全是undefined - 即使开启服务端fetch,当前代码里的逻辑也存在时序问题:直接操作DOM(
document.getElementById('blog-html').innerHTML)的写法在Nuxt服务端渲染阶段会直接报错,因为服务端没有document对象;同时数据更新后没有主动触发meta信息刷新,会导致客户端路由切换时meta不同步
修复步骤
1. 开启服务端数据拉取
删除配置项fetchOnServer: false,让fetch钩子在服务端渲染阶段就执行,提前拿到博客数据填充到响应式数据中,保证head钩子生成时能读到对应的值。
2. 修正fetch逻辑,避免服务端报错
- 移除直接操作DOM的代码,富文本渲染结果不要用原生DOM插入,改成存在响应式变量里,在模板中用
v-html渲染 - 给
metadataObject设置默认初始值,避免数据未返回时head读取属性报错 - 接口请求完成后,客户端环境下主动调用
this.$meta().refresh()触发meta更新,保证路由切换时meta同步
3. 修正head配置和笔误
- 给所有meta字段加默认兜底值,避免数据未加载完成时生成空meta
- 修正canonical链接的属性笔误:原代码写的是
herf,正确属性名是href - 修正重复hid冲突:原代码og:title的hid设置为
title,和页面默认title的hid重复,会导致标签覆盖,改成唯一值即可
修复后核心代码示例
<template> <div> <QcLoader v-if="$fetchState.pending" /> <heroSection :blog="blog" /> <remoteSection :blog="blog" /> <!-- 富文本内容用v-html渲染,禁止原生操作DOM --> <div id="blog-html" v-html="renderedBlogHtml"></div> <readmoreSection :more-blogs="moreBlogs" /> </div> </template> <script> import { BLOCKS } from '@contentful/rich-text-types' import { documentToHtmlString } from '@contentful/rich-text-html-renderer' import client from '@/plugins/contentful' export default { name: 'Blogid', components: { QcLoader: () => import('@/components/partials/QcLoader.vue'), heroSection: () => import('@/components/sections/company/blog/post/heroSection.vue'), remoteSection: () => import('@/components/sections/company/blog/post/remoteSection.vue'), readmoreSection: () => import('@/components/sections/company/blog/post/readmoreSection.vue'), }, data() { return { blog: [], moreBlogs: [], renderedBlogHtml: '', // 给meta对象加默认兜底值 metadataObject: { title: 'QED博客', description: 'QED技术博客内容', url: 'https://qedcode.io/company/blog', imageUrl: '', imageWidth: '', imageHeight: '' } } }, async fetch() { // 并行拉取博客详情和推荐博客内容,缩短加载时间 const [blogRes, moreBlogRes] = await Promise.all([ client.getEntries({ content_type: 'blogPost', 'fields.slug[in]': this.$route.params.blog, }), client.getEntries({ content_type: 'blogPost', order: '-sys.createdAt', }) ]).catch(err => { this.moreBlogs = [] return [null, {items: []}] }) if (blogRes?.items?.length) { const blog = blogRes.items[0] this.blog = blog // 赋值meta数据 this.metadataObject = { title: blog.fields.title, description: blog.fields.firstTestField.content[0].content[0].value || blog.fields.firstTestField.content[0].content[0].content[0].value, url: `https://qedcode.io/company/blog/${blog.fields.slug}`, imageUrl: blog.fields.image.fields.file.url, imageWidth: blog.fields.image.fields.file.details.image.width, imageHeight: blog.fields.image.fields.file.details.image.height, } // 服务端渲染富文本 const rawRichTextField = blog.fields.firstTestField const options = { renderNode: { [BLOCKS.EMBEDDED_ASSET]: (node) => { const file = node.data.target.fields.file if (['image/jpeg', 'image/png'].includes(file.contentType)) { return `<img src="${file.url}">` } }, }, } this.renderedBlogHtml = documentToHtmlString(rawRichTextField, options) // 客户端环境下主动刷新meta if (process.client) { this.$meta().refresh() } } this.moreBlogs = moreBlogRes.items }, head() { return { title: this.metadataObject.title, titleTemplate: '%s', meta: [ { name: 'description', hid: 'description', content: this.metadataObject.description, }, /* Open Graph */ { name: 'og:title', hid: 'og:title', content: this.metadataObject.title, }, { name: 'og:site_name', hid: 'site-name', content: 'QED', }, { name: 'og:url', hid: 'url', content: this.metadataObject.url, }, { name: 'og:description', hid: 'og:description', content: this.metadataObject.description, }, { name: 'og:type', hid: 'type', content: 'article', }, { name: 'og:image', hid: 'image', content: this.metadataObject.imageUrl, }, { name: 'og:image:width', hid: 'image-width', content: this.metadataObject.imageWidth, }, { name: 'og:image:height', hid: 'image-height', content: this.metadataObject.imageHeight, }, /* Twitter */ { name: 'twitter:card', content: 'summary_large_image' }, { name: 'twitter:site', content: '@QED_code' }, { name: 'twitter:title', content: this.metadataObject.title, }, { name: 'twitter:description', content: this.metadataObject.description, }, { name: 'twitter:url', content: this.metadataObject.url }, { name: 'twitter:image', content: this.metadataObject.imageUrl, }, ], link: [ { rel: 'canonical', href: this.metadataObject.url, }, ], } } } </script>
额外优化点
- 博客详情页的
og:type建议从website改成article,更符合内容页语义,社交平台抓取时展示效果更好 - 移除无意义的
fetchDelay: 1000配置,该配置会强制延迟1秒发起请求,拖慢服务端渲染速度,无特殊需求不需要添加 - 接口请求用
Promise.all并行拉取,相比原来的串行请求可减少近一半的加载等待时间
内容的提问来源于stack exchange,提问作者76marin76
相关产品推荐
相关产品推荐

