Generar URLs semánticas y amigables

Escrito por el , actualizado el .
java programacion planeta-codigo
Enlace permanente Comentarios

Java

En algunas web las urls incluyen el identificativo del objeto de la base de datos a partir de cual se muestra el contenido principal de la página, en Blog Stack esto podría ser un artículo pero en otras páginas webs podría ser un producto. Esto genera direcciones de páginas webs «feas» de cara al usuario y al SEO de los buscadores además de exponer cierta información de la base de datos que probablemente no interese a nadie excepto al desarrollador de la página. En este artículo voy a explicar una forma de generar urls semánticas, «bonitas» o amigables de cara al usuario y al SEO para los buscadores y como lo he implementado en un ejemplo real como es Blog Stack.

Lo primero que debemos conseguir es que las direcciones urls sean únicas para cualquier página de la web, por tanto, en la url deberemos incluir tanta información como sea necesaria pero al mismo tiempo la mínima para hacerlas únicas, sean cortas y que nos permitan identificar de forma unequívoca el contenido a mostrar o el objeto que nos permite obtener la información a visualizar en la página, esta información formará el denominado slug. En el caso de Blog Stack las direcciones «bonitas» se emplean en este momento en dos sitios, para los artículos y para las etiquetas. La información mínima para un artículo es el nombre de la fuente, el año, el mes y el título, para las etiquetas es simplemente el nombre de la etiqueta. Este es un desglose de las partes que forman una dirección url.

1
2
3
4
5
http://www.blogstack.info/post/blogbitix/2013/12/hola-nuevo-mundo/
^      ^                  ^    ^         ^    ^  ^
Protocolo                 Página         Año     Artículo
       Dominio                 Bitácora       Mes
                              < --- Slug ----------------------- >
url.txt

Pero eso no es todo además quizá queramos transliterar los caracteres de forma que las urls no tengan ciertos caracteres propios de cada idioma. La solución simple pero poco efectiva es hacer una serie de sustituciones como por ejemplo reemplazar á por a, ñ por n, etc… Esta solución aparte de tener que hacerla nosotros probablemente no seamos ni siquiera conscientes que deberíamos haber reemplazado algún carácter más, se complica más si hemos de hacer lo mismo con el resto de codificaciones de la que ni siquiera conocemos los caracteres. Una solución mejor es utilizar el comando iconv disponible en linux que hace precisamente lo que buscamos:

1
2
$ echo "áéíóúñ" | iconv -f UTF-8 -t ASCII//TRANSLIT
aeioun
iconv.sh

Para que la url sea más fácilmente legible es recomendable convertir las mayúsculas a minúsculas y sustituir los caracteres de espacio por un guión (-). En Blog Stack suponiendo un artículo de la fuente blogbitix publicado en diciembre de 2013 y de título «¡Hola nuevo mundo!» partiríamos de la siguiente url previamente a aplicar la transliteración de caracteres:

1
2
/blogbitix/2013/12/¡Hola nuevo mundo!

paso-1.txt

Convertida a minúsculas:

1
2
/blogbitix/2013/12/¡hola nuevo mundo!

paso-2.txt

Transliterada con iconv a ASCII:

1
2
/blogbitix/2013/12/?hola nuevo mundo!

paso-3.txt

Y finalmente sustituidos cualquier carácter que no esté entre en la siguiente expresión regular [^a-z1-9-] para eliminar por ejemplo signos de puntuación, múltiples guiones seguidos y si el resultado empieza o acaba por guión eliminándolo, al final tenemos el slug o parte de la url final a la que deberíamos añadir el protocolo y el dominio:

1
2
/blogbitix/2013/12/hola-nuevo-mundo

paso-4.txt

Todo esto en código java se traduce en:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package info.blogstack.misc;

...

public class Utils {

	...

	public static Object[] getContext(Post post) {
		String f = post.getSource().getAlias();
		String y = String.valueOf(post.getConsolidatedPublishDate().getYear());
		String m = StringUtils.leftPad(String.valueOf(post.getConsolidatedPublishDate().getMonthOfYear()), 2, "0");
		String e = Utils.urlize(post.getTitle());
		return new Object[] { f, y, m, e };
	}

	public static Object[] getContext(Label label) {
		String l = Utils.urlize(label.getName());
		return new Object[] { l };
	}

	public static String urlize(String text) {
		return Utils.transliterate(text.toLowerCase()).replaceAll("[^a-z1-9-]", "-").replaceAll("-+", "-").replaceAll("^-+", "").replaceAll("-+$", "");
	}
	
	public static String transliterate(String s) {
		try {
			Process p = Runtime.getRuntime().exec("iconv -f UTF-8 -t ASCII//TRANSLIT");
			Writer w = new OutputStreamWriter(p.getOutputStream());
			Reader r = new InputStreamReader(p.getInputStream());
			IOUtils.copy(new StringReader(s), w);
			w.close();

			Writer sw = new StringWriter();
			IOUtils.copy(r, sw);
			r.close();

			return sw.toString();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	...
}
Utils-urlize.java

Pero ¿si el identificativo del artículo no está en la url como lo asociamos con el artículo? Nos queda proporcionar una solución a esta necesidad de como identificar esa dirección url semántica y más amigable con el objeto del artículo guardado en la base de datos para mostrarlo al visualizar la página solicitada.

La idea para asociar la url con un objeto de base de datos es crear un hash de la url y tenerlo precalculado en la base de datos, con el hash que generamos a partir de la url y su slug cuando recibimos la petición buscamos el objeto que en la base de datos tenga ese hash. ¿Por qué guardar el hash y no el slug? Un motivo es porque el hash tiene una longitud constante, probablemente mas corto que el slug además de mayor dispersión en el valor del dato que usando un índice de base de datos es beneficioso en la búsqueda. Si la url es única podemos suponer que el hash será único. Si en un futuro cambiásemos la información del slug para calcular el hash lógicamente deberíamos recalcular todos los hashs. Para calcular el hash podemos usar la función MD5 o SHA con el siguiente código en java.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package info.blogstack.misc;

...

public class Utils {

	public static String getHash(Post post) {
		return Utils.getHash(Utils.getContext(post));
	}

	public static String getHash(Object[] context) {
		try {
			String[] s = new String[context.length];
			for (int i = 0; i < s.length; ++i) {
				s[i] = "%s";
			}
			String ss = String.format(StringUtils.join(s, "/"), context);
			byte[] h = MessageDigest.getInstance("MD5").digest(ss.getBytes());
			return Base64.encodeBase64String(h);
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException(e);
		}
	}

	...

	public static Object[] getContext(Post post) {
		String f = post.getSource().getAlias();
		String y = String.valueOf(post.getConsolidatedPublishDate().getYear());
		String m = StringUtils.leftPad(String.valueOf(post.getConsolidatedPublishDate().getMonthOfYear()), 2, "0");
		String e = Utils.urlize(post.getTitle());
		return new Object[] { f, y, m, e };
	}

	...
}
Utils-hash.java

Esta solo es una forma de crear las urls pero suficiente para el propósito de Blog Stack. Quizá en otro caso podríamos querer generar direcciones con caracteres que no solo sean ASCII o incluyan los propios de otra codificación como por ejemplo caracteres cirílicos, chinos o japoneses. También en vez de incluir en la url la referencia a un solo objeto con el slug incluir los slugs de varios objetos, sin esta solución deberíamos incluir un segundo identificativo de la base de datos y las direcciones serán aún más feas, menos amigables y peores en cuanto a SEO.

El código fuente completo de la clase Utils.java lo puedes encontrar en el repositorio de GitHub de Blog Stack.


Comparte el artículo: