it-swarm.dev

Como tornar um SEO SEO rastreável?

Eu tenho trabalhado em como tornar um SPA rastreável pelo google com base em instruções do google. Embora haja algumas explicações gerais, não consegui encontrar em nenhum lugar um tutorial passo a passo mais completo com exemplos reais. Depois de ter terminado isso, gostaria de compartilhar minha solução para que outros também possam utilizá-la e, possivelmente, melhorá-la ainda mais. 
Estou usando MVC com Webapi controllers e Phantomjs no lado do servidor e Durandal no lado do cliente com Push-state enabled; Eu também uso Breezejs para interação de dados cliente-servidor, que eu recomendo fortemente, mas vou tentar dar uma explicação geral suficiente que também ajudará pessoas usando outras plataformas.

142
beamish

Antes de começar, certifique-se de entender o que o google requer , particularmente o uso de bonitafeio URLs. Agora vamos ver a implementação:

Lado do Cliente 

No lado do cliente, você tem apenas uma única página html que interage com o servidor dinamicamente através de chamadas AJAX. é disso que trata o SPA. Todas as tags a no lado do cliente são criadas dinamicamente no meu aplicativo, depois veremos como tornar esses links visíveis para o bot do google no servidor. Cada tag a precisa ser capaz de ter um pretty URL na tag href para que o bot do Google o rastreie. Você não quer que a parte href seja usada quando o cliente clica nela (mesmo que você queira que o servidor possa analisá-la, veremos isso mais tarde), porque talvez não queiramos que uma nova página seja carregada , apenas para fazer uma chamada AJAX obtendo alguns dados para serem exibidos em parte da página e alterar o URL via javascript (por exemplo, usando HTML5 pushstate ou com Durandaljs). Então, temos tanto um atributo href para o google quanto o onclick que faz o trabalho quando o usuário clica no link. Agora, como eu uso Push-state, não quero nenhum # no URL, portanto, uma tag típica a pode ter esta aparência: 
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>

'categoria' e 'subcategoria' provavelmente seriam outras frases, como 'comunicação' e 'telefones' ou 'computadores' e 'laptops' para uma loja de eletrodomésticos. Obviamente haveria muitas categorias e subcategorias diferentes. Como você pode ver, o link é diretamente para a categoria, subcategoria e o produto, não como parâmetros extras para uma página específica de 'armazenamento', como http://www.xyz.com/store/category/subCategory/product111. Isso porque prefiro links mais curtos e simples. Isso implica que não haverá uma categoria com o mesmo nome de uma das minhas "páginas", ou seja, "sobre". 
Eu não vou entrar em como carregar os dados via AJAX (a parte onclick), pesquisar no google, existem muitas boas explicações. A única coisa importante aqui que eu quero mencionar é que quando o usuário clica nesse link, eu quero que o URL no navegador tenha esta aparência:
http://www.xyz.com/category/subCategory/product111. E isso é URL não é enviado para o servidor! lembre-se, este é um SPA onde toda a interação entre o cliente e o servidor é feita via AJAX, sem nenhum link! todas as "páginas" são implementadas no lado do cliente, e a URL diferente não faz uma chamada para o servidor (o servidor precisa saber como lidar com essas URLs, caso elas sejam usadas como links externos de outro site para o site, vamos ver isso mais tarde na parte do lado do servidor). Agora, isso é tratado maravilhosamente por Durandal. Eu recomendo fortemente, mas você também pode pular esta parte se preferir outras tecnologias. Se você escolher, e também estiver usando o MS Visual Studio Express 2012 para Web como eu, você pode instalar o Durandal Starter Kit e, em Shell.js, usar algo assim: 

define(['plugins/router', 'durandal/app'], function (router, app) {
    return {
        router: router,
        activate: function () {
            router.map([
                { route: '', title: 'Store', moduleId: 'viewmodels/store', nav: true },
                { route: 'about', moduleId: 'viewmodels/about', nav: true }
            ])
                .buildNavigationModel()
                .mapUnknownRoutes(function (instruction) {
                    instruction.config.moduleId = 'viewmodels/store';
                    instruction.fragment = instruction.fragment.replace("!/", ""); // for pretty-URLs, '#' already removed because of Push-state, only ! remains
                    return instruction;
                });
            return router.activate({ pushState: true });
        }
    };
});

Existem algumas coisas importantes para notar aqui:

  1. A primeira rota (com route:'') é para o URL que não contém dados extras, ou seja, http://www.xyz.com. Nesta página você carrega dados gerais usando o AJAX. Na verdade, pode não haver nenhuma tag a nesta página. Você desejará adicionar a seguinte tag para que o bot do Google saiba o que fazer com ele: 
    <meta name="fragment" content="!">. Essa tag fará com que o bot do Google transforme o URL em www.xyz.com?_escaped_fragment_=, que veremos mais adiante.
  2. A rota "sobre" é apenas um exemplo de um link para outras "páginas" que você pode desejar em seu aplicativo da web.
  3. Agora, a parte complicada é que não existe uma rota de 'categoria', e pode haver muitas categorias diferentes - nenhuma delas tem uma rota predefinida. É aí que entra o mapUnknownRoutes. Ele mapeia essas rotas desconhecidas para a rota 'store' e também remove qualquer '!' da URL no caso de ser um pretty URL gerado pelo mecanismo de busca do google. A rota 'store' pega as informações na propriedade 'fragment' e faz a chamada AJAX para obter os dados, exibi-los e alterar o URL localmente. No meu aplicativo, não carrego uma página diferente para cada chamada desse tipo; Só mudo a parte da página em que esses dados são relevantes e também altero o URL localmente.
  4. Observe o pushState:true que instrui Durandal a usar URLs de estado de envio. 

Isso é tudo que precisamos no lado do cliente. Pode ser implementado também com URLs com hash (no Durandal você simplesmente remove o pushState:true para isso). A parte mais complexa (pelo menos para mim ...) era a parte do servidor:

Lado do servidor

Estou usando MVC 4.5 no lado do servidor com os controladores WebAPI. O servidor realmente precisa lidar com 3 tipos de URLs: aquelas geradas pelo google - tanto pretty quanto ugly e também uma URL 'simples' com o mesmo formato que aparece no navegador do cliente. Vamos ver como fazer isso:

URLs bonitas e 'simples' são primeiro interpretadas pelo servidor como se tentassem referenciar um controlador inexistente. O servidor vê algo como http://www.xyz.com/category/subCategory/product111 e procura por um controlador chamado 'category'. Então, em web.config, adiciono a seguinte linha para redirecioná-las para um controlador específico de tratamento de erros:

<customErrors mode="On" defaultRedirect="Error">
    <error statusCode="404" redirect="Error" />
</customErrors><br/>

Agora, isso transforma a URL em algo como: http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111. Eu quero que a URL seja enviada para o cliente que irá carregar os dados via AJAX, então o truque aqui é chamar o controlador 'index' padrão como se não estivesse referenciando nenhum controlador; Eu faço isso adicionando um hash para o URL antes de todos os parâmetros 'category' e 'subCategory'; a URL com hash não requer nenhum controlador especial, exceto o controlador 'index' padrão, e os dados são enviados para o cliente, que então remove o hash e usa a informação após o hash para carregar os dados via AJAX. Aqui está o código do controlador do manipulador de erros:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;

using System.Web.Routing;

namespace eShop.Controllers
{
    public class ErrorController : ApiController
    {
        [HttpGet, HttpPost, HttpPut, HttpDelete, HttpHead, HttpOptions, AcceptVerbs("PATCH"), AllowAnonymous]
        public HttpResponseMessage Handle404()
        {
            string [] parts = Request.RequestUri.OriginalString.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries);
            string parameters = parts[ 1 ].Replace("aspxerrorpath=","");
            var response = Request.CreateResponse(HttpStatusCode.Redirect);
            response.Headers.Location = new Uri(parts[0].Replace("Error","") + string.Format("#{0}", parameters));
            return response;
        }
    }
}


Mas e quanto ao URLs feias? Estes são criados pelo bot do Google e devem retornar HTML simples que contém todos os dados que o usuário vê no navegador. Para isso eu uso phantomjs . O Phantom é um navegador sem cabeçalho que faz o que o navegador está fazendo no lado do cliente - mas no lado do servidor. Em outras palavras, o fantasma sabe (entre outras coisas) como obter uma página da web por meio de uma URL, analisá-la incluindo a execução de todo o código javascript (assim como obter dados por meio de chamadas AJAX) e retornar o HTML que reflete o DOM. Se você está usando o MS Visual Studio Express, muitos querem instalar o phantom através deste link
Mas primeiro, quando uma URL feia é enviada para o servidor, precisamos pegá-la; Para isso, adicionei à pasta 'App_start' o seguinte arquivo:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace eShop.App_Start
{
    public class AjaxCrawlableAttribute : ActionFilterAttribute
    {
        private const string Fragment = "_escaped_fragment_";

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var request = filterContext.RequestContext.HttpContext.Request;

            if (request.QueryString[Fragment] != null)
            {

                var url = request.Url.ToString().Replace("?_escaped_fragment_=", "#");

                filterContext.Result = new RedirectToRouteResult(
                    new RouteValueDictionary { { "controller", "HtmlSnapshot" }, { "action", "returnHTML" }, { "url", url } });
            }
            return;
        }
    }
}

Isso é chamado de 'filterConfig.cs' e também em 'App_start':

using System.Web.Mvc;
using eShop.App_Start;

namespace eShop
{
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            filters.Add(new AjaxCrawlableAttribute());
        }
    }
}

Como você pode ver, 'AjaxCrawlableAttribute' encaminha URLs feias para um controlador chamado 'HtmlSnapshot', e aqui está este controlador:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace eShop.Controllers
{
    public class HtmlSnapshotController : Controller
    {
        public ActionResult returnHTML(string url)
        {
            string appRoot = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);

            var startInfo = new ProcessStartInfo
            {
                Arguments = String.Format("{0} {1}", Path.Combine(appRoot, "seo\\createSnapshot.js"), url),
                FileName = Path.Combine(appRoot, "bin\\phantomjs.exe"),
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                RedirectStandardInput = true,
                StandardOutputEncoding = System.Text.Encoding.UTF8
            };
            var p = new Process();
            p.StartInfo = startInfo;
            p.Start();
            string output = p.StandardOutput.ReadToEnd();
            p.WaitForExit();
            ViewData["result"] = output;
            return View();
        }

    }
}

O view associado é muito simples, apenas uma linha de código:
@Html.Raw( ViewBag.result ) 
Como você pode ver no controlador, o fantasma carrega um arquivo javascript chamado createSnapshot.js em uma pasta que criei chamada seo. Aqui está este arquivo javascript:

var page = require('webpage').create();
var system = require('system');

var lastReceived = new Date().getTime();
var requestCount = 0;
var responseCount = 0;
var requestIds = [];
var startTime = new Date().getTime();

page.onResourceReceived = function (response) {
    if (requestIds.indexOf(response.id) !== -1) {
        lastReceived = new Date().getTime();
        responseCount++;
        requestIds[requestIds.indexOf(response.id)] = null;
    }
};
page.onResourceRequested = function (request) {
    if (requestIds.indexOf(request.id) === -1) {
        requestIds.Push(request.id);
        requestCount++;
    }
};

function checkLoaded() {
    return page.evaluate(function () {
        return document.all["compositionComplete"];
    }) != null;
}
// Open the page
page.open(system.args[1], function () { });

var checkComplete = function () {
    // We don't allow it to take longer than 5 seconds but
    // don't return until all requests are finished
    if ((new Date().getTime() - lastReceived > 300 && requestCount === responseCount) || new Date().getTime() - startTime > 10000 || checkLoaded()) {
        clearInterval(checkCompleteInterval);
        var result = page.content;
        //result = result.substring(0, 10000);
        console.log(result);
        //console.log(results);
        phantom.exit();
    }
}
// Let us check to see if the page is finished rendering
var checkCompleteInterval = setInterval(checkComplete, 300);

Primeiramente quero agradecer a Thomas Davis pela página onde recebi o código básico :-). 
Você notará algo estranho aqui: o fantasma mantém o carregamento da página até que a função checkLoaded() retorne true. Por que é que? isso ocorre porque meu SPA específico faz várias chamadas AJAX para obter todos os dados e colocá-los no DOM em minha página, e o fantasma não pode saber quando todas as chamadas foram concluídas antes de retornar a reflexão HTML do DOM. O que eu fiz aqui é depois da chamada final AJAX eu adiciono um <span id='compositionComplete'></span>, de forma que se esta tag existir eu sei que o DOM está completo. Eu faço isso em resposta ao evento compositionComplete de Durandal, veja aqui para mais. Se isso não acontecer dentro de 10 segundos eu desisto (deve demorar apenas um segundo para o máximo). O HTML retornado contém todos os links que o usuário vê no navegador. O script não funcionará corretamente porque as tags <script> que existem no instantâneo HTML não fazem referência ao URL correto. Isso pode ser alterado também no arquivo fantasma de javascript, mas eu não acho que isso é necessário porque o snapshort HTML é usado apenas pelo google para obter os links a e não para executar o javascript; esses links faz faça referência a um URL bonito e, se for verdade, se você tentar ver o snapshot HTML em um navegador, receberá erros de javascript, mas todos os links funcionarão corretamente e direcioná-lo ao servidor novamente com um URL bonito. página de trabalho.
É isso. Agora, o servidor sabe como lidar com URLs bonitas e feias, com o estado de envio ativado no servidor e no cliente. Todas as URLs feias são tratadas da mesma maneira usando o fantasma, portanto, não há necessidade de criar um controlador separado para cada tipo de chamada.
Uma coisa que você pode preferir mudar é não fazer uma chamada geral de 'categoria/subcategoria/produto', mas adicionar uma 'loja' para que o link seja parecido com: http://www.xyz.com/store/category/subCategory/product111. Isso evitará o problema na minha solução de que todas as URLs inválidas são tratadas como se fossem realmente chamadas para o controlador 'index', e suponho que elas possam ser tratadas dentro do controlador 'store' sem a adição ao web.config que mostrei acima.

122
beamish

O Google agora pode processar as páginas do SPA: Descontinuar nosso esquema de rastreamento AJAX

32
Edward Olamisan

Aqui está um link para uma gravação de screencast da minha aula de Treinamento Ember.js que recebi em Londres em 14 de agosto. Ele descreve uma estratégia para o aplicativo do lado do cliente e para o aplicativo do lado do servidor, além de uma demonstração ao vivo de como a implementação desses recursos fornecerá ao JavaScript Single-Page-App uma degradação elegante mesmo para usuários com JavaScript desativado . 

Ele usa o PhantomJS para ajudar no rastreamento do seu site. 

Em suma, os passos necessários são: 

  • Tenha uma versão hospedada do aplicativo da web que você deseja rastrear. Esse site precisa ter TODOS os dados que você tem em produção
  • Escreva um aplicativo JavaScript (Script PhantomJS) para carregar seu site
  • Adicione index.html (ou “/“) à lista de URLs para rastrear
    • Pop o primeiro URL adicionado à lista de rastreamento
    • Carregar página e renderizar seu DOM 
    • Encontre quaisquer links na página carregada com links para seu próprio site (filtragem de URL)
    • Adicione este link a uma lista de URLs "rastreáveis", se ainda não rastreados
    • Armazene o DOM renderizado em um arquivo no sistema de arquivos, mas retire TODAS as tags de script primeiro
    • No final, crie um arquivo Sitemap.xml com os URLs rastreados

Depois que essa etapa for concluída, ela será enviada para o back-end para veicular a versão estática do seu HTML como parte da tag noscript nessa página. Isso permitirá que o Google e outros mecanismos de pesquisa rastreiem todas as páginas do seu site, mesmo que seu aplicativo seja originalmente um aplicativo de página única. 

Link para o screencast com os detalhes completos: 

http://www.devcasts.io/p/spas-phantomjs-and-seo/#

4
Joachim H. Skeie

Você pode usar http://sparender.com/ que permite que aplicativos de página única sejam rastreados corretamente.

0
ddtxra

Você pode usar ou criar seu próprio serviço para pré-renderizar seu SPA com o serviço chamado prerender. Você pode conferir em seu site prerender.io e em seu projeto github (Ele usa o PhantomJS e ele traduz o seu site para você). 

É muito fácil começar com. Você só precisa redirecionar as solicitações dos rastreadores para o serviço e elas receberão o html renderizado.

0
gabrielperales