Como fazer um SPA SEO rastreável?

Eu tenho trabalhado em como tornar um SPA rastreável pelo google com base nas instruções do google. Embora haja algumas explicações gerais, não consegui encontrar 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.
Eu estou usando MVC com controladores Webapi e Phantomjs no lado do servidor e Durandal no lado do cliente com push-state habilitado; Eu também uso o 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á as pessoas usando outras plataformas.

Antes de começar, certifique-se de entender o que o google exige , especialmente o uso de URLs bonitos e feios . Agora vamos ver a implementação:

Lado do Cliente

No lado do cliente, você só tem 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 do 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 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 seja capaz de 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 pushstate HTML5 ou com Durandaljs ). Então, temos tanto um atributo href para o google quanto onclick que faz o trabalho quando o usuário clica no link. Agora, como uso push-state não quero nenhum # no URL, portanto, a tag típica pode ter esta aparência:

Existem algumas coisas importantes para notar aqui:

  1. A primeira rota (com route:'' ) é para a 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 tags nessa página. Você desejará adicionar a seguinte tag para que o bot do Google saiba o que fazer com ele:
    . 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. É aqui que mapUnknownRoutes entra. Ele mapeia essas rotas desconhecidas para a rota ‘store’ e também remove qualquer ‘!’ da URL, caso seja uma pretty URL gerada 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 o MVC 4.5 no lado do servidor com controladores WebAPI . O servidor realmente precisa lidar com três tipos de URLs: as geradas pelo google – pretty e 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 estivessem tentando referenciar um controlador inexistente. O servidor vê algo como http://www.xyz.com/category/subCategory/product111 e procura por um controlador chamado ‘category’. Portanto, no web.config , adiciono a seguinte linha para redirecioná-los para um controlador específico de tratamento de erros:

   

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 à URL antes de todos os parâmetros ‘category’ e ‘subCategory’; o 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 as informações 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 as 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 header 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 fantasma 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(); } } } 

A view associada é 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); 

Quero primeiro agradecer a Thomas Davis pela página em que recebi o código básico de :-).
Você notará algo estranho aqui: o fantasma mantém o carregamento da página até que a function 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 da minha página, e o fantasma não pode saber quando todas as chamadas foram concluídas antes de retornar a reflection HTML do DOM. O que eu fiz aqui é após a chamada AJAX final eu adiciono um , de modo que se esta tag existe eu sei que o DOM está completo. Eu faço isso em resposta ao evento completeComplete 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 marcações 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 e não para executar javascript; esses links fazem referência a uma URL bonita 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 direcionarão você ao servidor novamente com uma URL bonita dessa vez obtendo a página totalmente funcional.
É 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 phantom, então 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 '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 Eu mostrei acima.

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

Aqui está um link para uma gravação de screencast da minha aula de Treinamento Ember.js que eu 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 resources 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 resumo, as etapas necessárias 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 a rastrear
    • Pop o primeiro URL adicionado à lista de rastreamento
    • Carregar a página e renderizar seu DOM
    • Encontre todos os 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 o 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/#

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 renderiza seu site para você).

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

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