非服务端渲染页面如何做SEO
前段时间对公司的社区h5网站,进行改版(整站重写)。老版本的网站是在一套古老的php框架下开发的,包含很多模板文件,大部分页面都是后端模板渲染,前端开发时要与后端沟通模板逻辑的编写,前后端耦合严重,非常不利于开发。为了实现前后端分离,减轻服务端的渲染压力,我们决定使用目前流行Vue框架,进行前端页面组件化开发,使用前端路由,后端只提供数据接口和必要的模板变量渲染。
但这样一来,网站的SEO就成为不得不考虑的重要问题之一,本文就是对我们实际开发中SEO解决方案的一个总结,介绍为什么要做SEO,客户端渲染应用的SEO解决方案,以及我们采用的方案。
为什么要做SEO
对于一般的功能性h5单页应用,因为其入口或使用场景的原因,使其对SEO并不敏感,例如微信下的滴滴打车。但对于社区类应用,通过搜索引擎搜索对应的帖子是基本的需求。因此在进行前期的技术方案调研时,我们首先考虑的是如何做网页的SEO。
对于服务端渲染的页面,由于页面的HTML结构直接由后端吐出,天然对搜索引擎支持良好,考虑更多的是如何让网站搜索排名更靠前。而对于页面由前端渲染,HTML结构是js动态生成的网站,由于搜索引擎目前并不支持js渲染内容的抓取,所以如何给搜索引擎爬虫提供收录的内容,成为要考虑的首要问题。
解决方案
客户端渲染应用的SEO
常见的单页应用中,页面的切换是通过URL中的哈希(#)来实现的,hash值得变化并不会发起浏览器请求,通过监听hashChnage事件,来实现前端的路由切换。对于这种应用中,搜索引擎很难抓取不同页面的内容,而且页面的渲染大多也是ajax异步获取数据后进行渲染,更加不利于SEO。为此,Google提供了一套针对这种类型的网站开发者的SEO解决方案。
方案规定:
网站提交sitemap给Google;
Google发现URL里有#!符号,例如example.com/#!/detail/1,于是Google开始抓取example.com/?_escaped_fragment_=/detail/1;
_escaped_fragment_这个参数是Google指定的命名,如果开发者希望把网站内容提交给Google,就必须通过这个参数生成静态页面。
这种方案本质上是为搜索引擎提供单独页面,以供爬虫收录。
目前流行的前端路由库,大多是使用了HTML5 History API,通过这种方式,使得前端hash跳转同样能够很好的记录历史,兼容浏览器的前进后退按钮,提供良好的用户体验。同时也都提供history模式,例如vue-router:
const router = new VueRouter({ mode: 'history', routes: routes });
这种模式下,加上服务端的配合,能够使前端路由更加接近后端路由,提供更加友好的url,
例如: http://domain.com/user/tom 等价于 非history模式下的http://domain.com/#/user/tom
至于如何设置服务端,可以参看vue router教程history-mode;
因为网页的的地址发生了变化,浏览器会发起请求,但由于服务端设置,其实访问的还是同一个资源。这种模式下,其实SEO就可以使用我们下面介绍的方案。
首屏渲染主要内容到noscript标签
这个方案是阮一峰的一篇文章如何让搜索引擎抓取AJAX内容?里提到的,也是我们最终采用的方案。
这个方案的主要思想是:
利用History api 实现前端路由跳转
通过服务端配置,支持不带#号的URL(这个可酌情考虑,是否有必要)
通过服务端将页面主要内容渲染近<noscript>标签,供搜索爬虫抓取
这种模式下,不仅使页面更好的被搜索引擎收录,同时使网站在禁用javascript的时候,也能够浏览基本的帖子内容。
项目实际操作
我们使用了第二种方案,来做网站的SEO。
后端提供了一套机制来将页面的主要内容渲染进模板,供搜索引擎收录。首次渲染之后,如果是用户正常访问页面,后续的翻页其实是ajax请求接口,获取数据后渲染进页面。如果是爬虫或者禁用js的情况下,页面通过noscript提供收录内容和渲染页面。
先来看我们列表页的结构:
<body> <div id="app"></div> <noscript> <!--板块列表--> <div class="item"> <?php if (isset($data_seo['forums'])): ?> <?php foreach ($data_seo['forums'] as $key => $value): ?> <div title="<?=$value['group']?>" class="item"> <h1 title="<?=$value['group']?>"><?=$value['group']?></h1> <div> <?php foreach ($value['list'] as $_k => $_v): ?> <a title="魅族社区板块<?=$_v['name']?>" href="<?=$_v['url']?>"><?=$_v['name']?></a> <?php endforeach ?> </div> </div> <?php endforeach ?> <?php endif ?> </div> <!--热门推荐列表--> <?php if (isset($data_seo['list'])): ?> <div> <?php foreach ((array)$data_seo['list'] as $key => $value): ?> <a href="<?=$value['url']?>" title="<?=$value['subject']?>" target="_blank" class="item"> <h1><?=$value['subject']?></h1> <div class="info"> <div class="author"> <span title="作者"><?=$value['author']?></span> <img src="<?=$value['avatar']?>" title="<?=$value['author']?>的头像" alt="<?=$value['author']?>" /> </div> <div class="view"> <span title="回复数"><?=$value['replies']?></span> <span title="浏览数"><?=$value['views']?></span> </div> </div> <!--图片搜索--> <div class="image"> <img src="<?=$value['pic']?>" title="<?=$value['subject']?>" alt="<?=$value['subject']?>" /> </div> </a> <?php endforeach ?> </div> <?php endif ?> <?=isset($data_pager) ? $data_pager : ''?> </noscript> <!-- built files will be auto injected --> </body>
在禁用js(爬虫访问时),得到的dom结构如下图
这样浏览器即使禁用了js,依然能够显示出网站的关键内容,而页面上的网址也是爬虫继续收录的入口。
优化
其实,上面的方案在首屏渲染的时候,已经包含了页面所需的数据,而这些数据是可以被js渲染页面时所利用的,将首屏数据渲染进js变量,就可以减少首屏渲染的http请求。
例如,我们将首屏的列表数据,渲染进全局变量,对应的地址: https://domain/forum-22-1.htm...
<script type="text/javascript"> var data_index_list = <?=isset($data_index_list) ? $data_index_list : 0?>; var data_current_page = <?=isset($data_current_page) ? $data_current_page : 0?>; </script>
然后在vuex获取列表数据时,我们就可以判断,如果当前页面前端路由的页面和后端的当前页面是同一个,就直接在data_thread_list 取数据:
[actions.FETCH_FORUM_LIST]({commit, state}, params) { commit(actions.FETCH_FORUM_LIST_PENDING); if (window.data_current_page === params.page) { // 如果当前前端路由的页面和后端的当前页面是同一个,就直接在data_thread_list 取数据 let forumlistData = window.data_thread_list.data; commit(actions.FETCH_FORUM_LIST_SUCCESS, forumlistData); return; } axios.get() // ajax请求获取页面数据。 }
这样一来,当页面首次渲染时,我们就不需要发起任何ajax请求: