Como tornar um SEO do SPA rastreável?

143

Eu tenho trabalhado em como tornar um SPA rastreável pelo Google com base nas instruções do Google . Embora existam algumas explicações gerais, não consegui encontrar em nenhum lugar um tutorial passo a passo mais completo com exemplos reais. Depois de terminar isso, eu gostaria de compartilhar minha solução para que outras pessoas também possam usá-la e possivelmente melhorá-la ainda mais.
Estou usando MVCcom Webapicontroladores e Phantomjs no lado do servidor e Durandal no lado do cliente com push-stateativado; Também uso Breezejs para interação de dados cliente-servidor, todos os quais recomendo vivamente, mas tentarei dar uma explicação geral o suficiente que também ajudará as pessoas que usam outras plataformas.

radiante
fonte
40
no que diz respeito ao "tópico fora de questão" - um programador de aplicativos da web precisa encontrar uma maneira de tornar seu aplicativo rastreável para SEO, este é um requisito básico na web. Fazer isso não é sobre programação em si, mas é relevante para o assunto "problemas práticos e responsáveis ​​que são exclusivos para a profissão de programador", conforme descrito em stackoverflow.com/help/on-topic . É um problema para muitos programadores sem soluções claras em toda a web. Eu esperava ajudar outras pessoas e investi horas apenas descrevendo aqui, obter pontos negativos certamente não me motiva a ajudar novamente.
precisa
3
Se a ênfase está na programação e não no óleo de cobra / molho secreto SEO vodu / spam, pode ser perfeitamente atual. Também gostamos de respostas automáticas, onde elas têm potencial para serem úteis para futuros leitores a longo prazo. Esse par de perguntas e respostas parece passar nos dois testes. (Alguns dos detalhes do fundo poderia aprofundar a pergunta melhor ao invés de ser introduzido na resposta, mas que é bastante menor)
Flexo
6
+1 para diminuir os votos. Independentemente de q / a ser mais adequado como postagem no blog, a pergunta é relevante para Durandal e a resposta é bem pesquisada.
RainerAtSpirit
2
Concordo que o SEO é uma parte importante hoje em dia da vida cotidiana dos desenvolvedores e deve ser definitivamente considerado um tópico no stackoverflow!
Kim D.
Além de implementar todo o processo, você pode tentar o SnapSearch snapsearch.io, que basicamente trata esse problema como um serviço.
precisa saber é o seguinte

Respostas:

121

Antes de começar, entenda o que o Google exige , principalmente o uso de URLs bonitos e feios . Agora vamos ver a implementação:

Lado do Cliente

No lado do cliente, você possui apenas uma única página html que interage dinamicamente com o servidor por meio de chamadas AJAX. é disso que se trata o SPA. Todas as atags no lado do cliente são criadas dinamicamente no meu aplicativo. Mais adiante, veremos como tornar esses links visíveis ao bot do google no servidor. Cada uma dessas atags precisa ter um pretty URLna hreftag para que o bot do Google o rastreie. Você não deseja que a hrefpeça seja usada quando o cliente clicar nela (mesmo que você queira que o servidor possa analisá-la, veremos isso mais tarde), porque talvez não desejemos que uma nova página seja carregada, apenas para fazer uma chamada AJAX, exibindo alguns dados em parte da página e alterar o URL via javascript (por exemplo, usando HTML5 pushstateou com Durandaljs). Então, nós temos umhrefatributo para o google, bem como sobre onclickqual faz o trabalho quando o usuário clica no link. Agora, como eu push-statenão quero usar nenhum #URL, uma atag típica pode ficar assim:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>

'category' e 'subCategory' 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 produto, não como parâmetros extras para uma página específica da "loja", como http://www.xyz.com/store/category/subCategory/product111. Isso ocorre porque eu prefiro links mais curtos e simples. Isso implica que não haverá uma categoria com o mesmo nome que uma das minhas 'páginas', ou seja, '
Não vou entrar em como carregar os dados via AJAX (a onclickparte), pesquisá-los no google, há muitas boas explicações. A única coisa importante aqui que quero mencionar é que, quando o usuário clica neste link, quero que o URL no navegador fique assim:
http://www.xyz.com/category/subCategory/product111. E este 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 links! todas as 'páginas' são implementadas no lado do cliente e o URL diferente não faz uma chamada para o servidor (o servidor precisa saber como lidar com esses URLs caso sejam usados ​​como links externos de outro site para o site, veremos isso mais tarde na parte 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, poderá instalar o Durandal Starter Kit e, em seguida shell.js, usar algo como isto:

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 });
        }
    };
});

Há algumas coisas importantes a serem observadas aqui:

  1. A primeira rota (com route:'') é para a URL que não possui dados extras, ou seja http://www.xyz.com. Nesta página, você carrega dados gerais usando o AJAX. Na verdade, pode não haver atags nesta página. Você vai querer adicionar a seguinte tag de modo bot que o Google vai saber o que fazer com ele:
    <meta name="fragment" content="!">. Essa tag fará o bot do google transformar o URL para o www.xyz.com?_escaped_fragment_=qual veremos mais adiante.
  2. A rota 'about' é apenas um exemplo para um link para outras 'páginas' que você pode querer no seu aplicativo da web.
  3. Agora, a parte complicada é que não há rota de 'categoria' e pode haver muitas categorias diferentes - nenhuma das quais tem uma rota predefinida. É aqui que mapUnknownRoutesentra. Ele mapeia essas rotas desconhecidas para a rota 'store' e também remove qualquer '!' do URL, caso seja pretty URLgerado pelo mecanismo de pesquisa 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; Apenas altero a parte da página em que esses dados são relevantes e também altero o URL localmente.
  4. Observe o pushState:trueque instrui Durandal a usar URLs de estado de envio.

É tudo o que precisamos no lado do cliente. Ele pode ser implementado também com URLs com hash (em Durandal, você simplesmente remove o pushState:truepara isso). A parte mais complexa (pelo menos para mim ...) foi a parte do servidor:

Lado do servidor

Estou usando MVC 4.5no lado do servidor com WebAPIcontroladores. O servidor realmente precisa lidar com três tipos de URLs: os gerados pelo google - ambos prettye uglytambém um URL 'simples' com o mesmo formato que o que aparece no navegador do cliente. Vamos ver como fazer isso:

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

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

Agora, isso transforma o URL para algo como: http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111. Quero que a URL seja enviada ao cliente que carregará os dados via AJAX, portanto, o truque aqui é chamar o controlador 'index' padrão como se não estivesse fazendo referência a nenhum controlador; Eu faço isso adicionando um hash ao URL antes de todos os parâmetros 'category' e 'subCategory'; o URL do hash não requer nenhum controlador especial, exceto o controlador 'index' padrão e os dados são enviados ao cliente, que 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 os URLs feios ? Eles 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ça que faz o que o navegador está fazendo no lado do cliente - mas no lado do servidor. Em outras palavras, o phantom 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 (e também a obtenção de dados por chamadas AJAX) e devolver o HTML que reflete o DOM. Se você estiver usando o MS Visual Studio Express, muitos desejam instalar o phantom através deste link .
Mas primeiro, quando uma URL feia é enviada para o servidor, precisamos capturá-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' 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' roteia 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 associado viewé muito simples, apenas uma linha de código:
@Html.Raw( ViewBag.result )
Como você pode ver no controlador, o phantom carrega um arquivo javascript chamado createSnapshot.jsem uma pasta que criei chamadaseo . 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);

Em primeiro lugar, quero agradecer a Thomas Davis pela página em que obtive o código básico de :-).
Você notará algo estranho aqui: o fantasma continua recarregando a página até mais. Se isso não acontecer dentro de 10 segundos, desisto (levará 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 existentes no instantâneo HTML não fazem referência ao URL correto. Isso também pode ser alterado no arquivo fantasma javascript, mas não acho necessário, porque o snapshort HTML é usado apenas pelo google para obter os links e não para executar o javascript; esses linkscheckLoaded() função retorne verdadeira. 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 na minha página, e o phantom não pode saber quando todas as chamadas foram concluídas antes de retornar o reflexo HTML do DOM. O que fiz aqui é após a chamada final do AJAX, adiciono um <span id='compositionComplete'></span>, para que, se essa tag existir, eu saiba que o DOM está concluído. Eu faço isso em resposta ao compositionCompleteevento de Durandal , veja aqui<script>a fazem referência a um URL bonito e, se for o caso, se você tentar ver o instantâneo HTML em um navegador, você receberá erros de javascript, mas todos os links funcionarão corretamente e direcionarão você ao servidor mais uma vez com um URL bonito dessa vez obtendo a página totalmente funcional.
É isso. Agora, o servidor sabe como lidar com URLs bonitas e feias, com o estado push ativado no servidor e no cliente. Todos os URLs feios são tratados da mesma maneira usando o phantom; portanto, não há necessidade de criar um controlador separado para cada tipo de chamada.
Uma coisa que você pode preferir a mudança não é para fazer a chamada uma 'categoria / subcategoria / produto' geral, mas para adicionar um 'loja' para que o link será algo parecido com: http://www.xyz.com/store/category/subCategory/product111. Isso evitará o problema na minha solução de que todos os URLs inválidos são tratados como se fossem realmente chamadas para o controlador 'index', e suponho que eles possam ser tratados no controlador 'store' sem a adição web.configmostrada acima. .

radiante
fonte
Tenho uma pergunta rápida, acho que consegui fazer isso funcionar agora, mas quando submeto meu site ao google e dou links para o google, mapas do site etc. preciso dar ao google mysite.com/# ! ou apenas mysite.com e google adicionarão o escaped_fragment porque eu o tenho na metatag?
Ccorrin
ccorrin - até onde sei, você não precisa fornecer nada ao google; o bot do google encontrará seu site e procurará URLs bonitos (não se esqueça de adicionar também a metatag na página inicial, pois ela pode não conter URLs). o URL feio que contém o escaped_fragment é sempre adicionado apenas pelo google - você nunca deve colocá-lo dentro de seus HTMLs. e obrigado pelo apoio :-)
beamish
obrigado Bjorn & Sandra :-) Estou trabalhando em uma versão melhor deste documento, que também inclui informações sobre como armazenar em cache as páginas, a fim de tornar o processo mais rápido e fazê-lo no uso mais comum em que o URL contém o nome do controlador; Vou postar-lo assim que ele está pronto
beamish
Esta é uma ótima explicação !!. Eu o implementei e funciona como um encanto no meu devbox localhost. O problema é ao implantar nos Sites do Azure porque o site congela e, após algum tempo, recebo um erro 502. Você tem alguma ideia sobre como implantar PhantomJS para Azure ?? ... Graças ( testypv.azurewebsites.net/?_escaped_fragment_=home/about )
yagopv
Não tenho experiência com sites do Azure, mas o que me vem à cabeça é que talvez o processo de verificação da carga completa da página nunca seja realizado, portanto o servidor continua tentando recarregar a página novamente sem sucesso. talvez seja aí que está o problema (mesmo que haja um limite de tempo para essas verificações, por isso pode não estar lá)? tente colocar 'return true;' como a primeira linha em 'checkLoaded ()' e veja se isso faz diferença.
beamish
4

Aqui está um link para uma gravação de screencast da minha aula de treinamento do Ember.js que hospedei em Londres em 14 de agosto. Ele descreve uma estratégia para o aplicativo do lado do cliente e para o aplicativo do servidor, além de demonstrar ao vivo como a implementação desses recursos fornecerá ao seu aplicativo de página única JavaScript uma degradação suave, mesmo para usuários com o JavaScript desativado. .

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

Em resumo, as etapas necessárias são:

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

Depois que essa etapa estiver concluída, cabe ao seu back-end veicular a versão estática do seu HTML como parte da tag noscript dessa 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/#

Joachim H. Skeie
fonte
0

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

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

gabrielperales
fonte
2
Embora esse link possa responder à pergunta, é melhor incluir aqui as partes essenciais da resposta e fornecer o link para referência. As respostas somente para links podem se tornar inválidas se a página vinculada for alterada. - Do comentário
timgeb
2
Você está certo. Eu atualizei meu comentário ... Espero que agora seja mais preciso.
Gabrielperales
0

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

ddtxra
fonte