Sitios web JS de "página única" y SEO


128

Hoy en día, existen muchas herramientas geniales para crear sitios web de JavaScript potentes de "página única". En mi opinión, esto se hace bien dejando que el servidor actúe como una API (y nada más) y dejando que el cliente maneje todas las cosas de generación de HTML. El problema con este "patrón" es la falta de soporte para motores de búsqueda. Se me ocurren dos soluciones:

  1. Cuando el usuario ingresa al sitio web, deje que el servidor renderice la página exactamente como lo haría el cliente al navegar. Entonces, si voy http://example.com/my_pathdirectamente al servidor, se representará lo mismo que el cliente si voy a /my_pathtravés de pushState.
  2. Deje que el servidor proporcione un sitio web especial solo para los robots de los motores de búsqueda. Si un usuario normal visita http://example.com/my_pathel servidor, debe darle una versión de JavaScript del sitio web. Pero si el bot de Google visita, el servidor debería darle un mínimo de HTML con el contenido que quiero que indexe Google.

La primera solución se discute más aquí . He estado trabajando en un sitio web haciendo esto y no es una experiencia muy agradable. No está SECO y en mi caso tuve que usar dos motores de plantillas diferentes para el cliente y el servidor.

Creo que he visto la segunda solución para algunos buenos sitios web Flash. Este enfoque me gusta mucho más que el primero y, con la herramienta adecuada en el servidor, podría hacerse sin problemas.

Entonces, lo que realmente me pregunto es lo siguiente:

  • ¿Se te ocurre alguna solución mejor?
  • ¿Cuáles son las desventajas con la segunda solución? Si Google de alguna manera descubre que no estoy sirviendo exactamente el mismo contenido para el bot de Google que un usuario normal, ¿me castigarían en los resultados de búsqueda?

Respuestas:


44

Si bien el # 2 puede ser "más fácil" para usted como desarrollador, solo proporciona rastreo de motores de búsqueda. Y sí, si Google descubre que estás sirviendo contenido diferente, podrías ser penalizado (no soy un experto en eso, pero he oído que sucede).

Tanto el SEO como la accesibilidad (no solo para las personas discapacitadas, sino también la accesibilidad a través de dispositivos móviles, dispositivos de pantalla táctil y otras plataformas habilitadas para Internet / computación no estándar) tienen una filosofía subyacente similar: marcado semánticamente rico que es "accesible" (es decir, puede se puede acceder, ver, leer, procesar o utilizar de otro modo) a todos estos navegadores diferentes. Un lector de pantalla, un buscador de motores de búsqueda o un usuario con JavaScript habilitado, deberían poder usar / indexar / comprender la funcionalidad principal de su sitio sin problemas.

pushStateNo agrega a esta carga, en mi experiencia. Solo trae lo que solía ser una idea de último momento y "si tenemos tiempo" a la vanguardia del desarrollo web.

Lo que describe en la opción n. ° 1 suele ser la mejor manera de hacerlo, pero, al igual que otros problemas de accesibilidad y SEO, hacerlo con pushStateuna aplicación con mucho JavaScript requiere una planificación inicial o se convertirá en una carga significativa. Debe integrarse en la página y la arquitectura de la aplicación desde el principio: la modificación es dolorosa y causará más duplicación de la necesaria.

He estado trabajando con pushStateSEO recientemente para un par de aplicaciones diferentes, y encontré lo que creo que es un buen enfoque. Básicamente sigue a su elemento # 1, pero explica no duplicar html / templates.

La mayor parte de la información se puede encontrar en estas dos publicaciones de blog:

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

y

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

La esencia de esto es que uso plantillas ERB o HAML (ejecutando Ruby on Rails, Sinatra, etc.) para mi render del lado del servidor y para crear las plantillas del lado del cliente que Backbone puede usar, así como para mis especificaciones JavaScript de Jasmine. Esto elimina la duplicación de marcado entre el lado del servidor y el lado del cliente.

A partir de ahí, debe seguir algunos pasos adicionales para que su JavaScript funcione con el HTML que representa el servidor: verdadera mejora progresiva; tomar el marcado semántico que se entregó y mejorarlo con JavaScript.

Por ejemplo, estoy creando una aplicación de galería de imágenes con pushState. Si lo solicita /images/1desde el servidor, renderizará toda la galería de imágenes en el servidor y enviará todo el HTML, CSS y JavaScript a su navegador. Si tiene JavaScript deshabilitado, funcionará perfectamente bien. Cada acción que realice solicitará una URL diferente del servidor y el servidor representará todo el marcado para su navegador. Sin embargo, si tiene habilitado JavaScript, el JavaScript recogerá el HTML ya representado junto con algunas variables generadas por el servidor y se hará cargo de allí.

Aquí hay un ejemplo:

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

Después de que el servidor procesa esto, el JavaScript lo recogerá (usando una vista Backbone.js en este ejemplo)

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

Este es un ejemplo muy simple, pero creo que se entiende.

Cuando instalo la vista una vez que se carga la página, proporciono el contenido existente del formulario que fue presentado por el servidor, a la instancia de vista como elpara la vista. Estoy no llamando render o tener la vista genere una elpara mí, cuando se carga la primera vista. Tengo un método de renderizado disponible después de que la vista esté en funcionamiento y la página sea todo JavaScript. Esto me permite volver a renderizar la vista más tarde si es necesario.

Al hacer clic en el botón "Decir mi nombre" con JavaScript habilitado, aparecerá un cuadro de alerta. Sin JavaScript, se volvería a publicar en el servidor y el servidor podría representar el nombre de un elemento html en alguna parte.

Editar

Considere un ejemplo más complejo, donde tiene una lista que debe adjuntarse (de los comentarios a continuación)

Digamos que tiene una lista de usuarios en una <ul>etiqueta. El servidor prestó esta lista cuando el navegador realizó una solicitud y el resultado se parece a:

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

Ahora debe recorrer esta lista y adjuntar una vista Backbone y un modelo a cada uno de los <li>elementos. Con el uso del data-idatributo, puede encontrar fácilmente el modelo del que proviene cada etiqueta. Luego, necesitará una vista de colección y una vista de elemento que sea lo suficientemente inteligente como para adjuntarse a este html.

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

En este ejemplo, UserListViewrecorrerá todas las <li>etiquetas y adjuntará un objeto de vista con el modelo correcto para cada una. configura un controlador de eventos para el evento de cambio de nombre del modelo y actualiza el texto visualizado del elemento cuando se produce un cambio.


Este tipo de proceso, para tomar el html que el servidor renderizó y hacer que mi JavaScript se haga cargo y lo ejecute, es una excelente manera de hacer que las cosas funcionen para SEO, accesibilidad y pushStatesoporte.

Espero que ayude.


Entiendo su punto, pero lo interesante es cómo se realiza la representación después de "su JavaScript se hace cargo". En un ejemplo más complicado, es posible que deba usar una plantilla no compilada en el cliente, recorriendo una matriz de usuarios para crear una lista. La vista se vuelve a representar cada vez que cambia el modelo de un usuario. ¿Cómo haría eso sin duplicar las plantillas (y sin pedirle al servidor que muestre la vista para el cliente)?
user544941

Las 2 publicaciones de blog que he vinculado deberían mostrarle colectivamente cómo tener plantillas que se puedan usar en el cliente y el servidor, sin necesidad de duplicación. el servidor deberá renderizar toda la página si desea que sea accesible y compatible con SEO. He actualizado mi respuesta para incluir un ejemplo más complejo de adjuntar a una lista de usuarios que fue procesada por el servidor
Derick Bailey

22

Creo que necesitas esto: http://code.google.com/web/ajaxcrawling/

También puede instalar un backend especial que "renderiza" su página ejecutando javascript en el servidor, y luego lo sirve en google.

Combina ambas cosas y tendrás una solución sin programar dos veces. (Siempre que su aplicación sea totalmente controlable a través de fragmentos de anclaje).


En realidad, no es lo que estoy buscando. Esas son algunas variantes de la primera solución y, como mencioné, no estoy muy contento con ese enfoque.
user544941

2
No leíste toda mi respuesta. También usa un backend especial que representa el javascript por usted, no escribe dos veces.
Ariel

Sí, lo leí. Pero si lo entendiera bien, sería un gran programa, ya que tendría que simular cada acción que desencadena el pushState. Alternativamente, podría darle las acciones directamente, pero ya no estamos tan SECOS.
user544941

2
Creo que es básicamente un navegador sin el frente. Pero sí, debe hacer que el programa sea completamente controlable desde fragmentos de anclaje. También debe asegurarse de que todos los enlaces tengan el fragmento adecuado, junto con, o en lugar de, onClicks.
Ariel

17

Entonces, parece que la principal preocupación es estar SECO

  • Si está utilizando pushState, haga que su servidor envíe el mismo código exacto para todas las direcciones URL (que no contengan una extensión de archivo para servir imágenes, etc.) "/ mydir / myfile", "/ myotherdir / myotherfile" o root "/ "- todas las solicitudes reciben el mismo código exacto. Necesita tener algún tipo de motor de reescritura de URL. También puede servir un poco de html y el resto puede provenir de su CDN (usando require.js para administrar dependencias; consulte https://stackoverflow.com/a/13813102/1595913 ).
  • (pruebe la validez del enlace convirtiendo el enlace a su esquema de URL y verificando la existencia de contenido consultando una fuente estática o dinámica. Si no es válida, envíe una respuesta 404).
  • Cuando la solicitud no es de un robot de Google, simplemente procesas normalmente.
  • Si la solicitud es de un robot de Google, utiliza phantom.js - navegador webkit sin cabeza ( "Un navegador sin cabeza es simplemente un navegador web con todas las funciones y sin interfaz visual" ) para representar html y javascript en el servidor y enviar el google bot el html resultante. A medida que el bot analiza el html, puede golpear sus otros enlaces "pushState" / alguna página en el servidor <a href="https://stackoverflow.com/someotherpage">mylink</a>, el servidor reescribe la url en su archivo de aplicación, la carga en phantom.js y el html resultante se envía al bot, y así sucesivamente. ..
  • Para su html, supongo que está utilizando enlaces normales con algún tipo de secuestro (por ejemplo, con backbone.js https://stackoverflow.com/a/9331734/1595913 )
  • Para evitar confusiones con los enlaces, separe su código de API que sirve a JSON en un subdominio separado, por ejemplo, api.mysite.com
  • Para mejorar el rendimiento, puede preprocesar las páginas de su sitio para los motores de búsqueda con anticipación durante las horas libres creando versiones estáticas de las páginas utilizando el mismo mecanismo con phantom.js y, en consecuencia, sirviendo las páginas estáticas a los robots de Google. El preprocesamiento se puede hacer con alguna aplicación simple que pueda analizar <a>etiquetas. En este caso, manejar 404 es más fácil ya que simplemente puede verificar la existencia del archivo estático con un nombre que contiene la ruta de URL.
  • Si utiliza #! la sintaxis de hash bang para los enlaces de su sitio se aplica a un escenario similar, excepto que el motor de reescritura del servidor de URL buscaría _escaped_fragment_ en la url y formatearía la url a su esquema de url.
  • Hay un par de integraciones de node.js con phantom.js en github y puede usar node.js como servidor web para producir resultados html.

Aquí hay un par de ejemplos que usan phantom.js para seo:

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

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


4

Si usa Rails, intente poirot . Es una gema que hace que sea muy simple reutilizar las plantillas de bigote o manillar del lado del cliente y del servidor.

Crea un archivo en tus vistas como _some_thingy.html.mustache.

Render del lado del servidor:

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

Ponga la plantilla de su cabeza para uso del lado del cliente:

<%= template_include_tag 'some_thingy' %>

Rendre lado del cliente:

html = poirot.someThingy(my_model)

3

Para tomar un ángulo ligeramente diferente, su segunda solución sería la correcta en términos de accesibilidad ... proporcionaría contenido alternativo a los usuarios que no pueden usar JavaScript (aquellos con lectores de pantalla, etc.).

Esto agregaría automáticamente los beneficios del SEO y, en mi opinión, Google no lo consideraría una técnica "traviesa".


¿Y alguien demostró que te equivocaste? Ha pasado algún tiempo desde que se publicó el comentario
jkulak

1

Interesante. He estado buscando soluciones viables, pero parece ser bastante problemático.

De hecho, me estaba inclinando más hacia su segundo enfoque:

Deje que el servidor proporcione un sitio web especial solo para los robots de los motores de búsqueda. Si un usuario normal visita http://example.com/my_path, el servidor debe proporcionarle una versión JavaScript del sitio web. Pero si el bot de Google visita, el servidor debería darle un mínimo de HTML con el contenido que quiero que indexe Google.

Aquí está mi opinión sobre la resolución del problema. Aunque no está confirmado que funcione, puede proporcionar alguna idea o idea para otros desarrolladores.

Suponga que está utilizando un marco JS que admite la funcionalidad de "estado de inserción", y su marco de back-end es Ruby on Rails. Tiene un sitio de blog simple y desea que los motores de búsqueda indexen todos sus artículos indexy showpáginas.

Digamos que tienes tus rutas configuradas así:

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

Asegúrese de que cada controlador del lado del servidor muestre la misma plantilla que su marco del lado del cliente requiere para ejecutarse (html / css / javascript / etc). Si ninguno de los controladores coincide con la solicitud (en este ejemplo, solo tenemos un conjunto RESTful de acciones para el ArticlesController), simplemente empareje cualquier otra cosa y solo renderice la plantilla y deje que el marco del lado del cliente maneje el enrutamiento. La única diferencia entre golpear un controlador y golpear el comodín sería la capacidad de representar el contenido en función de la URL que se solicitó a los dispositivos con JavaScript deshabilitado.

Por lo que entiendo, es una mala idea presentar contenido que no sea visible para los navegadores. Entonces, cuando Google lo indexa, las personas pasan por Google para visitar una página determinada y no hay ningún contenido, entonces es probable que lo penalicen. Lo que viene a la mente es que renderizas contenido en un divnodo que tienes display: noneen CSS.

Sin embargo, estoy bastante seguro de que no importa si simplemente haces esto:

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

Y luego usando JavaScript, que no se ejecuta cuando un dispositivo con JavaScript deshabilitado abre la página:

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

De esta manera, para Google y para cualquier persona con dispositivos con JavaScript deshabilitado, verían el contenido sin formato / estático. Por lo tanto, el contenido está físicamente allí y es visible para cualquier persona con dispositivos con JavaScript deshabilitado.

Pero, cuando un usuario visita la misma página y en realidad tiene JavaScript habilitado, el #no-jsnodo se eliminará para que no sature su aplicación. Luego, su marco del lado del cliente manejará la solicitud a través de su enrutador y mostrará lo que un usuario debería ver cuando JavaScript esté habilitado.

Creo que esta podría ser una técnica válida y bastante fácil de usar. Aunque eso podría depender de la complejidad de su sitio web / aplicación.

Sin embargo, corrígeme si no es así. Solo pensé en compartir mis pensamientos.


1
Bueno, si primero muestra contenido y un poco más tarde lo elimina, lo más probable es que el usuario final note que el contenido parpadea / parpadea en su navegador :) Especialmente si es un navegador lento, gran tamaño de contenido HTML que intenta mostrar / eliminar y algunos demora antes de que su código JS se cargue y se ejecute. ¿Qué piensas?
Evereq

1

Use NodeJS en el lado del servidor, busque su código del lado del cliente y enrute cada uri de solicitud http (excepto los recursos http estáticos) a través de un cliente del lado del servidor para proporcionar el primer 'arranque' (una instantánea de la página en que se encuentra). Use algo como jsdom para manejar jquery dom-ops en el servidor. Una vez que haya regresado el arranque, configure la conexión websocket. Probablemente sea mejor diferenciar entre un cliente websocket y un cliente del lado del servidor haciendo algún tipo de conexión envolvente en el lado del cliente (el cliente del lado del servidor puede comunicarse directamente con el servidor). He estado trabajando en algo como esto: https://github.com/jvanveen/rnet/


0

Use la plantilla de cierre de Google para representar páginas. Se compila a javascript o java, por lo que es fácil renderizar la página en el lado del cliente o del servidor. En el primer encuentro con cada cliente, renderice el html y agregue javascript como enlace en el encabezado. El rastreador solo leerá el html pero el navegador ejecutará su script. Todas las solicitudes posteriores del navegador podrían realizarse contra la API para minimizar el tráfico.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.