目录
①对于更成熟的网站,简单的index.html的入口文件的seo已经无法满足,需要在商品详情不同商品被搜索时赋予不同的title和description。
②通过设置站点所有页面都新增Canonical标签,指定规范链接地址给谷歌并规避联盟的重复内容页面。
①对于更成熟的网站,简单的index.html的入口文件的seo已经无法满足,需要在商品详情不同商品被搜索时赋予不同的title和description。
<!-- src/views/main/goods/goods-details/goods-details.vue -->
<template>
<!-- 模板部分保持不变 -->
</template>
<script>
// ... 其他导入保持不变
export default {
name: "goods-details",
// ... 其他配置保持不变
data() {
return {
// ... 其他数据保持不变
originalMetaTags: {} // 保存原始meta标签
}
},
// ... 其他配置保持不变
methods: {
// 设置产品页面的SEO信息
setProductSEO(dataInfo) {
if (!dataInfo) return;
// 保存原始的meta标签信息(只在第一次调用时保存)
if (!this.originalMetaTags.title) {
this.originalMetaTags.title = document.title;
this.originalMetaTags.description = this.getMetaContent('name', 'description');
this.originalMetaTags['og:title'] = this.getMetaContent('property', 'og:title');
this.originalMetaTags['og:description'] = this.getMetaContent('property', 'og:description');
this.originalMetaTags['twitter:title'] = this.getMetaContent('name', 'twitter:title');
this.originalMetaTags['twitter:description'] = this.getMetaContent('name', 'twitter:description');
}
// 获取产品标题
const productTitle = dataInfo.subject || '';
// 设置新的页面标题
const newTitle = `Buy ${productTitle} | Hipobuy`;
document.title = newTitle;
// 设置新的meta description
const newDescription = `Find the perfect ${productTitle} on Taobao or 1688? Hipobuy is your trusted China shopping agent. Start shopping now!`;
// 更新所有meta标签
this.updateMetaTag('name', 'description', newDescription);
this.updateMetaTag('property', 'og:title', newTitle);
this.updateMetaTag('property', 'og:description', newDescription);
this.updateMetaTag('name', 'twitter:title', newTitle);
this.updateMetaTag('name', 'twitter:description', newDescription);
},
// 获取meta标签内容的辅助方法
getMetaContent(attribute, value) {
const metaTag = document.querySelector(`meta[${attribute}="${value}"]`);
return metaTag ? metaTag.getAttribute('content') : '';
},
// 更新meta property标签
updateMetaProperty(property, content) {
let metaTag = document.querySelector(`meta[property="${property}"]`);
if (metaTag) {
metaTag.setAttribute('content', content);
} else {
metaTag = document.createElement('meta');
metaTag.setAttribute('property', property);
metaTag.content = content;
document.head.appendChild(metaTag);
}
},
// 更新meta标签的通用方法
updateMetaTag(attribute, value, content) {
let metaTag = document.querySelector(`meta[${attribute}="${value}"]`);
if (metaTag) {
metaTag.setAttribute('content', content);
} else {
metaTag = document.createElement('meta');
metaTag.setAttribute(attribute, value);
metaTag.content = content;
document.head.appendChild(metaTag);
}
},
// 完整恢复原始meta标签(页面销毁时调用)
restoreOriginalMetaTags() {
// 恢复title
if (this.originalMetaTags.title) {
document.title = this.originalMetaTags.title;
}
// 恢复description
if (this.originalMetaTags.description !== undefined) {
const descriptionMeta = document.querySelector('meta[name="description"]');
if (descriptionMeta) {
if (this.originalMetaTags.description) {
descriptionMeta.setAttribute('content', this.originalMetaTags.description);
} else {
descriptionMeta.remove(); // 如果原来没有,就删除添加的
}
} else if (this.originalMetaTags.description) {
// 如果原来有但现在没有,就重新创建
const meta = document.createElement('meta');
meta.name = 'description';
meta.content = this.originalMetaTags.description;
document.head.appendChild(meta);
}
}
// 恢复og:title
this.restoreMetaTag('property', 'og:title', this.originalMetaTags['og:title']);
// 恢复og:description
this.restoreMetaTag('property', 'og:description', this.originalMetaTags['og:description']);
// 恢复twitter:title
this.restoreMetaTag('name', 'twitter:title', this.originalMetaTags['twitter:title']);
// 恢复twitter:description
this.restoreMetaTag('name', 'twitter:description', this.originalMetaTags['twitter:description']);
},
// 恢复单个meta标签的辅助方法
restoreMetaTag(attribute, value, originalContent) {
const metaTag = document.querySelector(`meta[${attribute}="${value}"]`);
if (metaTag) {
if (originalContent) {
metaTag.setAttribute('content', originalContent);
} else {
metaTag.remove(); // 如果原来没有,删除添加的标签
}
} else if (originalContent) { // 如果原来有但现在没有,重新创建
const meta = document.createElement('meta');
meta.setAttribute(attribute, value);
meta.content = originalContent;
document.head.appendChild(meta);
}
},
getQueryProductDetail(force) {
this.loading = true;
queryProductDetail({
spuNo: this.id,
refresh: !!force ? 1 : 0,
channel: this.channel,
activityCode: this.activityCode
}).then((res) => {
if (res.code == 1010) {
this.loading = false;
this.$refs.tipsPopRef.open(res.message);
} else if (res.code == 200) {
if (res.result.productGrayscale) {
this.$refs.grayPopRef.open(res.result.productGrayscaleName)
}
this.loading = false;
this.dataInfo = res.result;
this.dataInfo.description = this.dataInfo.description.replace(/\s*href="[^"]*"/gi, '');
// 设置产品SEO信息——getQueryProductDetail此请求只添加了这个设置,其余不需改动
this.setProductSEO(this.dataInfo);
if (this.dataInfo.channel == 'TAOBAO') {
detailPointBtn(this.$route.params.spuNo, 'EXP', 'buy_icon');
detailPointBtn(this.$route.params.spuNo, 'EXP', 'addcart_icon');
}
if (res.result.spuActivityVO) {
if (this.$analytics) {
this.$analytics.logEvent('pt_1001');
}
}
}
}).catch(() => {
this.loading = false;
})
},
// ... 其他方法保持不变
},
beforeDestroy() {// 恢复原始的meta标签
this.restoreOriginalMetaTags();
}
}
</script>
<!-- 样式部分保持不变 -->
本地环境自测以html的标签为准即可;
②通过设置站点所有页面都新增Canonical标签,指定规范链接地址给谷歌并规避联盟的重复内容页面。
处理带有URL参数的页面
场景:电商网站的产品页面可能会因为不同的筛选条件(如颜色、尺寸、排序方式等)而生成带有不同参数的URL。
示例:https://example.com/product?id=123&color=blue
https://example.com/product?id=123&size=large
解决方案:在所有带参数的页面中,使用Canonical标签指向主要的产品页面,如:
<link rel="canonical" href="https://example.com/product?id=123" />
分页内容
场景:文章列表或产品列表页面经常会分页(如第1页、第2页)
示例:https://example.com/blog?page=1
https://example.com/blog?page=2
解决方案:可以让每个分页页面的Canonical标签指向第一页,或者让每页的Canonical标签指向自身。<link rel="canonical" href="https://example.com/blog?page=1" />
内容分发与跟踪参数
场景:网站在不同渠道(如社交媒体)分发内容,带有UTM等跟踪参数
示例:https://example.com/blog/post?utm_source=facebook
解决方案:使用Canonical标签指向无参数的主要URL.
<link rel="canonical" href="https://example.com/blog/post" />
下面是我的实践实例动态设置所有页面:
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en-US">
<head>
<!-- 其他head内容保持不变 -->
<!-- 添加基础canonical标签 -->
<link rel="canonical" href="https://hipobuy.com/" />
<!-- 其他head内容保持不变 -->
</head>
<body>
<div id="app"></div>
</body>
</html>
// 在 src/router/index.js 中添加
router.afterEach((to) => {
// 延迟执行确保DOM已更新
setTimeout(() => {
const canonicalLink = document.querySelector('link[rel="canonical"]');
if (canonicalLink) {
// 获取基础URL,移除查询参数和hash
const baseUrl = window.location.origin;
const pathname = to.path;
const canonicalUrl = baseUrl + pathname;
canonicalLink.href = canonicalUrl;
}
}, 100);
});
特殊情况:电商网站中如果路由配置了参数可以用这种配置,不过具体在控制台查看link标签,我发现以上路由设置已经足够,可以把参数也带进canonical标签,如果不行参考以下:
<!-- 在 src/views/main/goods/goods-details/goods-details.vue 的 methods 中添加 -->
methods: {
// ... 其他方法保持不变
// 添加这个方法来设置产品页的canonical
setProductCanonical() {
const canonicalLink = document.querySelector('link[rel="canonical"]');
if (canonicalLink) {
// 构建产品页的标准canonical URL
const channelMap = {
"0": "1688",
"1": "taobao",
"2": "weidian",
"3": "jd"
};
const channelName = channelMap[this.channel] || this.channel.toLowerCase();
const canonicalUrl = `https://hipobuy.com/product/${channelName}/${this.id}`;
canonicalLink.href = canonicalUrl;
}
},
// 在 getQueryProductDetail 方法的成功回调中调用
getQueryProductDetail(force) {
// ... 其他代码保持不变
queryProductDetail({
// ... 参数保持不变
}).then((res) => {
// ... 其他成功处理保持不变
if (res.code == 200) {
// ... 其他处理保持不变
// 设置产品SEO信息
this.setProductSEO(this.dataInfo);
// 添加这一行来设置产品页canonical
this.setProductCanonical();
// ... 其他处理保持不变
}
}).catch(() => {
this.loading = false;
})
}
}
③产品结构化微数据
这是代码效果示例,不可以直接添加在html:
<html>
<head>
<title>Executive Anvil</title>
<script type="application/ld+json">
{
"@context": "https://schema.org/",
"@type": "Product",
"name": "Executive Anvil",
"image": [
"https://example.com/photos/1x1/photo.jpg",
"https://example.com/photos/4x3/photo.jpg",
"https://example.com/photos/16x9/photo.jpg"
],
"description": "Sleeker than ACME's Classic Anvil, the Executive Anvil is perfect for the business traveler looking for something to drop from a height.",
"sku": "0446310786",
"mpn": "925872",
"brand": {
"@type": "Brand",
"name": "ACME"
},
"review": {
"@type": "Review",
"reviewRating": {
"@type": "Rating",
"ratingValue": 4,
"bestRating": 5
},
"author": {
"@type": "Person",
"name": "Fred Benson"
}
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": 4.4,
"reviewCount": 89
},
"offers": {
"@type": "AggregateOffer",
"offerCount": 5,
"lowPrice": 119.99,
"highPrice": 199.99,
"priceCurrency": "USD"
}
}
</script>
</head>
<body>
</body>
</html>
以下才是js效果代码:
<!-- src/views/main/goods/goods-details/goods-details.vue -->
<script>
export default {
name: "goods-details",
// ... 其他代码保持不变
data() {
return {
// ... 其他数据保持不变
structuredDataScript: null // 保存结构化数据script元素
}
},
methods: {
// 生成并插入结构化数据
generateStructuredData(dataInfo) {
if (!dataInfo) return;
// 移除已存在的结构化数据
this.removeStructuredData();
// 构建基础产品信息
const structuredData = {
"@context": "https://schema.org/",
"@type": "Product",
"name": dataInfo.subject || '',
"image": this.getProductImages(dataInfo),
"description": this.getProductDescription(dataInfo),
"sku": dataInfo.spuNo || '',
"mpn": dataInfo.spuNo || dataInfo.id || '',
"brand": this.getProductBrand(dataInfo),
"offers": this.getProductOffers(dataInfo)
};
// 添加评分信息
const ratingInfo = this.getProductRating(dataInfo);
if (ratingInfo) {
structuredData.aggregateRating = ratingInfo;
}
// 添加评论信息(如果有)
const reviews = this.getProductReviews(dataInfo);
if (reviews && reviews.length > 0) {
structuredData.review = reviews;
}
// 创建script标签并插入到head中
this.structuredDataScript = document.createElement('script');
this.structuredDataScript.type = 'application/ld+json';
this.structuredDataScript.textContent = JSON.stringify(structuredData, null, 2);
// 添加标识便于清理
this.structuredDataScript.setAttribute('data-structured-data', 'product');
document.head.appendChild(this.structuredDataScript);
},
// 获取产品图片列表
getProductImages(dataInfo) {
const images = [];
// 主图
if (dataInfo.mainImg) {
images.push(dataInfo.mainImg);
}
// 图片列表
if (dataInfo.imageList && Array.isArray(dataInfo.imageList)) {
images.push(...dataInfo.imageList);
}
// 去重
return [...new Set(images)].slice(0, 10); // 最多10张图片
},
// 获取产品描述
getProductDescription(dataInfo) {
if (!dataInfo.description) return '';
// 移除HTML标签
const tmp = document.createElement('div');
tmp.innerHTML = dataInfo.description;
return tmp.textContent || tmp.innerText || '';
},
// 获取品牌信息
getProductBrand(dataInfo) {
const channelNames = {
"0": "1688",
"1": "Taobao",
"2": "Weidian",
"3": "JD",
"1688": "1688",
"TAOBAO": "Taobao",
"WEIDIAN": "Weidian",
"JD": "JD"
};
const brandName = dataInfo.brandName ||
channelNames[dataInfo.channel] ||
channelNames[this.channel] ||
"Chinese E-commerce Platform";
return {
"@type": "Brand",
"name": brandName
};
},
// 获取产品报价信息
getProductOffers(dataInfo) {
const price = this.getProductPrice(dataInfo);
const currency = dataInfo.currencyCode || "CNY";
const availability = this.getAvailabilityStatus(dataInfo);
return {
"@type": "Offer",
"url": window.location.href,
"priceCurrency": currency,
"price": price,
"availability": availability,
"priceValidUntil": this.getPriceValidUntil(),
"seller": {
"@type": "Organization",
"name": "Hipobuy"
}
};
},
// 获取产品价格
getProductPrice(dataInfo) {
if (dataInfo.price) {
return dataInfo.priceCNY ? dataInfo.priceCNY : dataInfo.price;
} else if (dataInfo.saleInfo) {
if (dataInfo.saleInfo.priceRangeList && dataInfo.saleInfo.priceRangeList.length > 0) {
return dataInfo.saleInfo.priceRangeList[0].priceCNY ?
dataInfo.saleInfo.priceRangeList[0].priceCNY :
dataInfo.saleInfo.priceRangeList[0].price;
} else if (dataInfo.saleInfo.priceRanges && dataInfo.saleInfo.priceRanges.length > 0) {
return dataInfo.saleInfo.priceRanges[0].priceCNY ?
dataInfo.saleInfo.priceRanges[0].priceCNY :
dataInfo.saleInfo.priceRanges[0].price;
}
}
return "0";
},
// 获取产品评分信息
getProductRating(dataInfo) {
if (dataInfo.score || dataInfo.commentNum) {
return {
"@type": "AggregateRating",
"ratingValue": dataInfo.score || 0,
"reviewCount": dataInfo.commentNum || 0,
"bestRating": 5,
"worstRating": 1
};
}
return null;
},
// 获取产品评论信息
getProductReviews(dataInfo) {
// 如果有具体的评论数据,可以在这里处理
// 目前返回空数组
return [];
},
// 获取库存状态
getAvailabilityStatus(dataInfo) {
// 根据库存信息判断可用性
if (dataInfo.stock !== undefined) {
return dataInfo.stock > 0 ? "https://schema.org/InStock" : "https://schema.org/OutOfStock";
}
// 根据状态判断
if (dataInfo.status !== undefined) {
switch (dataInfo.status) {
case 'ON_SALE':
case 'AVAILABLE':
return "https://schema.org/InStock";
case 'SOLD_OUT':
case 'OFF_SALE':
return "https://schema.org/OutOfStock";
default:
return "https://schema.org/InStock";
}
}
return "https://schema.org/InStock"; // 默认有库存
},
// 获取价格有效期
getPriceValidUntil() {
const date = new Date();
date.setMonth(date.getMonth() + 3); // 价格有效期3个月
return date.toISOString().split('T')[0];
},
// 移除结构化数据
removeStructuredData() {
if (this.structuredDataScript) {
document.head.removeChild(this.structuredDataScript);
this.structuredDataScript = null;
}
},
},
// 组件销毁时移除结构化数据
beforeDestroy() {
this.removeStructuredData(); // 移除结构化数据
}
}
</script>
// 生成结构化数据---------在调用商品详情的接口内调用并传递res.dataInfo
this.generateStructuredData(this.dataInfo);
执行完之后可以在控制台head标签内查找是否展现: