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 a
etiquetas 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 a
etiqueta debe poder tener una etiqueta pretty URL
en la href
etiqueta para que el robot de Google la rastree. No desea que la href
parte 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 pushstate
o con Durandaljs
). Entonces, tenemos ambos unhref
atributo para google, así como en onclick
qué 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 a
etiqueta 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 onclick
parte), 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í:
- 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 a
etiquetas 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.
- La ruta 'acerca de' es solo un ejemplo de un enlace a otras 'páginas' que puede desear en su aplicación web.
- 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
mapUnknownRoutes
entra. Asigna estas rutas desconocidas a la ruta de 'tienda' y también elimina cualquier '!' desde la URL en caso de que sea pretty URL
generado 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.
- Observe lo
pushState:true
que 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:true
para eso). La parte más compleja (al menos para mí ...) fue la parte del servidor:
Lado del servidor
Estoy usando MVC 4.5
en el lado del servidor con WebAPI
controladores. El servidor realmente necesita manejar 3 tipos de URL: las generadas por google, ambas pretty
y ugly
tambié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/product111
y 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 view
es 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.js
bajo 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 compositionComplete
evento 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 a
enlaces 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.config
que mostré anteriormente .