如何使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 pushstate
或Durandaljs
)。 所以,我们既有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_=
,我们稍后会看到。 mapUnknownRoutes
进来的地方。它将这些未知路线映射到“商店”路线,并删除任何'!' 如果它是由Google的搜索引擎生成的pretty URL
可以使用该pretty URL
。 'store'路径获取'fragment'属性中的信息,并进行AJAX调用以获取数据,显示数据并在本地更改URL。 在我的应用程序中,我不会为每个此类调用加载不同的页面; 我只更改页面中与此数据相关的部分,并在本地更改URL。 pushState:true
。 这是我们在客户端所需要的。 它也可以通过哈希URL来实现(在Durandal中,您可以简单地删除pushState:true
)。 更复杂的部分(至少对我来说)是服务器部分:
服务器端
我使用WebAPI
控制器在服务器端使用MVC 4.5
。 服务器实际上需要处理3种类型的URL:由google生成的那些 - 既pretty
又ugly
,同时也是一种与客户端浏览器中显示格式相同的“简单”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来帮助抓取您的网站。
总之,所需的步骤是:
一旦完成了这一步,它会一直到您的后端为您的HTML的静态版本提供服务,作为该页面上noscript-tag的一部分。 这将允许Google和其他搜索引擎抓取您网站上的每个页面,即使您的应用最初是单页面应用。
链接到截屏视频并提供完整的详细信息:
http://www.devcasts.io/p/spas-phantomjs-and-seo/#
链接地址: http://www.djcxy.com/p/89609.html