SEO para SPA

Como tornar aplicações de página única (React Router, Vue Router, Angular Router) rastreáveis e indexáveis — os problemas de app-shell e soft-404, History API vs roteamento por hash, HTML por rota via SSR/prerender, canonicals e títulos por rota, e geração de sitemap para rotas no lado do cliente.

Publicado pela primeira vez: 3 de jul. de 2026 · Última atualização: 3 de ago. de 2026 · Avançado
Idiomas

Uma aplicação de página única carrega um documento e troca as visualizações com JavaScript em vez de solicitar uma nova página do servidor. Muitas SPAs implementam isso com um 'app shell' — o servidor retorna HTML real de uma URL, uma casca quase vazia, até que o JS renderize cada rota — mas isso é uma escolha comum de implementação, não uma regra que toda SPA segue; verifique o que uma solicitação direta a cada rota realmente retorna antes de assumir. A correção, quando um setup de app-shell vazio é o problema, tem duas metades independentes: endereçabilidade (History API, não fragmentos hash/#!, para que cada visualização tenha uma URL real — uma navegação suave do navegador via History API muda a URL e a interface, mas não cria por si só uma nova resposta do servidor) e disponibilidade de conteúdo (SSR, prerender ou um meta-framework para que cada uma dessas URLs possa retornar HTML exclusivo sob solicitação). Além disso, verifique o título, canonical e estado de robots de cada rota tanto na entrada direta quanto após a renderização, lide com soft 404s (os roteadores tendem a manter um status 200 para visualizações 'não encontrado' — redirecione para uma URL que realmente retorne 404, ou adicione um noindex renderizado, embora um noindex inicial possa fazer com que a renderização seja ignorada), e garanta que cada rota no seu sitemap realmente resolva para HTML real e indexável — não há formato especial de sitemap para SPA.

TL;DR — O risco central de SEO de um SPA não é “JavaScript” no abstrato — é que o roteamento do lado do cliente muda o que o usuário vê sem mudar o que o servidor retornaria. A correção tem duas metades independentes que as pessoas constantemente confundem: endereçabilidade (API History, URLs únicas por rota, sem fragmentos #!) e disponibilidade de conteúdo (SSR, prerenderização/SSG ou um meta-framework). Faça apenas a primeira e você obtém um sitemap organizado de URLs que todas renderizam o mesmo shell. Além de ambas, cada rota precisa de seu próprio canônico/título/descrição no DOM renderizado, você tem que lidar com 404s suaves que mantêm um 200, e cada rota no sitemap tem que resolver independentemente para HTML real.

O que realmente é SEO para SPA

SPA é uma arquitetura de aplicação, não um estado de falha garantido em SEO. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: MDN: Single-page application Renderização no servidor, pré-renderização ou renderização no cliente cuidadosamente implementada podem expor conteúdo, mas nenhuma garante indexação. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: Google: JavaScript SEO basics

Este é o mergulho profundo em aplicações de página única especificamente — roteamento no lado do cliente com React Router, Vue Router ou Angular Router, onde após o primeiro carregamento o servidor nunca mais envia uma página completa. Ele acompanha o guia mais amplo de JavaScript SEO (que cobre paridade, carregamento preguiçoso, rolagem infinita e renderização de JS em geral) e os artigos específicos por framework para React, Next.js, Nuxt, Angular, Vue, Svelte e Astro. Aqui estou interessado apenas na camada de roteamento e no que ela faz com o rastreamento e a indexação.

Por que SPAs com roteamento no lado do cliente falham em SEO

Uma URL, uma resposta HTML — o problema do app shell

O Google descreve o modo de falha com precisão: “Some JavaScript sites may use the app shell model where the initial HTML does not contain the actual content and Google needs to execute JavaScript before being able to see the actual page content.” (tradução) «Alguns sites com JavaScript podem usar o modelo de app shell em que o HTML inicial não contém o conteúdo real e o Google precisa executar JavaScript antes de conseguir ver o conteúdo real da página.» Em uma SPA pura, o “app shell” é tudo o que o servidor retorna. Busque /products do zero e você obtém o mesmo documento quase vazio que obteria para /about. O conteúdo só diverge depois que o navegador executa seu JavaScript e seu roteador decide o que mostrar.

Esse é o problema inteiro em uma frase: o roteamento no lado do cliente muda o que o usuário vê sem mudar o que o servidor retornaria. Tudo a jusante — 404s suaves, conteúdo duplicado, títulos ausentes — é um sintoma disso.

O servidor nunca vê qual “página” foi solicitada (por que o roteamento por hash falha)

SPAs mais antigas carregam visualizações a partir de fragmentos de URL — example.com/#/products. As palavras exatas do Google: “A SPA may use URL fragments (for example https://example.com/#/products) for loading different views.” (tradução) «Uma SPA pode usar fragmentos de URL (por exemplo https://example.com/#/products) para carregar diferentes visualizações.» Isso falha por um motivo específico e mecânico: os navegadores nunca enviam o fragmento (qualquer coisa após #) ao servidor na solicitação HTTP. O servidor literalmente não consegue saber qual “página” foi solicitada, então não pode retornar conteúdo diferente ou um código de status diferente para ela. Um rastreador que busca a URL uma vez obtém HTML idêntico, independentemente do fragmento.

É também por isso que o Google descontinuou formalmente seu esquema de rastreamento AJAX de 2009 em outubro de 2015: “In short: We are no longer recommending the AJAX crawling proposal we made back in 2009.” (tradução) «Em resumo: não estamos mais recomendando a proposta de rastreamento AJAX que fizemos em 2009.» A antiga solução alternativa _escaped_fragment_ permitia que servidores pré-renderizassem rotas de fragmento sob solicitação — mas ela contornava o problema de roteamento em vez de corrigi-lo. A própria recomendação do Google que a substitui é a History API, abordada abaixo.

404s suaves — roteadores no lado do cliente mantêm 200 para tudo

Isso é quase inevitável em uma SPA pura. Roteadores no lado do cliente, por design, mantêm o status 200 da página original para cada navegação virtual — incluindo estados de “não encontrado”. O Google sinaliza isso explicitamente: “In a single-page application (SPA), this can be especially difficult. To prevent error pages from being indexed, you can use one or both of the following strategies.” (tradução) «Em uma aplicação de página única (SPA), isso pode ser especialmente difícil. Para evitar que páginas de erro sejam indexadas, você pode usar uma ou ambas as estratégias a seguir.» E o porquê: “When a SPA is using client-side JavaScript to handle errors they often report a 200 HTTP status code instead of the appropriate status code.” (tradução) «Quando uma SPA usa JavaScript no lado do cliente para lidar com erros, ela frequentemente reporta um código de status HTTP 200 em vez do código de status apropriado.»

O resultado é que visualizações vazias ou de erro são indexadas como páginas 200 finas. O Google documenta exatamente duas estratégias aqui, com precisão: redirecionar (ou fazer uma solicitação completa) para uma URL cujo servidor retorna um 404/status de erro real, ou adicionar uma tag noindex à visualização de erro com JavaScript. Preste atenção na segunda: se esse noindex estiver presente desde a primeira pintura, em vez de ser adicionado depois que o aplicativo decide que a rota é inválida, isso pode fazer o Google pular a renderização da página por completo — então verifique o que uma solicitação direta retorna antes de qualquer JS ser executado, não apenas o que aparece no DOM renderizado depois. Evidence for this claim For a client-rendered not-found view, Google documents two approaches: redirect to a URL whose server returns a 404 response, or add noindex with JavaScript; an initial noindex can cause rendering to be skipped, so raw and rendered directives require separate testing. Scope: SPAs Confidence: high · Verified: Fix Search-related JavaScript problems

Duplicatas de shell compartilhado — uma causa possível, não um diagnóstico automático

Existe um segundo modo de falha mais sorrateiro: rotas distintas são indexadas como duplicatas umas das outras porque renderizam o mesmo boilerplate compartilhado de cabeçalho/navegação/rodapé. Gary Illyes descreveu uma forma como isso acontece: “I have a bunch of emails in my inbox where the issue is that the centerpiece took forever to load, so rendering timed out (my most likely explanation) and we were left with a bunch of pages that only had the boilerplate. With only the boilerplate, those pages are dups.” (tradução) «Tenho um monte de e-mails na minha caixa de entrada onde o problema é que a peça central demorou muito para carregar, então a renderização expirou (minha explicação mais provável) e ficamos com um monte de páginas que só tinham o boilerplate. Com apenas o boilerplate, essas páginas são duplicatas.» A correção dele: “Try to restructure the js calls such that the content (including marginal boilerplate) loads first.” (tradução) «Tente reestruturar as chamadas de js para que o conteúdo (incluindo o boilerplate marginal) carregue primeiro.» (Transmitido via uma postagem no LinkedIn, que resiste à verificação automatizada; trate como fonte secundária de alta confiança.)

Observe que Illyes o enquadra como “minha explicação mais provável”, não um diagnóstico confirmado — vale a pena levar isso ao pé da letra. “Tempo limite de renderização” não é algo que você possa diagnosticar apenas pelos sintomas; você precisa de evidências no nível de requisição (o que uma busca direta realmente retorna), evidências de saída renderizada (se o conteúdo da peça central está presente após a renderização) e evidências do Search Console (sinais de duplicata/indexação) antes de atribuir o problema de conteúdo duplicado de uma rota a um tempo limite especificamente, em vez de a um bug, a um recurso bloqueado ou a um shell genuinamente idêntico. Também não há um tempo limite fixo e publicado para projetar em torno dele — o próprio documento básico do Google diz que uma página “may stay on this queue for a few seconds, but it can take longer than that” (tradução) «pode permanecer nesta fila por alguns segundos, mas pode levar mais tempo do que isso», sem se comprometer com um número. Não planeje em torno de uma duração assumida da fila de renderização; em vez disso, garanta que o conteúdo principal carregue o mais cedo possível, independentemente de quanto tempo a renderização levar.

A correção: obter HTML real por rota

A correção completa tem duas partes independentes que as pessoas confundem constantemente:

  1. Endereçabilidade de URL — API History, URLs únicas por rota, sem fragmentos.
  2. Disponibilidade de conteúdo — SSR, pré-renderização estática/SSG ou um meta-framework.

Fazer apenas o nº 1 e você obtém URLs limpas e compartilháveis que ainda retornam o mesmo shell em branco. Para uma rota que você realmente deseja indexar, você precisa de ambos.

Isso não é um mandato universal para adicionar SSR em todos os lugares, no entanto. SSR e pré-renderização reduzem o quanto você depende de um crawler executar seu JavaScript com sucesso — eles não se tornam obrigatórios no instante em que um site é tecnicamente um SPA. Antes de escolher uma arquitetura para uma determinada rota, verifique três coisas: se essa rota precisa ranquear de alguma forma (um painel administrativo interno não precisa), o que uma requisição direta e sem JS a ela já retorna (algumas configurações já enviam HTML significativo) e quais crawlers você realmente precisa satisfazer (o Google renderiza JS de forma bastante confiável; o Bing e a maioria dos crawlers de IA menos — veja abaixo). CSR que já passa nessas verificações não precisa se tornar SSR apenas porque o site é um SPA.

Renderização no servidor (SSR)

O servidor executa seu aplicativo para cada requisição e retorna HTML totalmente formado para essa rota, então o cliente o “hidrata” em um SPA vivo. Esta é a opção mais robusta porque o crawler obtém conteúdo completo na primeira busca, sem necessidade de execução de JS.

Pré-renderização estática / SSG

Em vez de renderizar por requisição, você constrói o HTML de cada rota com antecedência no deploy. Perfeito para conteúdo que não muda por usuário. Da minha própria escrita sobre SEO de JavaScript: qualquer tipo de SSR, renderização estática e configuração de pré-renderização vai ser bom para mecanismos de busca — a coisa a evitar é deixar o conteúdo bloqueado atrás de renderização somente no cliente.

Ou: não faça manualmente — use um meta-framework

Para uma nova construção, a recomendação honesta é não criar roteamento do lado do cliente apenas com React-Router ou Vue-Router. Um meta-framework — Next.js, Nuxt, Angular com seu pacote SSR, SvelteKit, Remix — oferece SSR/SSG e metadados baseados em rotas prontos para uso, o que contorna toda essa categoria de problema. (Cada um deles tem seu próprio mergulho profundo neste site.) Seja honesto sobre o outro lado: adaptar SSR a um SPA existente feito à mão é um trabalho real de engenharia — um projeto de migração, não uma alternância de configuração.

Renderização dinâmica como paliativo, não como destino

Você pode servir aos rastreadores um snapshot HTML renderizado separadamente (renderização dinâmica). Tanto o Google quanto o Bing aceitam — o Bing “recomenda a renderização dinâmica como uma ótima alternativa para sites que dependem muito de JavaScript” (transmitido do blog de 2018 do Bing; as duas citações curtas sobre capacidades do bingbot abaixo são verificadas diretamente, esta mais longa não foi rechecada de forma independente) — mas o Google deixa claro que é um paliativo: “A renderização dinâmica era um paliativo e não uma solução de longo prazo… Em vez disso, recomendamos que você use renderização no servidor, renderização estática ou hidratação como solução.” (Transmitido do documento de renderização dinâmica do Google; a redação corresponde ao que já está citado no guia mais amplo de SEO para JavaScript deste site.) É uma ponte, não uma arquitetura.

API History vs. roteamento por hash (#!)

Cinco estados, não dois

Testar uma rota de SPA fica confuso porque “funciona” na verdade abrange cinco estados diferentes e verificáveis separadamente:

EstadoO que é
Resposta do servidorOs bytes que uma solicitação HTTP nova, sem JavaScript, a uma URL realmente recebe de volta.
DOM renderizadoO que um navegador (ou o renderizador do Googlebot) constrói após executar JavaScript contra essa resposta do servidor.
Processamento de buscaComo o Google rastreia separadamente a resposta do servidor, depois renderiza a página e indexa com base em ambos.
Navegação de documento completo no navegadorUma solicitação HTTP nova e real a uma URL — o único estado que pode alterar a resposta do servidor ou o código de status.
Navegação suave no navegadorUma transição da API History (pushState/replaceState) que altera a URL visível, o histórico do navegador e a interface na tela.

A que todos confundem: uma navegação suave altera a URL e a interface, mas não cria por si só uma nova resposta HTTP ou status — isso só acontece em uma navegação completa (ou em uma solicitação direta equivalente, como curl). Testar uma rota clicando pelo aplicativo a partir da página inicial exercita a navegação suave; testá-la solicitando a URL diretamente exercita a resposta do servidor. Ambos importam, e podem discordar.

O que a API History oferece

A recomendação do Google é inequívoca: “Recomendamos usar a API History para carregar conteúdo diferente com base na URL em um SPA.” (No documento ativo, “History API” é um link, então este link profundo tem como alvo a cláusula introdutória.) A API History (pushState/replaceState) permite que seu roteador altere a URL visível para um caminho real e marcável — /products, não /#/products — sem um recarregamento completo. Isso corrige a metade de endereçabilidade: agora cada visualização tem uma URL à qual um servidor poderia responder de forma diferente.

Por que URLs de fragmento/hash são invisíveis para o servidor — e para o Google

Como o fragmento nunca chega ao servidor (veja acima), a API History é a única maneira de dar a cada rota uma URL que o servidor possa realmente servir. O Google estabelece um piso em seu documento básico: “não use fragmentos para carregar conteúdo de página diferente. O exemplo a seguir é uma prática ruim, porque o Googlebot não consegue resolver as URLs de forma confiável.” Esta é a mesma orientação que escrevi em outros lugares — use URLs de aparência normal como /products, não URLs de hash como /#/products, porque o Google não consegue indexar as de hash de forma confiável.

A depreciação de 2015, brevemente

A era do hash-bang (#!) terminou com o post de 2015 do Google: “Times have changed. Today, as long as you’re not blocking Googlebot from crawling your JavaScript or CSS files, we are generally able to render and understand your web pages like modern browsers,” e “you can use the History API pushState() to ensure accessibility for a wider range of browsers (and our systems).” Uma nuance que vale manter: o Google não desindexou instantaneamente os sites antigos com hash-bang — “we’ll generally crawl, render, and index the #! URLs” — mas “we can still try” não é “you should still do this.”

Tornando cada rota indexável de forma independente

URLs limpas e HTML real fazem você ser rastreado. Para ser indexado corretamente, cada rota precisa de seus próprios sinais.

Tags canônicas por rota — e a armadilha da “diretiva mais restritiva”

Cada rota precisa de seu próprio rel=canonical no DOM renderizado. A armadilha: se um canonical de espaço reservado (ou um noindex) for enviado no shell HTML bruto e o JavaScript for sobrescrevê-lo depois, você pode ter um conflito. O Google resolve conflitos entre as versões bruta e renderizada escolhendo o sinal mais restritivo — como eu já disse antes, o Google escolherá as declarações mais restritivas entre o HTML e a versão renderizada de uma página. Um noindex perdido ou um canonical errado embutido no shell pode suprimir silenciosamente a rota inteira mesmo depois que seu JS a “corrige”.

Títulos e meta descrições por rota via JavaScript

Definir isso em JS é aceitável — o Google diz isso diretamente: “You can use JavaScript to set or change the meta description as well as the <title> element.” O requisito é que eles cheguem ao DOM renderizado que o Google avalia, não apenas pisquem brevemente. Dê a cada página seu próprio título e descrição que se atualizem quando a rota mudar.

Torne seus links entre rotas âncoras reais com atributos href, não manipuladores de clique em <div>s. O Google descobre URLs extraindo hrefs; um <div onClick> que navega via roteador é invisível para a extração de links do rastreador. E não dependa de estado no lado do cliente para carregar conteúdo nessas navegações — o renderizador do Google “does not retain state across page loads: Local Storage and Session Storage data are cleared across page loads. HTTP Cookies are cleared across page loads.”

Geração de sitemap para rotas SPA

Não existe um formato especial de “sitemap SPA” — é um problema de contabilidade

O protocolo de sitemap não muda para SPAs. O trabalho real é garantir que cada rota que você lista de forma independente resolva para HTML real, único e renderizado. Um sitemap de 500 rotas no lado do cliente é inútil se essas rotas todas retornarem o mesmo shell. Então o sitemap é consequência da sua estratégia de renderização, não um substituto para ela.

Lidando com rotas dinâmicas/parametrizadas (/product/:id)

Para aplicativos com rotas parametrizadas, você não pode manter a lista manualmente. O gerador de sitemap tem que rodar contra a mesma fonte de dados que o aplicativo usa — um script de build ou um endpoint de servidor que enumera cada id — para que o sitemap e o aplicativo nunca se distanciem. Essas URLs enumeradas só valem a pena listar depois que forem SSR’d ou pré-renderizadas.

Mantendo o sitemap sincronizado com o que é resolvível no servidor

Regenere o sitemap como parte do seu build ou em um cronograma vinculado à sua fonte de conteúdo. Uma rota que retorna 404 (ou pior, soft-404 em 200) mas está no seu sitemap é um sinal de orçamento de rastreamento e qualidade que você não quer.

Testando o que o Google realmente vê

Não verifique apenas uma URL. O ponto central do problema SPA é que as rotas podem diferir no navegador, mas não na rede, então teste várias rotas no nível do HTML bruto:

  • Busque o HTML bruto por rota com curl (um user-agent do Googlebot quando relevante) e confirme que o conteúdo é único por URL, não o shell compartilhado.
  • Inspeção de URL no Search Console — compare o HTML rastreado/renderizado de várias rotas e confirme que o título, a descrição e o canônico por rota estão presentes.
  • Confirme que as rotas de erro retornam o sinal correto — uma visualização de “não encontrado” deve redirecionar para um status de erro real ou carregar noindex no DOM renderizado.

Mitos comuns sobre SEO de SPA

  • “O Google não consegue indexar SPAs de forma alguma.” Desatualizado. O Google executa Chromium evergreen e geralmente renderiza conteúdo de SPA. Os riscos reais são específicos: soft 404s, roteamento por hash, timeouts de renderização e rastreadores que não são do Google (Bing com menos confiabilidade, a maioria dos rastreadores de IA nem sequer).
  • “Adicionar a History API corrige o SEO de SPA.” Isso corrige apenas a endereçabilidade. Se o servidor ainda retorna o mesmo shell para cada rota, o Google ainda precisa executar JS para ver qualquer coisa.
  • “O roteamento por hash ainda funciona como fallback.” Descontinuado desde 2015 e recomendado contra desde então.
  • “Renderização no cliente é uma penalidade de ranqueamento.” Não existe penalidade direta de CSR. O dano é indireto — renderização falha/atrasada, soft 404s e agrupamento duplicado de timeouts de renderização reduzem o que é indexado.
  • “Pré-renderização para bots é cloaking.” Não quando o conteúdo corresponde ao que os usuários eventualmente veem — apenas o quando/onde da renderização difere, não o conteúdo.
  • “Você precisa de um gerador de sitemap especial para SPA.” Não existe formato especial; o trabalho é fazer cada rota listada resolver para HTML real.

Perguntas frequentes

O Google consegue indexar uma aplicação de página única? Sim, se cada rota resolver para HTML real e único (via SSR/pré-renderização) em uma URL real. SPAs apenas com cliente muitas vezes não conseguem.

Preciso de SSR, ou a renderização no cliente é aceitável em algum caso? CSR pode funcionar para conteúdo que não precisa ranquear, mas para qualquer coisa que você queira indexar de forma confiável, pré-renderize ou use SSR — não aposte que o renderizador executará seu JS a tempo.

O roteamento por hash (#!) é ruim para SEO? Sim — o fragmento nunca chega ao servidor, então não é possível resolver essas URLs de forma confiável. Use a History API.

Toda rota precisa de sua própria tag canônica? Sim, no DOM renderizado — e certifique-se que nada mais restritivo (um noindex perdido ou canônico errado) seja enviado no shell bruto.

Um serviço de pré-renderização é cloaking? Não, desde que o conteúdo servido aos bots corresponda ao que os usuários veem.

Devo usar um meta-framework em vez de construir o roteamento eu mesmo? Para um novo build, geralmente sim — Next.js/Nuxt/Angular-SSR/SvelteKit oferecem SSR/SSG e metadados por rota de graça.

Por que meu SPA retorna 200 para páginas que não existem? Porque o roteador no cliente mantém o status 200 original para navegações virtuais. Corrija com um redirecionamento JS para um status de erro real ou um noindex renderizado.

Add an expert note

Pin an expert quote

New person? Create their unclaimed profile at /admin/experts/ → Pin a quote first.