Jak sprawić, by SEO SEO było indeksowane?

143

Pracowałem nad tym, jak umożliwić indeksowanie SPA przez Google na podstawie instrukcji Google . Mimo że istnieje kilka ogólnych wyjaśnień, nie mogłem nigdzie znaleźć dokładniejszego samouczka krok po kroku z rzeczywistymi przykładami. Po skończeniu tego chciałbym podzielić się moim rozwiązaniem, aby inni również mogli z niego skorzystać i ewentualnie dalej je ulepszać.
Używam MVCz Webapikontrolerami i Phantomjs po stronie serwera i Durandal po stronie klienta z push-statewłączoną; Używam także Breezejs do interakcji danych klient-serwer, które zdecydowanie polecam, ale postaram się podać wystarczająco ogólne wyjaśnienie, które pomoże również osobom korzystającym z innych platform.

Beamish
źródło
40
jeśli chodzi o „nie na temat” - programista aplikacji internetowych musi znaleźć sposób, w jaki sposób uczynić swoją aplikację możliwą do indeksowania pod kątem SEO, jest to podstawowy wymóg w sieci. Robienie tego nie polega na programowaniu jako takim, ale jest związane z tematem „praktycznych, możliwych do rozwiązania problemów, które są unikalne dla zawodu programisty”, jak opisano na stackoverflow.com/help/on-topic . Dla wielu programistów jest to problem bez jasnych rozwiązań w całej sieci. Miałem nadzieję, że pomogę innym i poświęciłem wiele godzin na opisanie tego tutaj, zdobywanie punktów ujemnych z pewnością nie motywuje mnie do ponownej pomocy.
beamish
3
Jeśli nacisk kładziony jest na programowanie, a nie na olej z węża / tajny sos SEO, voodoo / spam, może to być doskonale aktualne. Lubimy również odpowiedzi własne, w których mogą być przydatne dla przyszłych czytelników w dłuższej perspektywie. Ta para pytań i odpowiedzi zdaje oba te testy. (Niektóre szczegóły tła mogą lepiej ująć pytanie, niż zostać wprowadzonym do odpowiedzi, ale to dość mało
istotne
6
+1, aby złagodzić liczbę głosów w dół. Niezależnie od tego, czy pytanie / odpowiedzi lepiej pasowałoby jako post na blogu, pytanie jest istotne dla Durandala, a odpowiedź jest dobrze zbadana.
RainerAtSpirit
2
Zgadzam się, że SEO jest obecnie ważną częścią codziennego życia programistów i zdecydowanie powinno być traktowane jako temat w stackoverflow!
Kim D.
Oprócz samodzielnego wdrażania całego procesu, możesz wypróbować SnapSearch snapsearch.io, który zasadniczo rozwiązuje ten problem jako usługę.
CMCDragonkai

Odpowiedzi:

121

Zanim zaczniesz, upewnij się, że rozumiesz, czego wymaga Google , zwłaszcza dotyczące używania ładnych i brzydkich adresów URL. Teraz zobaczmy implementację:

Strona klienta

Po stronie klienta masz tylko jedną stronę html, która dynamicznie współdziała z serwerem za pośrednictwem wywołań AJAX. o to chodzi w SPA. Wszystkie atagi po stronie klienta są tworzone dynamicznie w mojej aplikacji, później zobaczymy, jak sprawić, by te linki były widoczne dla bota Google na serwerze. Każdy taki atag musi mieć możliwość umieszczenia pretty URLw hreftagu znaku, aby robot Google go zaindeksował. Nie chcesz, aby hrefczęść był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 dane do wyświetlenia w części strony i zmienić adres URL za pomocą javascript (np. używając HTML5 pushstatelub with Durandaljs). Tak więc mamy oba plikihrefatrybut dla google, a także onclickktóry wykonuje zadanie, gdy użytkownik kliknie link. Teraz, ponieważ używam push-state, nie chcę żadnego #w adresie URL, więc typowy atag może wyglądać tak:
<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” oraz „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 określonej strony sklepu, takiej jak http://www.xyz.com/store/category/subCategory/product111. Dzieje się tak, ponieważ wolę krótsze i prostsze linki. Oznacza to, że nie będzie kategorii o takiej samej nazwie jak jedna z moich „stron”, tj. „
Nie będę się zagłębiał w ładowanie danych przez AJAX ( onclickczęść), przeszukuję go w google, jest wiele dobrych wyjaśnień. Jedyną ważną rzeczą, o której chcę tutaj wspomnieć, jest to, że kiedy użytkownik kliknie ten link, chcę, aby adres URL w przeglądarce wyglądał następująco:
http://www.xyz.com/category/subCategory/product111. A to adres URL nie jest wysyłany do serwera! pamiętaj, jest to SPA, w którym cała interakcja między klientem a serwerem odbywa się za pośrednictwem AJAX, żadnych łączy! wszystkie `` strony '' są zaimplementowane po stronie klienta, a inny adres URL nie wywołuje serwera (serwer musi wiedzieć, jak obsługiwać te adresy URL, jeśli są używane jako linki zewnętrzne z innej witryny do Twojej witryny, zobaczymy to później w części po stronie serwera). Teraz Durandal wspaniale sobie z tym radzi. Gorąco polecam, ale możesz też pominąć tę część, jeśli wolisz inne technologie. Jeśli go wybierzesz, a także używasz MS Visual Studio Express 2012 for Web, tak jak ja, możesz zainstalować zestaw startowy Durandal , a tam, w programie shell.js, użyć 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 });
        }
    };
});

Należy tu zwrócić uwagę na kilka ważnych rzeczy:

  1. Pierwsza trasa (z route:'') dotyczy adresu URL, który nie zawiera żadnych dodatkowych danych, tj http://www.xyz.com. Na tej stronie ładujesz ogólne dane za pomocą AJAX. W rzeczywistości ana tej stronie może nie być żadnych tagów. Będziemy chcieli, aby dodać następujący tag tak że Google jest bot będzie wiedział, co z nim zrobić:
    <meta name="fragment" content="!">. Ten tag sprawi, że bot google przekształci adres URL, do www.xyz.com?_escaped_fragment_=którego zobaczymy później.
  2. Trasa „about” to tylko przykład odsyłacza do innych „stron”, które mogą znajdować się w 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 predefiniowanej trasy. I tu mapUnknownRoutespojawia się. Mapuje te nieznane trasy na trasę „sklepu”, a także usuwa wszelkie „!” z adresu URL, jeśli jest pretty URLwygenerowany przez wyszukiwarkę Google. Trasa „store” pobiera informacje z właściwości „fragment” i wykonuje wywołanie AJAX w celu pobrania danych, wyświetlenia ich i lokalnej zmiany adresu URL. W mojej aplikacji nie ładuję innej strony dla każdego takiego połączenia; Zmieniam tylko część strony, w której te dane są istotne, a także zmieniam lokalnie adres URL.
  4. Zwróć uwagę, pushState:trueco instruuje Durandala, aby używał adresów URL stanu wypychania.

To wszystko, czego potrzebujemy po stronie klienta. Można to zaimplementować również z zahaszowanymi adresami URL (w Durandal po prostu usuwasz w tym pushState:truecelu). Bardziej złożoną częścią (przynajmniej dla mnie ...) była część serwerowa:

Po stronie serwera

Używam MVC 4.5po stronie serwera z WebAPIkontrolerami. Serwer faktycznie musi obsługiwać 3 rodzaje adresów URL: te generowane przez Google - zarówno prettya ugly, a także „prosty” URL z takim samym formacie jak ten, 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łać się do nieistniejącego kontrolera. Serwer widzi coś podobnego http://www.xyz.com/category/subCategory/product111i szuka kontrolera o nazwie „kategoria”. Więc web.configdodaję 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, to przekształca URL do czegoś takiego: http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111. Chcę, aby adres URL był wysyłany do klienta, który będzie ładował dane przez AJAX, więc sztuczka polega na wywołaniu domyślnego kontrolera „indeksu”, jakby nie odwoływał się do żadnego kontrolera; Robię to, dodając skrót do adresu URL przed wszystkimi parametrami „category” i „subCategory”; zaszyfrowany adres URL nie wymaga żadnego specjalnego kontrolera z wyjątkiem domyślnego kontrolera „indeksu”, a dane są wysyłane do klienta, który następnie usuwa hash i używa informacji po hashu do załadowania danych przez AJAX. Oto kod kontrolera 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 brzydkimi adresami URL ? Są one tworzone przez bota Google i powinny zwracać zwykły kod HTML zawierający wszystkie dane, które użytkownik widzi w przeglądarce. Do tego używam phantomjs . Phantom to przeglądarka bezgłowa, która robi to, co przeglądarka robi po stronie klienta - ale po stronie serwera. Innymi słowy, fantom wie (między innymi), jak uzyskać stronę internetową za pośrednictwem adresu URL, przeanalizować ją, w tym uruchomić cały kod javascript (a także pobrać dane za pośrednictwem wywołań AJAX) i zwrócić kod HTML, który odzwierciedla DOM. Jeśli używasz MS Visual Studio Express, wielu z was chce zainstalować fantom za pomocą tego linku .
Ale najpierw, kiedy brzydki adres URL jest wysyłany do serwera, musimy go przechwycić; 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();
        }

    }
}

Skojarzony viewjest bardzo prosty, tylko jeden wiersz kodu:
@Html.Raw( ViewBag.result )
jak widać w kontrolerze, phantom ładuje plik javascript o nazwie createSnapshot.jspod utworzonym przeze mnie folderem seo. 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ę, z której otrzymałem podstawowy kod :-).
Zauważysz tutaj coś dziwnego: phantom ponownie ładuje stronę, dopóki checkLoaded()funkcja nie zwróci true. Dlaczego? Dzieje się tak, ponieważ moje konkretne SPA wykonuje kilka wywołań AJAX, aby pobrać 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 tutaj zrobiłem, to po ostatnim wywołaniu AJAX, dodałem a <span id='compositionComplete'></span>, więc jeśli ten tag istnieje, wiem, że DOM jest zakończony. Robię to w odpowiedzi na compositionCompletewydarzenie Durandala , patrz tutajpo więcej. Jeśli tak się nie stanie w ciągu 10 sekund, poddaję się (maksymalnie powinno to zająć tylko sekundę). Zwrócony kod HTML zawiera wszystkie linki, które użytkownik widzi w przeglądarce. Skrypt nie będzie działał poprawnie, ponieważ <script>tagi, które istnieją w migawce HTML, nie odwołują się do właściwego adresu URL. Można to również zmienić w pliku phantom javascript, ale nie sądzę, aby to było konieczne, ponieważ skrót HTML jest używany tylko przez Google do pobierania alinków, a nie do uruchamiania javascript; Te linki zrobić odwoływać się ładny URL, a jeśli rzeczywistości, jeśli starają się zobaczyć zrzut HTML w przeglądarce, dostaniesz błędy JavaScript, ale wszystkie linki będą działać poprawnie i skieruje cię do serwera po raz kolejny z dość URL tego czasu uzyskanie w pełni działającej strony.
To jest to. Teraz serwer wie, jak obsługiwać zarówno ładne, jak i brzydkie adresy URL, z włączonym stanem wypychania zarówno na serwerze, jak i kliencie. Wszystkie brzydkie adresy URL są traktowane w ten sam sposób za pomocą fantomu, więc nie ma potrzeby tworzenia oddzielnego kontrolera dla każdego typu wywołania.
Jedna rzecz może wolisz do zmian nie jest do zwołania walnego 'kategorii / podkategorii / produktu, ale dodać „magazyn”, tak, że związek będzie wyglądać mniej więcej tak: http://www.xyz.com/store/category/subCategory/product111. Pozwoli to uniknąć problemu w moim rozwiązaniu, że wszystkie nieprawidłowe adresy URL są traktowane tak, jakby były faktycznie wywołaniami kontrolera „indeksu” i przypuszczam, że można je wtedy obsłużyć w kontrolerze „sklepu” bez dodatku do web.configpokazanego powyżej .

Beamish
źródło
Mam krótkie pytanie, myślę, że teraz to działa, ale kiedy zgłaszam moją witrynę do Google i podam linki do Google, mapy witryn itp., Czy muszę podać google mysite.com/# ! czy po prostu mysite.com i google dodadzą escaped_fragment, ponieważ mam to w metatagu?
ccorrin,
ccorrin - zgodnie z moją najlepszą wiedzą nie musisz niczego podawać Google; bot google znajdzie Twoją witrynę i poszuka w niej ładnych adresów URL (nie zapomnij również dodać metatagu na stronie głównej, ponieważ może ona nie zawierać żadnych adresów URL). brzydki adres URL zawierający escaped_fragment jest zawsze dodawany tylko przez Google - nigdy nie powinieneś umieszczać go samodzielnie w swoich kodach HTML. i dzięki za wsparcie :-)
beamish
dzięki Bjorn i Sandra :-) Pracuję nad lepszą wersją tego dokumentu, która będzie zawierała również informacje o tym, jak buforować strony, aby przyspieszyć proces i robić to w bardziej powszechnym użyciu, gdzie adres URL zawiera nazwisko kontrolera;
Wyślę
To świetne wyjaśnienie !!. Zaimplementowałem to i działa jak urok w moim lokalnym devboxie. Problem występuje podczas wdrażania w witrynie Azure Websites, ponieważ witryna zawiesza się i po pewnym czasie pojawia się błąd 502. Masz pomysł, jak wdrożyć phantomjs na platformie Azure? ... Dzięki ( testypv.azurewebsites.net/?_escaped_fragment_=home/about )
yagopv
Nie mam doświadczenia z witrynami sieci Web platformy Azure, ale przychodzi mi do głowy, że być może proces sprawdzania pełnego załadowania strony nigdy nie jest wykonywany, więc serwer nieustannie próbuje ponownie załadować stronę bez powodzenia. być może w tym tkwi problem (nawet jeśli te kontrole są ograniczone czasowo, więc może go nie być)? spróbuj wpisać „return true”; jako pierwszą linię w 'checkLoaded ()' i zobacz, czy to robi różnicę.
beamish
4

Oto link do nagrania screencast z moich zajęć szkoleniowych Ember.js, które gościłem w Londynie 14 sierpnia. Przedstawia strategię zarówno dla aplikacji po stronie klienta, jak i dla aplikacji po stronie serwera, a także przedstawia na żywo, w jaki sposób wdrożenie tych funkcji zapewni Twojej aplikacji jednostronicowej JavaScript z gracją degradacji nawet dla użytkowników z wyłączoną obsługą JavaScript .

Używa PhantomJS do pomocy w indeksowaniu Twojej witryny.

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

  • Masz hostowaną wersję aplikacji internetowej, którą chcesz indeksować, ta witryna musi mieć WSZYSTKIE dane, które masz w produkcji
  • Napisz aplikację JavaScript (PhantomJS Script), aby załadować swoją witrynę
  • 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ź wszystkie linki na załadowanej stronie, które prowadzą do Twojej witryny (filtrowanie adresów URL)
    • Dodaj ten link do listy adresów URL „możliwych do zindeksowania”, jeśli nie został jeszcze zaindeksowany
    • Zapisz wyrenderowany model DOM w pliku w systemie plików, ale najpierw usuń WSZYSTKIE tagi skryptów
    • Na koniec utwórz plik Sitemap.xml zawierający zindeksowane adresy URL

Po wykonaniu tego kroku backend będzie obsługiwał statyczną wersję kodu HTML jako część tagu noscript na tej stronie. Umożliwi to Google i innym wyszukiwarkom zaindeksowanie każdej strony w Twojej witrynie, nawet jeśli Twoja aplikacja pierwotnie była aplikacją z pojedynczą stroną.

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

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

Joachim H. Skeie
źródło
0

Możesz skorzystać lub stworzyć własną usługę prerender swojego SPA z usługą o nazwie prerender. Możesz to sprawdzić na jego stronie prerender.io oraz w jego projekcie na githubie (używa PhantomJS i renderuje twoją stronę dla Ciebie).

Bardzo łatwo jest zacząć. Musisz tylko przekierować żądania robotów do usługi, a one otrzymają wyrenderowany kod HTML.

gabrielperales
źródło
2
Chociaż ten link może odpowiedzieć na pytanie, lepiej jest zawrzeć tutaj zasadnicze części odpowiedzi i podać link do odniesienia. Odpowiedzi zawierające tylko łącze mogą stać się nieprawidłowe, jeśli połączona strona ulegnie zmianie. - Z recenzji
timgeb
2
Masz rację. Zaktualizowałem swój komentarz ... Mam nadzieję, że teraz będzie bardziej precyzyjny.
gabrielperales