gpt4 book ai didi

ajax - 如何使 SPA SEO 可抓取?

转载 作者:行者123 更新时间:2023-12-03 04:25:20 26 4
gpt4 key购买 nike

我一直在研究如何根据谷歌的 instructions 使 SPA 可被谷歌抓取。 .尽管有很多一般性解释,但我在任何地方都找不到带有实际示例的更详尽的分步教程。完成此操作后,我想分享我的解决方案,以便其他人也可以使用它并可能进一步改进它。
我正在使用 MVCWebapi Controller 和 Phantomjs在服务器端,和 Durandal在客户端使用 push-state启用;我也用 Breezejs对于客户端-服务器数据交互,我强烈推荐所有这些,但我将尝试给出足够通用的解释,这也将有助于使用其他平台的人。

最佳答案

在开始之前,请确保您了解google requires ,尤其是使用漂亮和丑陋的 URL。现在让我们看看实现:

客户端

在客户端,您只有一个 html 页面,它通过 AJAX 调用与服务器动态交互。这就是SPA的意义所在。所有a客户端中的标签是在我的应用程序中动态创建的,稍后我们将看到如何使这些链接对服务器中的谷歌机器人可见。每个这样的a标签需要能够有 pretty URLhref标记,以便谷歌的机器人抓取它。你不想要 href当客户端点击它时使用的部分(即使您确实希望服务器能够解析它,我们稍后会看到),因为我们可能不希望加载新页面,只是为了进行 AJAX 调用让一些数据显示在页面的一部分并通过javascript更改URL(例如使用HTML5 pushstate 或使用 Durandaljs )。所以,我们有一个 href google 和 onclick 上的属性当用户点击链接时,它会完成这项工作。现在,因为我使用 push-state我不要任何 #在 URL 上,所以一个典型的 a标签可能如下所示:<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>'category' 和 'subCategory' 可能是其他短语,例如电器商店的“communications”和“phones”或“computers”和“laptops”。显然会有许多不同的类别和子类别。如您所见,该链接直接指向类别、子类别和产品,而不是作为特定“商店”页面(例如 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 });
}
};
});

这里有一些重要的事情需要注意:
  • 第一条路线(带有 route:'' )用于其中没有额外数据的 URL,即 http://www.xyz.com .在此页面中,您使用 AJAX 加载一般数据。可能实际上没有 a此页面中的所有标签。您需要添加以下标签,以便 google 的机器人知道如何处理它:<meta name="fragment" content="!"> .此标签将使谷歌的机器人将 URL 转换为 www.xyz.com?_escaped_fragment_=我们稍后会看到。
  • 'about' 路由只是一个示例,链接到您的 Web 应用程序中可能需要的其他“页面”。
  • 现在,棘手的部分是没有“类别”路线,而且可能有许多不同的类别——没有一个有预定义的路线。这是哪里mapUnknownRoutes进来了。它将这些未知路由映射到“商店”路由并删除任何“!”来自 URL,以防它是 pretty URL由谷歌搜索引擎生成。 'store' 路由获取 'fragment' 属性中的信息,并进行 AJAX 调用以获取数据、显示数据并在本地更改 URL。在我的应用程序中,我不会为每个此类调用加载不同的页面;我只更改与此数据相关的页面部分,并在本地更改 URL。
  • 请注意 pushState:true它指示 Durandal 使用推送状态 URL。

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

    服务器端

    我正在使用 MVC 4.5在服务器端使用 WebAPI Controller 。服务器实际上需要处理 3 种类型的 URL:由 google 生成的 URL - 两者都是 prettyugly以及与客户端浏览器中显示的格式相同的“简单”URL。让我们看看如何做到这一点:

    漂亮的 URL 和“简单”的 URL 首先被服务器解释为试图引用一个不存在的 Controller 。服务器看到类似 http://www.xyz.com/category/subCategory/product111 的内容并查找名为“类别”的 Controller 。所以在 web.config我添加以下行以将它们重定向到特定的错误处理 Controller :
    <customErrors mode="On" defaultRedirect="Error">
    <error statusCode="404" redirect="Error" />
    </customErrors><br/>

    现在,这会将 URL 转换为类似: http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111 .我希望将 URL 发送到将通过 AJAX 加载数据的客户端,所以这里的技巧是调用默认的“索引” Controller ,就像不引用任何 Controller 一样;我通过在所有 'category' 和 'subCategory' 参数之前向 URL 添加一个散列来做到这一点;除了默认的“索引” Controller 之外,散列的 URL 不需要任何特殊的 Controller ,数据被发送到客户端,然后删除散列并使用散列后的信息通过 AJAX 加载数据。这是错误处理程序 Controller 代码:
    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 的机器人创建的,应该返回包含用户在浏览器中看到的所有数据的纯 HTML。为此,我使用 phantomjs . Phantom 是一个 headless 浏览器,它做浏览器在客户端所做的事情——但在服务器端。换句话说,phantom 知道(除其他外)如何通过 URL 获取网页,对其进行解析,包括运行其中的所有 javascript 代码(以及通过 AJAX 调用获取数据),并将反射(reflect)的 HTML 返回给您DOM。如果您使用的是 MS Visual Studio Express,您可能希望通过此 link 安装 phantom .
    但是首先,当一个丑陋的 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”的 Controller ,这是这个 Controller :
    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 )正如你在 Controller 中看到的,phantom 加载了一个名为 createSnapshot.js 的 javascript 文件。在我创建的名为 seo 的文件夹下.这是这个 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()函数返回真。为什么?这是因为我的特定 SPA 进行了几次 AJAX 调用以获取所有数据并将其放置在我页面上的 DOM 中,并且在将 DOM 的 HTML 反射返回给我之前,幻影无法知道所有调用何时完成。我在这里所做的是在最后一次 AJAX 调用之后我添加了一个 <span id='compositionComplete'></span> ,这样如果这个标签存在,我就知道 DOM 已经完成。我这样做是为了回应 Durandal 的 compositionComplete事件,见 here更多。如果这在 10 秒内没有发生,我放弃(最多只需要一秒钟)。返回的 HTML 包含用户在浏览器中看到的所有链接。该脚本将无法正常工作,因为 <script> HTML 快照中确实存在的标签没有引用正确的 URL。这也可以在 javascript 幻像文件中更改,但我认为这不是必要的,因为 HTML snapshort 仅由 google 用于获取 a链接而不是运行 javascript;这些链接确实引用了一个漂亮的 URL,事实上,如果您尝试在浏览器中查看 HTML 快照,您将收到 javascript 错误,但所有链接都可以正常工作,并且这次使用漂亮的 URL 再次将您引导至服务器获得完全工作的页面。
    就是这样。现在服务器知道如何处理漂亮和丑陋的 URL,同时在服务器和客户端上启用推送状态。使用 phantom 以相同的方式处理所有丑陋的 URL,因此无需为每种类型的调用创建单独的 Controller 。
    您可能更喜欢更改的一件事不是进行一般的“类别/子类别/产品”调用,而是添加一个“商店”,以便链接看起来像: http://www.xyz.com/store/category/subCategory/product111 .这将避免我的解决方案中的所有无效 URL 都被视为实际上是对“索引” Controller 的调用的问题,并且我认为这些可以在“存储” Controller 中处理,而无需添加 web.config我上面显示了。

    关于ajax - 如何使 SPA SEO 可抓取?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18530258/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com