前些时候,接到一个任务,我们给第三方开发的官网,一直无法在百度中搜索到,产品提出做个seo优化。由于此官网前端使用vue开发,打开网站只有首页有一个html页面,其余菜单打开并无html页面,更不用说给各个页面设置title、description、keywords(以下简称tdk)了,当时开发项目并没有考虑SEO,现在提出问题,我们技术也得做一些工作了。
考虑到已经使用vue开发,vue开发的应用本身对SEO就不是很友好,但是我们也不可能推倒原本的方案重做,只能在原本的项目中想办法。
针对此vue项目,我们前端在技术层面针对以下方面做了SEO优化:
1、默认语言设置为中文(en改为zh-CN),tdk重新合理布局关键词;
2、网站链接改为伪静态(路由mode模式由hash改为history);
3、网站各个页面设置各自独立的tdk;
4、页面中图片设置好合适的alt标签,便于搜索引擎理解;
5、完善sitemap和robots文件。
至于其他的提高应用seo的方案,比如持续更新网站内容,网站外链建设等,则不在我们前端开发的范围内,我们就不考虑了啊。
扯远了,今天议题是介绍rerender-spa-plugin
,我们针对上面提到的SEO方案第3点(网站各个页面设置各自独立的tdk),发现目前市场上刚好有人针对Vue项目SEO的痛点,开发了一个预编译(或者称预渲染)插件prerender-spa-plugin
。它可以在我们不改变原本项目代码和结构的基础上,只需要增加一些配置,在构建时,生成一些我们需要的静态页面,并按照我们的需要,合理设置tdk,省去了我们使用jsp、php、python重新开发项目,或者前端修改为SSR服务端渲染方案(比如 Vue.js 的 Nuxt、 React 的 Next)只为提高SEO而产生的巨大工作量。
好像又讲了太多废话了啊,我们现在直接上干货,参照从网上各个同行还有npmjs上对rerender-spa-plugin
的介绍,具体步骤如下:
1、项目根目录下npm下载prerender-spa-plugin
js
npm install prerender-spa-plugin --save-dev
2、在根目录下,创建seoMeta.js
js
module.exports = {
'/': {
title : "首页title",
keywords: '首页keywords1,首页keywords2,首页keywords3',
description: '首页description1,首页description2,首页description3,首页description4'
},
'/caterer-list': {
title : "美食店铺title",
keywords: '美食店铺keywords1,美食店铺keywords2,美食店铺keywords3',
description: '美食店铺description1,美食店铺description2,美食店铺description3,美食店铺description4'
},
'/weather': {
title : "天气title",
keywords: '天气keywords1,天气keywords2,天气keywords3',
description: '天气description1,天气description2,天气description3,天气description4'
},
'/good-tags': {
title : "推荐商品title",
keywords: '推荐商品keywords1,推荐商品keywords2,推荐商品keywords3',
description: '推荐商品description1,推荐商品description2,推荐商品description3,推荐商品description4'
},
'/scenic-list': {
title : "景区title",
keywords: '景区keywords1,景区keywords2,景区keywords3',
description: '景区description1,景区description2,景区description3,景区description4'
},
// '/scenic-list?type=01': {
// title : "景点列表",
// keywords: '自然景观, 公园, 户外景点',
// description: '自然景观类景点列表,包含公园、山川等户外旅游资源'
// },
// '/scenic-list?type=02': {
// title : "人文景观",
// keywords: '人文景观, 历史遗迹, 文化景点',
// description: '人文景观类景点列表,探索历史遗迹和文化景点'
// },
'/scenicIntroDetail': {
title : "景区介绍title",
keywords: '景区介绍keywords1,景区介绍keywords2,景区介绍keywords3',
description: '景区介绍description1,景区介绍description2,景区介绍description3,景区介绍description4'
}
// ,'/intro?id=1877252793975181313&type=article': {
// title : "详情页面title",
// keywords: '详情页面keywords1,详情页面keywords2,详情页面keywords3',
// description: '详情页面description1,详情页面description2,详情页面description3,详情页面description4'
// },
};
3、在vue.config.js中,添加以下代码:
js
/*** 下方代码中,与rerender-spa-plugin无关的代码已省略或者注释 ***/
const seoMetaConfig = require('./seoMeta.js')
const seoMetaKeyList = Object.keys(seoMetaConfig);
const PrerenderSPAPlugin = require('prerender-spa-plugin');
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
// 定义在这个位置,方面在后面的configureWebpack中,plugins按条件动态添加
const PrerenderSPAPluginFn = new PrerenderSPAPlugin({
// 生成文件的路径,也可以与webpakc打包的一致。
// 下面这句话非常重要!!!
// 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。
staticDir: path.join(__dirname,'dist'),
// // 对应自己的路由文件,比如a有参数,就需要写成 /a/param1。
// routes: ['/', '/caterer-list','/weather','/good-tags','/scenic-list?type=01','/scenic-list?type=02','/intro?id=1877252793975181313&type=article'],
routes: seoMetaKeyList,
// 添加路由meta信息处理
postProcessHtml: (context) => {
let { html, route } = context;
// 获取当前路由的meta配置
const { title="", keywords="", description="" } = seoMetaConfig[route] || { title:'', keywords: '', description: '' };
if(route=='/'){
return html
}else{
// 1. 移除已存在的title和keywords、description meta标签(如果有)
html = html.replace(/<title>[^<]*<\/title>/i, ''); // 注意这里不能用gi
html = html.replace(/<meta\s+name=(?:"keywords"|keywords)[^>]*>/gi, '');
html = html.replace(/<meta\s+name=(?:"description"|description)[^>]*>/gi, '');
// 2. 在head结束前添加新的meta标签
return html.replace(/<\/head>/i,
` <title>${title}</title>
<meta name="keywords" content="${keywords}" />
<meta name="description" content="${description}" />
</head>`
);
}
},
// 这个很重要,如果没有配置这段,也不会进行预编译
renderer: new Renderer({
headless: false,
// 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。
renderAfterDocumentEvent: 'render-event'
})
})
module.exports = {
publicPath: '/',
configureWebpack: () => {
const plugins = []
// 直接将PrerenderSPAPluginFn写在plugins中,会导致开发调试阶段(执行npm run dev、npm run dev:test)时候,触发PrerenderSPAPlugin的逻辑,
// 导致改一点代码,就会触发弹出页面,无法进行正常的开发操作,
// 同时修改的还有main.js中new Vue时的`document.dispatchEvent(new Event('render-event'))`,同样加入了条件判断
if(process.env.NODE_ENV === 'production' && process.argv.includes('--preRender')){
plugins.push(PrerenderSPAPluginFn)
}
return {
//resolve: {
//alias: {
//'@': resolve('src')
//}
//},
plugins
}
}
}
4、根目录下,找到package.json
js
"scripts": {
"build:prod": "vue-cli-service build --mode production --preRender",
},
5、根目录下,找到main.js
js
const renderEvent= new Event('render-event')
if(process.env.NODE_ENV === 'production' && renderEvent){
new Vue({
router,
store,
render: h => h(App),
mounted () {
document.dispatchEvent(renderEvent)
}
}).$mount('#app')
}else{
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
}
// 或者
const renderEvent= new Event('render-event')
new Vue({
router,
store,
render: h => h(App),
mounted () {
if(process.env.NODE_ENV === 'production' && renderEvent)
document.dispatchEvent(renderEvent)
}
}).$mount('#app')
在步骤2中,我创建了一个名为seoMeta.js的js文件,用于统一配置各个路由页面的tdk,或许有些同行已经意识到了,为什么不在router中直接配置tdk呢,其实肯定可以了,而且肯定是最好的,我这里之所以没有采用,主要是我们的这个项目比较特殊,根据当时业主方的需求,我们的页面的每一个菜单都是通过后台配置出来,除了部分列表页面路由地址固定不变之外,大部分页面类似一个详情页,完全是可配置的,在我们的路由js中,根本没有配置各个页面的路由,所以我没有在路由js中设置,而是另外创建了一个seoMeta.js文件。
在步骤3中,我声明了一个插件函数PrerenderSPAPluginFn,在 postProcessHtml 中,将从seoMeta.js获取到的各个路由页面tdk,替换title、description、keywords生成在构建的html中。在PrerenderSPAPluginFn函数中,只有在routes中指定包含的路由地址,prerender-spa-plugin的PuppeteerRenderer方法才会帮我们构建静态HTML页面,简单来讲,就是只有指定的路由才会生成HTML。
目前网上还有一个插件vue-meta-info
或者vue-meta
,也能实现和我目前方案类似的效果,不过不同的是,使用vue-meta-info
是在各自的路由页面按它的规则配置tdk,并且vue-meta-info
是在最终构建的页面后面追加tdk,从而我们可以看到,最终构建出来的html页面代码中最先会出现首页的tdk,后面又有一个本页面的tdk,vue-meta-info
的核心是通过在它后面追加的tdk覆盖前面的首页tdk,从而达到给各个路由页面设置tdk的目的。
显而易见,vue-meta-info
可以根据页面内容合理的设置tdk,虽然最终构建的html静态页面会生成两个tdk,但是这个瑕疵是可以忽略的。
本人实际尝试使用vue-meta-info
或者vue-meta
,使用vue-meta-info
一直并未成功生成tdk,vue-meta
可以,但是生成的title偶尔会出现为空的情况,同时针对'/scenic-list?type=01'
和'/scenic-list?type=02'
这种带query查询参数的路由,rerender-spa-plugin
结合vue-meta
,并不能生成两种不同的tdk的页面,个人认为应该是rerender-spa-plugin
的机制引起,生成的 scenic-list 文件中并无区分查询参数的文件,只有一个index.html,如下图 这在预渲染方案中是无法实现区分对不同query参数来生成不同tdk的静态html页面的。所以使用
rerender-spa-plugin
和vue-meta
组合同样无法正确处理带query查询参数路由的页面。毕竟rerender-spa-plugin
只是预渲染,不是SSR服务端渲染方案。我们不能苛求太高太多,能实现对部分页面(固定内容不变的页面、固定路由的列表页面)生成一些静态html设置tdk用于seo,已经比只有一个主页面有html强了,您说是不是呢?
因为结合vue-meta
同样无法实现对带query查询参数路由的页面有个正确的预渲染效果,所以最终我并没有采用vue-meta
,所以在步骤3中的PrerenderSPAPluginFn函数中,postProcessHtml的逻辑就是您前面看到的那种通过替换tdk字符串来实现的方案。
js
// 添加路由meta信息处理
postProcessHtml: (context) => {
let { html, route } = context;
// 获取当前路由的meta配置
const { title="", keywords="", description="" } = seoMetaConfig[route] || { title:'', keywords: '', description: '' };
if(route=='/'){
return html
}else{
// 1. 移除已存在的title和keywords、description meta标签(如果有)
html = html.replace(/<title>[^<]*<\/title>/i, ''); // 注意这里不能用gi
html = html.replace(/<meta\s+name=(?:"keywords"|keywords)[^>]*>/gi, '');
html = html.replace(/<meta\s+name=(?:"description"|description)[^>]*>/gi, '');
// 2. 在head结束前添加新的meta标签
return html.replace(/<\/head>/i,
` <title>${title}</title>
<meta name="keywords" content="${keywords}" />
<meta name="description" content="${description}" />
</head>`
);
}
},
如果vue-meta
是用在SSR服务端渲染方案 Nuxt 中,个人觉的应该是可行的,没实际实验过,如果有误,欢迎大家指正。
言归正传,在步骤3和步骤4中,我定义了一个变量--preRender
,至于为什么定义这个变量,在步骤3中的注释中已经说明,我在这里就不做说明了。
在步骤5中加上判断,原由同样和上面提到的--preRender
一样,在步骤3和步骤4中增加了--preRender
后,只会在npm run执行build:prod
命令时会触发prerender-spa-plugin
的预编译,npm run 执行dev
、dev:test
等命令时候并不会,所以我才加了--preRender
这个条件。
好了,关于使用rerender-spa-plugin
预渲染vue应用,我就啰嗦到这了。本文没有具体介绍rerender-spa-plugin
的定义和用法,有不清楚的请自己查询资料啊。