技术博客

越是喧闹,越是孤独。越是寂寞,越是丰富
The more noisy, the more lonely. The more lonely, the more rich

越是喧闹,越是孤独。越是寂寞,越是丰富
The more noisy, the more lonely. The more lonely, the more rich

技术博客


使用 Puppeteer 处理单页应用(SPA)的 SEO

2023-12-19 Mendel
后端


最近,“科目三”的舞蹈爆红,虽然该舞蹈跟驾考的科目三考试关系不大,但是“科目三”已经成为我的网站的搜索关键词之一,同时还有“科目二”。于是打算趁这个热度,整理一下目前我的网站对于SEO(Search Engine Optimization,搜索引擎优化)的处理方式。



01

Bing对本站的收录



我的网站于2021年4月部署上线,并同步上线了科目二和科目三的网页版模拟程序,如下图所示。

可以到“编程作品”里查看 https://mengchen.cc/applications)


当时因为刚拿到驾照,同时想丰富一下网站的内容,于是趁热打铁做了这两个小程序。之后我在公众号上发布了科目二/三的学习笔记,并在“海淀驾校”APP上发帖引流,虽然最后粉丝也不多,但也算是一次有意义的尝试,也有学车的小伙伴对我表示了称赞,还是颇有成就感的。

(截图来自 https://zhuanlan.zhihu.com/p/371224539)


后来我将这两个页面提交给百度、Bing、Google等搜索引擎收录,起初也就是碰碰运气,没指望能收录到什么程度,但万万没想到的是,Bing居然对这两个页面收录得还不错!


搜索“科目二模拟”,我的页面排第一:


搜索“科目三灯光模拟”,我的页面依然排第一:


这下就有点让我喜出望外了!我只是做了两个很简单的页面,咋就能排行第一了呢?同时,这两个页面也成了我的网站的主要流量,由于百度并没有收录我的科目二/三的关键词,所以我的流量基本上是Bing提供的。虽然Bing在国内的市场占有率不及百度,但鉴于Bing内置于Edge浏览器,而Edge又内置于Windows系统,对于国内庞大的windows用户群体,所以也是有一定流量的入口。另外我发现在夏季的时候,网站访问量会增大一些,我猜测应该是暑假学车的人比较多的原因。


通过Bing Webmaster,可以看到我的网站的各个关键词的搜索排名:


可以看出,基本上所有关于“科目二模拟”和“科三灯光模拟的关键词都被我“霸占”了,这些词的排名基本上都是数一数二。我感到开心的同时,也有一丝惭愧,毕竟我觉得我做的这两个程序还有不足,却长期霸占搜索榜第一,若不能给用户带来有效的帮助,有点“德不配位”的感觉,哈哈哈。


言归正传,本文主要是来介绍一下我是如何对我的单页应用(Single Page Application,SPA)进行SEO的。





02


使用Puppeteer处理SEO



我的网站主站是Vue+VueRouter的单页面应用,纯前端构建,用Nginx部署静态页面,这样带来的问题就是对搜索引擎不友好。多数搜索引擎一般只能获取首次请求从服务端返回的网页源代码,无法通过执行页面中的异步脚本来获取动态内容。而我的网站用的前后端分离,静态页面和API是两个不同的域名,静态页面通过Ajax获取数据并渲染内容。因此对于爬虫们来说,它们通常并不能获取到我网站的实际内容,进而会影响收录情况。


关于SPA的SEO问题,一种解决方案就是用SSR(Server-Side Rendering,服务端渲染)。但经评估后发现如果使用Vue SSR改造,工作量不小,而且我更倾向于前端渲染的开发过程和交互方式。相比之下,预渲染(Pre-rendering)或静态站点生成(SSG,Static-Site Generation)的方式更适合我。关于SSR和SSG这两方式在Vue的官方文档中有详细介绍(接见录)


我最终采用的方式,是一种懒加载(lazy-load)的预渲染,我称之为“按需预渲染”。其大致流程是,在Nginx端针对普通请求与爬虫请求执行不同的操作,普通请求直接返回原始的静态文件并在前端动态渲染,而爬虫请求,则是被重定向到另一个模块,这个模块将会启动puppeteer去加载页面,并将渲染后的页面内容返回。整体大致流程图为:


关于判断请求是否是爬虫请求,我们可以直接从user-agent中查找是否含有"spider"或者"bot"字样即可,同时对css/js/txt/ico等静态文件的爬虫请求不做额外处理。最终nginx配置文件代码如下:

location / {    try_files $uri $uri/ /index.html;
# 0 - Non-static files 1 - Static files set $flag 0;
if ($uri ~* "\.css|\.js|\.txt|\.ico") { set $flag 1; }
if ($http_user_agent ~* "spider|bot") { set $flag 1$flag; }
# SEO Condition: Spider && Non-static files if ($flag = 10) { proxy_pass http://127.0.0.1:3001; }
}


在Node.js程序中启动Puppeteer加载页面的主要代码如下:

if (!globalBrowser) {    globalBrowser = await puppeteer.launch({        headless: true,        args: ['--no-sandbox']    });}
const page = await globalBrowser.newPage();await page.setCacheEnabled(false);await page.goto(url);
try { await page.waitForSelector('.seo-tag', { timeout: 3000 });} catch (err) { error(`Error: page.waitForSelector, URL: ${url} message: ${err.message}`);}let renderedContent = await page.content();
await page.close()


其中globalBrowser是通过puppeteer创建的headless browser全局变量,随server进程只保留一份,通过该变量创建不同的page来加载相应的页面。同时为了指定何时加载完成,这里我选择了等待某个css选择器的方式,我将我的网站页面的主要内容部分的外层div加上class="seo-tag",这样当seo-tag这个元素出现时,也就说明网页的内容已渲染完成,此时puppeteer检测到这个元素出现,就可以通过page.content()获取渲染后的html代码。


为了提升性能,将页面渲染后的内容保存到文件,同时在Redis中缓存当前爬虫请求的url,这样可以在缓存期内对相同的请求可以立即返回,减少开销。


关于Puppeteer,我们直接拿官网(链接见附录)中的介绍:

Puppeteer is a Node.js library which provides a high-level API to control Chrome/Chromium over the DevTools Protocol. Puppeteer runs in headless mode by default, but can be configured to run in full ("headful") Chrome/Chromium.

Puppeteer能做哪些事情呢?

  • Generate screenshots and PDFs of pages.

  • Crawl a SPA (Single-Page Application) and generate pre-rendered content (i.e. "SSR" (Server-Side Rendering)).

  • Automate form submission, UI testing, keyboard input, etc.

  • Create an automated testing environment using the latest JavaScript and browser features.

  • Capture a timeline trace of your site to help diagnose performance issues.

  • Test Chrome Extensions.


其中第二点就是本文所介绍的——抓取SPA页面并生成预渲染的内容。关于第一点:生成网页屏幕截图和保存网页为PDF也非常有用,我们将在下一期中介绍。


(全文完)



附录链接:

1、Vue SSR

https://cn.vuejs.org/guide/scaling-up/ssr.html

2、Puppeteer

https://pptr.dev/