如何使 SPA 搜索引擎优化爬行?

我一直在研究如何使一个水疗抓取谷歌的基础上的 指示的谷歌。即使有相当多的一般性解释,我找不到任何地方更彻底的一步一步的教程与实际例子。完成后,我想分享我的解决方案,以便其他人也可以使用它,并可能进一步改进它。
我使用 MVCWebapi控制器,和 幻影在服务器端,和 Durandal在客户端与 push-state启用; 我也使用 微风的客户端-服务器数据交互,所有这些我强烈推荐,但我会试图给出一个一般性的足够的解释,也将帮助人们使用其他平台。< br/>

35433 次浏览

在开始之前,请确保您了解什么谷歌 需要,特别是使用 漂亮丑陋的网址。现在让我们看看实现:

客户端

在客户端,您只有一个 html 页面,它通过 AJAX 调用与服务器动态交互。这就是 SPA 的意义所在。客户端的所有 a标签都是在我的应用程序中动态创建的,我们稍后将看到如何使这些链接对服务器中的谷歌机器人可见。每个这样的 a标签需要能够有一个 pretty URLhref标签,以便谷歌的机器人将抓取它。当客户端点击它时,你不希望使用 href部分(即使你希望服务器能够解析它,我们稍后会看到这一点) ,因为我们可能不希望一个新的页面加载,只是让一个 AJAX 调用获得一些数据显示在页面的一部分,并通过 javascript 改变 URL (例如使用 HTML5 pushstate或与 Durandaljs)。因此,我们既有一个谷歌的 href属性,以及对 onclick的工作时,用户点击链接。现在,因为我使用的是 push-state,所以我不希望在 URL 上有任何 a0,所以一个典型的 a标签可能看起来像这样:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a> < br/> < br/> “分类”和“子分类”可能是其他短语,如“通讯”和“电话”或“电脑”和“笔记本电脑”的电器商店。显然会有许多不同的类别和子类别。如您所见,链接直接指向类别、子类别和产品,而不是作为特定“ store”页面(如 http://www.xyz.com/store/category/subCategory/product111)的额外参数。这是因为我喜欢更短和更简单的链接。这意味着我不会有一个类别与我的一个’页面’,即’关于’相同的名称。
我不会进入如何加载数据通过 AJAX (onclick的一部分) ,搜索它在谷歌,有很多很好的解释。这里我想提到的唯一重要的事情是,当用户点击这个链接时,我希望浏览器中的 URL 是这样的:
http://www.xyz.com/category/subCategory/product111.而这个 URL 是不会发送到服务器的!记住,这是一个 SPA,客户端和服务器之间的所有交互都是通过 AJAX 完成的,根本没有链接!所有的“页面”都是在客户端实现的,不同的 URL 不会对服务器进行调用(服务器确实需要知道如何处理这些 URL,以防它们被用作从另一个站点到您的站点的外部链接,我们稍后将在服务器端部分看到这一点)。Durandal 处理得很好。我强烈推荐它,但是如果您更喜欢其他技术,也可以跳过这一部分。如果你选择了它,并且你也像我一样使用 MS Visual Studio Express 2012 for Web,你可以安装 Durandal 初学者工具包,在 shell.js中,使用这样的东西: < br/>

define(['plugins/router', 'durandal/app'], function (router, app) {
return {
router: router,
activate: function () {
router.map([
{ route: '', title: 'Store', moduleId: 'viewmodels/store', nav: true },
{ route: 'about', moduleId: 'viewmodels/about', nav: true }
])
.buildNavigationModel()
.mapUnknownRoutes(function (instruction) {
instruction.config.moduleId = 'viewmodels/store';
instruction.fragment = instruction.fragment.replace("!/", ""); // for pretty-URLs, '#' already removed because of push-state, only ! remains
return instruction;
});
return router.activate({ pushState: true });
}
};
});

这里有一些重要的事情需要注意:

  1. 第一个路由(使用 route:'')是没有额外数据的 URL,即 http://www.xyz.com。在这个页面中,您使用 AJAX 加载一般数据。实际上,这个页面中可能根本没有 a标记。你可以添加下面的标签,这样谷歌的机器人就知道该怎么做了:
    这个标签将使谷歌的机器人转换的网址为 www.xyz.com?_escaped_fragment_=,我们将在稍后看到。
  2. “ about”路由只是一个示例,指向你的 web 应用程序中可能需要的其他“页面”的链接。
  3. 现在,棘手的部分是,没有“类别”路线,可能有许多不同的类别-其中没有一个预定义的路线。这就是 mapUnknownRoutes的用武之地。它将这些未知的路由映射到“存储”路由,并删除任何路由!从网址,以防它是一个由谷歌搜索引擎生成的 pretty URL。“ store”路由获取“片段”属性中的信息,并进行 AJAX 调用以获取数据、显示数据并在本地更改 URL。在我的应用程序中,我不会为每个这样的调用加载不同的页面; 我只是更改页面中与此数据相关的部分,并在本地更改 URL。
  4. 请注意指示 Durandal 使用 push 状态 URL 的 pushState:true

这就是我们在客户端所需要的。它也可以通过散列 URL 来实现(在 Durandal,你只需删除它的 pushState:true)。更复杂的部分(至少对我来说)是服务器部分:

服务器端

我在服务器端使用 MVC 4.5WebAPI控制器。服务器实际上需要处理3种类型的 URL: 由 google 生成的 URL ——包括 prettyugly,还有一个“简单”的 URL,其格式与出现在客户端浏览器中的 URL 相同。让我们看看如何做到这一点:

漂亮的 URL 和“简单的”URL 首先由服务器解释,就好像试图引用一个不存在的控制器。服务器看到类似于 http://www.xyz.com/category/subCategory/product111的东西,并寻找一个名为“ Category”的控制器。因此,在 web.config中,我添加了以下代码行,将它们重定向到一个特定的错误处理控制器:

<customErrors mode="On" defaultRedirect="Error">
<error statusCode="404" redirect="Error" />
</customErrors><br/>

现在,这将 URL 转换为类似于: http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111的内容。我希望 URL 被发送到客户端,将通过 AJAX 加载数据,所以这里的技巧是调用默认的“索引”控制器,就像没有引用任何控制器; 我通过 增加在所有的“类别”和“子类别”参数之前对 URL 进行散列; 散列 URL 不需要任何特殊的控制器,除了默认的“索引”控制器和数据被发送到客户端,然后删除散列后使用散列后的信息通过 AJAX 加载数据。下面是错误处理程序控制器代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;


using System.Web.Routing;


namespace eShop.Controllers
{
public class ErrorController : ApiController
{
[HttpGet, HttpPost, HttpPut, HttpDelete, HttpHead, HttpOptions, AcceptVerbs("PATCH"), AllowAnonymous]
public HttpResponseMessage Handle404()
{
string [] parts = Request.RequestUri.OriginalString.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries);
string parameters = parts[ 1 ].Replace("aspxerrorpath=","");
var response = Request.CreateResponse(HttpStatusCode.Redirect);
response.Headers.Location = new Uri(parts[0].Replace("Error","") + string.Format("#{0}", parameters));
return response;
}
}
}


丑陋的网址呢?这些都是由谷歌的机器人创建的,应该返回包含用户在浏览器中看到的所有数据的纯 HTML。为此,我使用 幻影。Phantom 是一个无头浏览器,它在客户端做着浏览器正在做的事情——但是在服务器端。换句话说,幽灵知道(在其他事情中)如何通过 URL 获取网页,解析它,包括运行其中的所有 javascript 代码(以及通过 AJAX 调用获取数据) ,并返回反映 DOM 的 HTML。如果你正在使用微软视觉工作室快车,你很多想安装幻灯片通过这个 链接
但首先,当一个难看的 URL 被发送到服务器时,我们必须捕获它; 为此,我在“ App _ start”文件夹中添加了以下文件:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;


namespace eShop.App_Start
{
public class AjaxCrawlableAttribute : ActionFilterAttribute
{
private const string Fragment = "_escaped_fragment_";


public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var request = filterContext.RequestContext.HttpContext.Request;


if (request.QueryString[Fragment] != null)
{


var url = request.Url.ToString().Replace("?_escaped_fragment_=", "#");


filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary { { "controller", "HtmlSnapshot" }, { "action", "returnHTML" }, { "url", url } });
}
return;
}
}
}

这在‘ App _ start’中也被称为‘ filterConfig.cs’:

using System.Web.Mvc;
using eShop.App_Start;


namespace eShop
{
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
filters.Add(new AjaxCrawlableAttribute());
}
}
}

可以看到,‘ AjaxCrawlableAttribute’将难看的 URL 路由到一个名为‘ HtmlSnapshot’的控制器,下面是这个控制器:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;


namespace eShop.Controllers
{
public class HtmlSnapshotController : Controller
{
public ActionResult returnHTML(string url)
{
string appRoot = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);


var startInfo = new ProcessStartInfo
{
Arguments = String.Format("{0} {1}", Path.Combine(appRoot, "seo\\createSnapshot.js"), url),
FileName = Path.Combine(appRoot, "bin\\phantomjs.exe"),
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
StandardOutputEncoding = System.Text.Encoding.UTF8
};
var p = new Process();
p.StartInfo = startInfo;
p.Start();
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
ViewData["result"] = output;
return View();
}


}
}

相关的 view非常简单,只需要一行代码:
@Html.Raw( ViewBag.result )
正如您在控制器中看到的那样,幻象在我创建的名为 seo的文件夹下加载一个名为 createSnapshot.js的 javascript 文件。下面是这个 javascript 文件:

var page = require('webpage').create();
var system = require('system');


var lastReceived = new Date().getTime();
var requestCount = 0;
var responseCount = 0;
var requestIds = [];
var startTime = new Date().getTime();


page.onResourceReceived = function (response) {
if (requestIds.indexOf(response.id) !== -1) {
lastReceived = new Date().getTime();
responseCount++;
requestIds[requestIds.indexOf(response.id)] = null;
}
};
page.onResourceRequested = function (request) {
if (requestIds.indexOf(request.id) === -1) {
requestIds.push(request.id);
requestCount++;
}
};


function checkLoaded() {
return page.evaluate(function () {
return document.all["compositionComplete"];
}) != null;
}
// Open the page
page.open(system.args[1], function () { });


var checkComplete = function () {
// We don't allow it to take longer than 5 seconds but
// don't return until all requests are finished
if ((new Date().getTime() - lastReceived > 300 && requestCount === responseCount) || new Date().getTime() - startTime > 10000 || checkLoaded()) {
clearInterval(checkCompleteInterval);
var result = page.content;
//result = result.substring(0, 10000);
console.log(result);
//console.log(results);
phantom.exit();
}
}
// Let us check to see if the page is finished rendering
var checkCompleteInterval = setInterval(checkComplete, 300);

首先,我要感谢 Thomas Davis,因为在这个页面中我得到了基本代码: ——)。
您会注意到这里有些奇怪的地方: 幽灵一直重新加载页面,直到 checkLoaded()函数返回 true。为什么?这是因为我的特定 SPA 进行了几次 AJAX 调用,以获取所有数据并将其放在我页面上的 DOM 中,并且在返回 DOM 的 HTML 反射之前幽灵无法知道所有调用何时完成。我在这里所做的是在最后的 AJAX 调用之后添加一个 <span id='compositionComplete'></span>,这样如果这个标记存在,我就知道 DOM 已经完成了。我这样做是为了回应 Durandal 的 abc2事件,请参阅 abc5了解更多。如果10秒钟内没有发生这种情况,我就放弃(最多只需要一秒钟)。返回的 HTML 包含用户在浏览器中看到的所有链接。脚本将无法正常工作,因为 HTML 快照中确实存在的 <script>标记没有引用正确的 URL。这也可以在 javascript 幻像文件中改变,但我不认为这是必要的,因为 HTML 快照只被谷歌用来获取 a链接,而不是运行 javascript; 这些链接 引用一个漂亮的 URL,如果事实上,如果你试图在浏览器中看到 HTML 快照,你会得到 javascript 错误,但所有的链接将工作正常,并指导你到服务器再次与一个漂亮的 URL 这一次得到完整的工作页面。
就是这里。现在服务器知道如何处理漂亮和难看的 URL,并在服务器和客户机上启用推送状态。所有难看的 URL 都使用幽灵以相同的方式处理,因此没有必要为每种类型的调用创建单独的控制器。
您可能希望更改的一件事情是不要进行一般的“ type/subCategory/product”调用,而是添加一个“ store”,这样链接看起来就像: http://www.xyz.com/store/category/subCategory/product111。这将避免我的解决方案中的问题,即所有无效的 URL 都被视为实际上是对“ index”控制器的调用,我假设这些可以在“ store”控制器中处理,而不需要添加上面显示的 web.config

这里是我8月14日在伦敦主持的 Ember.js 培训班的一个视频录制链接。它概述了客户端应用程序和服务器端应用程序的策略,并给出了实现这些功能将如何为您的 JavaScript 单页应用程序提供优雅的降级,即使用户关闭了 JavaScript。

它使用 PhantomJS 来帮助你抓取你的网站。

简而言之,所需的步骤如下:

  • 有一个托管版本的网络应用程序,你想抓取,这个网站需要有所有的数据,你在生产
  • 编写一个 JavaScript 应用程序(PhantomJS Script)来加载您的网站
  • 将 index.html (或“/”)添加到要抓取的 URL 列表中
    • 弹出添加到爬网列表中的第一个 URL
    • 加载页面并呈现其 DOM
    • 在加载的页面上找到链接到您自己站点的任何链接(URL 过滤)
    • 将这个链接添加到一个“可抓取”的 URL 列表,如果它还没有被抓取的话
    • 将渲染的 DOM 存储到文件系统中的一个文件中,但首先要去掉所有的脚本标记
    • 最后,用抓取的 URL 创建一个 Sitemap.xml 文件

一旦完成了这一步骤,就由后端提供静态版本的 HTML 作为该页面上 noscript-tag 的一部分。这将允许谷歌和其他搜索引擎抓取你网站上的每一个页面,即使你的应用程序最初是一个单页应用程序。

详情请连结至:

Http://www.devcasts.io/p/spas-phantomjs-and-seo/#

您可以使用或创建您自己的服务,使用称为 prerender 的服务预先呈现您的 SPA。你可以在他的网站 准备好和他的 Github 项目上查看(它使用 PhantomJS 和它为你渲染你的网站)。

很容易开始。您只需要将爬虫请求重定向到服务,它们就会接收到呈现的 html。

谷歌现在可以渲染 SPA 页面: 不推荐我们的 AJAX 爬行方案

我使用 Rendertron来解决客户端的 ASP.net core和 Angular 中的 SEO 问题,它是一个基于爬虫或客户端区分请求的中间件,所以当请求来自爬虫端时,会在运行中产生简短而快速的响应。

  • 为普通客户提供的网站: http://i.stack.imgur.com/Zopw7.jpg”rel = “ nofollow noReferrer”> < img src = “ https://i.stack.imgur.com/Zopw7.jpg”alt = “ image 1”/>

  • 爬虫的渲染网站: http://i.stack.imgur.com/yZqMB.jpg”rel = “ nofollow noReferrer”> < img src = “ https://i.stack.imgur.com/yZqMB.jpg”alt = “ image 2”/>

Startup.cs

配置渲染控制器服务:

public void ConfigureServices(IServiceCollection services)
{
// Add rendertron services
services.AddRendertron(options =>
{
// rendertron service url
options.RendertronUrl = "http://rendertron:3000/render/";


// proxy url for application
options.AppProxyUrl = "http://webapplication";


// prerender for firefox
//options.UserAgents.Add("firefox");


// inject shady dom
options.InjectShadyDom = true;
        

// use http compression
options.AcceptCompression = true;
});
}

的确,这种方法有些不同,需要一段简短的代码来生成特定于爬虫程序的内容,但是它对于小型项目(如 CMS 或门户网站等)非常有用。

这种方法可以在大多数编程语言或服务器端框架中实现,如 ASP.net corePython (Django)Express.jsFirebase

查看源代码和更多细节: https://github.com/GoogleChrome/rendertron

二零二一年最新情况

  • SPA 应该使用 历史空气污染指数,以便搜索引擎优化友好。

    SPA 页面之间的转换通常通过 history.pushState(path)调用实现。接下来发生的是依赖于框架的。在使用 React 的情况下,一个名为 React Router 的组件监视 history并显示/呈现为所使用的 path配置的 React 组件。

  • 实现一个简单的 SPA 搜索引擎优化是 直截了当

  • 文章所示,为更高级的 SPA (使用选择性预渲染以获得更好的性能)实现 SEO 更为复杂。我是作者。