it-swarm.com.de

Wie mache ich einen SPA SEO für Crawler?

Ich habe daran gearbeitet, wie ein SPA von Google auf Basis von Anweisungen - crawlierbar gemacht werden kann. Obwohl es einige allgemeine Erklärungen gibt, konnte ich nirgendwo ein gründlicheres Schritt-für-Schritt-Tutorial mit konkreten Beispielen finden. Nachdem dies abgeschlossen ist, möchte ich meine Lösung mitteilen, damit auch andere sie nutzen und möglicherweise weiter verbessern können. 
Ich verwende MVC mit Webapi-Controllern und Phantomjs auf der Serverseite und Durandal auf der Clientseite, wobei Push-state aktiviert ist. Ich benutze auch Breezejs für die Client-Server-Dateninteraktion, die ich ausdrücklich empfehle, aber ich versuche, eine allgemeine Erklärung zu geben, die auch den Benutzern anderer Plattformen hilft.

142
beamish

Bevor Sie beginnen, stellen Sie bitte sicher, dass Sie verstehen, was google erfordert , insbesondere die Verwendung von ziemlich und hässlich URLs. Nun sehen wir uns die Implementierung an:

Client-Seite

Auf der Clientseite gibt es nur eine einzige HTML-Seite, die über AJAX Aufrufe dynamisch mit dem Server interagiert. Darum geht es bei SPA. Alle a -Tags auf der Clientseite werden in meiner Anwendung dynamisch erstellt. Wir werden später sehen, wie diese Links für den Google-Bot auf dem Server sichtbar gemacht werden. Jedes solche a -Tag muss in der Lage sein, ein pretty URL Im href -Tag zu haben, damit es von Googles Bot gecrawlt wird. Sie möchten nicht, dass der Teil href verwendet wird, wenn der Client darauf klickt (obwohl Sie möchten, dass der Server ihn analysieren kann, werden wir das später sehen), weil wir es möglicherweise nicht möchten eine neue Seite zum Laden, nur um einen AJAX -Aufruf auszuführen, um einige Daten in einem Teil der Seite anzuzeigen und die URL über Javascript zu ändern (z. B. mit HTML5 pushstate oder mit Durandaljs). Wir haben also sowohl ein href -Attribut für Google als auch ein onclick -Attribut, das die Aufgabe erfüllt, wenn der Benutzer auf den Link klickt. Da ich nun Push-state Verwende, möchte ich kein # Für die URL, sodass ein typisches a -Tag so aussehen kann:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>

"Kategorie" und "Unterkategorie" wären wahrscheinlich andere Ausdrücke wie "Kommunikation" und "Telefone" oder "Computer" und "Laptops" für ein Geschäft für Elektrogeräte. Offensichtlich würde es viele verschiedene Kategorien und Unterkategorien geben. Wie Sie sehen können, ist der Link direkt zur Kategorie, Unterkategorie und zum Produkt, nicht als zusätzlicher Parameter zu einer bestimmten "Store" -Seite wie http://www.xyz.com/store/category/subCategory/product111. Das liegt daran, dass ich kürzere und einfachere Links bevorzuge. Dies bedeutet, dass es keine Kategorie mit demselben Namen wie eine meiner "Seiten" gibt, d. H. "Über".
Ich werde nicht näher darauf eingehen, wie die Daten über AJAX (den onclick -Teil) geladen werden, sondern auf Google suchen. Es gibt viele gute Erklärungen. Das einzig Wichtige, was ich hier erwähnen möchte, ist, dass die URL im Browser so aussehen soll, wenn der Benutzer auf diesen Link klickt:
http://www.xyz.com/category/subCategory/product111. Und diese URL wird nicht an den Server gesendet! Denken Sie daran, dies ist ein SPA, bei dem die gesamte Interaktion zwischen dem Client und dem Server über AJAX erfolgt und überhaupt keine Links! Alle "Seiten" werden auf der Clientseite implementiert, und die andere URL ruft den Server nicht auf (der Server muss wissen, wie diese URLs zu behandeln sind, wenn sie als externe Links von einer anderen Site zu Ihrer Site verwendet werden. wir werden das später auf der Serverseite sehen). Nun, das wird von Durandal wunderbar gehandhabt. Ich kann es nur empfehlen, aber Sie können diesen Teil auch überspringen, wenn Sie andere Technologien bevorzugen. Wenn Sie sich dafür entscheiden und MS Visual Studio Express 2012 für Web wie ich verwenden, können Sie das Durandal Starter Kit installieren und dort in Shell.js Etwas verwenden so was:

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

Hier sind einige wichtige Dinge zu beachten:

  1. Die erste Route (mit route:'') Bezieht sich auf die URL, die keine zusätzlichen Daten enthält, d. H. http://www.xyz.com. Auf dieser Seite laden Sie allgemeine Daten mit AJAX. Möglicherweise enthält diese Seite überhaupt keine a Tags. Sie sollten das folgende Tag hinzufügen, damit der Google-Bot weiß, was er damit machen soll:
    <meta name="fragment" content="!">. Mit diesem Tag wandelt der Google-Bot die URL in www.xyz.com?_escaped_fragment_= Um, was wir später sehen werden.
  2. Die "Über" -Route ist nur ein Beispiel für einen Link zu anderen "Seiten", die Sie möglicherweise in Ihrer Webanwendung verwenden möchten.
  3. Der schwierige Teil ist, dass es keine "Kategorien" -Route gibt und es viele verschiedene Kategorien geben kann - von denen keine eine vordefinierte Route hat. Hier kommt mapUnknownRoutes ins Spiel. Es ordnet diese unbekannten Routen der Route 'store' zu und entfernt auch alle '!' von der URL für den Fall, dass es sich um einen pretty URL handelt, der von Googles Suchmaschine generiert wurde. Die Route 'store' übernimmt die Informationen in der Eigenschaft 'fragment' und ruft AJAX auf, um die Daten abzurufen, anzuzeigen und die URL lokal zu ändern. In meiner Anwendung lade ich nicht für jeden solchen Aufruf eine andere Seite. Ich ändere nur den Teil der Seite, in dem diese Daten relevant sind, und ändere auch die URL lokal.
  4. Beachten Sie den pushState:true, Der Durandal anweist, Push-Status-URLs zu verwenden.

Das ist alles, was wir auf der Client-Seite brauchen. Es kann auch mit gehashten URLs implementiert werden (in Durandal entfernen Sie einfach den pushState:true Dafür). Der komplexere Teil (zumindest für mich ...) war der Serverteil:

Serverseite

Ich verwende MVC 4.5 Auf der Serverseite mit WebAPI Controllern. Der Server muss tatsächlich drei Arten von URLs verarbeiten: die von Google generierten - sowohl pretty als auch ugly und auch eine "einfache" URL mit demselben Format wie die im Client angezeigte Browser. Schauen wir uns an, wie das geht:

Hübsche und "einfache" URLs werden vom Server zunächst so interpretiert, als würde versucht, auf einen nicht vorhandenen Controller zu verweisen. Der Server sieht so etwas wie http://www.xyz.com/category/subCategory/product111 Und sucht nach einem Controller namens 'category'. Daher füge ich in web.config Die folgende Zeile hinzu, um diese an einen bestimmten Fehlerbehandlungs-Controller umzuleiten:

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

Dadurch wird die URL in etwa wie folgt umgewandelt: http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111. Ich möchte, dass die URL an den Client gesendet wird, der die Daten über AJAX lädt. Daher besteht der Trick hier darin, den Standard-Index-Controller so aufzurufen, als würde er keinen Controller referenzieren. Ich mache das durch hinzufügen ein Hash zur URL vor allen Parametern 'category' und 'subCategory'; Für die Hash-URL ist kein spezieller Controller erforderlich, mit Ausnahme des standardmäßigen 'Index'-Controllers. Die Daten werden an den Client gesendet, der dann den Hash entfernt und die Informationen nach dem Hash verwendet, um die Daten über AJAX zu laden. Hier ist der Fehlerbehandlungs-Controller-Code:

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


Aber was ist mit dem Hässliche URLs? Diese werden von Googles Bot erstellt und sollten einfaches HTML zurückgeben, das alle Daten enthält, die der Benutzer im Browser sieht. Dafür benutze ich Phantomjs . Phantom ist ein Browser ohne Kopf, der das tut, was der Browser auf der Clientseite tut - aber auf der Serverseite. Mit anderen Worten, Phantom weiß (unter anderem), wie man eine Webseite über eine URL abruft, sie analysiert und den gesamten Javascript-Code darin ausführt (sowie Daten über AJAX Aufrufe abruft) und gibt Sie sichern den HTML-Code, der das DOM widerspiegelt. Wenn Sie MS Visual Studio Express verwenden, möchten viele Phantom über dieses Link installieren.
Aber zuerst müssen wir eine hässliche URL abfangen, wenn sie an den Server gesendet wird. Dazu habe ich dem Ordner 'App_start' folgende Datei hinzugefügt:

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

Dies wird von 'filterConfig.cs' auch in 'App_start' aufgerufen:

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

Wie Sie sehen, leitet 'AjaxCrawlableAttribute' hässliche URLs an einen Controller mit dem Namen 'HtmlSnapshot' weiter. Hier ist dieser Controller:

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

    }
}

Das zugehörige view ist sehr einfach, nur eine Codezeile:
@Html.Raw( ViewBag.result )
Wie Sie im Controller sehen können, lädt Phantom eine Javascript-Datei mit dem Namen createSnapshot.js In einen von mir erstellten Ordner mit dem Namen seo. Hier ist diese Javascript-Datei:

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

Ich möchte mich zuerst bei Thomas Davis für die Seite bedanken, von der ich den Basiscode erhalten habe :-).
Hier werden Sie etwas Merkwürdiges bemerken: Das Phantom lädt die Seite so lange neu, bis die Funktion checkLoaded() true zurückgibt. Warum ist das so? Dies liegt daran, dass mein spezifischer SPA mehrere AJAX Aufrufe ausführt, um alle Daten abzurufen und im DOM auf meiner Seite abzulegen. Das Phantom kann nicht wissen, wann alle Aufrufe abgeschlossen wurden, bevor es mir die HTML-Reflektion des zurückgibt DOM. Nach dem letzten Aufruf von AJAX füge ich einen <span id='compositionComplete'></span> Hinzu. Wenn dieses Tag existiert, weiß ich, dass das DOM vollständig ist. Ich mache das als Reaktion auf Durandals compositionComplete -Ereignis, siehe hier für mehr. Wenn dies nicht innerhalb von 10 Sekunden passiert, gebe ich auf (es sollte nur eine Sekunde dauern, bis es so weit ist). Der zurückgegebene HTML-Code enthält alle Links, die der Benutzer im Browser sieht. Das Skript funktioniert nicht ordnungsgemäß, da die im HTML-Snapshot vorhandenen Tags <script> Nicht auf die richtige URL verweisen. Dies kann auch in der Javascript-Phantom-Datei geändert werden, aber ich halte dies nicht für notwendig, da der HTML-Snapshort nur von Google verwendet wird, um die a -Links abzurufen und kein Javascript auszuführen. diese links machen Verweisen Sie auf eine hübsche URL, und wenn Sie tatsächlich versuchen, den HTML-Schnappschuss in einem Browser anzuzeigen, erhalten Sie Javascript-Fehler, aber alle Links funktionieren ordnungsgemäß und leiten Sie erneut mit einer hübschen URL zum Server, wobei Sie diesmal die vollständige URL erhalten Arbeitsseite.
Das ist es. Jetzt weiß der Server, wie er mit hübschen und hässlichen URLs umgeht, wobei der Push-Status sowohl auf dem Server als auch auf dem Client aktiviert ist. Alle hässlichen URLs werden mit Phantom auf die gleiche Weise behandelt, sodass nicht für jeden Anruftyp ein separater Controller erstellt werden muss.
Eine Sache, die Sie vielleicht lieber ändern möchten, ist nicht, einen allgemeinen "Kategorie/Unterkategorie/Produkt" -Aufruf zu tätigen, sondern einen "Laden" hinzuzufügen, damit der Link ungefähr so ​​aussieht: http://www.xyz.com/store/category/subCategory/product111. Dies vermeidet das Problem in meiner Lösung, dass alle ungültigen URLs so behandelt werden, als wären sie tatsächlich Aufrufe des 'Index'-Controllers, und ich nehme an, dass diese dann innerhalb des' Store'-Controllers ohne den Zusatz von web.config Ich zeigte oben.

122
beamish

Google kann nun SPA-Seiten darstellen: Ablehnen unseres AJAX = Crawling-Schema

32
Edward Olamisan

Hier ist ein Link zu einer Screencast-Aufnahme aus meiner Ember.js-Schulung, die ich am 14. August in London veranstaltet habe. Es skizziert eine Strategie sowohl für Ihre clientseitige Anwendung als auch für Ihre serverseitige Anwendung und zeigt live, wie die Implementierung dieser Funktionen Ihre JavaScript-Single-Page-App selbst für Benutzer mit deaktivierter JavaScript-Funktion verschlechtert . 

Es verwendet PhantomJS, um das Crawlen Ihrer Website zu unterstützen. 

Kurz gesagt, sind folgende Schritte erforderlich: 

  • Wenn Sie über eine gehostete Version der Webanwendung verfügen, die Sie crawlen möchten, müssen auf dieser Site ALLE Daten vorhanden sein, die Sie in der Produktion haben
  • Schreiben Sie eine JavaScript-Anwendung (PhantomJS-Skript), um Ihre Website zu laden
  • Fügen Sie der Liste der zu durchsuchenden URLs index.html (oder „/“) hinzu
    • Pop die erste URL, die der Durchforstungsliste hinzugefügt wurde
    • Seite laden und DOM rendern 
    • Suchen Sie nach Links auf der geladenen Seite, die auf Ihre eigene Website verweisen (URL-Filterung).
    • Fügen Sie diesen Link zu einer Liste "durchsuchbarer" URLs hinzu, sofern diese nicht bereits gecrawlt wurde
    • Speichern Sie das gerenderte DOM in einer Datei im Dateisystem, entfernen Sie jedoch zunächst ALLE Skript-Tags
    • Erstellen Sie am Ende eine Datei "Sitemap.xml" mit den durchforsteten URLs

Sobald dieser Schritt abgeschlossen ist, müssen Sie die statische Version Ihres HTML-Codes als Teil des Noscript-Tags auf dieser Seite bereitstellen. Dadurch können Google und andere Suchmaschinen jede einzelne Seite Ihrer Website crawlen, auch wenn Ihre App ursprünglich eine Single-Page-App ist. 

Link zum Screencast mit allen Details: 

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

4

Sie können http://sparender.com/ verwenden, um Single Crawler-Anwendungen korrekt zu crawlen.

0
ddtxra

Sie können Ihren eigenen Service für den Prerender Ihres SPA mit dem Service namens Prerender verwenden oder erstellen. Sie können es auf seiner Website prerender.io und in seinem github-Projekt überprüfen (Es verwendet PhantomJS und macht Ihre Website für Sie rendern). 

Es ist sehr leicht mit zu beginnen. Sie müssen nur Crawler-Anforderungen an den Dienst umleiten, und diese erhalten die gerenderte HTML-Datei.

0
gabrielperales