it-swarm.dev

Jak przeszukiwać SEO SPA?

Pracowałem nad tym, jak przekształcić SPA w wyszukiwarkę Google na podstawie instrukcji google. Chociaż istnieje kilka ogólnych wyjaśnień, nie mogłem znaleźć nigdzie dokładniejszego samouczka krok po kroku z rzeczywistymi przykładami. Po skończeniu tego chciałbym podzielić się moim rozwiązaniem, aby inni mogli z niego skorzystać i ewentualnie poprawić go. 
Używam MVC z Webapi kontrolerami i Phantomjs po stronie serwera i Durandal po stronie klienta z włączonym Push-state; Używam również Breezejs do interakcji między klientem a serwerem, z czego zdecydowanie zalecam, ale postaram się podać ogólne wyjaśnienie, które pomoże również osobom korzystającym z innych platform.

142
beamish

Przed rozpoczęciem upewnij się, że rozumiesz, czego wymaga Google , w szczególności korzystanie z ładny i brzydki Adresy URL. Teraz zobaczmy implementację:

Strona klienta

Po stronie klienta masz tylko jedną stronę HTML, która dynamicznie współdziała z serwerem poprzez wywołania AJAX. o to właśnie chodzi w SPA. Wszystkie tagi apo stronie klienta są tworzone dynamicznie w mojej aplikacji, później zobaczymy, jak udostępnić te linki botowi Google na serwerze. Każdy taki tag amusi mieć możliwość umieszczenia pretty URL w tagu hrefname__, aby bot go zaindeksował. Nie chcesz, aby część hrefbyła używana, gdy klient ją kliknie (nawet jeśli chcesz, aby serwer mógł ją przeanalizować, zobaczymy to później), ponieważ możemy nie chcieć załadować nowej strony , tylko po to, aby wykonać wywołanie AJAX, uzyskując pewne dane do wyświetlenia na części strony i zmienić adres URL za pomocą javascript (np. używając HTML5 pushstatelub z Durandaljsname__). Mamy więc zarówno atrybut hrefdla Google, jak i onclickname__, który wykonuje zadanie, gdy użytkownik kliknie link. Teraz, ponieważ używam Push-state nie chcę żadnego # na adres URL, więc typowy tag amoże wyglądać następująco:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>

„kategoria” i „podkategoria” to prawdopodobnie inne wyrażenia, takie jak „komunikacja” i „telefony” lub „komputery” i „laptopy” dla sklepu z urządzeniami elektrycznymi. Oczywiście byłoby wiele różnych kategorii i podkategorii. Jak widać, link prowadzi bezpośrednio do kategorii, podkategorii i produktu, a nie jako dodatkowe parametry do konkretnej strony „sklepu”, takiej jak http://www.xyz.com/store/category/subCategory/product111. To dlatego, że wolę krótsze i prostsze linki. Oznacza to, że nie będzie kategorii o takiej samej nazwie jak jedna z moich „stron”, tj. „Około”.
Nie będę wchodził w sposób ładowania danych przez AJAX (część onclickname__), szukaj w google, istnieje wiele dobrych wyjaśnień. Jedyną ważną rzeczą, o której chcę wspomnieć, jest to, że kiedy użytkownik kliknie ten link, chcę, aby adres URL w przeglądarce wyglądał tak:
http://www.xyz.com/category/subCategory/product111. I to nie jest adres URL wysyłany na serwer! pamiętaj, jest to SPA, w którym cała interakcja między klientem a serwerem odbywa się za pośrednictwem AJAX, w ogóle żadnych linków! wszystkie „strony” są zaimplementowane po stronie klienta, a inny adres URL nie wywołuje połączenia z serwerem (serwer musi wiedzieć, jak obsługiwać te adresy URL w przypadku, gdy są one używane jako zewnętrzne linki z innej witryny do Twojej witryny, zobaczymy to później w części po stronie serwera). Teraz Durandal wspaniale sobie z tym radzi. Zdecydowanie go polecam, ale możesz również pominąć tę część, jeśli wolisz inne technologie. Jeśli wybierzesz tę opcję, a także używasz MS Visual Studio Express 2012 dla Internetu, takiego jak ja, możesz zainstalować Durandal Starter Kit i tam , w Shell.js, użyj czegoś takiego:

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 });
        }
    };
});

Warto zwrócić uwagę na kilka ważnych rzeczy:

  1. Pierwsza trasa (z route:'') jest dla adresu URL, który nie zawiera żadnych dodatkowych danych, tj. http://www.xyz.com. Na tej stronie ładujesz dane ogólne za pomocą AJAX. Na tej stronie mogą nie być wcale tagi aname__. Będziesz chciał dodać następujący tag, aby bot Google wiedział, co z nim zrobić:
    <meta name="fragment" content="!">. Ten tag spowoduje, że bot Google przekształci adres URL na www.xyz.com?_escaped_fragment_=, co zobaczymy później.
  2. Trasa „about” jest tylko przykładem linku do innych „stron”, które możesz chcieć w swojej aplikacji internetowej.
  3. Problem polega na tym, że nie ma trasy „kategorii” i może istnieć wiele różnych kategorii - z których żadna nie ma zdefiniowanej trasy. W tym miejscu pojawia się mapUnknownRoutesname__. Mapuje te nieznane trasy na trasę „przechowywania”, a także usuwa wszelkie „!” z adresu URL, jeśli jest to pretty URL wygenerowany przez silnik wyszukiwania Google. Trasa „przechowywanie” pobiera informacje z właściwości „fragment” i wywołuje wywołanie AJAX w celu uzyskania danych, wyświetlenia ich i zmiany adresu lokalnego. W mojej aplikacji nie ładuję innej strony dla każdego takiego połączenia; Zmieniam tylko część strony, na której te dane są istotne, a także zmieniam adres URL lokalnie.
  4. Zwróć uwagę na pushState:true, który instruuje Durandal, aby używał adresów URL stanu push.

To wszystko, czego potrzebujemy po stronie klienta. Może być zaimplementowany również z zaszyfrowanymi adresami URL (w Durandal po prostu usuwasz dla tego pushState:true). Bardziej złożoną częścią (przynajmniej dla mnie ...) była część serwerowa:

Po stronie serwera

Używam MVC 4.5 po stronie serwera z WebAPIkontrolerami. Serwer faktycznie musi obsługiwać 3 typy adresów URL: te generowane przez google - zarówno prettyi uglyname__, a także „prosty” adres URL w tym samym formacie, który pojawia się w przeglądarce klienta. Zobaczmy, jak to zrobić:

Ładne adresy URL i „proste” są najpierw interpretowane przez serwer tak, jakby próbowały odwoływać się do nieistniejącego kontrolera. Serwer widzi coś w stylu http://www.xyz.com/category/subCategory/product111 i szuka kontrolera o nazwie „kategoria”. Więc w web.config dodaję następujący wiersz, aby przekierować je do określonego kontrolera obsługi błędów:

<customErrors mode="On" defaultRedirect="Error">
    <error statusCode="404" redirect="Error" />
</customErrors><br/>

Teraz przekształca to adres URL na coś takiego: http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111. Chcę, aby adres URL został wysłany do klienta, który załaduje dane przez AJAX, więc sztuczka polega na wywołaniu domyślnego kontrolera „indeksu”, tak jakby nie odwoływał się do żadnego kontrolera; Robię to przez dodawanie skrót do adresu URL przed wszystkimi parametrami „category” i „subCategory”; zaszyfrowany adres URL nie wymaga żadnego specjalnego kontrolera oprócz domyślnego kontrolera „indeksu”, a dane są wysyłane do klienta, który następnie usuwa skrót i używa informacji po skrócie do załadowania danych przez AJAX. Oto kod kontrolera procedury obsługi błędów:

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;
        }
    }
}


Ale co z Brzydkie adresy URL? Są one tworzone przez bota Google i powinny zwracać zwykły HTML, który zawiera wszystkie dane, które użytkownik widzi w przeglądarce. Do tego używam phantomjs . Phantom to bezgłowa przeglądarka, która robi to, co robi przeglądarka po stronie klienta - ale po stronie serwera. Innymi słowy, fantom wie (między innymi), jak uzyskać stronę internetową za pomocą adresu URL, parsować ją, włączając w to uruchomienie całego kodu javascript (a także uzyskiwanie danych za pomocą wywołań AJAX) i dawać cofasz HTML, który odzwierciedla DOM. Jeśli używasz MS Visual Studio Express, wielu chce zainstalować fantom za pomocą tego linku .
Ale najpierw, kiedy brzydki adres URL zostanie wysłany na serwer, musimy go złapać; W tym celu dodałem do folderu „App_start” następujący plik:

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;
        }
    }
}

Jest to wywoływane z „filterConfig.cs” również w „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());
        }
    }
}

Jak widać, „AjaxCrawlableAttribute” kieruje brzydkie adresy URL do kontrolera o nazwie „HtmlSnapshot”, a oto ten kontroler:

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();
        }

    }
}

Powiązana viewjest bardzo prosta, tylko jeden wiersz kodu:
@Html.Raw( ViewBag.result )
Jak widać w kontrolerze, fantom ładuje plik javascript o nazwie createSnapshot.js do utworzonego przeze mnie folderu o nazwie seoname__. Oto ten plik 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);

Najpierw chciałbym podziękować Thomasowi Davisowi za stronę, na której otrzymałem podstawowy kod :-).
Zauważysz tutaj coś dziwnego: fantom ciągle ładuje stronę, dopóki funkcja checkLoaded() nie zwróci wartości true. Dlaczego? dzieje się tak, ponieważ moje określone SPA wykonuje kilka wywołań AJAX, aby uzyskać wszystkie dane i umieścić je w DOM na mojej stronie, a fantom nie może wiedzieć, kiedy wszystkie wywołania zostały zakończone, zanim zwróci mi odbicie HTML DOM. To, co zrobiłem tutaj, jest po ostatnim wywołaniu AJAX Dodaję <span id='compositionComplete'></span>, więc jeśli ten znacznik istnieje, wiem, że DOM jest gotowy. Robię to w odpowiedzi na compositionCompleteDurandala, patrz tutaj więcej. Jeśli tak się nie stanie w ciągu 10 sekund, poddaję się (powinno to zająć tylko sekundę). Zwrócony kod HTML zawiera wszystkie łącza, które użytkownik widzi w przeglądarce. Skrypt nie będzie działał poprawnie, ponieważ tagi <script>, które istnieją w migawce HTML, nie odwołują się do poprawnego adresu URL. Można to również zmienić w pliku fantomu javascript, ale nie sądzę, że jest to konieczne, ponieważ snapshort HTML jest używany tylko przez Google, aby uzyskać linki ai nie uruchamiać javascript; te linki zrobić odwołać się do ładnego adresu URL, a jeśli faktem jest, że jeśli spróbujesz zobaczyć migawkę HTML w przeglądarce, otrzymasz błędy javascript, ale wszystkie linki będą działały poprawnie i ponownie przekierują cię na serwer z ładnym adresem URL, tym razem uzyskując pełne strona robocza.
To jest to. Teraz serwer wie, jak obsługiwać zarówno ładne, jak i brzydkie adresy URL, z włączonym trybem push na serwerze i kliencie. Wszystkie brzydkie adresy URL są traktowane w ten sam sposób przy użyciu fantomu, więc nie trzeba tworzyć osobnego kontrolera dla każdego rodzaju połączenia.
Jedną rzeczą, którą wolisz zmienić, jest nie wywoływanie ogólnej „kategorii/podkategorii/produktu”, ale dodanie „sklepu”, aby link wyglądał mniej więcej tak: http://www.xyz.com/store/category/subCategory/product111. Pozwoli to uniknąć problemu w moim rozwiązaniu, że wszystkie niepoprawne adresy URL są traktowane tak, jakby były faktycznie wywołaniami kontrolera „indeksu”, i przypuszczam, że można je obsłużyć następnie w kontrolerze „sklepu” bez dodawania web.config Pokazałem powyżej.

122
beamish

Google może teraz renderować strony SPA: Przestępuje nasz AJAX schemat indeksowania

32
Edward Olamisan

Oto link do nagrania screencast z mojej klasy szkoleniowej Ember.js, która odbyła się 14 sierpnia w Londynie. Przedstawia strategię zarówno dla aplikacji po stronie klienta, jak i dla aplikacji po stronie serwera, a także pokazuje na żywo, w jaki sposób implementacja tych funkcji zapewni aplikacji z pojedynczą stroną JavaScript pełną wdzięku degradację nawet dla użytkowników z wyłączonym JavaScriptem . 

Korzysta z PhantomJS, aby pomóc w indeksowaniu Twojej witryny. 

Krótko mówiąc, wymagane kroki: 

  • Masz hostowaną wersję aplikacji internetowej, którą chcesz zaindeksować, ta witryna musi zawierać WSZYSTKIE dane, które masz w produkcji
  • Napisz aplikację JavaScript (PhantomJS Script), aby załadować swoją stronę
  • Dodaj index.html (lub „/”) do listy adresów URL do indeksowania
    • Pop pierwszy adres URL dodany do listy indeksowania
    • Załaduj stronę i wyrenderuj jej DOM 
    • Znajdź wszelkie linki na załadowanej stronie, które prowadzą do Twojej witryny (filtrowanie adresów URL)
    • Dodaj ten link do listy „indeksowanych” adresów URL, jeśli nie została jeszcze zaindeksowana
    • Przechowuj renderowany DOM w pliku w systemie plików, ale najpierw usuń WSZYSTKIE znaczniki skryptów
    • Na koniec utwórz plik Sitemap.xml z przeszukiwanymi adresami URL

Po wykonaniu tego kroku do Twojego zaplecza należy podanie wersji statycznej kodu HTML jako części tagu noscript na tej stronie. Umożliwi to Google i innym wyszukiwarkom indeksowanie każdej pojedynczej strony w witrynie, mimo że aplikacja jest pierwotnie aplikacją jednostronicową. 

Link do screencastu z pełnymi szczegółami: 

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

4
Joachim H. Skeie

Możesz użyć http://sparender.com/ który umożliwia poprawne indeksowanie aplikacji jednostronicowych.

0
ddtxra

Możesz użyć lub utworzyć własną usługę do prerenderowania swojego SPA za pomocą usługi zwanej prerender. Możesz to sprawdzić na swojej stronie prerender.io i na jego projekcie github (Używa PhantomJS i renderuje twoją stronę dla Ciebie). 

Bardzo łatwo jest zacząć. Wystarczy przekierować żądania przeszukiwaczy do usługi, a otrzymają renderowany HTML.

0
gabrielperales