AngularJS y SEO

En esta entrada voy a explicar de manera resumida un trabajo que he ido haciendo las últimas semanas a ratos libres para actualizar mi CV online a una versión más facil de gestionar en múltiples idiomas.

Introducción

Hace unos meses, con motivo de mi cambio laboral, preparé una página que me sirviera de CV online para enlazar desde diferentes portales laborales y para enviar por correo a potenciales personas interesadas en mi.

Quería aprovechar para mostrar una página moderna, representativa de la tecnologia y tendencia actual. Una página creada con un framework moderno (en este caso Bootstrap), que fuera adaptable a diferentes dispositivos (responsive) y en varios idiomas. Pero no quería montar un CMS completo sólo para una página. Y tenia claro que buscaba un diseño muy concreto, tenía que tocar directamente el HTML y el CSS. Así que decidí crearla completamente estática.

Aun se puede consultar esa primera versión. Para confeccionarla cree una versión en un idioma para ir ajustando el código y la presentación. Cuando estuve satisfecho hice dos copias más y traduje los contenidos. Sin embargo cualquier cambio que no fuera meramente de contenido implicaba que tendría que cambiar el código en tres versiones.

Por otro lado hace ya un tiempo que busco la oportunidad de crear o entrar en un proyecto que utilice AngularJS. Así que durante las últimas semanas he ido encontrando momentos para ir investigando y creando una versión de la página utilizando esa libreria junto con un módulo específico de i18n: angular-translate. Esta es la versión que podéis encontrar actualmente publicada en https://sargue.net/cv/.

Codigo de la página

He publicado todo el código fuente de la página así como los diferentes scripts asociados en un repositorio de GitHub.

Convertir la página a AngularJS no fue muy complicado, al fin y al cabo es una web tremendamente sencilla: una sóla página con tres idiomas. Quise aprovechar también para utilizar bower y de esa manera tener más controlada la gestión de todos los paquetes externos de JavaScript y CSS que utilizo, que no son pocos. Y por último, pese a no ser una página con carga crítica, quería cargar todo el contenido CSS y JavaScript comprimido y en un sólo recurso. De ahí el script para Grunt donde se definen las operaciones de minificación, concatenación y copia.

El problema con los buscadores

Cuando ya lo tenía todo listo me dí cuenta que Google o cualquier otro buscador no indexaría correctamente la página ya que el HTML que se descarga no contiene los textos hasta que no se ejecuta el código de Angular. Así que estuve un tiempo investigando (básicamente lo que iba encontrando al buscar en google angular+seo). Los enlaces más relevantes los indico al final en la sección de referencia.

El problema no es exclusivo de aplicaciones Angular sino de cualquier aplicación que utilice intensamente Ajax y especialmente las páginas SPA (Single Page Application). Además precisamente Angular es un proyecto de Google. Como no puede ser de otra manera la gente de Google ya ha pensando en como indexar el contenido de estas nuevas páginas.

De manera muy resumida el sistema se basa en utilizar una navegación en la URL utilizando el hash (tal como ya hace Angular) pero con la particularidad de indicarlo con una admiración final, lo que llaman un hashbang (#!). Cuando el Googlebot encuentra una referencia a una página con el hashbang sustituye este por un parámetro del query string con el nombre _escaped_fragment_. El truco está en detectar a nivel del servidor web que se nos está solicitando una página con ese parámetro y enviar la versión estática en HTML puro en vez de la versión dinámica.

Lo primero que tuve que hacer fue modificar el código Angular para que el cambio de idioma se reflejara tras un hashbang de los utilizados para indicarle al Googlebot que se trata de una página Ajax. Tras resolver este detalle me embarqué en la generación de versiones estáticas de las diferentes vistas de la página, una por idioma.

Creación de los snapshots

Para enviar las versiones estáticas de las páginas hay diferentes versiones: desde generar totas las páginas previamente, montar un navegador interno que las genere dinámicamente o utilizar un servicio externo como prerender.io.

Dado mi caso muy simple utilizo el navegador headless PhantomJS junto con algunos scripts de Grunt para generar tres versiones de la página, una por cada idioma.

Configuración de Apache

Una vez generados los snapshots y publicado todo en el servidor es necesario crear algunas redirecciones internas para que las peticiones con el _escaped_fragment_ vayan a donde deben. Esta es la configuración en mi caso para un Apache 2.4.

RewriteEngine On  
RewriteCond %{REQUEST_URI}  ^/cv/$  
RewriteCond %{QUERY_STRING} ^_escaped_fragment_=\?lang=(.*)$  
RewriteRule ^/cv/ /cv/snapshots/%1/index.html [NC,L]

RewriteCond %{QUERY_STRING} _escaped_fragment_  
RewriteCond %{HTTP:Accept-Language} (^en) [NC]  
RewriteRule ^/cv/ /cv/snapshots/en/index.html? [NC,L]

RewriteCond %{QUERY_STRING} _escaped_fragment_  
RewriteCond %{HTTP:Accept-Language} (^ca) [NC]  
RewriteRule ^/cv/ /cv/snapshots/ca/index.html? [NC,L]

RewriteCond %{QUERY_STRING} _escaped_fragment_  
RewriteRule ^/cv/ /cv/snapshots/es/index.html? [NC,L]  

Conclusión

Como se puede ver es perfectamente compatible la creación de una página web con las últimas tecnologias y aún así tener un control bastante exacto de lo que ve un rastreador de un buscador. Espero que el ejemplo, las referencias y el código publicado os puedan ser de utilidad.

Salut!

Referencias