使用rerender-spa-plugin在构建时预渲染静态HTML文件优化SEO

前些时候,接到一个任务,我们给第三方开发的官网,一直无法在百度中搜索到,产品提出做个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-pluginvue-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 执行devdev:test等命令时候并不会,所以我才加了--preRender这个条件。

好了,关于使用rerender-spa-plugin预渲染vue应用,我就啰嗦到这了。本文没有具体介绍rerender-spa-plugin的定义和用法,有不清楚的请自己查询资料啊。

相关推荐
星栈11 小时前
Dioxus 接数据库最容易写歪的 3 个地方:sqlx + SQLite 怎么接才顺
前端·rust·前端框架
晴虹11 小时前
vue3-scroll-more:横向滚动条-元素或页签过多滚动显示处理的组件
前端·vue.js
代码搬运媛11 小时前
Claude 全栈开发专用 Rules 配置
前端
PedroQue9911 小时前
uni-router v1.7.0重磅更新:守卫重定向自由掌控
前端·uni-app
Forever7_11 小时前
尤雨溪转发:Vue-tui 0.1 发布!Vue 终于杀进终端!
vue.js
逸铭11 小时前
Day 4:登录与 Token——桌面端怎么存密钥
前端·客户端
默_笙11 小时前
🍞 我用 CSS 画了一个会转的 3D 立方体,同事以为我学了 Three.js(这节课真的很神奇,我很喜欢)
javascript
dkbnull11 小时前
Vue 虚拟 DOM Diff 算法与 key 机制原理
vue.js
溯朢11 小时前
TokUI 流式渲染的 SSE 全链路拆解
前端
京东云开发者11 小时前
京东 Oxygen xLLM 大模型推理引擎正式捐赠开放原子开源基金会,共建国产 AI Infra 生态
前端