Я работал над тем, как сделать сканирование SPA с помощью Google на основе Google инструкций. Несмотря на то, что существует довольно много общих объяснений, я не смог найти более подробный пошаговый учебник с фактическими примерами. Закончив это, я хотел бы поделиться своим решением, чтобы другие могли его использовать и, возможно, улучшить его.
Я использую MVC
с контроллерами Webapi
и Phantomjs на стороне сервера, а Durandal на стороне клиента с включенным push-state
; Я также использую Breezejs для взаимодействия данных клиент-сервер, все из которых я настоятельно рекомендую, но я попытаюсь дать достаточно общее объяснение, которое также поможет людям используя другие платформы.
Как сделать поисковый робот SPA?
Ответ 1
Перед запуском убедитесь, что вы понимаете, что для Google требуется, особенно использование pretty и уродливые. Теперь рассмотрим реализацию:
Клиентская сторона
На стороне клиента у вас есть только одна страница html, которая динамически взаимодействует с сервером через вызовы AJAX. что о чем идет SPA. Все теги a
на стороне клиента динамически создаются в моем приложении, и мы увидим, как сделать эти ссылки видимыми для бота Google на сервере. Каждый тег a
должен иметь тег pretty URL
в теге href
, чтобы робот Google обскакивал его. Вы не хотите, чтобы часть href
использовалась, когда клиент нажимает на нее (даже если вы хотите, чтобы сервер мог ее разобрать, мы увидим это позже), потому что нам может не понадобиться новая страница для загрузки, только для того, чтобы сделать вызов AJAX, чтобы некоторые данные отображались на части страницы и изменяли URL-адрес через javascript (например, с помощью 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>
"категория" и "подкатегория", вероятно, будут другими фразами, такими как "общение" и "телефоны" или "компьютеры" и "ноутбуки" для магазина электроприборов. Очевидно, что будет много разных категорий и подкатегорий. Как вы можете видеть, ссылка напрямую относится к категории, подкатегории и продукту, а не к дополнительным параметрам для конкретной страницы "магазина", например, http://www.xyz.com/store/category/subCategory/product111
. Это потому, что я предпочитаю более короткие и простые ссылки. Это означает, что я не буду иметь категорию с тем же именем, что и одна из моих "страниц", т.е. "Около".
Я не буду рассказывать, как загружать данные через AJAX (часть onclick
), искать его в google, есть много хороших объяснений. Единственное, что я хочу сказать, это то, что когда пользователь нажимает на эту ссылку, я хочу, чтобы URL-адрес в браузере выглядел следующим образом: http://www.xyz.com/category/subCategory/product111
. И это URL-адрес не отправляется на сервер! помните, что это SPA, где все взаимодействие между клиентом и сервером осуществляется через AJAX, никаких ссылок вообще! все "страницы" реализованы на стороне клиента, а другой URL-адрес не делает вызов на сервер (серверу необходимо знать, как обращаться с этими URL-адресами, если они используются как внешние ссылки с другого сайта на ваш сайт, мы увидим это позже на стороне сервера). Теперь это прекрасно подойдет Дюрандалю. Я настоятельно рекомендую его, но вы также можете пропустить эту часть, если вы предпочитаете другие технологии. Если вы это сделаете, и вы также используете MS Visual Studio Express 2012 для Интернета, как я, вы можете установить 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
. Вам нужно добавить следующий тег, чтобы бот-робот знал, что с ним делать:<meta name="fragment" content="!">
. Этот тег заставит google-бота преобразовать URL-адрес вwww.xyz.com?_escaped_fragment_=
, который мы увидим позже. - Маршрут "about" - это всего лишь пример ссылки на другие "страницы", которые вы можете захотеть в своем веб-приложении.
- Теперь сложная часть состоит в том, что нет маршрута категории, и может быть много разных категорий - ни один из которых не имеет предопределенного маршрута. Здесь находится
mapUnknownRoutes
. Он сопоставляет эти неизвестные маршруты маршруту "магазин" , а также удаляет любые "!" от URL в случае, если онpretty URL
, сгенерированный движком google seach. Маршрут "магазин" берет информацию в свойстве "фрагмент" и вызывает вызов AJAX для получения данных, их отображения и изменения локального URL. В моем приложении я не загружаю другую страницу для каждого такого вызова; Я изменяю только часть страницы, где эти данные актуальны, а также локально изменяет URL. - Обратите внимание на
pushState:true
, который инструктирует Durandal использовать URL состояния push.
Это все, что нам нужно на стороне клиента. Он может быть реализован также с хэшированными URL-адресами (в Durandal вы просто удалите pushState:true
для этого). Более сложная часть (по крайней мере для меня...) была частью сервера:
Сторона сервера
Я использую MVC 4.5
на стороне сервера с помощью контроллеров WebAPI
. Серверу действительно необходимо обрабатывать 3 типа URL-адресов: те, которые генерируются Google - как pretty
, так и ugly
, а также "простой" URL-адрес с тем же форматом, что и в браузере клиента. Давайте посмотрим, как это сделать:
Довольно URL-адреса и "простые" сначала интерпретируются сервером, как будто они пытаются ссылаться на несуществующий контроллер. Сервер видит что-то вроде http://www.xyz.com/category/subCategory/product111
и ищет контроллер с именем "категория" . Поэтому в 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, поэтому трюк здесь заключается в вызове контроллера индекса по умолчанию, как если бы он не ссылался на какой-либо контроллер; Я делаю это, добавляя хэш к 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;
}
}
}
Но как насчет Ugly URLs? Они создаются ботом google и должны возвращать простой HTML, который содержит все данные, которые пользователь видит в браузере. Для этого я использую phantomjs. Phantom - браузер без браузера, выполняющий то, что браузер делает на стороне клиента, но на стороне сервера. Другими словами, Phantom знает (среди прочего), как получить веб-страницу через URL-адрес, проанализировать его, включая запуск всего javascript-кода в нем (а также получение данных через вызовы AJAX) и вернуть HTML-код который отражает DOM. Если вы используете MS Visual Studio Express, многие из них захотят установить 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;
}
}
}
Это вызвано из 'filterConfig.cs' также в 'App_start':
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 )
Как вы можете видеть в контроллере, Phantom загружает файл javascript с именем createSnapshot.js
под созданной мной папкой с именем 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()
не вернет значение true. Почему это? это связано с тем, что мой специальный SPA делает несколько вызовов AJAX, чтобы получить все данные и поместить их в DOM на моей странице, а Phantom не может знать, когда все вызовы завершились, прежде чем вернуть мне HTML-отражение DOM. То, что я сделал здесь, после окончательного вызова AJAX, я добавляю <span id='compositionComplete'></span>
, так что, если этот тег существует, я знаю, что DOM завершен. Я делаю это в ответ на событие Durandal compositionComplete
, см. здесь. Если это не произойдет с 10 секундами, я сдаюсь (это займет всего секунду, поэтому больше всего). Возвращенный HTML содержит все ссылки, которые пользователь видит в браузере. script не будет работать должным образом, потому что теги <script>
, которые существуют в снимке HTML, не ссылаются на правильный URL. Это также можно изменить в файле javascript Phantom, но я не думаю, что это неслучайно, потому что HTML snapshort используется только Google для получения ссылок a
, а не для запуска javascript; эти ссылки doссылку на симпатичный URL-адрес, и если факт, если вы попытаетесь увидеть снимок HTML в браузере, вы получите ошибки javascript, но все ссылки будут работать исправно и снова перенаправить вас на сервер с симпатичным URL-адресом на этот раз, чтобы получить полностью рабочая страница.
Это оно. Теперь сервер знает, как обрабатывать как красивые, так и уродливые URL-адреса, причем push-state включен как на сервере, так и на клиенте. Все уродливые URL-адреса обрабатываются одинаково с помощью Phantom, поэтому нет необходимости создавать отдельный контроллер для каждого типа вызова.
Одна вещь, которую вы, возможно, предпочтете изменить, - это не сделать общий вызов категории/подкатегории/продукта, а добавить "магазин" , чтобы ссылка выглядела примерно так: http://www.xyz.com/store/category/subCategory/product111
. Это позволит избежать проблемы в моем решении, что все недопустимые URL-адреса обрабатываются так, как если бы они фактически вызывали контроллер "index", и я полагаю, что они могут быть обработаны тогда в контроллере "store" без добавления в web.config
Я показал выше.
Ответ 2
Теперь Google может отображать страницы SPA: Устаревшая схема обхода AJAX
Ответ 3
Вот ссылка на запись в скринкасте из моего учебного класса Ember.js, который я провел в Лондоне 14 августа. В нем описывается стратегия как для вашего клиентского приложения, так и для серверного приложения, а также демонстрируется, как реализация этих функций обеспечит ваше одностраничное приложение JavaScript с грациозным ухудшением даже для пользователей с отключенным JavaScript,
Он использует PhantomJS для помощи в сканировании вашего сайта.
Короче говоря, требуются следующие шаги:
- У вас есть размещенная версия веб-приложения, которое вы хотите обходить, этот сайт должен иметь ВСЕ данные, которые у вас есть в производстве.
- Напишите приложение JavaScript (PhantomJS Script) для загрузки вашего сайта.
- Добавьте index.html(или "/" ) в список URL-адресов для обхода
- Введите первый URL-адрес, добавленный в список обхода
- Загрузите страницу и отрисуйте ее DOM
- Найдите ссылки на загруженной странице, которая ссылается на ваш собственный сайт (фильтрация URL-адресов)
- Добавьте эту ссылку в список "crawlable" URLS, если она уже не сканируется
- Храните рендер DOM в файл в файловой системе, но сначала удалите ВСЕ script -tags
- В конце создайте файл Sitemap.xml с обходными URL-адресами
Как только этот шаг будет выполнен, его до вашего бэкэнд будет обслуживать статическую версию вашего HTML как часть тега noscript на этой странице. Это позволит Google и другим поисковым системам сканировать каждую страницу на вашем веб-сайте, даже если ваше приложение первоначально является одностраничным.
Ссылка на скринкаст со всеми подробностями:
Ответ 4
Вы можете использовать или создать свой собственный сервис для prerender вашего SPA с услугой prerender. Вы можете проверить его на своем веб-сайте prerender.io и на своем проекте github (Он использует PhantomJS, и он отображает ваш сайт для вас).
С этим очень легко начать. Вам нужно только перенаправить запросы об искателях в службу, и они получат отображаемый html.
Ответ 5
Вы можете использовать http://sparender.com/, который позволяет корректно сканировать приложения одной страницы.