Як зробити SPA SEO для сканування?


143

Я працював над тим, як зробити SPA-сканування за допомогою Google на основі вказівок Google . Незважаючи на те, що є досить багато загальних пояснень, я не міг ніде знайти більш ретельного покрокового підручника з фактичними прикладами. Закінчивши це, я хотів би поділитися своїм рішенням, щоб інші могли також скористатися ним і, можливо, вдосконалити його.
Я використовую MVCз Webapiконтролерами і Phantomjs на стороні сервера, і Durandal на стороні клієнта з push-stateпідтримкою; Я також використовую Breezejs для взаємодії з даними клієнт-сервер, і все це настійно рекомендую, але я спробую дати досить загальне пояснення, яке також допоможе людям, що використовують інші платформи.


40
що стосується "поза темою" - програміст веб-додатків повинен знайти спосіб, як зробити його / її додаток сканувальним для SEO, це основна вимога в Інтернеті. Це не стосується програмування як такого, але це стосується теми "практичних, відповідальних проблем, які є унікальними для професії програмування", як описано в stackoverflow.com/help/on-topic . Це проблема для багатьох програмістів, які не мають чітких рішень у всій мережі. Я сподівався допомогти іншим і вклав години, щоб просто описати це, отримуючи негативні моменти, безумовно, не мотивує мене знову допомогти.
баміш

3
Якщо акцент робиться на програмуванні, а не на зміїному маслі / секретному соусі SEO вуду / спам, то це може бути абсолютно актуальним. Нам також подобаються власні відповіді, де вони можуть бути корисними для майбутніх читачів довгостроково. Ця пара з питаннями та відповідями, здається, пройшла обидва тести. (Деякі з фонових деталей можуть краще вирішити питання, а не вносити у відповідь, але це досить незначно)
Flexo

6
+1, щоб пом’якшити голоси. Незалежно від того, чи краще відповідатиме питання q / a як допис у блозі, питання стосується Дурандала, і відповідь добре вивчений.
RainerAtSpirit

2
Я погоджуюся з тим, що SEO є важливою частиною повсякденного життя розробників, і його, безумовно, слід розглядати як тему стаціонарного потоку!
Кім Д.

Окрім як самостійно реалізувати весь процес, ви можете спробувати SnapSearch snapsearch.io, який в основному вирішує цю проблему як послугу.
CMCDragonkai

Відповіді:


121

Перш ніж почати, переконайтеся, що ви розумієте, що вимагає google , зокрема використання красивих та некрасивих URL-адрес. Тепер переглянемо реалізацію:

Сторона клієнта

На стороні клієнта у вас є лише одна 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 для 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 });
        }
    };
});

Тут слід помітити кілька важливих речей:

  1. Перший маршрут (з route:'') призначений для URL, яка не має в ньому додаткових даних, тобто http://www.xyz.com. На цій сторінці ви завантажуєте загальні дані за допомогою AJAX. На aцій сторінці взагалі не може бути тегів. Ви хочете , щоб додати наступний тег так бот Google буде знати , що робити з ним:
    <meta name="fragment" content="!">. Цей тег змусить бота Google перетворити URL-адресу, до www.xyz.com?_escaped_fragment_=якої ми побачимось пізніше.
  2. Маршрут "про" - це лише приклад посилання на інші "сторінки", які ви можете захотіти у своєму веб-додатку.
  3. Тепер складною частиною є те, що маршруту "категорії" немає, і може бути багато різних категорій, жодна з яких не має заздалегідь визначеного маршруту. Це місце, де mapUnknownRoutesнадходить. Він відображає ці невідомі маршрути до маршруту "магазин", а також видаляє будь-які "!" від URL-адреси на випадок, якщо це pretty URLгенерується пошуковим механізмом google. Маршрут "зберігати" приймає інформацію у властивості "фрагмент" і здійснює дзвінок AJAX, щоб отримати дані, відобразити їх та змінити локальну URL-адресу. У своїй програмі я не завантажую іншу сторінку для кожного такого дзвінка; Я змінюю лише частину сторінки, де ці дані є релевантними, а також змінюю URL-адресу локально.
  4. Зверніть увагу, 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;
        }
    }
}


А як щодо некрасивих URL-адрес ? Вони створені ботом google і повинні повертати звичайний HTML, який містить усі дані, які користувач бачить у браузері. Для цього я використовую phantomjs . Phantom - це безголовий браузер, який робить те, що робить браузер на стороні клієнта - але на стороні сервера. Іншими словами, фантом знає (серед іншого), як отримати веб-сторінку за допомогою URL-адреси, проаналізувати її, включаючи запуск у неї всього коду javascript (а також отримання даних за допомогою дзвінків AJAX) та повернути HTML, який відображає ДОМ. Якщо ви використовуєте 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 )
Як ви бачите в контролері, фантом завантажує файл 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);

Я спершу хочу подякувати Томасу Девісу за сторінку, на якій я отримав основний код від :-).
Тут ви помітите щось дивне: phantom продовжує повторно завантажувати сторінку, поки checkLoaded()функція не повернеться true. Чому так? це тому, що мій конкретний SPA здійснює кілька дзвінків AJAX, щоб отримати всі дані та розмістити їх у DOM на моїй сторінці, а фантом не може знати, коли всі виклики завершилися, перш ніж повернути мені HTML-відображення DOM. Що я тут зробив - це після остаточного дзвінка AJAX, який я додаю <span id='compositionComplete'></span>, так що якщо цей тег існує, я знаю, що DOM завершений. Я роблю це у відповідь на compositionCompleteподію Дурандала , дивіться тутдля більш. Якщо цього не відбувається за 10 секунд, я здаюсь (на це найбільше знадобиться секунда). Повернений HTML містить усі посилання, які бачить користувач у браузері. Сценарій не працюватиме належним чином, оскільки <script>теги, які існують у знімку HTML, не посилаються на правильну URL-адресу. Це також можна змінити у фантомному файлі javascript, але я не думаю, що це не обов'язково, оскільки HTML-знімок використовується лише Google для отримання aпосилань та не для запуску javascript; ці посилання роблять посилатися на досить URL, і якщо насправді, якщо ви намагаєтеся побачити HTML знімок в браузері, ви отримаєте JavaScript помилки , але все посилання працюватимуть належним чином і направити вас на сервер ще раз з симпатичною URL на цей раз отримання повністю працюючої сторінки.
Це воно. Тепер сервер знає, як обробляти як гарні, так і некрасиві URL-адреси, з включеним push-state і на сервері, і на клієнті. Усі потворні URL-адреси обробляються однаково за допомогою фантома, тому немає необхідності створювати окремий контролер для кожного типу виклику.
Одна річ , яку ви могли б віддати перевагу зміни не зробити скликати загальні «категорію / підкатегорію / продукт» , але , щоб додати «магазин» так , що посилання буде виглядати приблизно так: http://www.xyz.com/store/category/subCategory/product111. Це дозволить уникнути проблеми в моєму рішенні, що всі недійсні URL-адреси обробляються так, ніби вони насправді є викликами до контролера "індексу", і я вважаю, що з ними можна обробляти потім в контролері "store" без додавання до web.configпоказаного вище .


У мене швидке запитання, я думаю, у мене це працює зараз, але коли я надсилаю свій сайт в google і даю посилання на google, карти сайту тощо, чи потрібно мені давати google mysite.com/# ! або просто mysite.com і google додадуть у escape_fragment, оскільки він у мене є метатегом?
ccorrin

ccorrin - наскільки мені відомо, вам нічого не потрібно давати google; бот google знайде ваш сайт і знайде на ньому гарні URL-адреси (не забудьте на домашній сторінці також додати метатег, оскільки він може не містити жодних URL-адрес). некрасива URL-адреса, що містить escape_fragment, завжди додається лише google - ви ніколи не повинні розміщувати її самостійно у своїх HTML-кодах. і дякую за підтримку :-)
баміш

дякую Bjorn & Sandra :-) Я працюю над кращою версією цього документа, яка також міститиме інформацію про кешування сторінок, щоб зробити процес швидшим і зробити це в більш поширеному використанні, коли URL містить ім'я контролера; Я опублікую її, як тільки буде готова
1313

Це чудове пояснення !!. Я реалізував це і працює як шарм у своєму локальному розробнику. Проблема полягає в розгортанні на веб-сайтах Azure, оскільки сайт застигає і через деякий час я отримую помилку 502. Чи маєте ви якесь уявлення про те, як розгорнути phantomjs до Azure ?? ... Спасибі ( testypv.azurewebsites.net/?_escaped_fragment_=home/about )
yagopv

Я не маю досвіду роботи з веб-сайтами Azure, але мені спадає на думку, що, можливо, процес перевірки для повного завантаження сторінки ніколи не виконується, тому сервер продовжує намагатися перезавантажувати сторінку знову і знову без успіху. можливо, тут проблема (навіть якщо для цих чеків є обмежений термін, щоб його не було)? спробуйте поставити 'return true;' як перший рядок у "checkLoaded ()", і перевірте, чи має значення це.
баміш


4

Ось посилання на скріншот із мого навчального класу Ember.js, який я провів у Лондоні 14 серпня. Він окреслює стратегію як для вашої клієнтської програми, так і для вашої програми на стороні сервера, а також дає наочну демонстрацію того, як реалізація цих функцій забезпечить вашій JavaScript-одне сторінки-додаток витонченою деградацією навіть для користувачів, які вимкнено JavaScript. .

Він використовує PhantomJS, щоб допомогти сканувати ваш веб-сайт.

Коротше кажучи, необхідні кроки:

  • Маючи розміщену версію веб-програми, яку ви хочете сканувати, цей сайт повинен мати ВСІ дані, які ви маєте у виробництві
  • Напишіть програму JavaScript (PhantomJS Script) для завантаження вашого веб-сайту
  • Додайте index.html (або "/") до списку URL-адрес для сканування
    • Виведіть першу URL-адресу, додану до списку сканування
    • Завантажте сторінку та надайте її DOM
    • Знайдіть будь-які посилання на завантаженій сторінці, що посилаються на ваш власний сайт (фільтрування URL-адрес)
    • Додайте це посилання до списку URL-адрес, "які можна сканувати", якщо його ще не сканували
    • Зберігайте виведений DOM у файл у файловій системі, але спочатку зніміть ВСІ скрипти-теги
    • Наприкінці створіть файл Sitemap.xml із сканованими URL-адресами

Як тільки цей крок виконаний, його залежить від вашого сервера, щоб він розміщував статичну версію HTML як частини noscript-тегу на цій сторінці. Це дозволить Google та іншим пошуковим системам сканувати кожну окрему сторінку на вашому веб-сайті, навіть якщо ваш додаток спочатку є додатком для однієї сторінки.

Посилання на скріншот із усіма деталями:

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


0

Ви можете використовувати або створити власну послугу для попереднього надання SPA-послуги за допомогою послуги під назвою prerender. Ви можете перевірити це на його веб-сайті prerender.io та в його проекті github (Він використовує PhantomJS, і він рендерінгує ваш веб-сайт для вас).

Почати це дуже просто. Вам потрібно лише перенаправити запити сканерів на сервіс, і вони отримають наданий html.


2
Хоча це посилання може відповісти на питання, краще включити сюди суттєві частини відповіді та надати посилання для довідки. Відповіді лише на посилання можуть стати недійсними, якщо пов’язана сторінка зміниться. - З огляду
timgeb

2
Ти правий. Я оновив свій коментар ... Сподіваюсь, зараз це буде більш точним.
gabrielperales

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.