SEO 就是让搜索引擎了解网站,在用户进行关键词搜索时,网站能出现在搜索结果中比较靠前的位置。
搜索引擎如何工作
- 找出存在的网页:跟踪已知网页上的链接、与网站托管服务合作、站长提交站点地图;
- 编入索引:解析网页内容并构建搜索索引,网页的标题、关键词和描述可以改善编入效果;
- 呈现:根据搜索内容和其他因素(位置、语言、设备等)从索引中找到最相关的信息返回给用户;
- 排名:提高网页加载速度并保持一定的更新频率有利于网站排名。
搜索引擎蜘蛛
蜘蛛是搜索引擎抓用来页面信息和构建信息索引的工具,例如:
Baiduspider
和
Googlebot
;蜘蛛在访问页面的过程中,处理文字内容、关键内容标记和属性,例如网站标题、Alt 属性、图片、视频等;此外,蜘蛛还会查找页面上的链接,并将这些链接添加到它要抓取的网页的列表中;这张列表记录着新建立的网站、对现有网站进行的更改以及无效链接,并据此更新索引。
百度曾发布过一个
公告
,提到 Baiduspider 会访问页面的 CSS、JS 和图片资源,但是 JS 会不会执行、JS 的执行结果会不会被编入索引则没有明说;Google 在
文档
中指出:Googlebot 支持对 SPA 和动态内容的抓取,在抓取过程中会使用 Chrome 加载网页并运行 JS。
显然,搜索引擎更喜欢“有内容的”HTML。
SPA 如何产生“有内容的”HTML
预渲染
在客户端访问之前提前渲染好 HTML(一般是在打包阶段);在客户端初次访问页面时,会从静态服务器请求事先渲染好的 HTML;之后 JS 会接管交互、渲染和路由等工作;这是一种混合模式,从客户端来看,页面的行为就像 SPA 一样。
优点:
- 更好的 SEO,爬虫可以直接抓取页面内容;
- 更快的(非用户相关内容的)首屏渲染。
缺点:
- 无法实时更新静态服务器端 HTML 的内容(除非在数据发生变动时重新打包部署)。
- 无法用于不可穷举的动态路由场景;
- 不支持哈希路由。
服务端同构渲染
在客户端发起请求时实时渲染 HTML,服务端总会请求最新的数据完成渲染;加载完第一个页面之后,客户端还是会进入 JS 渲染的模式(混合渲染,例如:Nuxt 和 Next);这种方式和预渲染模式的差别在于渲染 HTML 的时机不同。
优点:
- 更好的 SEO,爬虫可以直接抓取页面内容;
- 更快的首屏渲染。
缺点:
- 需要动态服务,不能 CDN 部署,有可能出现服务只能部署在 A 地,但用户却在 B 地的情况;
- 不是所有的开发依赖都支持在服务端运行;
- 动态服务器比静态服务器更耗资源;
- 客户端与服务端的状态同步;
- 不支持哈希路由。
基于 UA 的动态渲染
Google 搜索中心提到的一种模式,根据 UA 判断,客户端是不是“蜘蛛”;蜘蛛访问的是渲染之后(不一定是动态渲染)的 HTML,普通用户访问的还是一个 SPA。这种模式将客户端分为“用户”和“蜘蛛”两种,可以减轻服务器压力、对搜索引擎抓取的内容进行针对性的优化。
预渲染配置
通过 HtmlWebpackPlugin 配置项目 TWD:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
const title = '我是标题...';
const meta = {
keywords: '我是,关键词,...',
description: '我是描述...'
};
module.exports = {
plugins: [
new HtmlWebpackPlugin({
entry: 'src/project/home/main.js',
template: 'public/main.html',
filename: 'index.html',
chunks: ['chunk-vendors', 'chunk-common', 'home'],
title,
meta
})
]
};
|
配置 main.js 触发预渲染事件:
1
2
3
4
5
6
7
8
9
|
new Vue({
el: '#app',
router,
store,
render: h => h(App),
mounted() {
document.dispatchEvent(new Event('render-event'));
}
});
|
预渲染完成之后,根结点会被替换掉;需要在项目根组件添加对应的 id,否则会报“Cannot find element: #xxx”错误:
1
2
3
4
5
6
7
8
9
10
11
|
<template>
<div id="app" class="app">
<top-header :isHome="true" />
<div class="router-view">
<transition name="rv">
<router-view />
</transition>
</div>
<bottom-footer />
</div>
</template>
|
引入 PrerenderSPAPlugin 插件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
const PrerenderSPAPlugin = require('prerender-spa-plugin');
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
module.exports = {
plugins: [
// 在非开发环境下执行
!process.env.LOCAL_DEVELOP &&
new PrerenderSPAPlugin({
// 打包输出目录,也可以配置 outputDir 将预渲染文件输出到单独的目录
staticDir: path.join(__dirname, 'dist'),
// 对输出的 HTML 压缩配置(html-minifier)
minify: {
collapseBooleanAttributes: true,
removeEmptyAttributes: false,
removeComments: true,
minifyCSS: true,
minifyJS: true
},
// 渲染器配置
renderer: new Renderer({
// 挂载在 window 上属性
injectProperty: '__PRERENDER__INJECTED__',
// 挂载在 window.__PRERENDER__INJECTED__ 的对象
inject: {
hostname: prerenderHostname()
},
// 是否显示浏览器(多用于调试,开启之后部分打包环境会打包失败)
headless: true,
// 在预渲染之前等待的事件
renderAfterTime: 500,
// 最多同时渲染多少个路由
maxConcurrentRoutes: 1,
// 触发渲染的事件名称
renderAfterDocumentEvent: 'render-event',
// 浏览器运行参数
args: ['--no-sandbox', '--disable-setuid-sandbox']
}),
// 请求代理配置
proxy: {
'^/api/exp': {
target: 'https://www.example.com'
},
changeOrigin: true
},
// 要预渲染的路由
routes: [
'/',
'/introduction/service-one',
'/introduction/service-two',
'/solution/way-one',
'/solution/way-two',
'/support/ask',
'/about/us'
]
})
]
};
|
为了 SEO,你可能需要 extract-text-webpack-plugin 将样式单独打包。
PrerenderSPAPlugin 逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
function PrerenderSPAPlugin(...args) {
// 对入参做处理
this._options = args[0] || {};
}
PrerenderSPAPlugin.prototype.apply(compiler => {
// apply 方法在安装插件时,会被 Webpack 编译器调用一次,在异步操作完成之后调用 callback
const afterEmit = (compilation, callback) => {
// Prerenderer 会开启 express 服务器和 puppeteer 实例
const prender = new Prerenderer(this._options);
prender
.initialize()
// express 会代理静态目录,通过 puppeteer 访问页面并将页面保存
.then(() => prender.renderRoutes(this._options.routes || []))
.then(renderedRoutes => {
/**
* [{
* originalRoute: route,
* route: await page.evaluate('window.location.pathname'),
* html: await page.content()
* }, {...}...]
*/
const isValid = renderedRoutes.every(r => typeof r === 'object');
if (isValid) {
return renderedRoutes;
} else {
throw new Error('...');
}
})
.then(renderedRoutes => {
if (this._options.minify) {
// minify...
return renderedRoutes;
}
return renderedRoutes;
})
.then(renderedRoutes =>
Promise.all(
renderedRoutes.map(
renderedRoute =>
new Promise((resolve, reject) => {
// write files...
resolve();
})
)
)
)
.then(() => {
// 完成文件写入之后退出
prender.destory();
callback();
})
.catch(error => {
prender.destory();
// 如果有报错,在编译过程抛出
compilation.errors.push(new Error(error));
callback();
});
};
const plugin = { name: 'PrerenderSPAPlugin' };
// 向编译器的钩子上注册异步方法,可以对当前编译实例(过程)做操作
compiler.hooks.afterEmit.tapAsync(plugin, afterEmit);
});
|
两种文件
公司的 SEO 服务商让在服务器根目录放了几个“文件”,最有用的是两种是 Sitemap 和 robots。
Sitemap 是用来告诉蜘蛛网站上有哪些可供抓取的网页;Sitemap 可以是 XML、TXT、HTML 文件,其中包含了站点中的网址以及关于每个网址的元数据(更新时间、更新频率、重要程度、语言版本等),以便搜索引擎可以更加智能地抓取网站;Sitemap 并不能保证网页会被蜘蛛抓取,但可向蜘蛛提供一些提示以便网站被更有效地抓取:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
<?xml version='1.0' encoding='UTF-8'?>
<urlset>
<url>
<loc><![CDATA[https://www.example.com]]></loc>
<lastmod>2020-10-28</lastmod>
<changefreq>weekly</changefreq>
</url>
<url>
<loc><![CDATA[https://www.example.com/about/contact]]></loc>
<lastmod>2020-10-28</lastmod>
<changefreq>weekly</changefreq>
</url>
<url>
<loc><![CDATA[https://www.example.com/about/us]]></loc>
<lastmod>2020-10-28</lastmod>
<changefreq>weekly</changefreq>
</url>
<url>
<loc><![CDATA[https://www.example.com/solution/agriculture]]></loc>
<lastmod>2020-10-28</lastmod>
<changefreq>weekly</changefreq>
</url>
<url>
<loc><![CDATA[https://www.example.com/solution/hybrid]]></loc>
<lastmod>2020-10-28</lastmod>
<changefreq>weekly</changefreq>
</url>
<url>
<loc><![CDATA[https://www.example.com/support/ask]]></loc>
<lastmod>2020-10-28</lastmod>
<changefreq>weekly</changefreq>
</url>
</urlset>
|
Sitemap 适用情形:
- 网站规模很大,蜘蛛有可能在抓取时漏掉部分新网页或最近更新的网页;
- 有大量内容页归档,并且内容页之间互不关联或缺少有效链接;
- 网站为新网站且指向该网站的外部链接不多;
- 您的网站包含大量媒体内容(视频、图片)。
robots 文件是用来告诉搜索引擎,网站上的哪些页面可以抓取,哪些页面不能抓取;User-agent、Allow、Disallow 等字段精确地对不同搜索引擎的抓取工作进行控制:
1
2
3
4
5
6
7
8
9
|
User-agent: Googlebot
Disallow: /
Allow: /content/
User-agent: Baiduspider
Disallow: /admin/
Sitemap: https://www.example.com/sitemap.txt
Sitemap: https://www.example.com/sitemap.html
|
robots 适用情形:
- 在某个阶段不公开某些网页 (例如:在搭建网站雏形时);
- 防止抓取内部搜索页面;
- 防止抓取重复页面;
- 防止服务器过载。
参考