SEO para SPA - parte 2
Leia em: 30 minutosComo falei no post (SEO para SPA), neste irei explicar uma solução para resolver o problema de SEO em SPAs, implementando uma técnica simples sem precisar alterar nada so seu site.
Ferramentas
- Nginx (Gerenciar o destino das requests de acordo com o user-agent)
- Express (Spawn do script phantom e devolução do html gerado como response da request)
- Phantom (Processar a url enviada e capturar todo o HTML da página)
Vamos Codar
Primeiro é preciso criar uma APP básica com o express:
- Instale o
express-generator
com o seguinte comando:
npm install express-generator -g
- Execute este comando para criar sua aplicação:
express seo_server
- Vamos alterar o
app.js
deixando-o mais simples possível:
// app.js
var express = require('express')
, path = require('path')
, logger = require('morgan')
, routes = require('./routes/index')
, app = express();
app.use(logger('dev'));
app.use('/', routes);
module.exports = app;
Sendo assim, o app.js
tem a responsabilidade de: requerer o express, requerer um arquivo de configuração para rota, requerer o path
para lidar com os caminhos de diretórios, requerer o morgan
para gerar logs.
- Para configurar nossa rota principal (
/
) vamos criar um novo arquivo emroutes/index.js
com o seguinte conteúdo:
var express = require('express')
, router = express.Router()
, SeoServer = require('../lib/seo_server');
router.get(/(.*)/, function(req, res, next) {
SeoServer.init(req, res, next)
});
module.exports = router;
A configuração da routa GET /
requer de um módulo SeoServer
com uma função exportada que espera receber os três parâmetros da request.
- Para definir o nosso modulo
SeoServer
, vamos criar um novo arquivo emlib/seo_server.js
com o seguinte conteúdo:
var childProcess = require('child_process')
/*
------------------
Função construtura
------------------
Responsabilidade:
1 - Receber os parâmetros (req, res, next) da request e
armazenar cada um sendo um atributo do objeto SeoServer
*/
function SeoServer(req, res, next) {
this.req = req;
this.res = res;
this.next = next;
}
/*
------------------
Função init
------------------
Responsabilidades:
1 - Ler o atributo `x-forwarded-host` do header da request.
2 - Ler o atributo `x-loading-class` do header da request.
3 - Formar a url que será processada pelo phantom.
4 - Verificar a existência do conteúdo armazenado nas variáveis `host` e `loadingClass`.
Caso a condição seja satisfeita o função `getContent` é chamada com envio
de três parâmetros (url, loadingClass e uma função de `callback`
que fará o send da request enviando o conteúdo processado).
Caso a condição não seja satisfeita a request é finalizada.
*/
SeoServer.prototype.init = function() {
var host = this.req.headers['x-forwarded-host'],
loadingClass = this.req.headers['x-loading-class'],
url = 'http://' + host + this.req.params[0];
if (host && loadingClass) {
this.getContent(url, loadingClass, function(content) {
this.res.send(content);
}.bind(this));
} else {
this.res.end()
}
}
/*
------------------
Função getContent
------------------
Responsabilidades:
1 - Receber a url, loadingClass e uma funcão callback de parâmetro.
2 - Fazer o spawn do arquivo `phantom_server.js` passando a url e o loadingClass como argumentos.
3 - Fazer o encode para `utf-8` de qualquer saída do objeto `phantom`.
4 - Registrar um output dos dados convertendo o resultado para uma `String`
5 - Registrar um `listerner` no objeto `phantom` para o evento de `exit`.
Se o code recebido no `callback` do `listener` for diferente de 0,
significa que algo deu errado, então uma excessão é lançada.
Se o code recebido no `callback` do `listener` for igual a 0,
o `callback` que foi enviado como terceiro parâmetro em `getContent`
é executado passando o conteúdo processado pelo objeto `phantom`.
*/
SeoServer.prototype.getContent = function(url, loadingClass, callback) {
var content = '';
phantom = childProcess.spawn('phantomjs', ['./lib/phantom_server.js', url, loadingClass]);
phantom.stdout.setEncoding('utf8');
phantom.stdout.on('data', function(data) {
content += data.toString();
});
phantom.on('exit', function(code) {
if (code !== 0) {
throw new Error('Seoserver crashed!');
} else {
callback(content);
}
});
}
/*
------------------
Export da função init no módulo
------------------
Responsabilidades:
1 - Instânciar do objeto SeoServer enviando todos os parâmetros recebido.
2 - Utilizar o objeto instânciado para executa a função init.
*/
module.exports = {
init: function(req, res, next) {
seoServer = new SeoServer(req, res, next);
seoServer.init();
}
};
O arquivo que acabamos de criar requer um arquivo phantom_server.js
em lib
, sendo assim vamos criar este arquivo lib/phantom_server.js
e inserir o seguinte conteúdo:
/*
1 - Requerer o `system` e o `webpage`.
2 - Criar um `webpage`
3 - Leitura e armazenamento do argumento 1 (url)
4 - Leitura e armazenamento do argumento 2 (loadingClass)
5 - Definir timeout (15 segundos)
6 = Definir a variável startTime
6 = Definir a variável isLoaded
*/
var system = require('system'),
page = require('webpage').create(),
url = system.args[1],
loadingClass = system.args[2]
timeout = 15000,
startTime,
isLoaded;
/*
1 - Desabilitar `webSecurityEnabled`
2 - Desabilitar `loadImages`
3 - Habilitar `localToRemoteUrlAccessEnabled`
*/
page.settings.webSecurityEnabled = false;
page.settings.loadImages = false;
page.settings.localToRemoteUrlAccessEnabled = true;
/*
------------------
Função onLoadStarted
------------------
Responsabilidades:
1 - Registrar a hora que o load da página iniciou
2 - Inserir false como valor da variável isLoaded
*/
page.onLoadStarted = function(request) {
startTime = new Date().getTime();
isLoaded = false;
};
/*
------------------
Função onLoadFinished
------------------
Responsabilidades:
1 - Verificar se o DOM da página carregada não possui nenhum elemento com uma classe de load.
2 - Armazenar o resultado da verificação (true ou false) na variável isLoaded.
*/
page.onLoadFinished = function(response) {
isLoaded = page.evaluate(function(loadingClass) {
return document.getElementsByClassName(loadingClass).length == 0;
});
};
/*
------------------
Função checkComplete
------------------
Responsabilidades:
1 - Verificar se isLoaded é true ou se tempo de processamento
é maior que o timeout definido no início deste arquivo.
2 - Caso ambas partes do condinal seja satisfeita, é feito
uma limpeza do registrador de `setInterval`, o conteúdo
da página (HTML) é imprimido por console.log
e processo do phantom é finalizado.
*/
var checkComplete = function() {
if (isLoaded || new Date().getTime() - startTime > timeout) {
clearInterval(checkCompleteInterval);
console.log(page.content);
phantom.exit();
}
}
// Registra um setInterval a cada 1 milisegundo para executar o método checkComplete
var checkCompleteInterval = setInterval(checkComplete, 1);
// Abre a url que foi requisitada
page.open(url, function(status) {});
Agora que nossa a aplicação express e o arquivo do phantom para parsear o HTML estão configurados, precisamos fazer a configuração de um proxy
reverso para redicionar as requests ou para o aplicação do SPA ou para aplicação SeoServer que acabamos de criar.
O que diferencia uma request
feita por um crawler
de uma request
feita por usuários é o user-agent
. Sendo assim o trabalho de redirecionamento será feito em cima do user-agent
.
Primeiro vamos criar um arquivo de base para configurar o Nginx da nossa aplicação:
worker_processes 1;
events {
worker_connections 1024;
}
http {
types_hash_max_size 2048;
server_names_hash_bucket_size 64;
sendfile on;
keepalive_timeout 65;
access_log /home/server-config/www/shared/log/nginx.access.log;
error_log /home/server-config/www/shared/log/nginx.error.log debug;
gzip on;
gzip_static on;
gzip_http_version 1.1;
gzip_proxied expired no-cache no-store private auth;
gzip_disable "MSIE [1-6]\.";
gzip_vary on;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Host $host;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
include conf/upstreams/app.conf;
include conf/servers/app.conf;
}
Depois que definimos o arquivo de configuração base do Nginx, vamos criar os arquivos que estão sendo feito os includes (upstreams
e os servers
).
Vamos iniciar pelo upstream
, que basicamente vai referenciar em quais portas nossas aplicações estam rodando.
upstream seo_server {
server localhost:6666;
}
upstream app {
server localhost:3000;
}
E para finalizar nossa configuração no nginx vamos definir os nossos servers
.
server {
server_name app.com.br;
listen 80;
# Defini onde será salvo os logs
access_log /home/app/www/shared/log/nginx.access.log;
error_log /home/app/www/shared/log/nginx.error.log debug;
root /home/app/www/current/public;
# Adiciona os headers que será utilizado pelo SeoServer
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header x-Loading-Class spinner;
proxy_set_header X-Forwarded-Host $host;
# Verifica o user-agent da request
# caso o user-agent seja de um crawler
# a variável bot recebe o valor true.
if ($http_user_agent ~ (facebookexternalhit|Googlebot|bingbot|Screaming|rogerbot)) {
set $bot true;
}
# Rewrite para servir os assets
location ~* ^.+.(jpg|jpeg|gif|png|ico|css|tgz|gz|js|swf|html)$ {
expires max;
rewrite (.*) $uri break;
}
location / {
# Caso a variável `bot` tenha seu valor como false
# é feito o proxy pass para a aplicação normal
if ($bot = false) {
proxy_pass http://app.localhost;
}
# Caso a variável `bot` tenha seu valor como false
# é feito o proxy pass para a aplicação SeoServer
# que construímos a pouco.
if ($bot != false) {
proxy_pass http://seoserver.localhost;
}
}
}
Definição do server para a aplicação.
server {
server_name app.localhost;
root /home/app/www/current/public;
location / {
proxy_pass http://app;
}
}
Definição do server para o SeoServer.
server {
server_name seoserver.localhost;
root /home/seoserver/www/current/public;
location / {
proxy_pass http://seoserver;
}
}
Agora é preciso testar se as configurações do Nginx
que escrevemos não possui nenhum erro e após este teste fazer o start dele.
$ nginx -t
$ nginx
E finalmente colocar nosso servidor express para funcionar.
$ bin/www
Para realizar um teste que verifica se implementação feita devolve o HTML diretamente do servidor, basta fazer um curl com o user-agent de um crawler (neste exemplo irei utilizar o user-agent do google bot). Veja o seguinte exemplo:
curl -A 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' http://url-seu-site.com.br
Se a resposta do curl
retornar todo o conteúdo de sua página HTML parseado seu site esta pronto para ser indexado pelos search engines.
Para saber mais sobre boas práticas de SEO clique aqui para ler mais.
Comentários:
Deixe sua dúvida, sugestão ou crítica, estou ansioso para saber tudo o que você achou sobre este post: