"Single-Page" JS-Websites und SEO


128

Es gibt heutzutage viele coole Tools, um leistungsstarke JavaScript-Websites mit nur einer Seite zu erstellen. Meiner Meinung nach geschieht dies richtig, indem der Server als API (und nicht mehr) fungiert und der Client die gesamte HTML-Generierung übernimmt. Das Problem mit diesem "Muster" ist der Mangel an Suchmaschinenunterstützung. Ich kann mir zwei Lösungen vorstellen:

  1. Wenn der Benutzer die Website betritt, lässt der Server die Seite genau so rendern, wie es der Client bei der Navigation tun würde. Wenn ich also http://example.com/my_pathdirekt zu gehe, würde der Server dasselbe rendern wie der Client, wenn ich /my_pathüber pushState gehe.
  2. Lassen Sie den Server nur für die Suchmaschinen-Bots eine spezielle Website bereitstellen. Wenn ein normaler Benutzer http://example.com/my_pathden Server besucht, sollte er eine JavaScript-schwere Version der Website erhalten. Wenn der Google-Bot jedoch besucht, sollte der Server ihm minimalen HTML-Code mit dem Inhalt geben, den Google indizieren soll.

Die erste Lösung wird hier weiter diskutiert . Ich habe an einer Website gearbeitet und es ist keine sehr schöne Erfahrung. Es ist nicht trocken und in meinem Fall musste ich zwei verschiedene Template-Engines für den Client und den Server verwenden.

Ich glaube, ich habe die zweite Lösung für einige gute alte Flash-Websites gesehen. Ich mag diesen Ansatz viel mehr als den ersten und mit dem richtigen Tool auf dem Server könnte es ziemlich schmerzlos gemacht werden.

Was ich mich also wirklich wundere, ist Folgendes:

  • Können Sie sich eine bessere Lösung vorstellen?
  • Was sind die Nachteile der zweiten Lösung? Wenn Google in irgendeiner Weise feststellt, dass ich nicht den gleichen Inhalt für den Google-Bot wie ein normaler Nutzer bereitstelle, werde ich dann in den Suchergebnissen bestraft?

Antworten:


44

Während # 2 für Sie als Entwickler "einfacher" sein könnte, bietet es nur Suchmaschinen-Crawlen. Und ja, wenn Google herausfindet, dass Sie unterschiedliche Inhalte bereitstellen, werden Sie möglicherweise bestraft (ich bin kein Experte dafür, aber ich habe davon gehört).

Sowohl SEO als auch Barrierefreiheit (nicht nur für behinderte Menschen, sondern auch Barrierefreiheit über mobile Geräte, Touchscreen-Geräte und andere nicht standardmäßige Computer- / internetfähige Plattformen) haben beide eine ähnliche Philosophie: semantisch reichhaltiges Markup, das "zugänglich" ist (dh kann) auf all diese verschiedenen Browser zugegriffen, angezeigt, gelesen, verarbeitet oder anderweitig verwendet werden. Ein Bildschirmleser, ein Suchmaschinen-Crawler oder ein Benutzer mit aktiviertem JavaScript sollte in der Lage sein, die Kernfunktionen Ihrer Website ohne Probleme zu verwenden / zu indizieren / zu verstehen.

pushStateträgt meiner Belastung nach meiner Erfahrung nicht bei. Es bringt nur das, was früher ein nachträglicher Gedanke war und "wenn wir Zeit haben", in den Vordergrund der Webentwicklung.

Was Sie in Option 1 beschreiben, ist normalerweise der beste Weg - aber wie bei anderen Problemen mit der Barrierefreiheit und der Suchmaschinenoptimierung pushStateerfordert dies in einer JavaScript-lastigen App eine Vorausplanung, da dies zu einer erheblichen Belastung wird. Es sollte von Anfang an in die Seiten- und Anwendungsarchitektur integriert werden - das Nachrüsten ist schmerzhaft und führt zu mehr Duplikaten als erforderlich.

Ich habe pushStatekürzlich mit und SEO für ein paar verschiedene Anwendungen gearbeitet und fand, was ich für einen guten Ansatz halte. Es folgt im Wesentlichen Ihrem Artikel Nr. 1, berücksichtigt jedoch, dass HTML / Vorlagen nicht dupliziert werden.

Die meisten Informationen finden Sie in diesen beiden Blog-Beiträgen:

http://lostechies.com/derickbailey/2011/09/06/test-driving-backbone-views-with-jquery-templates-the-jasmine-gem-and-jasmine-jquery/

und

http://lostechies.com/derickbailey/2011/06/22/rendering-a-rails-partial-as-a-jquery-template/

Das Wesentliche dabei ist, dass ich ERB- oder HAML-Vorlagen (mit Ruby on Rails, Sinatra usw.) für mein serverseitiges Rendern und zum Erstellen der clientseitigen Vorlagen verwende, die Backbone verwenden kann, sowie für meine Jasmine-JavaScript-Spezifikationen. Dies verhindert das Duplizieren von Markups zwischen der Serverseite und der Clientseite.

Von dort aus müssen Sie einige zusätzliche Schritte ausführen, damit Ihr JavaScript mit dem vom Server gerenderten HTML funktioniert - echte progressive Verbesserung; Nehmen Sie das gelieferte semantische Markup und erweitern Sie es mit JavaScript.

Zum Beispiel baue ich eine Bildergalerie-Anwendung mit pushState. Wenn Sie dies /images/1vom Server anfordern , wird die gesamte Bildergalerie auf dem Server gerendert und der gesamte HTML-, CSS- und JavaScript-Code an Ihren Browser gesendet. Wenn Sie JavaScript deaktiviert haben, funktioniert es einwandfrei. Jede Aktion, die Sie ausführen, fordert eine andere URL vom Server an und der Server rendert das gesamte Markup für Ihren Browser. Wenn Sie jedoch JavaScript aktiviert haben, nimmt das JavaScript den bereits gerenderten HTML-Code zusammen mit einigen vom Server generierten Variablen auf und übernimmt von dort aus.

Hier ist ein Beispiel:

<form id="foo">
  Name: <input id="name"><button id="say">Say My Name!</button>
</form>

Nachdem der Server dies gerendert hat, nimmt das JavaScript es auf (in diesem Beispiel mithilfe der Ansicht Backbone.js).

FooView = Backbone.View.extend({
  events: {
    "change #name": "setName",
    "click #say": "sayName"
  },

  setName: function(e){
    var name = $(e.currentTarget).val();
    this.model.set({name: name});
  },

  sayName: function(e){
    e.preventDefault();
    var name = this.model.get("name");
    alert("Hello " + name);
  },

  render: function(){
    // do some rendering here, for when this is just running JavaScript
  }
});

$(function(){
  var model = new MyModel();
  var view = new FooView({
    model: model,
    el: $("#foo")
  });
});

Dies ist ein sehr einfaches Beispiel, aber ich denke, es bringt den Punkt auf den Punkt.

Wenn ich die Ansicht nach dem Laden der Seite instanziiere, stelle ich der Ansichtsinstanz den vorhandenen Inhalt des vom Server gerenderten Formulars als elfür die Ansicht bereit. Ich rufe nicht rendern auf oder lasse die Ansicht elfür mich generieren , wenn die erste Ansicht geladen wird. Ich habe eine Rendermethode zur Verfügung, nachdem die Ansicht ausgeführt wurde und die Seite ausschließlich aus JavaScript besteht. Auf diese Weise kann ich die Ansicht später bei Bedarf erneut rendern.

Wenn Sie bei aktiviertem JavaScript auf die Schaltfläche "Sag meinen Namen" klicken, wird ein Warnfeld angezeigt. Ohne JavaScript würde es auf den Server zurückgesendet und der Server könnte den Namen irgendwo in ein HTML-Element rendern.

Bearbeiten

Stellen Sie sich ein komplexeres Beispiel vor, in dem Sie eine Liste haben, die angehängt werden muss (aus den Kommentaren darunter).

Angenommen, Sie haben eine Liste von Benutzern in einem <ul>Tag. Diese Liste wurde vom Server gerendert, als der Browser eine Anfrage stellte, und das Ergebnis sieht ungefähr so ​​aus:

<ul id="user-list">
  <li data-id="1">Bob
  <li data-id="2">Mary
  <li data-id="3">Frank
  <li data-id="4">Jane
</ul>

Jetzt müssen Sie diese Liste durchlaufen und jedem <li>Element eine Backbone-Ansicht und ein Backbone-Modell hinzufügen. Mithilfe des data-idAttributs können Sie leicht das Modell finden, von dem jedes Tag stammt. Sie benötigen dann eine Sammlungsansicht und eine Elementansicht, die intelligent genug sind, um sich an dieses HTML anzuhängen.

UserListView = Backbone.View.extend({
  attach: function(){
    this.el = $("#user-list");
    this.$("li").each(function(index){
      var userEl = $(this);
      var id = userEl.attr("data-id");
      var user = this.collection.get(id);
      new UserView({
        model: user,
        el: userEl
      });
    });
  }
});

UserView = Backbone.View.extend({
  initialize: function(){
    this.model.bind("change:name", this.updateName, this);
  },

  updateName: function(model, val){
    this.el.text(val);
  }
});

var userData = {...};
var userList = new UserCollection(userData);
var userListView = new UserListView({collection: userList});
userListView.attach();

In diesem Beispiel UserListViewwerden alle <li>Tags durchlaufen und ein Ansichtsobjekt mit dem richtigen Modell für jedes angehängt. Es richtet einen Ereignishandler für das Ereignis zur Namensänderung des Modells ein und aktualisiert den angezeigten Text des Elements, wenn eine Änderung auftritt.


Diese Art von Prozess, um das vom Server gerenderte HTML zu übernehmen und mein JavaScript übernehmen und ausführen zu lassen, ist eine großartige Möglichkeit, die Dinge für SEO, Barrierefreiheit und pushStateSupport ins Rollen zu bringen .

Hoffentlich hilft das.


Ich verstehe, aber was interessant ist, ist, wie das Rendern nach "Ihr JavaScript übernimmt" durchgeführt wird. In einem komplizierteren Beispiel müssen Sie möglicherweise eine nicht kompilierte Vorlage auf dem Client verwenden und eine Reihe von Benutzern durchlaufen, um eine Liste zu erstellen. Die Ansicht wird jedes Mal neu gerendert, wenn sich das Modell eines Benutzers ändert. Wie würden Sie das tun, ohne die Vorlagen zu duplizieren (und den Server nicht zu bitten, die Ansicht für den Client zu rendern)?
user544941

Die 2 Blog-Beiträge, die ich verlinkt habe, sollten Ihnen gemeinsam zeigen, wie Sie Vorlagen haben, die auf dem Client und Server verwendet werden können - keine Duplizierung erforderlich. Der Server muss die gesamte Seite rendern, wenn Sie möchten, dass sie zugänglich und SEO-freundlich ist. Ich habe meine Antwort aktualisiert, um ein komplexeres Beispiel für das Anhängen an eine Benutzerliste aufzunehmen, die vom Server gerendert wurde
Derick Bailey

22

Ich denke, Sie brauchen dies: http://code.google.com/web/ajaxcrawling/

Sie können auch ein spezielles Backend installieren, das Ihre Seite durch Ausführen von Javascript auf dem Server "rendert" und dieses dann an Google weiterleitet.

Kombinieren Sie beide Dinge und Sie haben eine Lösung, ohne die Dinge zweimal zu programmieren. (Solange Ihre App über Ankerfragmente vollständig steuerbar ist.)


Eigentlich ist es nicht das, wonach ich suche. Dies sind einige Varianten der ersten Lösung, und wie bereits erwähnt, bin ich mit diesem Ansatz nicht sehr zufrieden.
user544941

2
Du hast meine ganze Antwort nicht gelesen. Sie verwenden auch ein spezielles Backend, das das Javascript für Sie rendert - Sie schreiben die Dinge nicht zweimal.
Ariel

Ja, das habe ich gelesen. Aber wenn ich Sie richtig verstanden hätte, wäre das ein verdammt gutes Programm, da es jede Aktion simulieren müsste, die den pushState auslöst. Alternativ könnte ich die Aktionen direkt dazu geben, aber dann sind wir nicht mehr so ​​trocken.
user544941

2
Ich denke, es ist im Grunde ein Browser ohne Front. Aber ja, Sie müssen das Programm vollständig von Ankerfragmenten aus steuern. Sie müssen auch sicherstellen, dass alle Links das richtige Fragment enthalten, zusammen mit oder anstelle von onClicks.
Ariel

17

Es scheint also, dass das Hauptanliegen DRY ist

  • Wenn Sie pushState verwenden, muss Ihr Server für alle URLs (die keine Dateierweiterung zum Bereitstellen von Bildern usw. enthalten) denselben genauen Code senden: "/ mydir / myfile", "/ myotherdir / myotherfile" oder root "/ "- Alle Anfragen erhalten den gleichen genauen Code. Sie benötigen eine Art URL-Rewrite-Engine. Sie können auch ein kleines Stück HTML bereitstellen, und der Rest kann von Ihrem CDN stammen (verwenden Sie require.js, um Abhängigkeiten zu verwalten - siehe https://stackoverflow.com/a/13813102/1595913 ).
  • (Testen Sie die Gültigkeit des Links, indem Sie den Link in Ihr URL-Schema konvertieren und die Existenz von Inhalten testen, indem Sie eine statische oder dynamische Quelle abfragen. Wenn dies nicht gültig ist, senden Sie eine 404-Antwort.)
  • Wenn die Anfrage nicht von einem Google Bot stammt, verarbeiten Sie sie einfach normal.
  • Wenn die Anfrage von einem Google Bot stammt, verwenden Sie phantom.js - Headless Webkit Browser ( "Ein Headless Browser ist einfach ein voll funktionsfähiger Webbrowser ohne visuelle Oberfläche." ), Um HTML und Javascript auf dem Server zu rendern und das zu senden Google Bot das resultierende HTML. Während der Bot das HTML analysiert, kann er auf Ihre anderen "pushState" -Links / einige Seiten auf dem Server zugreifen <a href="https://stackoverflow.com/someotherpage">mylink</a>, der Server schreibt die URL in Ihre Anwendungsdatei neu, lädt sie in phantom.js und das resultierende HTML wird an den Bot gesendet und so weiter. ..
  • Für Ihr HTML gehe ich davon aus, dass Sie normale Links mit einer Art Hijacking verwenden (z. B. mit backbone.js https://stackoverflow.com/a/9331734/1595913 ).
  • Um Verwechslungen mit Links zu vermeiden, trennen Sie Ihren API-Code, der json dient, in eine separate Subdomain, z. B. api.mysite.com
  • Um die Leistung zu verbessern, können Sie Ihre Websiteseiten außerhalb der Geschäftszeiten vorab für Suchmaschinen vorverarbeiten, indem Sie statische Versionen der Seiten mit demselben Mechanismus wie phantom.js erstellen und die statischen Seiten folglich für Google Bots bereitstellen. Die Vorverarbeitung kann mit einer einfachen App erfolgen, die <a>Tags analysieren kann . In diesem Fall ist die Handhabung von 404 einfacher, da Sie einfach überprüfen können, ob die statische Datei mit einem Namen vorhanden ist, der den URL-Pfad enthält.
  • Wenn du benutzt #! Hash-Bang-Syntax für Ihre Site-Links Es gilt ein ähnliches Szenario, mit der Ausnahme, dass die URL-Server-Engine zum Umschreiben in der URL nach _escaped_fragment_ Ausschau hält und die URL in Ihr URL-Schema formatiert.
  • Es gibt einige Integrationen von node.js mit phantom.js auf github, und Sie können node.js als Webserver verwenden, um eine HTML-Ausgabe zu erstellen.

Hier einige Beispiele für die Verwendung von phantom.js für SEO:

http://backbonetutorials.com/seo-for-single-page-apps/

http://thedigitalself.com/blog/seo-and-javascript-with-phantomjs-server-side-rendering


4

Wenn Sie Rails verwenden, versuchen Sie es mit Poirot . Es ist ein Juwel, das es kinderleicht macht, Schnurrbart- oder Lenkervorlagen auf Client- und Serverseite wiederzuverwenden .

Erstellen Sie eine Datei in Ihren Ansichten wie _some_thingy.html.mustache.

Serverseite rendern:

<%= render :partial => 'some_thingy', object: my_model %>

Legen Sie die Vorlage für die clientseitige Verwendung in Ihren Kopf:

<%= template_include_tag 'some_thingy' %>

Rendre Client-Seite:

html = poirot.someThingy(my_model)

3

Um einen etwas anderen Blickwinkel einzunehmen, wäre Ihre zweite Lösung in Bezug auf die Barrierefreiheit die richtige. Sie würden Benutzern, die kein Javascript verwenden können (Benutzer mit Bildschirmleseprogrammen usw.), alternative Inhalte bereitstellen.

Dies würde automatisch die Vorteile von SEO hinzufügen und würde meiner Meinung nach von Google nicht als "ungezogene" Technik angesehen.


Und hat dir jemand das Gegenteil bewiesen? Es ist einige Zeit her, seit der Kommentar veröffentlicht wurde
jkulak

1

Interessant. Ich habe nach tragfähigen Lösungen gesucht, aber es scheint ziemlich problematisch zu sein.

Ich habe mich tatsächlich mehr auf Ihren zweiten Ansatz konzentriert:

Lassen Sie den Server nur für die Suchmaschinen-Bots eine spezielle Website bereitstellen. Wenn ein normaler Benutzer http://example.com/my_path besucht, sollte der Server ihm eine JavaScript-schwere Version der Website geben. Wenn der Google-Bot jedoch besucht, sollte der Server ihm minimalen HTML-Code mit dem Inhalt geben, den Google indizieren soll.

Hier ist meine Einstellung zur Lösung des Problems. Obwohl nicht bestätigt wurde, dass es funktioniert, bietet es möglicherweise einige Einblicke oder Ideen für andere Entwickler.

Angenommen, Sie verwenden ein JS-Framework, das die Push-Status-Funktionalität unterstützt, und Ihr Backend-Framework ist Ruby on Rails. Sie haben eine einfache Blog-Site und möchten, dass Suchmaschinen alle Ihre Artikel indexund showSeiten indizieren .

Angenommen, Sie haben Ihre Routen wie folgt eingerichtet:

resources :articles
match "*path", "main#index"

Stellen Sie sicher, dass jeder serverseitige Controller dieselbe Vorlage rendert, die Ihr clientseitiges Framework zum Ausführen benötigt (html / css / javascript / etc). Wenn keiner der Controller in der Anforderung übereinstimmt (in diesem Beispiel haben wir nur einen RESTful-Satz von Aktionen für den ArticlesController), stimmen Sie einfach mit etwas anderem überein und rendern Sie einfach die Vorlage und lassen Sie das clientseitige Framework das Routing übernehmen. Der einzige Unterschied zwischen dem Aufrufen eines Controllers und dem Aufrufen des Platzhalter-Matchers besteht in der Möglichkeit, Inhalte basierend auf der URL zu rendern, die für Geräte mit deaktiviertem JavaScript angefordert wurde.

Soweit ich weiß, ist es eine schlechte Idee, Inhalte zu rendern, die für Browser nicht sichtbar sind. Wenn Google es indiziert, gehen die Leute über Google, um eine bestimmte Seite zu besuchen, und es gibt keinen Inhalt. Dann werden Sie wahrscheinlich bestraft. Was Ihnen in den Sinn kommt, ist, dass Sie Inhalte in einem divKnoten rendern , den Sie display: nonein CSS verwenden.

Ich bin mir jedoch ziemlich sicher, dass es keine Rolle spielt, wenn Sie dies einfach tun:

<div id="no-js">
  <h1><%= @article.title %></h1>
  <p><%= @article.description %></p>
  <p><%= @article.content %></p>
</div>

Und dann JavaScript verwenden, das nicht ausgeführt wird, wenn ein Gerät mit deaktiviertem JavaScript die Seite öffnet:

$("#no-js").remove() # jQuery

Auf diese Weise wird für Google und für alle Benutzer mit JavaScript-deaktivierten Geräten der rohe / statische Inhalt angezeigt. Der Inhalt ist also physisch vorhanden und für jeden mit JavaScript-deaktivierten Geräten sichtbar.

Aber, wenn ein Nutzer die gleiche Seite und tatsächlich hat JavaScript aktiviert, die #no-jsden Knoten entfernt werden , damit es nicht Ihre Anwendung verunstalteten ist. Anschließend verarbeitet Ihr clientseitiges Framework die Anforderung über seinen Router und zeigt an, was ein Benutzer sehen sollte, wenn JavaScript aktiviert ist.

Ich denke, dies könnte eine gültige und ziemlich einfach zu verwendende Technik sein. Dies kann jedoch von der Komplexität Ihrer Website / Anwendung abhängen.

Bitte korrigieren Sie mich, wenn dies nicht der Fall ist. Ich dachte nur, ich würde meine Gedanken teilen.


1
Nun, wenn Sie Inhalte zuerst anzeigen und etwas später entfernen, wird der Endbenutzer höchstwahrscheinlich feststellen, dass Inhalte in seinem Browser blinken / flackern :) Insbesondere, wenn es sich um einen langsamen Browser handelt, große HTML-Inhalte, die Sie anzeigen / entfernen möchten, und einige Verzögerung, bevor Ihr JS-Code geladen und ausgeführt wird. Was denkst du?
Evereq

1

Verwenden Sie NodeJS auf der Serverseite, durchsuchen Sie Ihren clientseitigen Code und leiten Sie die URL jeder http-Anforderung (mit Ausnahme statischer http-Ressourcen) durch einen serverseitigen Client, um den ersten "Bootsnap" (eine Momentaufnahme der Seite, auf der sich der Status befindet) bereitzustellen. Verwenden Sie so etwas wie jsdom, um jquery dom-ops auf dem Server zu verarbeiten. Richten Sie nach der Rückkehr des Bootsnaps die Websocket-Verbindung ein. Am besten unterscheiden Sie wahrscheinlich zwischen einem Websocket-Client und einem serverseitigen Client, indem Sie eine Art Wrapper-Verbindung auf der Clientseite herstellen (der serverseitige Client kann direkt mit dem Server kommunizieren). Ich habe an so etwas gearbeitet: https://github.com/jvanveen/rnet/


0

Verwenden Sie die Google Closure-Vorlage , um Seiten zu rendern. Es wird in Javascript oder Java kompiliert, sodass es einfach ist, die Seite entweder auf der Client- oder der Serverseite zu rendern. Rendern Sie bei der ersten Begegnung mit jedem Client das HTML und fügen Sie Javascript als Link in den Header ein. Crawler liest nur das HTML, aber der Browser führt Ihr Skript aus. Alle nachfolgenden Anfragen vom Browser könnten gegen die API erfolgen, um den Datenverkehr zu minimieren.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.