Juanma Santoyo

En ocasiones me llaman friki

Enrutamiento Seo Friendly con asp.Net

| No hay comentarios

En los últimos años se ha ido estableciendo en los sitios web una buena práctica: las Seo Friendly URL. Friendly de amistoso, por que hace que nuestras rutas sean fáciles de indexar por los buscadores (los parámetros GET nunca les han gustado demasiado) y, mucho más importante; fáciles de comprender por los usuarios.

Es de sentido común que la URL de una página tenga que tener cierta relación con lo que contiene. A nadie le dice nada una URL de tipo http://www.dominio.com?seccion=5, pero la cosa es más interesante si es algo del tipo http://www.dominio.com/contacto. La propia URL de este artículo es un buen ejemplo.

En este artículo veremos un pequeño ejemplo de como podemos crear Friendly URLs en asp.Net

¿En que consisten técnicamente las Friendly URL?

La idea es interceptar las peticiones por URL que se realizan a nuestro sitio web antes de que se sirva ningún contenido, de forma que podamos analizar la ruta y servir un archivo u otro en consecuencia. También podemos considerar que parte de la URL es un parámetro de entrada.

Por ejemplo, la URL de este artículo podría ser una como http://www.dominio.com/articulos/enrutamiento-friendly-url/, donde la parte “enrutamiento-friendly-url” es la que determina qué artículo se está mostrando.

¿Cómo montamos el nuevo sistema de direccionamiento?

Cada tecnología de servidor tiene su forma de hacerlo. En este artículo veremos un ejemplo sobre asp.Net.

La clave es el archivo Global.asax. Este es un archivo que proporciona manejadores para una serie de eventos producidos por la aplicación. A nosotros nos interesará el evento Application_BeginRequest: es ahí donde podemos interceptar las peticiones antes de que se sirva ningún contenido, y donde analizaremos las URL para reprogramar el direccionamiento a donde nos interese. Obviamente, el archivo Global.asax tiene más utilidades que el enrutamiento, pero de momento no nos interesan.

Un ejemplo

Vamos a programar un pequeño ejemplo que nos serviría para gestionar los direccionamientos de una red social.

Haremos una dirección para las secciones principales, y una dirección que será dinámica para cada usuario miembro de la red social.

Diseñamos el nuevo enrutamiento

Como siempre, antes de empezar a hacer nada; necesitamos tener claro qué queremos hacer. Vamos a considerar todas las rutas que tendremos.

  1. /inicio/
    Todos los sitios web, las redes sociales también; necesitan una página de inicio.
  2. /buscar/
    Toda red social necesita un buscador.
  3. /usuarios/
    Lo más importante de una red social son los usuarios. Esta es la página genérica de usuarios, que podría ser por ejemplo un formulario de Login.
  4. /usuarios/<nombre de usuario>/
    Todos los usuarios tienen un perfil con su nombre, su foto, las cosas que hacen los domingos… Fijaos en que ya incluímos un parámetro, el nombre del usuario.
  5. /usuarios/<nombre de usuario>/contacto/
    La gracia de una red social es poder contactar con los usuarios. Esta página proporcionaría funcionalidad a una red de mensajería interna. Tambien tenemos el parámetro de nombre de usuario.

Creamos el proyecto

Vamos a crear un proyecto web normal y corrente desde el Visual Studio.

Crearemos cuatro .aspx, uno para cada una de nuestras rutas, excepto para /inicio/, que será el Default.aspx que se ha creado por defecto. Tambien crearemos una clase .cs para simular el origen de datos de los usuarios.

  • Buscar.aspx
  • Usuarios.aspx
  • UsuariosPerfil.aspx
  • UsuariosContacto.aspx
  • Datos/Usuarios.cs

Algo de código para los .aspx

Vamos a añadir algo de código en los .aspx. Para diferenciarlos, más que nada; y para probar unos links.

Default.aspx
	<head runat="server">
		<title>Inicio</title>
	</head>
	<body>
		<form id="form1" runat="server">
		<div>
			<h1>Inicio</h1>

			<ul>
				<li><asp:LinkButton runat="server" ID="lbBuscar" PostBackUrl="~/buscar/">Buscar</asp:LinkButton></li>
				<li><asp:LinkButton runat="server" ID="lbUsuarios" PostBackUrl="~/usuarios/">Usuarios</asp:LinkButton></li>
			</ul>
		</div>
		</form>
	</body>
Buscar.aspx
	<head runat="server">
		<title>Buscar</title>
	</head>
	<body>
		<form id="form1" runat="server">
		<div>
			<h1>Buscar</h1>

			<ul>
				<li><asp:LinkButton ID="lbInicio" runat="server" PostBackUrl="~/inicio/">Inicio</asp:LinkButton></li>
				<li><asp:LinkButton ID="lbUsuarios" runat="server" PostBackUrl="~/usuarios/">Usuarios</asp:LinkButton></li>
			</ul>
		</div>
		</form>
	</body>
Usuarios.aspx
	<head runat="server">
		<title>Usuarios</title>
	</head>
	<body>
		<form id="form1" runat="server">
		<div>
			<h1>Usuarios</h1>

			<ul>
				<li><asp:LinkButton ID="lbInicio" runat="server" PostBackUrl="~/inicio/">Inicio</asp:LinkButton></li>
				<li><asp:LinkButton ID="lbBuscar" runat="server" PostBackUrl="~/buscar/">Buscar</asp:LinkButton></li>
			</ul>
		</div>
		</form>
	</body>
UsuariosPerfil.aspx
	<head runat="server">
		<title>Perfil de </title>
	</head>
	<body>
		<form id="form1" runat="server">
		<div>
			<h1 runat="server" id="hTitle"></h1>

			<ul>
				<li><asp:LinkButton ID="lbInicio" runat="server" PostBackUrl="~/inicio/">Inicio</asp:LinkButton></li>
				<li><asp:LinkButton ID="lbUsuariosContacto" runat="server">Perfíl de </asp:LinkButton></li>
			</ul>
		</div>
		</form>
	</body>
UsuariosContacto.aspx
	<head runat="server">
		<title>Contactar con </title>
	</head>
	<body>
		<form id="form1" runat="server">
		<div>
			<h1 runat="server" id="hTitle">Contactar con </h1>

			<ul>
				<li><asp:LinkButton ID="lbinicio" runat="server" PostBackUrl="~/inicio/">Inicio</asp:LinkButton></li>
				<li><asp:LinkButton ID="lbUsuariosPerfil" runat="server">Perfíl de <%= getUserName() %></asp:LinkButton></li>
			</ul>
		</div>
		</form>
	</body>

Debemos considerar, además, un poco de código en los .cs:

Datos/Usuarios.cs

Este será nuestro origen de datos. Normalmente será una tabla en base de datos, un xml, o algo por el estilo; pero para nuestro ejemplo nos basta una clase que extienda de List. Le hemos programado además dos nuevos métodos: uno para convertir un valor de la lista al formato que usamos en las URL, y otro para encontrar valores. Destacar que el algoritmo de búsqueda podría ser bastante mejor, pero para el caso ya nos sirve.

	using System;
	using System.Collections.Generic;

	namespace JuanmaSantoyoSeoFriendly.Datos
	{
		public class Usuarios : List<string>
		{
			public Usuarios()
			{
				this.Add("John Travolta");
				this.Add("Samuel L Jackson");
				this.Add("Bruce Willis");
				this.Add("Uma Thurman");
				this.Add("Quentin Tarantino");
			}
		}
		
		private string urlFormat(string str)
        {
            return str.ToLowerInvariant().Replace(" ", "-");
        }

        public int search(string username)
        {
            int userid = -1;            
            int i = 0;

            while(this.Count > i)
            {
                if (urlFormat(this[i]) == username)
                {
                    userid = i;
                    break;
                }

                i  ;
            }

            return userid;
        }
	}
UsuariosPerfil.aspx.cs

En este .cs daremos valores a algunos elementos del .aspx, y además proporcionaremos métodos que rescatarán de sesión el ID del usuario y devolverán su nombre. Hay dos versiones para este método, uno devolverá el nombre tal cual, y el otro nos dará la posiblidad de devolver el nombre formateado para URL.

	protected void Page_Load(object sender, EventArgs e)
	{
		this.hTitle.InnerText = this.getUserName();
		
		Page.Title  = this.getUserName();
		
		lbUsuariosContacto.PostBackUrl = "~/usuarios/"   this.getUserName(true)   "/contacto/";
		lbUsuariosContacto.Text  = this.getUserName();
	}

	public string getUserName()
	{
		Datos.Usuarios usu = new Datos.Usuarios();
		
		if (HttpContext.Current.Items.Contains("userid") && HttpContext.Current.Items["userid"].ToString() != "-1")
		{
			return usu[Convert.ToInt32(HttpContext.Current.Items["userid"])];
		}
		else
		{
			return "user not found";
		}
	}

	public string getUserName(bool encode)
	{
		Datos.Usuarios usu = new Datos.Usuarios();

		if (HttpContext.Current.Items.Contains("userid"))
		{
			return usu[Convert.ToInt32(HttpContext.Current.Items["userid"])].ToLowerInvariant().Replace(" ", "-");
		}
		else
		{
			return "user not found";
		}
	}
UsuariosContacto.cs

Este .cs es bastante similar al anterior, así que no merece demasiados comentarios:

	protected void Page_Load(object sender, EventArgs e)
	{
		hTitle.InnerHtml  = getUserName();

		Page.Title  = this.getUserName();

		lbUsuariosPerfil.PostBackUrl = "~/usuarios/"   getUserName(true)   "/";
		lbUsuariosPerfil.Text  = getUserName();
	}

	public string getUserName()
	{
		Datos.Usuarios usu = new Datos.Usuarios();

		if (HttpContext.Current.Items.Contains("userid"))
		{
			return usu[Convert.ToInt32(HttpContext.Current.Items["userid"])];
		}
		else
		{
			return "user not found";
		}
	}

	public string getUserName(bool encode)
	{
		Datos.Usuarios usu = new Datos.Usuarios();

		if (HttpContext.Current.Items.Contains("userid") && HttpContext.Current.Items["userid"].ToString() != "-1")
		{
			return usu[Convert.ToInt32(HttpContext.Current.Items["userid"])].ToLowerInvariant().Replace(" ", "-");
		}
		else
		{
			return "user not found";
		}
	}

Añadimos el Global.asax

Añadimos un nuevo elemento en el proyecto. El nuevo elemento será un Artchivo de aplicación global. No le cambiaremos el nombre: lo dejaremos como “Global.asax”.

Global.asax

Configuramos las nuevas rutas

Antes que nada, debemos agregar la librería “System.Web.Routing”.

System.Web.Routing

Programaremos nuestra propia función para aplicar el enrutamiento. La llamaremos “ApplyRouting”:

	private void ApplyRouting(RouteCollection Routes)
	{
		Routes.Add(new Route("", new RouteHandler("~/Default.aspx")));
		Routes.Add(new Route("inicio/", new RouteHandler("~/Default.aspx")));
		Routes.Add(new Route("buscar/", new RouteHandler("~/Buscar.aspx")));
		Routes.Add(new Route("usuarios/", new RouteHandler("~/Usuarios.aspx")));
		Routes.Add(new Route("usuarios/{username}/", new RouteHandler("~/UsuariosPerfil.aspx")));
		Routes.Add(new Route("usuarios/{username}/contacto/", new RouteHandler("~/UsuariosContacto.aspx")));
	}

Y la llamaremos desde el evento Application_BeginRequest:

	protected void Application_BeginRequest(object sender, EventArgs e)
	{
		ApplyRouting(RouteTable.Routes);
	}

Lo que hacemos es, básicamente; añadir nuevas rutas creadas por nosotros a la tabla de rutas de nuestra aplicación. Hay varias cosas destacables:

  1. El primer parámetro es la nueva ruta. Si escribimos el valor entre llaves, será un parámetro. Los parámetros que antes enviábamos por GET deberían ser sustituídos por estos.
  2. Estamos creando instancias de RouteHandler. Esta clase no existe en la Api de C#, la tenemos que programar nosotros y será donde añadiremos toda la lógica de redireccionamiento.
  3. Como la clase RouteHandler la programamos nosotros, tenemos ciertas ventajas. En este caso, pasamos como parámetro del constructor el archivo al cual redirigimos. No tendría por qué ser así, pero como una de las características de nuestras URL es que siempre redirigen a un único archivo; este sistema nos viene bastante bien.

Programamos la lógica de redireccionamiento

Vamos a crear ahora la clase Routing/RouteHandler.cs. Esta clase, deberá heredar de IRouteHandler y por tanto deberá incorporar un método público llamado GetHttpHandler, que recibirá un único parámetro que es un RequestContext y que retornará un IHttpHandler.

En este método pasaran escencialmente dos cosas: por una parte, se meterán los parámetros recibidos en sesión. Por otra, se retornará un IHttpHandler que construiremos usando la ruta del .aspx al que queremos redirigir.

	public IHttpHandler GetHttpHandler(RequestContext requestContext)
	{
		/*
		 * Tratamos los parámetros de la URL:
		 * Si es el nombre de usuario, buscamos su ID y lo metemos en sesión.
		 * Si no, símplemente lo metemos en sesión
		 */
		foreach (KeyValuePair<string, object> val in requestContext.RouteData.Values)
		{
			if (val.Key == "username")
			{ 
				Datos.Usuarios usu = new Datos.Usuarios();
				int userid = usu.search(val.Value.ToString());
				requestContext.HttpContext.Items["userid"] = userid;
			}

			requestContext.HttpContext.Items[val.Key] = val.Value;
		}

		return (Page) BuildManager.CreateInstanceFromVirtualPath(VirtualPath, typeof(Page));
	}

Añadimos además un constructor y una variable global para el parámetro que indica el archivo al que redirigimos.

	string VirtualPath = string.Empty;

	public RouteHandler(string path)
	{
		this.VirtualPath = path;
	}

Hecho esto, nuestras nuevas rutas ya deberían estar funcionando. Sólo nos queda un detalle.

Ignorando rutas

Generalmente, no sólo nos va a interesar añadir rutas a nuestra aplicación, sino tambien quitarlas. Por ejemplo, no es una mala idea que se ignoren las rutas directas a un .aspx, o aquellas rutas que no acaben con el carácter “/” (para tener un sistema de rutas uniforme). Vamos a programar una función en Global.asax, a la cual llamaremos justo antes de ApplyRouting y que se encargará de comprobar si nuestra ruta debe ser ignorada y, en ese caso, redirigir a /inicio/.

El método Application_BeginRequest en Global.asax queraría de la siguiente forma:

	protected void Application_BeginRequest(object sender, EventArgs e)
	{
		Redirect();
		ApplyRouting(RouteTable.Routes);
	}

El método Redirect:

	private void Redirect()
	{
		string url = Request.Url.ToString();
		
		if (url.Contains(".aspx"))
		{
			Response.Redirect("~/inicio/");
		}

		if (url[url.Length - 1] != '/' && !url.Contains("?"))
		{
			Response.Redirect(url + "/");
		}
	}

Descarga el código fuente

Sé que me odias por no haber puesto este archivo al principio, pero más vale tarde que nunca ¿no?.

Código fuente.

¡Hemos acabado!

Como véis, en asp.Net el enrutamiento Seo Friendly se reduce a dos clases: Global.asax y el manejador de rutas. Es bastante simple, y no requiere demasiado tiempo de implementación. Los beneficios de usar este tipo de enrutamiento son múltiples, desde aspectos de seguridad, hasta aspectos de usuabilidad y posicionamiento en buscadores. ¡Ya no hay excusa para no implementarlo!.

Share on FacebookTweet about this on TwitterShare on Google+Share on LinkedInEmail this to someone

Deja un comentario

Los campos obligatorios están marcados con *.