Sites JS de “página única” e SEO


128

Atualmente, existem muitas ferramentas interessantes para criar sites JavaScript poderosos de "página única". Na minha opinião, isso é feito corretamente, permitindo que o servidor atue como uma API (e nada mais) e permitindo que o cliente lide com todo o material de geração de HTML. O problema com esse "padrão" é a falta de suporte ao mecanismo de pesquisa. Eu posso pensar em duas soluções:

  1. Quando o usuário entrar no site, deixe o servidor renderizar a página exatamente como o cliente faria na navegação. Portanto, se eu for http://example.com/my_pathdiretamente, o servidor renderizará a mesma coisa que o cliente faria se eu passar /my_pathpelo pushState.
  2. Deixe o servidor fornecer um site especial apenas para os robôs do mecanismo de pesquisa. Se um usuário normal visita http://example.com/my_patho servidor, ele deve fornecer uma versão pesada do site em JavaScript. Mas se o bot do Google o visita, o servidor deve fornecer um mínimo de HTML com o conteúdo que eu quero que o Google indexe.

A primeira solução é discutida mais aqui . Eu tenho trabalhado em um site fazendo isso e não é uma experiência muito agradável. Não é SECO e, no meu caso, tive que usar dois mecanismos de modelo diferentes para o cliente e o servidor.

Acho que vi a segunda solução para alguns bons sites em Flash. Eu gosto dessa abordagem muito mais do que a primeira e, com a ferramenta certa no servidor, isso pode ser feito sem problemas.

Então, o que eu realmente estou pensando é o seguinte:

  • Você consegue pensar em alguma solução melhor?
  • Quais são as desvantagens da segunda solução? Se, de alguma forma, o Google descobrir que não estou exibindo exatamente o mesmo conteúdo para o bot do Google como um usuário comum, eu seria punido nos resultados da pesquisa?

Respostas:


44

Embora o número 2 possa ser "mais fácil" para você como desenvolvedor, ele fornece apenas o rastreamento do mecanismo de pesquisa. E sim, se o Google descobrir que você exibe conteúdo diferente, você poderá ser penalizado (não sou especialista nisso, mas já ouvi falar disso).

Tanto o SEO quanto a acessibilidade (não apenas para pessoas com deficiência, mas a acessibilidade por meio de dispositivos móveis, dispositivos com tela sensível ao toque e outras plataformas não padronizadas para computação / Internet) têm uma filosofia subjacente semelhante: marcação semanticamente rica que é "acessível" (ou seja, pode acessados, visualizados, lidos, processados ​​ou usados) em todos esses navegadores diferentes. Um leitor de tela, um rastreador de mecanismo de pesquisa ou um usuário com JavaScript habilitado devem ser capazes de usar / indexar / entender a funcionalidade principal do seu site sem problemas.

pushStatenão aumenta esse fardo, na minha experiência. Ele apenas traz o que costumava ser uma reflexão tardia e "se tivermos tempo" para a vanguarda do desenvolvimento da web.

O que você descreve na opção nº 1 geralmente é o melhor caminho a percorrer - mas, como outros problemas de acessibilidade e SEO, fazer isso pushStateem um aplicativo pesado em JavaScript requer planejamento antecipado ou isso se tornará um fardo significativo. Ele deve ser inserido na arquitetura da página e do aplicativo desde o início - a atualização é dolorosa e causará mais duplicação do que o necessário.

Eu tenho trabalhado pushStaterecentemente com o SEO em algumas aplicações diferentes e achei o que considero uma boa abordagem. Ele segue basicamente o item nº 1, mas é responsável por não duplicar html / templates.

A maioria das informações pode ser encontrada nessas duas postagens no blog:

http://lostechies.com/derickbailey/2011/09/06/test-driving-backbone-views-with-jquery-templates-the-jasmine-gem-and-jasmine-jquery/

e

http://lostechies.com/derickbailey/2011/06/22/rendering-a-rails-partial-as-a-jquery-template/

O essencial disso é que eu uso modelos ERB ou HAML (executando Ruby on Rails, Sinatra etc.) para renderizar no lado do servidor e criar os modelos do lado do cliente que o Backbone pode usar, bem como para minhas especificações JavaScript do Jasmine. Isso elimina a duplicação da marcação entre o lado do servidor e o lado do cliente.

A partir daí, é necessário executar algumas etapas adicionais para que seu JavaScript funcione com o HTML renderizado pelo servidor - verdadeiro aprimoramento progressivo; pegando a marcação semântica que foi entregue e aprimorando-a com JavaScript.

Por exemplo, estou criando um aplicativo de galeria de imagens com pushState. Se você solicitar /images/1do servidor, ele renderizará toda a galeria de imagens no servidor e enviará todo o HTML, CSS e JavaScript para o seu navegador. Se você tiver o JavaScript desativado, ele funcionará perfeitamente. Todas as ações que você executar solicitarão um URL diferente do servidor e o servidor renderizará toda a marcação para o seu navegador. Se você tiver o JavaScript ativado, ele pegará o HTML já renderizado junto com algumas variáveis ​​geradas pelo servidor e assumirá o controle a partir daí.

Aqui está um exemplo:

<form id="foo">
  Name: <input id="name"><button id="say">Say My Name!</button>
</form>

Depois que o servidor renderiza isso, o JavaScript o seleciona (usando uma visualização Backbone.js neste exemplo)

FooView = Backbone.View.extend({
  events: {
    "change #name": "setName",
    "click #say": "sayName"
  },

  setName: function(e){
    var name = $(e.currentTarget).val();
    this.model.set({name: name});
  },

  sayName: function(e){
    e.preventDefault();
    var name = this.model.get("name");
    alert("Hello " + name);
  },

  render: function(){
    // do some rendering here, for when this is just running JavaScript
  }
});

$(function(){
  var model = new MyModel();
  var view = new FooView({
    model: model,
    el: $("#foo")
  });
});

Este é um exemplo muito simples, mas acho que é o ponto principal.

Quando instante a exibição após o carregamento da página, estou fornecendo o conteúdo existente do formulário que foi renderizado pelo servidor para a instância da exibição como ela exibição. Estou não chamar prestar ou ter a visão de gerar uma elpara mim, quando a primeira vista é carregado. Eu tenho um método de renderização disponível para depois que a exibição estiver em execução e a página estiver toda em JavaScript. Isso me permite renderizar novamente a exibição mais tarde, se necessário.

Clicar no botão "Diga meu nome" com o JavaScript ativado fará com que uma caixa de alerta. Sem JavaScript, ele seria postado de volta no servidor e o servidor poderia renderizar o nome para um elemento html em algum lugar.

Editar

Considere um exemplo mais complexo, em que você tem uma lista que precisa ser anexada (dos comentários abaixo)

Digamos que você tenha uma lista de usuários em uma <ul>tag. Essa lista foi renderizada pelo servidor quando o navegador fez uma solicitação e o resultado se parece com:

<ul id="user-list">
  <li data-id="1">Bob
  <li data-id="2">Mary
  <li data-id="3">Frank
  <li data-id="4">Jane
</ul>

Agora você precisa percorrer esta lista e anexar uma visualização e modelo de Backbone a cada um dos <li>itens. Com o uso do data-idatributo, você pode encontrar o modelo de onde cada tag vem facilmente. Você precisará de uma visualização de coleção e de itens que seja inteligente o suficiente para se conectar a esse html.

UserListView = Backbone.View.extend({
  attach: function(){
    this.el = $("#user-list");
    this.$("li").each(function(index){
      var userEl = $(this);
      var id = userEl.attr("data-id");
      var user = this.collection.get(id);
      new UserView({
        model: user,
        el: userEl
      });
    });
  }
});

UserView = Backbone.View.extend({
  initialize: function(){
    this.model.bind("change:name", this.updateName, this);
  },

  updateName: function(model, val){
    this.el.text(val);
  }
});

var userData = {...};
var userList = new UserCollection(userData);
var userListView = new UserListView({collection: userList});
userListView.attach();

Neste exemplo, o UserListViewloop percorrerá todas as <li>tags e anexará um objeto de visualização com o modelo correto para cada uma. ele configura um manipulador de eventos para o evento de alteração de nome do modelo e atualiza o texto exibido do elemento quando ocorre uma alteração.


Esse tipo de processo, para pegar o html que o servidor renderizou e fazer com que meu JavaScript o controle e execute, é uma ótima maneira de fazer as coisas rolarem para SEO, acessibilidade e pushStatesuporte.

Espero que ajude.


Entendi, mas o interessante é como a renderização é feita após "o JavaScript assumir". Em um exemplo mais complicado, talvez você precise usar um modelo não compilado no cliente, percorrendo uma matriz de usuários para criar uma lista. A exibição é renderizada novamente toda vez que o modelo de um usuário é alterado. Como você faria isso sem duplicar os modelos (e sem pedir ao servidor para renderizar a exibição para o cliente)?
user544941

as 2 postagens do blog que eu vinculei devem mostrar coletivamente como ter modelos que podem ser usados ​​no cliente e no servidor - sem necessidade de duplicação. o servidor precisará renderizar a página inteira se você quiser que ela seja acessível e seja compatível com SEO. Eu atualizei a minha resposta para incluir um exemplo mais complexo de anexar a uma lista de usuário que foi processado pelo servidor
Derick Bailey

22

Eu acho que você precisa disso: http://code.google.com/web/ajaxcrawling/

Você também pode instalar um back-end especial que "renderiza" sua página executando javascript no servidor e, em seguida, o exibe no Google.

Combine as duas coisas e você terá uma solução sem programar as coisas duas vezes. Desde que seu aplicativo seja totalmente controlável por meio de fragmentos de âncora.)


Na verdade, não é o que estou procurando. Essas são algumas variantes da primeira solução e, como mencionei, não estou muito feliz com essa abordagem.
user544941

2
Você não leu toda a minha resposta. Você também usa um back-end especial que processa o javascript para você - não escreve as coisas duas vezes.
Ariel

Sim, eu li isso. Mas se eu entendi direito, esse seria um programa infernal, pois teria que simular todas as ações que acionam o pushState. Como alternativa, eu poderia dar as ações diretamente, mas depois não estamos mais tão SECOS.
user544941

2
Eu acho que é basicamente um navegador sem a frente. Mas, sim, você precisa tornar o programa completamente controlável a partir de fragmentos de âncora. Você também precisa garantir que todos os links tenham o fragmento adequado, junto com, ou em vez de, onClicks.
Ariel

17

Então, parece que a principal preocupação é estar SECO

  • Se você estiver usando o pushState, seu servidor enviará o mesmo código exato para todos os URLs (que não contenham uma extensão de arquivo para veicular imagens etc.) "/ mydir / myfile", "/ myotherdir / myotherfile" ou root "/ "- todos os pedidos recebem o mesmo código exato. Você precisa ter algum tipo de mecanismo de reescrita de URL. Você também pode servir um pouco de html e o restante pode vir da sua CDN (usando o require.js para gerenciar dependências - consulte https://stackoverflow.com/a/13813102/1595913 ).
  • (teste a validade do link convertendo-o em seu esquema de URL e testando a existência de conteúdo consultando uma fonte estática ou dinâmica. se não for válido, envie uma resposta 404).
  • Quando a solicitação não é de um bot do Google, você apenas processa normalmente.
  • Se a solicitação for de um bot do Google, use o phantom.js - navegador sem kit da web ( "Um navegador sem cabeçalho é simplesmente um navegador completo com nenhuma interface visual." ) Para renderizar html e javascript no servidor e enviar o google bot o html resultante. Como o bot analisa o html, ele pode acessar os outros links / páginas "pushState" no servidor <a href="https://stackoverflow.com/someotherpage">mylink</a>, o servidor reescreve o URL do arquivo do aplicativo, carrega-o no phantom.js e o html resultante é enviado ao bot, e assim por diante. ..
  • Para o seu html, suponho que você esteja usando links normais com algum tipo de seqüestro (por exemplo, usando com backbone.js https://stackoverflow.com/a/9331734/1595913 )
  • Para evitar confusão com qualquer link, separe o código da API que serve json em um subdomínio separado, por exemplo, api.mysite.com
  • Para melhorar o desempenho, você pode pré-processar as páginas do site para os mecanismos de pesquisa com antecedência, fora do horário de expediente, criando versões estáticas das páginas usando o mesmo mecanismo do phantom.js e, consequentemente, veicular as páginas estáticas no google bots. O pré-processamento pode ser feito com algum aplicativo simples que pode analisar <a>tags. Nesse caso, o tratamento 404 é mais fácil, pois você pode simplesmente verificar a existência do arquivo estático com um nome que contenha o caminho da URL.
  • Se você usar #! sintaxe de hash bang para os links do site, um cenário semelhante se aplica, exceto que o mecanismo de servidor de URL de reescrita procuraria _escaped_fragment_ no URL e formataria o URL para o seu esquema de URL.
  • Existem algumas integrações do node.js com o phantom.js no github e você pode usar o node.js como servidor da web para produzir saída html.

Aqui estão alguns exemplos usando o phantom.js para seo:

http://backbonetutorials.com/seo-for-single-page-apps/

http://thedigitalself.com/blog/seo-and-javascript-with-phantomjs-server-side-rendering


4

Se você estiver usando o Rails, tente poirot . É uma jóia que simplifica a reutilização do bigode ou do guidão modelos de lado do cliente e do servidor.

Crie um arquivo em suas visualizações como _some_thingy.html.mustache .

Renderizar lado do servidor:

<%= render :partial => 'some_thingy', object: my_model %>

Coloque o modelo na cabeça para uso do lado do cliente:

<%= template_include_tag 'some_thingy' %>

Rendre do lado do cliente:

html = poirot.someThingy(my_model)

3

Para adotar um ângulo um pouco diferente, sua segunda solução seria a correta em termos de acessibilidade ... você forneceria conteúdo alternativo para usuários que não podem usar javascript (aqueles com leitores de tela etc.).

Isso adicionaria automaticamente os benefícios do SEO e, na minha opinião, não seria visto como uma técnica "impertinente" pelo Google.


E alguém provou que você estava errado? Já faz algum tempo desde que o comentário foi postado
jkulak

1

Interessante. Tenho procurado soluções viáveis, mas isso parece bastante problemático.

Na verdade, eu estava mais inclinado para a sua segunda abordagem:

Deixe o servidor fornecer um site especial apenas para os robôs do mecanismo de pesquisa. Se um usuário normal visitar http://example.com/my_path, o servidor deverá fornecer a ele uma versão pesada do site em JavaScript. Mas se o bot do Google o visita, o servidor deve fornecer um mínimo de HTML com o conteúdo que eu quero que o Google indexe.

Aqui está minha opinião sobre como resolver o problema. Embora não esteja confirmado para o trabalho, ele pode fornecer algumas idéias ou idéias para outros desenvolvedores.

Suponha que você esteja usando uma estrutura JS que suporta a funcionalidade "push state", e sua estrutura de back-end é Ruby on Rails. Você tem um site de blog simples e deseja que os mecanismos de pesquisa indexem todo o seu artigo indexeshow páginas.

Digamos que você tenha suas rotas configuradas assim:

resources :articles
match "*path", "main#index"

Assegure-se de que todo controlador do lado do servidor renderize o mesmo modelo que sua estrutura do lado do cliente exige para ser executado (html / css / javascript / etc). Se nenhum dos controladores for correspondido na solicitação (neste exemplo, temos apenas um conjunto de ações RESTful para o ArticlesController), basta corresponder a qualquer outra coisa, renderizar o modelo e deixar que a estrutura do lado do cliente lide com o roteamento. A única diferença entre acertar um controlador e acertar o correspondente curinga seria a capacidade de renderizar o conteúdo com base na URL que foi solicitada aos dispositivos com JavaScript desativado.

Pelo que entendi, é uma má idéia renderizar conteúdo que não é visível para os navegadores. Então, quando o Google indexa, as pessoas acessam o Google para visitar uma determinada página e não há conteúdo, então você provavelmente será penalizado. O que vem à mente é que você processa o conteúdo em um divdisplay: noneem CSS.

No entanto, tenho certeza de que não importa se você simplesmente faz isso:

<div id="no-js">
  <h1><%= @article.title %></h1>
  <p><%= @article.description %></p>
  <p><%= @article.content %></p>
</div>

E, em seguida, usando JavaScript, que não é executado quando um dispositivo desabilitado para JavaScript abre a página:

$("#no-js").remove() # jQuery

Dessa forma, para o Google e para qualquer pessoa com dispositivos desabilitados para JavaScript, eles veriam o conteúdo bruto / estático. Portanto, o conteúdo está fisicamente lá e é visível para qualquer pessoa com dispositivos desabilitados para JavaScript.

Mas, quando um usuário visita a mesma página e realmente tem o JavaScript ativado, o#no-js nó será removido para não bagunçar seu aplicativo. Em seguida, sua estrutura do lado do cliente manipulará a solicitação através do roteador e exibirá o que um usuário deve ver quando o JavaScript estiver ativado.

Eu acho que essa pode ser uma técnica válida e bastante fácil de usar. Embora isso possa depender da complexidade do seu site / aplicativo.

Embora, por favor, corrija-me se não for. Apenas pensei em compartilhar meus pensamentos.


1
Bem, se você exibir o conteúdo pela primeira vez e removê-lo um pouco mais tarde, provavelmente o usuário final poderá perceber que o conteúdo pisca / pisca no navegador :) Especialmente se for um navegador lento, um tamanho enorme de conteúdo HTML você tenta exibir / remover e alguns demora antes que seu código JS seja carregado e executado. O que você acha?
Evereq

1

Use o NodeJS no lado do servidor, navegue no código do lado do cliente e roteie cada uri da solicitação http (exceto recursos http estáticos) através de um cliente no lado do servidor para fornecer o primeiro 'bootsnap' (um instantâneo da página em que está o estado). Use algo como jsdom para lidar com jquery dom-ops no servidor. Após o bootnap retornar, configure a conexão do websocket. Provavelmente, é melhor diferenciar entre um cliente de websocket e um cliente do servidor, estabelecendo algum tipo de conexão de wrapper no cliente (o cliente do servidor pode se comunicar diretamente com o servidor). Estou trabalhando em algo assim: https://github.com/jvanveen/rnet/


0

Use o Google Closure Template para renderizar páginas. Ele é compilado em javascript ou java, por isso é fácil renderizar a página no lado do cliente ou do servidor. No primeiro encontro com todos os clientes, renderize o html e adicione javascript como link no cabeçalho. O rastreador lerá apenas o html, mas o navegador executará seu script. Todas as solicitações subsequentes do navegador podem ser feitas na API para minimizar o tráfego.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.