ajax seo phantomjs single-page-application durandal

ajax - ¿Cómo hacer un spa SEO rastreable?



phantomjs single-page-application (5)

Antes de comenzar, asegúrese de comprender lo que instructions google, especialmente el uso de URL bonitas y feas . Ahora veamos la implementación:

Lado del cliente

En el lado del cliente, solo tiene una sola página html que interactúa dinámicamente con el servidor a través de llamadas AJAX. de eso se trata SPA. Todas las etiquetas del lado del cliente se crean dinámicamente en mi aplicación, más adelante veremos cómo hacer que estos enlaces sean visibles para el robot de Google en el servidor. Cada a esas etiquetas debe poder tener una pretty URL en la etiqueta href para que el robot de google la rastree. No quiere que se use la parte href cuando el cliente hace clic en ella (aunque desea que el servidor pueda analizarla, lo veremos más adelante), porque es posible que no queramos cargar una nueva página. , solo para hacer una llamada AJAX para que algunos datos se muestren en una parte de la página y cambiar la URL a través de javascript (por ejemplo, usando HTML5 pushstate o con Durandaljs ). Entonces, tenemos tanto un atributo href para google como onclick que hace el trabajo cuando el usuario hace clic en el enlace. Ahora, dado que uso push-state , no quiero ningún # en la URL, por lo que a etiqueta típica puede ser así:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct(''category'',''subCategory'',''product111'')>see product111...</a>

''categoría'' y ''subcategoría'' probablemente serían otras frases, como ''comunicación'' y ''teléfonos'' o ''computadoras'' y ''computadoras portátiles'' para una tienda de electrodomésticos. Obviamente, habría muchas categorías y subcategorías diferentes. Como puede ver, el enlace está directamente relacionado con la categoría, la subcategoría y el producto, no como parámetros adicionales a una página de "tienda" específica, como http://www.xyz.com/store/category/subCategory/product111 . Esto es porque prefiero enlaces más cortos y simples. Implica que no habrá una categoría con el mismo nombre que una de mis ''páginas'', es decir, ''sobre''.
No entraré en cómo cargar los datos a través de AJAX (la parte onclick ), buscarlos en google, hay muchas buenas explicaciones. Lo único importante aquí que quiero mencionar es que cuando el usuario hace clic en este enlace, quiero que la URL en el navegador se vea así:
http://www.xyz.com/category/subCategory/product111 . ¡Y esta es la URL que no se envía al servidor! recuerde, este es un SPA donde toda la interacción entre el cliente y el servidor se realiza a través de AJAX, ¡sin enlaces en absoluto! todas las ''páginas'' se implementan en el lado del cliente, y las diferentes URL no hacen una llamada al servidor (el servidor necesita saber cómo manejar estas URL en caso de que se utilicen como enlaces externos desde otro sitio a su sitio, lo veremos más adelante en la parte del servidor). Ahora, Durandal lo maneja maravillosamente. Lo recomiendo encarecidamente, pero también puede omitir esta parte si prefiere otras tecnologías. Si lo eliges y también utilizas MS Visual Studio Express 2012 para la Web como yo, puedes instalar el Durandal Starter Kit , y allí, en shell.js , usar algo como esto:

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

Hay algunas cosas importantes que notar aquí:

  1. La primera ruta (con la route:'''' ) es para la URL que no tiene datos adicionales, es decir, http://www.xyz.com . En esta página usted carga datos generales usando AJAX. En realidad, puede que no haya ninguna etiqueta en esta página. Deberá agregar la siguiente etiqueta para que el robot de Google sepa qué hacer con ella:
    <meta name="fragment" content="!"> . Esta etiqueta hará que el bot de Google transforme la URL a www.xyz.com?_escaped_fragment_= que veremos más adelante.
  2. La ruta ''sobre'' es solo un ejemplo de un enlace a otras ''páginas'' que puede desear en su aplicación web.
  3. Ahora, la parte difícil es que no hay una ruta de "categoría", y puede haber muchas categorías diferentes, ninguna de las cuales tiene una ruta predefinida. Aquí es donde aparece mapUnknownRoutes . mapUnknownRoutes estas rutas desconocidas en la ruta ''store'' y también elimina cualquier ''!'' de la URL en caso de que sea una pretty URL generada por el motor de búsqueda de Google. La ruta ''tienda'' toma la información en la propiedad ''fragmento'' y realiza la llamada AJAX para obtener los datos, mostrarlos y cambiar la URL localmente. En mi aplicación, no cargo una página diferente para cada llamada; Solo cambio la parte de la página donde estos datos son relevantes y también cambio la URL localmente.
  4. Observe pushState:true que instruye a Durandal a usar URL de estado de inserción.

Esto es todo lo que necesitamos en el lado del cliente. Puede implementarse también con URL hash (en Durandal simplemente eliminas el pushState:true para eso). La parte más compleja (al menos para mí ...) era la parte del servidor:

Lado del servidor

Estoy usando MVC 4.5 en el lado del servidor con los controladores WebAPI . El servidor realmente necesita manejar 3 tipos de URL: las generadas por google, tanto pretty como ugly y también una URL ''simple'' con el mismo formato que el que aparece en el navegador del cliente. Veamos cómo hacer esto:

Las URL bonitas y las "simples" son interpretadas primero por el servidor como si trataran de hacer referencia a un controlador que no existe. El servidor ve algo como http://www.xyz.com/category/subCategory/product111 y busca un controlador llamado ''categoría''. Entonces en web.config agrego la siguiente línea para redirigirlos a un controlador de manejo de errores específico:

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

Ahora, esto transforma la URL en algo así como: http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111 . Quiero que la URL se envíe al cliente que cargará los datos a través de AJAX, por lo que el truco aquí es llamar al controlador ''índice'' predeterminado como si no hiciera referencia a ningún controlador; Lo hago agregando un hash a la URL antes de todos los parámetros de ''categoría'' y ''subcategoría''; la URL hash no requiere ningún controlador especial, excepto el controlador ''índice'' predeterminado y los datos se envían al cliente, que luego elimina el hash y utiliza la información después del hash para cargar los datos a través de AJAX. Aquí está el código del controlador del controlador de errores:

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


Pero, ¿qué pasa con las URL feas ? Estos son creados por el bot de Google y deben devolver HTML simple que contiene todos los datos que el usuario ve en el navegador. Para esto yo uso Phantomjs . Phantom es un navegador sin cabeza que hace lo que el navegador está haciendo en el lado del cliente, pero en el lado del servidor. En otras palabras, phantom sabe (entre otras cosas) cómo obtener una página web a través de una URL, analizarla, ejecutar todo el código de JavaScript (así como obtener datos a través de llamadas AJAX) y devolverle el código HTML que refleja el DOM. Si está utilizando MS Visual Studio Express, muchos desean instalar el fantasma a través de este link .
Pero primero, cuando se envía una URL fea al servidor, debemos atraparla; Para esto, agregué a la carpeta ''App_start'' el siguiente archivo:

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

Esto se llama desde ''filterConfig.cs'' también en ''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()); } } }

Como puede ver, ''AjaxCrawlableAttribute'' enruta URLs feas a un controlador llamado ''HtmlSnapshot'', y aquí está este controlador:

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

La view asociada es muy simple, solo una línea de código:
@Html.Raw( ViewBag.result )
Como puede ver en el controlador, fantasma carga un archivo javascript llamado createSnapshot.js en una carpeta que creé llamada seo . Aquí está este archivo 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);

Primero quiero agradecer a Thomas Davis por la página donde obtuve el código básico de :-).
Notarás algo raro aquí: fantasma sigue checkLoaded() la página hasta que la función checkLoaded() devuelva verdadero. ¿Porqué es eso? esto se debe a que mi SPA específico hace varias llamadas AJAX para obtener todos los datos y colocarlos en el DOM en mi página, y el fantasma no puede saber cuándo se han completado todas las llamadas antes de devolverme el reflejo HTML del DOM. Lo que hice aquí es después de la última llamada AJAX. Agregué una <span id=''compositionComplete''></span> , de modo que si esta etiqueta existe, sé que el DOM está completo. Hago esto en respuesta al evento de compositionComplete de Durandal. Consulte here para obtener más información. Si esto no ocurre dentro de los 10 segundos, me rindo (debería tomar solo un segundo más). El HTML devuelto contiene todos los enlaces que el usuario ve en el navegador. La secuencia de comandos no funcionará correctamente porque las etiquetas <script> que existen en la instantánea HTML no hacen referencia a la URL correcta. Esto también se puede cambiar en el archivo fantasma de javascript, pero no creo que sea necesario porque el snapshort HTML solo lo utiliza google para obtener los enlaces y no para ejecutar javascript; estos enlaces hacen referencia a una URL bonita y, de hecho, si intenta ver la instantánea HTML en un navegador, obtendrá errores de javascript pero todos los enlaces funcionarán correctamente y lo dirigirá al servidor una vez más con una bonita URL esta vez obteniendo la página completamente funcional.
Eso es todo. Ahora el servidor sabe cómo manejar las URL bonitas y feas, con el estado de inserción habilitado tanto en el servidor como en el cliente. Todas las direcciones URL feas se tratan de la misma manera que con fantasma, por lo que no es necesario crear un controlador por separado para cada tipo de llamada.
Una cosa que quizás prefiera cambiar es no hacer una llamada general de ''categoría / subcategoría / producto'', sino agregar una ''tienda'' para que el enlace tenga el siguiente aspecto: http://www.xyz.com/store/category/subCategory/product111 . Esto evitará el problema en mi solución de que todas las URL inválidas son tratadas como si fueran llamadas al controlador ''index'', y supongo que éstas pueden manejarse dentro del controlador ''store'' sin la adición a la web.config Mostré arriba.

He estado trabajando sobre cómo hacer que un SPA sea rastreable según las instructions de google. Aunque hay bastantes explicaciones generales, no pude encontrar en ninguna parte un tutorial paso a paso más completo con ejemplos reales. Después de haber terminado esto, me gustaría compartir mi solución para que otros también puedan usarla y posiblemente mejorarla aún más.
Estoy usando MVC con controladores Webapi , y Phantomjs en el lado del servidor, y Durandal en el lado del cliente con push-state activado; También uso Breezejs para la interacción de datos entre el cliente y el servidor, todo lo cual recomiendo encarecidamente, pero intentaré dar una explicación general suficiente que también ayude a las personas que usan otras plataformas.


Aquí hay un enlace a una grabación de screencast de mi clase de entrenamiento de Ember.js que organicé en Londres el 14 de agosto. Describe una estrategia tanto para su aplicación del lado del cliente como para la aplicación del lado del servidor, y brinda una demostración en vivo de cómo la implementación de estas características proporcionará su aplicación de una sola página de JavaScript con degradación elegante, incluso para usuarios con JavaScript desactivado. .

Utiliza PhantomJS para ayudar a rastrear su sitio web.

En resumen, los pasos requeridos son:

  • Tener una versión alojada de la aplicación web que desea rastrear, este sitio necesita tener TODA la información que tiene en producción
  • Escriba una aplicación JavaScript (PhantomJS Script) para cargar su sitio web
  • Agregue index.html (o "/") a la lista de URL para rastrear
    • Muestra la primera URL agregada a la lista de rastreo
    • Cargar página y renderizar su DOM
    • Encuentre cualquier enlace en la página cargada que enlaza con su propio sitio (filtrado de URL)
    • Agregue este enlace a una lista de URLs "rastreables", si aún no se ha rastreado
    • Almacene el DOM renderizado en un archivo en el sistema de archivos, pero elimine TODAS las etiquetas de secuencia de comandos primero
    • Al final, cree un archivo Sitemap.xml con las URL rastreadas

Una vez que se haya completado este paso, estará listo para servir la versión estática de su HTML como parte de la etiqueta noscript en esa página. Esto permitirá que Google y otros motores de búsqueda rastreen todas las páginas de su sitio web, aunque originalmente su aplicación sea de una sola página.

Enlace al screencast con todos los detalles:

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



Puede usar http://sparender.com/ que permite que las Aplicaciones de una sola página se rastreen correctamente.


Puede usar o crear su propio servicio para prerender su SPA con el servicio llamado prerender. Puede verificarlo en su sitio web prerender.io y en su proyecto github (utiliza PhantomJS y renderiza su sitio web para usted).

Es muy fácil comenzar con. Solo debe redirigir las solicitudes de rastreadores al servicio y recibirán el html representado.