如何使SPA SEO可抓取?

时间:2013-08-30 10:05:18

标签: ajax seo phantomjs single-page-application durandal

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

5 个答案:

答案 0 :(得分:122)

在开始之前,请确保您了解Google requires的内容,尤其是漂亮丑陋网址的使用情况。现在让我们看一下实现:

客户端

在客户端,您只有一个html页面,它通过AJAX调用动态地与服务器交互。这就是SPA的意义所在。客户端中的所有a标记都是在我的应用程序中动态创建的,我们稍后会看到如何在服务器中将这些链接显示给谷歌的僵尸程序。每个此类a代码都需要在pretty URL代码中包含href,以便Google的机器人抓取它。当客户点击它时,您不希望使用href部分(即使您确实希望服务器能够解析它,我们稍后会看到它),因为我们可能不希望加载新页面,只是进行AJAX调用以获得一些数据显示在页面的一部分并通过javascript更改URL(例如使用HTML5 pushstateDurandaljs)。因此,我们同时拥有谷歌和href的{​​{1}}属性,当用户点击链接时,该属性可以完成工作。现在,由于我使用onclick,我不希望在网址上显示任何push-state,因此典型的#代码可能如下所示:
a

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

shell.js

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

  1. 第一条路线(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 }); } }; }); )用于没有额外数据的网址,即route:''。在此页面中,您可以使用AJAX加载常规数据。此页面中可能根本没有http://www.xyz.com个标签。您需要添加以下标记,以便Google的机器人知道如何处理它:
    a。此标记会让Google的机器人将网址转换为我们稍后会看到的<meta name="fragment" content="!">
  2. &#39; about&#39;路由只是链接到其他网页的一个示例&#39;你可能想要在你的网络应用程序上。
  3. 现在,棘手的部分是没有&#39;类别&#39;路线,可能有许多不同的类别 - 其中没有一个具有预定义的路线。这就是www.xyz.com?_escaped_fragment_=的用武之地。它将这些未知路线映射到&#39;商店&#39;路线并删除任何&#39;!&#39;如果它是由谷歌搜索引擎生成的mapUnknownRoutes,则来自网址。该商店&#39;路由获取&#39;片段中的信息&#39;属性并进行AJAX调用以获取数据,显示数据并在本地更改URL。在我的申请中,我没有为每个这样的电话加载不同的页面;我只更改与此数据相关的页面部分,并在本地更改URL。
  4. 注意pretty URL指示Durandal使用推送状态URL。
  5. 这是我们在客户端需要的全部内容。它也可以使用散列URL实现(在Durandal中,您可以简单地删除pushState:true)。更复杂的部分(至少对我而言......)是服务器部分:

    服务器端

    我在服务器端使用pushState:true MVC 4.5个控制器。服务器实际上需要处理3种类型的URL:谷歌生成的URL - WebAPIpretty以及简单的&#39;网址格式与客户端浏览器中显示的格式相同。让我们看看如何做到这一点:

    漂亮的网址和简单的&#39;这些服务器首先被服务器解释为尝试引用不存在的控制器。服务器看到类似ugly的内容,并查找名为&#39; category&#39;的控制器。因此,在http://www.xyz.com/category/subCategory/product111中,我添加以下行以将这些行重定向到特定的错误处理控制器:

    web.config

    现在,这会将URL转换为:<customErrors mode="On" defaultRedirect="Error"> <error statusCode="404" redirect="Error" /> </customErrors><br/> 。我希望将URL发送到将通过AJAX加载数据的客户端,因此这里的诀窍是调用默认的&#39;索引&#39;控制器好像没有引用任何控制器;我通过在所有&#39;类别之前添加哈希到URL来做到这一点。和&#39; subCategory&#39;参数;散列URL不需要任何特殊控制器,除了默认的&#39;索引&#39;控制器和数据被发送到客户端,然后客户端删除哈希并使用哈希后的信息通过AJAX加载数据。这是错误处理程序控制器代码:

    http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111


    但是 Ugly URL 呢?这些是由谷歌的机器人创建的,应返回包含用户在浏览器中看到的所有数据的纯HTML。为此我使用phantomjs。 Phantom是一个无头浏览器,可以在客户端执行浏览器操作 - 但在服务器端。换句话说,幻影知道(除其他事项外)如何通过URL获取网页,解析它包括运行其中的所有javascript代码(以及通过AJAX调用获取数据),并返回反映的HTML DOM。如果您正在使用MS Visual Studio Express,则许多人希望通过此link安装幻像。
    但首先,当一个丑陋的URL被发送到服务器时,我们必须抓住它;为此,我添加了“App_start”&#39;文件夹中的以下文件:

    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;
            }
        }
    }
    

    这是从&#39; filterConfig.cs&#39;也在&#39; App_start&#39;:

    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;
            }
        }
    }
    

    正如您所看到的,&#39; AjaxCrawlableAttribute&#39;将丑陋的URL路由到名为&#39; HtmlSnapshot&#39;的控制器,这里是这个控制器:

    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());
            }
        }
    }
    

    关联的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 )的文件夹下加载名为createSnapshot.js的javascript文件。这是这个javascript文件:

    seo

    我首先要感谢Thomas Davis获取基本代码的页面:-)。
    你会发现一些奇怪的事情:幻像不断重新加载页面,直到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); 函数返回true。这是为什么?这是因为我的特定SPA进行了几次AJAX调用以获取所有数据并将其放在我页面上的DOM中,并且幻象无法知道所有调用何时完成,然后再返回DOM的HTML反射。我在这里做的是在最后的AJAX调用之后添加checkLoaded(),这样如果这个标记存在,我就知道DOM已经完成了。我这样做是为了回应Durandal的<span id='compositionComplete'></span>事件,请参阅here了解更多信息。如果10秒钟没有发生这种情况,我会放弃(最多只需要一秒钟)。返回的HTML包含用户在浏览器中看到的所有链接。该脚本无法正常运行,因为HTML快照中存在的compositionComplete标记未引用正确的URL。这也可以在javascript幻像文件中更改,但我不认为这是必要的,因为HTML快照只用于谷歌获取<script>链接而不是运行javascript;这些链接引用一个漂亮的URL,如果你试图在浏览器中看到HTML快照,你会得到javascript错误,但所有的链接都能正常工作,并引导你到服务器一次再次使用漂亮的URL,这次获得完整工作页面 就是这个。现在,服务器知道如何处理漂亮和丑陋的URL,并在服务器和客户端上启用了push-state。所有丑陋的URL都使用幻像处理相同的方式,因此无需为每种类型的呼叫创建单独的控制器。
    您可能更愿意改变的一件事是不要制作一般的&#39;类别/子类别/产品&#39;请致电,但要添加商店&#39;以便链接看起来像:a。这样可以避免我的解决方案中的问题,即所有无效的网址都被视为实际调用了&#39;索引&#39;控制器,我想这些可以在&#39;商店内处理。没有添加到上面显示的http://www.xyz.com/store/category/subCategory/product111的控制器。

答案 1 :(得分:32)

Google现在能够呈现SPA页面: Deprecating our AJAX crawling scheme

答案 2 :(得分:4)

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

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

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

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

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

使用详细信息链接到截屏视频:

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

答案 3 :(得分:0)

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

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

答案 4 :(得分:0)

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