如何使SPA SEO可以被抓取?

我一直在研究如何使谷歌根据谷歌的指示进行搜索。 即使有很多一般的解释,我都无法在任何地方找到更详尽的循序渐进的教程和实例。 完成后我想分享我的解决方案,以便其他人也可以使用它,并可能进一步改进。
我在Webapi控制器上使用MVC ,在服务器端使用Webapi ,在客户端使用push-state Durandal; 我还使用Breezejs进行客户端 - 服务器数据交互,所有这些我都强烈建议,但我会尝试给出足够的解释,以帮助使用其他平台的人员。


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

客户端

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

“类别”和“子类别”可能是其他短语,例如电器商店的“通信”和“电话”或“电脑”和“笔记本电脑”。 显然会有很多不同的类别和子类别。 如您所见,链接直接指向类别,子类别和产品,而不是作为特定“商店”页面的额外参数,例如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 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 });
        }
    };
});

有几件重要的事情需要注意:

  • 第一条路线( route:'' )是用于没有额外数据的URL,即http://www.xyz.com 。 在这个页面中,您使用AJAX加载一般数据。 实际上可能没有a在所有在此页面的标签。 您需要添加以下标签,以便Google的机器人知道如何处理它:
    <meta name="fragment" content="!"> 。 这个标签会让谷歌的机器人将URL转换为www.xyz.com?_escaped_fragment_= ,我们稍后会看到。
  • 'about'路由仅仅是一个链接到其他'网页'的例子,你可能想要在你的web应用程序中。
  • 现在,棘手的部分是没有'类别'路线,并且可能有许多不同的类别 - 其中没有一个具有预定义的路线。 这是mapUnknownRoutes进来的地方。它将这些未知路线映射到“商店”路线,并删除任何'!' 如果它是由Google的搜索引擎生成的pretty URL可以使用该pretty URL 。 'store'路径获取'fragment'属性中的信息,并进行AJAX调用以获取数据,显示数据并在本地更改URL。 在我的应用程序中,我不会为每个此类调用加载不同的页面; 我只更改页面中与此数据相关的部分,并在本地更改URL。
  • 注意指示Durandal使用推送状态URL的pushState:true
  • 这是我们在客户端所需要的。 它也可以通过哈希URL来实现(在Durandal中,您可以简单地删除pushState:true )。 更复杂的部分(至少对我来说)是服务器部分:

    服务器端

    我使用WebAPI控制器在服务器端使用MVC 4.5 。 服务器实际上需要处理3种类型的URL:由google生成的那些 - 既prettyugly ,同时也是一种与客户端浏览器中显示格式相同的“简单”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 : http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111 category/subCategory/ http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111 。 我希望将URL发送给通过AJAX加载数据的客户端,因此这里的技巧是调用默认的'index'控制器,就好像不引用任何控制器一样; 我通过在所有'category'和'subCategory'参数之前向URL添加哈希来实现这一点; 哈希URL不需要任何特殊的控制器,除了默认的'index'控制器,并且数据被发送到客户端,然后该客户端移除哈希并使用哈希后的信息通过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;
            }
        }
    }
    


    但是丑陋的URL呢? 这些是由谷歌机器人创建的,并应返回包含用户在浏览器中看到的所有数据的纯HTML。 为此,我使用phantomjs。 Phantom是一个浏览器在客户端执行浏览器的无头浏览器 - 但在服务器端。 换句话说,幽灵知道(除其他外)如何通过URL获取网页,解析它,包括运行其中的所有JavaScript代码(以及通过AJAX调用获取数据),并将您反馈的HTML DOM。 如果您使用的是MS Visual Studio Express,则很多人希望通过此链接安装幻像。
    但首先,当一个丑陋的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, "seocreateSnapshot.js"), url),
                    FileName = Path.Combine(appRoot, "binphantomjs.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从那里获得基本代码的页面:-)。
    你会注意到一些奇怪的地方:phantom不断重新加载页面,直到checkLoaded()函数返回true。 这是为什么? 这是因为我的特定SPA会进行几次AJAX调用来获取所有数据并将其放置在我的页面上的DOM中,并且在将我返回DOM的HTML反射之前,幻像无法知道所有调用何时完成。 我在这里做的是在最后的AJAX调用之后添加一个<span id='compositionComplete'></span> ,以便如果这个标记存在,我知道DOM已经完成。 我在回应Durandal的compositionComplete事件时这样做,请参阅这里以获得更多信息。 如果这种情况在10秒内没有发生,我放弃了(最多只需要一秒钟)。 返回的HTML包含用户在浏览器中看到的所有链接。 该脚本将无法正常工作,因为HTML快照中存在的<script>标记不会引用正确的URL。 这可以在JavaScript幻像文件中更改,但我不认为这是必须的,因为HTML snapshort只能用于获取a链接而不运行javascript; 这些链接引用了一个漂亮的URL,如果事实上,如果你尝试在浏览器中看到HTML快照,你会得到javascript错误,但所有链接都能正常工作,并且这次再次用一个漂亮的URL指引你到服务器获得完整的工作页面。
    就是这个。 现在服务器知道如何处理漂亮和丑陋的URL,并且在服务器和客户端都启用了推送状态。 所有丑陋的URL都使用幻像以相同的方式处理,因此不需要为每种类型的呼叫创建单独的控制器。
    你可能更喜欢改变的一件事不是做一般的'category / subCategory / product'调用,而是添加一个'store',以便链接看起来像这样: http://www.xyz.com/store/category/subCategory/product111 ://www.xyz.com/store/category http://www.xyz.com/store/category/subCategory/product111 。 这将避免我的解决方案中的所有无效URL被视为实际上调用'index'控制器的问题,我想这些可以在'store'控制器内处理,而不需要添加到web.config我上面显示。


    Google现在可以呈现SPA页面:弃用我们的AJAX爬取方案


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

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

    总之,所需的步骤是:

  • 有一个托管版本的网络应用程序要抓取,该网站需要有生产中的所有数据
  • 编写一个JavaScript应用程序(PhantomJS Script)来加载您的网站
  • 将index.html(或“/”)添加到要爬网的URL列表中
  • 弹出添加到抓取列表的第一个网址
  • 加载页面并呈现其DOM
  • 查找链接到您自己网站的加载页面上的任何链接(URL过滤)
  • 将此链接添加到“可抓取”网址列表(如果其尚未抓取)
  • 将渲染的DOM存储到文件系统上的文件中,但首先将所有脚本标签剥离
  • 最后,使用抓取的网址创建Sitemap.xml文件
  • 一旦完成了这一步,它会一直到您的后端为您的HTML的静态版本提供服务,作为该页面上noscript-tag的一部分。 这将允许Google和其他搜索引擎抓取您网站上的每个页面,即使您的应用最初是单页面应用。

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

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

    链接地址: http://www.djcxy.com/p/89609.html

    上一篇: How to make a SPA SEO crawlable?

    下一篇: What is an AngularJS directive?