¿Cómo hacer que un SEO SEO pueda rastrearse?


143

He estado trabajando en cómo hacer que un SPA sea rastreable por Google según las instrucciones de Google . Aunque hay bastantes explicaciones generales, no pude encontrar en ningún lado un tutorial paso a paso más completo con ejemplos reales. Después de terminar esto, me gustaría compartir mi solución para que otros también puedan usarla y posiblemente mejorarla aún más.
Estoy usando MVCcon Webapicontroladores, y Phantomjs en el lado del servidor, y Durandal en el lado del cliente con push-statehabilitado; También uso Breezejs para la interacción de datos cliente-servidor, todo lo cual recomiendo encarecidamente, pero intentaré dar una explicación lo suficientemente general que también ayudará a las personas que usan otras plataformas.


40
en relación con el tema "fuera de tema": un programador de aplicaciones web tiene que encontrar la manera de hacer que su aplicación sea rastreable para SEO, este es un requisito básico en la web. Hacer esto no se trata de la programación per se, pero es relevante para el tema de "problemas prácticos y responsables que son exclusivos de la profesión de programación" como se describe en stackoverflow.com/help/on-topic . Es un problema para muchos programadores sin soluciones claras en toda la web. Tenía la esperanza de ayudar a otros e invertí horas en solo describirlo aquí, obtener puntos negativos ciertamente no me motiva a ayudar de nuevo.
beamish

3
Si el énfasis está en la programación y no en el aceite de serpiente / salsa secreta vudú SEO / spam, entonces puede ser perfectamente actual. También nos gustan las respuestas personales donde tienen el potencial de ser útiles para futuros lectores a largo plazo. Este par de preguntas y respuestas parece pasar ambas pruebas. (Algunos de los detalles del fondo podría dar cuerpo a la mejor pregunta en lugar de ser introducido en la respuesta, pero eso es bastante menor)
Flexo

66
+1 para mitigar los votos. Independientemente de si q / a sería más adecuado como publicación de blog, la pregunta es relevante para Durandal y la respuesta está bien investigada.
RainerAtSpirit

2
¡Estoy de acuerdo en que el SEO es una parte importante hoy en día de la vida cotidiana de los desarrolladores y definitivamente debería considerarse como un tema en stackoverflow!
Kim D.

Además de implementar todo el proceso usted mismo, puede probar SnapSearch snapsearch.io, que básicamente aborda este problema como un servicio.
CMCDragonkai

Respuestas:


121

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

Lado del cliente

En el lado del cliente, solo tiene una única página html que interactúa dinámicamente con el servidor a través de llamadas AJAX. de eso se trata SPA. Todas las aetiquetas en el lado del cliente se crean dinámicamente en mi aplicación, luego veremos cómo hacer que estos enlaces sean visibles para el robot de Google en el servidor. Cada aetiqueta debe poder tener una etiqueta pretty URLen la hrefetiqueta para que el robot de Google la rastree. No desea que la hrefparte se use 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 que se cargue una nueva página, solo para hacer una llamada AJAX obteniendo algunos datos que se mostrarán en parte de la página y cambiar la URL a través de javascript (por ejemplo, usando HTML5 pushstateo con Durandaljs). Entonces, tenemos ambos unhrefatributo para google, así como en onclickqué hace el trabajo cuando el usuario hace clic en el enlace. Ahora, como uso push-state, no quiero ninguno #en la URL, por lo que una aetiqueta típica puede verse 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 es directamente a la categoría, subcategoría y el producto, no como parámetros adicionales a una página específica de 'tienda' como http://www.xyz.com/store/category/subCategory/product111. Esto se debe a que 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, '
No voy a entrar en cómo cargar los datos a través de AJAX (la onclickparte), buscar en Google, hay muchas buenas explicaciones. Lo único importante que quiero mencionar aquí 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 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! 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 usen como enlaces externos desde otro sitio a su sitio, lo veremos más adelante en la parte del servidor). Ahora, esto es manejado maravillosamente por Durandal. Lo recomiendo encarecidamente, pero también puede omitir esta parte si prefiere otras tecnologías. Si lo elige, y también está utilizando MS Visual Studio Express 2012 para Web como yo, puede instalar el Kit de inicio de Durandal y, allí 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 para notar aquí:

  1. La primera ruta (con route:'') es para la URL que no tiene datos adicionales, es decir http://www.xyz.com. En esta página carga datos generales usando AJAX. Es posible que en realidad no haya aetiquetas en esta página. Usted tendrá que añadir la etiqueta siguiente manera bot de que Google va a saber qué hacer con ella:
    <meta name="fragment" content="!">. Esta etiqueta hará que el robot de Google transforme la URL a la www.xyz.com?_escaped_fragment_=que veremos más adelante.
  2. La ruta 'acerca de' 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 mapUnknownRoutesentra. Asigna estas rutas desconocidas a la ruta de 'tienda' y también elimina cualquier '!' desde la URL en caso de que sea pretty URLgenerado por el motor de búsqueda de google. La ruta 'store' toma la información en la propiedad 'fragment' 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 lo pushState:trueque le indica a Durandal que use URL de estado de inserción.

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

Lado del servidor

Estoy usando MVC 4.5en el lado del servidor con WebAPIcontroladores. El servidor realmente necesita manejar 3 tipos de URL: las generadas por google, ambas prettyy uglytambién una URL 'simple' con el mismo formato que la 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 inexistente. El servidor ve algo parecido http://www.xyz.com/category/subCategory/product111y busca un controlador llamado 'categoría'. Entonces 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 bien, esto transforma la URL a algo 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 predeterminado 'index' como si no hiciera referencia a ningún controlador; Lo hago agregando un hash a la URL antes de todos los parámetros 'categoría' y 'subcategoría'; la URL hash no requiere ningún controlador especial, excepto el controlador 'index' predeterminado y los datos se envían al cliente que luego elimina el hash y usa 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 deberían devolver HTML sin formato que contenga todos los datos que el usuario ve en el navegador. Para esto 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 incluyendo la ejecución de todo el código de JavaScript (así como obtener datos a través de llamadas AJAX) y devolverle el HTML que refleja el DOM Si está utilizando MS Visual Studio Express, es posible que desee instalar Phantom a través de este enlace .
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();
        }

    }
}

Lo asociado viewes muy simple, solo una línea de código:
@Html.Raw( ViewBag.result )
como puede ver en el controlador, phantom carga un archivo javascript llamado createSnapshot.jsbajo 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á algo extraño aquí: phantom sigue volviendo a cargar la página hasta que la checkLoaded()función vuelva a ser verdadera. ¿Porqué es eso? Esto se debe a que mi SPA específico realiza varias llamadas AJAX para obtener todos los datos y colocarlos en el DOM de mi página, y phantom 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 agrego un <span id='compositionComplete'></span>, de modo que si esta etiqueta existe, sé que se ha completado el DOM. Hago esto en respuesta al compositionCompleteevento de Durandal , mira aquípara más. Si esto no sucede dentro de 10 segundos, me doy por vencido (solo debería tomar un segundo). El HTML devuelto contiene todos los enlaces que el usuario ve en el navegador. La secuencia de comandos no funcionará correctamente porque las <script>etiquetas 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 esto sea necesario porque Google solo usa el resumen de HTML para obtener los aenlaces y no para ejecutar JavaScript; estos enlaces hacen referencia a una URL bonita y, de hecho, si intentas ver la instantánea HTML en un navegador, obtendrás errores de JavaScript, pero todos los enlaces funcionarán correctamente y te dirigirán nuevamente al servidor con una URL bonita esta vez obteniendo la página completamente funcional.
Eso es todo. Ahora el servidor sabe cómo manejar URLs bonitas y feas, con el estado push habilitado tanto en el servidor como en el cliente. Todas las URL feas se tratan de la misma manera usando phantom, por lo que no es necesario crear un controlador separado para cada tipo de llamada.
Una cosa es posible que prefiera el cambio no es hacer una llamada "categoría / subcategoría / productos en general, pero para añadir una 'tienda' de manera que el enlace se verá algo como: http://www.xyz.com/store/category/subCategory/product111. Esto evitará el problema en mi solución de que todas las URL no válidas se tratan como si realmente fueran llamadas al controlador 'index', y supongo que se pueden manejar luego dentro del controlador 'store' sin la adición a la web.configque mostré anteriormente .


Tengo una pregunta rápida, creo que tengo esto funcionando ahora, pero cuando envío mi sitio a google y le doy enlaces a google, mapas del sitio, etc., ¿necesito darle google mysite.com/# ! o simplemente mysite.com y google agregarán el escaped_fragment porque lo tengo en la metaetiqueta?
ccorrin

ccorrin: que yo sepa, no es necesario que le des nada a Google; El robot de Google encontrará su sitio y buscará en él URL bonitas (no olvide en la página de inicio agregar también la metaetiqueta, ya que puede no contener ninguna URL). la URL fea que contiene el escaped_fragment siempre es agregada solo por google; nunca debes ponerla tú mismo dentro de tus HTML. y gracias por el apoyo :-)
beamish

gracias Bjorn & Sandra :-) Estoy trabajando en una mejor versión de este documento, que también incluirá información sobre cómo almacenar en caché las páginas para acelerar el proceso y hacerlo en el uso más común donde la URL contiene el nombre del controlador; Lo publicaré tan pronto como esté listo
2013

¡Esta es una gran explicación! Lo implementé y funciona de maravilla en mi devbox localhost. El problema es cuando se implementa en sitios web de Azure porque el sitio se congela y después de un tiempo obtengo un error 502. ¿Tienes alguna idea sobre cómo implementar phantomjs en Azure ?? ... Gracias ( testypv.azurewebsites.net/?_escaped_fragment_=home/about )
yagopv

No tengo experiencia con los sitios web de Azure, pero lo que me viene a la mente es que quizás el proceso de verificación para que la página no se cargue por completo nunca se completa, por lo que el servidor sigue intentando volver a cargar la página una y otra vez sin éxito. ¿Tal vez ahí es donde está el problema (a pesar de que hay un límite de tiempo para estas comprobaciones, por lo que puede que no esté allí)? intenta poner 'return true'; como la primera línea en 'checkLoaded ()' y vea si hace la diferencia.
beamish


4

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

Utiliza PhantomJS para ayudarlo a rastrear su sitio web.

En resumen, los pasos necesarios son:

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

Una vez que se realiza este paso, depende de su servidor 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 cada página de su sitio web, a pesar de que su aplicación originalmente era una aplicación de una sola página.

Enlace al screencast con todos los detalles:

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


0

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

Es muy fácil empezar. Solo tiene que redirigir las solicitudes de rastreadores al servicio y recibirán el html prestado.


2
Si bien este enlace puede responder la pregunta, es mejor incluir las partes esenciales de la respuesta aquí y proporcionar el enlace como referencia. Las respuestas de solo enlace pueden volverse inválidas si la página vinculada cambia. - De la opinión
timgeb

2
Tienes razón. He actualizado mi comentario ... Espero que ahora sea más preciso.
gabrielperales

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.