it-swarm.cn

如何使SPA SEO可抓取?

我一直在研究如何根据谷歌 说明 谷歌谷歌抓取SPA。尽管有很多一般性的解释,但我找不到更详尽的逐步教程和实际示例。完成此操作后,我想分享我的解决方案,以便其他人也可以使用它,并可能进一步改进它。
我正在MVC使用Webapi控制器,和 Phantomjs 在服务器端,和 Durandal 在客户端使用Push-state;我也使用 Breezejs 用于客户端 - 服务器数据交互,我强烈推荐所有这些,但我会尝试给出一个足够的解释,也可以帮助人们使用其他平台。

143
beamish

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

客户端

在客户端,您只有一个html页面,它通过AJAX调用动态地与服务器交互。这就是SPA的意义所在。客户端中的所有a标记都是在我的应用程序中动态创建的,稍后我们将看到如何在服务器中将这些链接显示为google的bot。每个这样的a标记都需要能够在href标记中包含pretty URL,以便google的bot会抓取它。您不希望在客户端点击它时使用href部分(即使您确实希望服务器能够解析它,我们稍后会看到),因为我们可能不希望加载新页面,只是让AJAX调用获取一些数据显示在页面的一部分,并通过javascript更改URL(例如使用HTML5 pushstateDurandaljs)。因此,我们同时拥有google和hrefonclick属性,当用户点击链接时,该属性可以完成。现在,因为我使用Push-state我不希望在URL上有任何#,所以典型的a标记可能如下所示:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>

'category'和'subCategory'可能是其他短语,例如电子商店的'通信'和'电话'或'计算机'和'笔记本电脑'。显然会有许多不同的类别和子类别。如您所见,链接直接指向类别,子类别和产品,而不是作为特定“商店”页面的额外参数,例如http://www.xyz.com/store/category/subCategory/product111。这是因为我更喜欢更短更简单的链接。这意味着我不会有一个与我的一个“页面”同名的类别,即“关于”。
我不会介绍如何通过AJAX(onclick部分)加载数据,在google上搜索,有很多很好的解释。我想提到的唯一重要的事情是,当用户点击此链接时,我希望浏览器中的URL看起来像这样:
http://www.xyz.com/category/subCategory/product111。这是URL不发送到服务器!记住,这是一个SPA,客户端和服务器之间的所有交互都是通过AJAX完成的,根本没有链接!所有“页面”都在客户端实现,不同的URL不会调用服务器(服务器确实需要知道如何处理这些URL,以防它们被用作从另一个站点到您站点的外部链接,我们稍后会在服务器端部分看到)。现在,Durandal精彩地处理了这个问题。我强烈推荐它,但如果您更喜欢其他技术,也可以跳过此部分。如果您选择它,并且您也像我一样使用MS Visual Studio Express 2012 for Web,您可以安装 Durandal Starter Kit ,并在Shell.js中使用以下内容:

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标记。您需要添加以下标记,以便谷歌的机器人知道如何处理它:
    <meta name="fragment" content="!">。这个标签会让google的机器人将URL转换为www.xyz.com?_escaped_fragment_=,稍后我们会看到。
  2. “关于”路线只是您在Web应用程序中可能需要的其他“页面”链接的示例。
  3. 现在,棘手的部分是没有“类别”路线,并且可能有许多不同的类别 - 其中没有一个具有预定义的路线。这是mapUnknownRoutes的来源。它将这些未知路线映射到'store'路线,并删除任何'!'来自URL,如果它是由google的搜索引擎生成的pretty URL。 'store'路径获取'fragment'属性中的信息并进行AJAX调用以获取数据,显示数据并在本地更改URL。在我的应用程序中,我没有为每个这样的调用加载不同的页面;我只更改了与此数据相关的页面部分,并在本地更改了URL。
  4. 注意pushState:true指示Durandal使用Push状态URL。

这就是我们在客户端所需要的一切。它也可以用散列URL实现(在Durandal中你可以简单地删除pushState:true)。更复杂的部分(至少对我而言......)是服务器部分:

服务器端

我在WebAPI控制器的服务器端使用MVC 4.5。服务器实际上需要处理3种类型的URL:由google生成的URL - prettyugly以及与客户端浏览器中显示的格式相同的“简单”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加载数据的客户端,因此这里的技巧是调用默认的“索引”控制器,就像没有引用任何控制器一样;我这样做 加入 所有'category'和'subCategory'参数之前的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;
        }
    }
}


但那个怎么样? 丑陋的网址?这些是由google的bot创建的,应该返回包含用户在浏览器中看到的所有数据的纯HTML。为此,我使用 phantomjs 。 Phantom是一个无头浏览器,可以在客户端执行浏览器操作 - 但在服务器端。换句话说,幻影知道(除其他外)如何通过URL获取网页,解析它包括运行其中的所有javascript代码(以及通过AJAX调用获取数据),并给出你支持反映DOM的HTML。如果你正在使用MS Visual Studio Express,你很多人想通过这个 link 安装幻像。
但首先,当一个丑陋的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中,并且幻像无法知道所有调用何时完成,然后再返回给我的HTML反射DOM。我在这里做的是在最后的AJAX调用之后我添加一个<span id='compositionComplete'></span>,这样如果这个标签存在,我就知道DOM已经完成了。我这样做是为了回应Durandal的compositionComplete事件,请参阅 here 了解更多信息。如果10秒钟没有发生这种情况,我会放弃(最多只需要一秒钟)。返回的HTML包含用户在浏览器中看到的所有链接。该脚本无法正常运行,因为HTML快照中存在的<script>标记未引用正确的URL。这也可以在javascript幻像文件中更改,但我不认为这是必要的,因为HTML快照只用于谷歌获取a链接而不是运行javascript;这些链接  引用一个漂亮的URL,如果你试图在浏览器中看到HTML快照,你会得到javascript错误,但是所有的链接都能正常工作,并且再次使用漂亮的URL将你引导到服务器工作页面。
就是这个。现在服务器知道如何处理漂亮和丑陋的URL,在服务器和客户端上都启用了Push-state。所有丑陋的URL都使用幻像以相同的方式处理,因此不需要为每种类型的调用创建单独的控制器。
你可能更喜欢改变的一件事是不要进行一般的'category/subCategory/product'调用,而是添加一个'store',使链接看起来像:http://www.xyz.com/store/category/subCategory/product111。这将避免我的解决方案中的问题,即所有无效的URL都被视为实际调用'index'控制器,我想这些可以在'store'控制器中处理,而不添加web.config我显示以上。

122
beamish

Google现在能够呈现SPA页面: 弃用我们的AJAX抓取方案

32
Edward Olamisan

这是我8月14日在伦敦举办的Ember.js培训班的截屏视频链接。它概述了客户端应用程序和服务器端应用程序的策略,并且实时演示了实现这些功能如何为JavaScript单页面应用程序提供优雅降级,即使对于关闭JavaScript的用户也是如此。

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

简而言之,所需的步骤是:

  • 拥有要爬网的Web应用程序的托管版本,此站点需要拥有生产中的所有数据
  • 编写JavaScript应用程序(PhantomJS Script)来加载您的网站
  • 将index.html(或“/”)添加到要抓取的网址列表中
    • 弹出添加到爬网列表的第一个URL
    • 加载页面并呈现其DOM
    • 在加载的页面上查找链接到您自己站点的任何链接(URL过滤)
    • 如果尚未抓取,则将此链接添加到“可抓取”网址列表中
    • 将呈现的DOM存储到文件系统上的文件中,但首先剥离所有脚本标记
    • 最后,使用已爬网的URL创建Sitemap.xml文件

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

链接到截屏视频的完整详细信息:

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

4
Joachim H. Skeie

您可以使用 http://sparender.com/ ,它可以正确地抓取单页应用程序。

0
ddtxra

您可以使用或创建自己的服务,通过名为prerender的服务预呈现您的SPA。您可以在他的网站上查看 prerender.io 和他的 github项目 (它使用PhantomJS并为您渲染您的网站)。

这很容易入手。您只需将爬虫请求重定向到服务,他们就会收到呈现的html。

0
gabrielperales