Como implementar SEO en aplicaciones hechas en Javascript

June 18, 2021

Los crawlers de los motores de búsqueda están diseñados para rastrear contenido HTML en las paginas web; y durante el tiempo las paginas han evolucionado ya que existen muchas paginas que generan su contenido mediante JS, y estos sitios que generan ese tipo de contenido se ven afectados ya que los crawlers o bots no saben como manejarlos de manera correcta.

enter image description here

Pero existen herramientas que nos ayudan a solucionar este problema como es el caso de Prerender.io

Prerender.io es un middleware que se instala en su servidor y comprobará cada solicitud para ver si es una solicitud de un crawler. Si se trata de una solicitud de un crawler, el middleware enviará una solicitud a Prerender.io para que retorne el HTML estático de esa página. Si no, la solicitud continuará en sus rutas normales del servidor. El crawler nunca sabe que está utilizando Prerender.io ya que la respuesta siempre pasa a través de su servidor.

Nosotros preparamos una guía que te puede servir de ayuda para que puedas solucionar tus problemas de SEO con las aplicaciones de JS

Para efectos prácticos usaremos una aplicación básica de Angular. Esta aplicación no pretende ser perfecta ni sigue ninguna guía de estilos, solo esta hecha con propósitos de demostrar como funciona Prerender.

Crear una aplicación básica en Angular

El siguiente paso es crear una aplicación que hará los llamados al servidor de Prerender, usaremos Express para crear esta aplicación.

Si no lo tenemos instalado, ejecutemos los siguientes comandos: [prism:bash] npm install -g express npm install -g express-generator [/prism:bash]

Nos movemos a la carpeta donde crearemos el app [prism:bash]cd /var/opt/[/prism:bash]

Creamos el app [prism:bash] express testapp cd testapp/ npm install [/prism:bash]

Editamos el archivo views/layout.jade [prism:jade] doctype html html head title= title meta(name="fragment" content="!") link(rel='stylesheet', href='/stylesheets/style.css') script(src='//ajax.googleapis.com/ajax/libs/angularjs/1.2.6/angular.min.js') script(src='javascripts/app.js') body(ng-app="PrerenderApp") block content [/prism:jade]

Creamos el archivo public/javascripts/app.js con el siguiente código:

[prism:javascript] var app = angular.module("PrerenderApp", []);

app.controller("ExampleController", function($scope) { $scope.message = "Hello World"; }); [/prism:javascript]

Editamos el archivo views/index.jade

[prism:jade] extends layout

block content div(ng-controller="ExampleController") h1= title p Welcome to #{title} p {{message}} [/prism:jade]

cambiamos el puerto de 3000 a 8080 modificando el archivo bin/www [prism:javascript] var port = normalizePort(process.env.PORT || '8080'); [/prism:javascript]

Ahora si, iniciamos el servidor de Express

[prism:bash]DEBUG=testapp:* npm start[/prism:bash] deberíamos ver algo así

[prism:bash]

testapp@0.0.0 start /var/opt/testapp node ./bin/www

testapp:server Listening on port 8080 +0ms [/prism:bash]

Probar el App

En este momentos hacemos un llamado con curl para ver como los crawlers verían nuestra app, para esta demostración usaremos el Useragent del crawler de twitter [prism:bash]curl -A "Twitterbot" "http://localhost:8080"" [/prism:bash]

[prism:markup]

htmlExpress
Express

Welcome to Express

{{message}}

GET / 200 71.506 ms - 429 [/prism:markup] Si accedemos desde el navegador a nuestra app [Aqui](http://localhost:8080) ![browser](http://i68.tinypic.com/2ytqiig.png) Como podemos observar la variable __message__ ha sido reemplazada por __Hello World__ Podemos evidenciar que todavía aparece las variable __message__ sin reemplazar, pero ¿por que pasa esto? Los crawlers no procesan JS como lo hacen browsers, así que al obtener el código HTTP 200, los crawlers asumen que ya la pagina cargo por completo, así que no esperan que el JS termine de armar el app, en este caso no espera que Angular cargue para que luego el controller reemplace la variable __message__ por __Hello World__ ## Instalar y configurar Prerender ### Instalar PM2 PM2 es un gestor de procesos de producción para aplicaciones Node.js con un equilibrador de carga incorporado. Le permite mantener las aplicaciones vivas para siempre, recargarlas sin tiempo de inactividad y facilitar las tareas comunes del administrador del sistema. [prism:bash]npm install pm2 -g[/prism:bash] ### Instalar Prerender Middleware [prism:bash] $ git clone https://github.com/prerender/prerender.git $ cd prerender $ npm install [/prism:bash] Por defecto el servidor de Prerender no tiene ningún tipo de cache, así que si iniciamos el servidor de Prerender y solicitamos una pagina, se va a generar el HTML y luego nos lo va a servir; si volvemos a pedir la misma pagina la va a generar de nuevo; primero vamos a verificar que nuestro servicio de Prerender esta funcionando correctamente Ejecutamos el siguiente comando [prism:bash]pm2 start server.js[/prism:bash] [prism:bash] [PM2] Starting /server.js in fork_mode (1 instance) [PM2] Done. ┌──────────┬────┬──────┬───────┬────────┬─────────┬────────┬─────┬───────────┬──────────┐ │ App name │ id │ mode │ pid │ status │ restart │ uptime │ cpu │ mem │ watching │ ├──────────┼────┼──────┼───────┼────────┼─────────┼────────┼─────┼───────────┼──────────┤ │ server │ 0 │ fork │ 30710 │ online │ 0 │ 0s │ 3% │ 22.4 MB │ disabled │ └──────────┴────┴──────┴───────┴────────┴─────────┴────────┴─────┴───────────┴──────────┘ [/prism:bash] En este momento procedemos a probar el servicio de Prerender [prism:bash]curl -A "Twitterbot" "http://localhost:3000/http://localhost:8080"[/prism:bash] y nos debería retornar [prism:markup] @charset "UTF-8";[ng\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide{display:none !important;}ng\:form{display:block;}Express
Express

Welcome to Express

Hello World

[/prism:markup] Como podemos observar, Angular ya proceso el DOM y vemos que la variable __message__ fue reemplazada por __Hello World__ En este momento procedemos a configurar el servidor de nuestra app para que retorne el HTML generado por Prerender cuando sea necesario, este paso varia depende de cual servidor estemos configurando, en nuestro caso ExpressJS, pero podemos hacerlo con Apache, Nginx o Heroku Instalamos el proxy de Prerender para node [prism:bash] cd /var/opt/testapp npm install prerender-node --save [/prism:bash] Editamos el archivo __app.js__ y agregamos la linea después de las lineas de __app.set__ [prism:javascript] app.use(require('prerender-node').set('prerenderServiceUrl', 'http://localhost:3000'));[/prism:javascript] Esta es toda la configuración para el servidor de Express. [prism:httpt]http://localhost:3000[/prism:httpt] es la url de nuestro servicio de Prerender Reiniciamos el servidor de nuestra app Podemos usar pm2 para esto también [prism:bash]pm2 start bin/www[/prism:bash] [prism:bash] ┌──────────┬────┬──────┬───────┬────────┬─────────┬────────┬─────┬───────────┬──────────┐ │ App name │ id │ mode │ pid │ status │ restart │ uptime │ cpu │ mem │ watching │ ├──────────┼────┼──────┼───────┼────────┼─────────┼────────┼─────┼───────────┼──────────┤ │ server │ 3 │ fork │ 9891 │ online │ 0 │ 8m │ 0% │ 35.1 MB │ disabled │ │ www │ 4 │ fork │ 10361 │ online │ 0 │ 0s │ 0% │ 17.0 MB │ disabled │ └──────────┴────┴──────┴───────┴────────┴─────────┴────────┴─────┴───────────┴──────────┘ [/prism:bash] Ahora que tenemos el servicio de Prerender y el Express Webserver corriendo, podemos probar que todo este funcionando correctamente [prism:bash]curl -A "Twitterbot" "http://localhost:8080"[/prism:bash] o [prism:bash]curl "http://localhost:8080/?_escaped_fragment_="[/prism:bash] [prism:markup] @charset "UTF-8";[ng\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide{display:none !important;}ng\:form{display:block;}htmlExpress
Express

Welcome to Express

Hello World

[/prism:markup] Ese es el resultado que estábamos esperando, Prerender nos sirvió el código HTML ya generado. Para mas informacion de como configurar un servidor en particular, visita el siguiente [enlace](https://prerender.io/documentation/install-middleware) Ahora bien, ya tenemos el Prerender configurado y nuestra app verifica el *user-agent* o el *_escaped_fragment_* query string para traer el HTML desde el servicio de Prerender, pero no tenemos ningún tipo de cache configurado hasta el momento, esto quiere decir que cada vez que se le hace una petición al servicio de Prerender, este tiene que generar todo el HTML una y otra vez ## Instalar Redis linux debian/ubuntu correr este [sh](https://gist.github.com/rogerleite/5927948#file-redis-install-sh) ### Run the redis-server [prism:bash]start redis-server[/prism:bash] Nos dirigimos a la carpeta del Prerender [prism:bash]cd /var/opt/prerender[/prism:bash] Instalamos el plugin de Redis para Prerender [prism:bash]npm install prerender-redis-cache --save[/prism:bash] Y agregamos esta linea al archivo __server.js__ [prism:javascript]server.use(require('prerender-redis-cache'));[/prism:javascript] Por defecto el plugin se conectara al Redis en localhost WN, el puerto por defecto (6379) sin ningún tipo de autenticacion, tu puedes sobre-escribir estos settings seteando las siguientes variables de entorno __REDISTOGO_URL, REDISCLOUD_URL, REDISGREEN_URL o REDIS_URL__ con el siguiente formato *redis://user:password@host:port/databaseNumber* Reiniciamos el servicio de Prerender y probamos [prism:bash]curl -A "Twitterbot" "http://localhost:8080"[/prism:bash] La primera vez tardara lo que siempre ha tardado (para este ejemplo lo normal seria alrededor de 1 segundo o un poco mas), pero las siguientes veces que pidamos la misma URL el resultado sera casi inmediato Ya tenemos listo nuestro servicio Prerender funcionando con cache. ### Ejemplo con Apache Para usar Prerender con Apache debemos estar seguro de tener activados los siguientes módulos + mod_rewrite + mod_proxy + proxy_html + proxy_http virtual-host de __apache__ para __angular__ en modo __html5__ [prism:bash] ServerAdmin webmaster@localhost ServerName servername.local ServerAlias subdomain.domain.local DocumentRoot "/dir/to/site/root" ProxyRequests On ProxyPreserveHost On Require all granted RewriteEngine on AllowOverride All Options Indexes MultiViews FollowSymLinks Require all granted # If requested resource exists as a file or directory # (REQUEST_FILENAME is only relative in virtualhost context, so not usable) # RewriteCond %{REQUEST_FILENAME} -f [OR] # RewriteCond %{REQUEST_FILENAME} -d # Go to it as is # RewriteRule ^ - [L] # If non existent # Accept everything on index.html # RewriteRule ^ /index.html # If non existent # If path ends with / and is not just a single /, redirect to without the trailing / RewriteCond %{REQUEST_URI} !^/$ RewriteCond %{REQUEST_URI} ^(.*)/$ RewriteRule ^ %1 [R,QSA,L] # Handle Prerender.io RewriteCond %{HTTP_USER_AGENT} Googlebot|bingbot|Googlebot-Mobile|Baiduspider|Yahoo|YahooSeeker|DoCoMo|Twitterbot|TweetmemeBot|Twikle|Netseer|Daumoa|SeznamBot|Ezooms|MSNBot|Exabot|MJ12bot|sogou\sspider|YandexBot|bitlybot|ia_archiver|proximic|spbot|ChangeDetection|NaverBot|MetaJobBot|magpie-crawler|Genieo\sWeb\sfilter|Qualidator.com\sBot|Woko|Vagabondo|360Spider|ExB\sLanguage\sCrawler|AddThis.com|aiHitBot|Spinn3r|BingPreview|GrapeshotCrawler|CareerBot|ZumBot|ShopWiki|bixocrawler|uMBot|sistrix|linkdexbot|AhrefsBot|archive.org_bot|SeoCheckBot|TurnitinBot|VoilaBot|SearchmetricsBot|Butterfly|Yahoo!|Plukkie|yacybot|trendictionbot|UASlinkChecker|Blekkobot|Wotbox|YioopBot|meanpathbot|TinEye|LuminateBot|FyberSpider|Infohelfer|linkdex.com|Curious\sGeorge|Fetch-Guess|ichiro|MojeekBot|SBSearch|WebThumbnail|socialbm_bot|SemrushBot|Vedma|alexa\ssite\saudit|SEOkicks-Robot|Browsershots|BLEXBot|woriobot|AMZNKAssocBot|Speedy|oBot|HostTracker|OpenWebSpider|WBSearchBot|FacebookExternalHit [NC,OR] RewriteCond %{QUERY_STRING} _escaped_fragment_ # Proxy the request RewriteRule ^(?!.*?(\.js|\.css|\.xml|\.less|\.png|\.jpg|\.jpeg|\.gif|\.pdf|\.doc|\.txt|\.ico|\.rss|\.zip|\.mp3|\.rar|\.exe|\.wmv|\.doc|\.avi|\.ppt|\.mpg|\.mpeg|\.tif|\.wav|\.mov|\.psd|\.ai|\.xls|\.mp4|\.m4a|\.swf|\.dat|\.dmg|\.iso|\.flv|\.m4v|\.torrent|\.ttf|\.woff))(.*) http://localhost:3000/http://%{HTTP_HOST}/$2 [P,L] # If requested resource exists as a file or directory # (REQUEST_FILENAME is only relative in virtualhost context, so not usable) RewriteCond %{REQUEST_FILENAME} -f [OR] RewriteCond %{REQUEST_FILENAME} -d # Go to it as is RewriteRule ^ - [L] # If non existent # Accept everything on index.html RewriteRule ^ /index.html ErrorLog "/var/log/apache2/domain.local.error.log" [/prism:bash] ## Para tener en cuenta * Prerender usa Phantomjs como motor así que el uso de JS features de __ES6/ES7__ phantomjs fallara, por ello se recomienda el uso de __BabelJS__ para pasar su código a __ES5__ * Si estas en __linux__ asegúrate de tener seteado bien tus __locale__, esto también puede afectar a Phantomjs, [Conoce algunos issues relacionados](https://github.com/ariya/phantomjs/issues/13433)