schema-markup-at-scale-pseo.html
< BACK Corredor de longa sala de servidores com painéis LED piscantes, estética cinematográfica 35mm com tons azuis e âmbar

Schema Markup em Larga Escala: JSON-LD para 91.000 Páginas

Em 2021, um cliente de viagens entregou para a Seahawk um briefing de migração que fez meu estômago cair um pouco. Noventa e um mil páginas de destinos e hotéis. Cada uma precisava de markup de schema válido, específico e testado — não o tipo WebPage preguiçoso genérico que a maioria dos plugins coloca e chama de feito. O cliente já tinha testado dois plugins WordPress de "schema automático". Ambos tinham produzido JSON-LD tecnicamente válido que também era, em todo sentido significativo, inútil — nomes genéricos, sem entidades aninhadas, preços ausentes, agregados de reviews apontando para a coisa errada. O Rich Results Test do Google estava polidamente confuso.tested schema markup -- not the lazy one-size-fits-all WebPage type that most plugins slap on and call it a day. The client had already tried two "automatic schema" WordPress plugins. Both had produced technically valid JSON-LD that was also, in every meaningful sense, useless -- generic names, no nested entities, prices missing, review aggregates pointing at the wrong thing. Google's Rich Results Test was politely confused.

Conclusão-chave: Schema para 91 mil páginas é um problema de arquitetura, não de plugin: gere-o a partir da camada de dados no tempo de build e valide-o no pipeline.Schema for 91,000 pages is an architecture problem, not a plugin problem: generate it from the data layer at build time and validate it in the pipeline.

Esse projeto me ensinou mais sobre schema em larga escala do que os oito anos anteriores combinados. Então aqui está o que eu realmente sei.

---

Por Que "Apenas Instale um Plugin" Quebra em Larga Escala

Olha, não estou aqui para criticar Yoast ou Rank Math. Para um site de 40 páginas eles são genuinamente adequados. Mas em algum lugar perto da marca de 500 páginas, o schema gerado por plugin começa a ceder sob suas próprias suposições.

O problema central é que plugins são construídos em torno de templates de página, não de modelos de dados. Eles leem o título do post, talvez um ou dois campos customizados, e constroem um blob de schema. Quando seu site tem 91 mil páginas em seis tipos de conteúdo — hotéis, destinos, tours, reviews, FAQs e perfis de autores — uma única configuração de plugin não consegue expressar essa variedade sem um trabalho enorme de override manual. E se você está fazendo overrides manuais nessa escala, você já perdeu.page templates, not data models. They read the post title, maybe a custom field or two, and construct a schema blob. When your site has 91,000 pages across six content types -- hotels, destinations, tours, reviews, FAQs, and author profiles -- a single plugin configuration cannot express that variety without enormous manual override work. And if you're doing manual overrides at that scale, you've already lost.

Aqui está o ponto: schema markup é fundamentalmente um problema de data transformation. Você tem dados estruturados em um banco de dados; você precisa deles expressos como JSON-LD em uma <script>tag. É isso. No momento em que você enquadra assim, a arquitetura correta fica muito mais clara.<script>tag. That's it. The moment you frame it that way, the right architecture becomes much clearer.

Os Três Failure Modes Que Continuo Vendo

  • Blobs de schema estáticos hardcoded em templates. Tudo bem até o nome do produto mudar, aí você tem 12 mil páginas mentindo para o Google. hardcoded in templates. Fine until the product name changes, then you've got 12,000 pages lying to Google.
  • Configs de plugin que não conseguem lidar com lógica condicional — como mostrar aggregateRating apenas quando há reviews de verdade, ou diferentes @type por categoria de post. that can't handle conditional logic -- like only showing aggregateRating when there are actually reviews, or different@type per post category.
  • Arquivos gerados em lote, enviados uma vez e nunca atualizados. Já auditei sites onde o schema estava dezoito meses desatualizado. Os preços estavam errados. As datas dos eventos já tinham passado. uploaded once and never updated. I've audited sites where the schema was eighteen months stale. The prices were wrong. The event dates had passed.

---

Como JSON-LD Realmente Funciona em Escala

Antes de entrar em tooling: um rápido contexto. JSON-LD — JSON for Linked Data — é o formato de schema preferido do Google precisamente porque vive em um bloco <script>, separado do seu HTML. Isso significa que você pode gerá-lo server-side, injetá-lo de forma limpa e atualizá-lo sem tocar na markup. Essa separação é tudo quando você está lidando com dezenas de milhares de páginas.JSON-LD -- JSON for Linked Data -- is Google's preferred schema format precisely because it lives in a<script>block, separate from your HTML. That means you can generate it server-side, inject it cleanly, and update it without touching markup. That separation is everything when you're dealing with tens of thousands of pages.

O vocabulário Schema.org é vasto. A maioria das pessoas usa cerca de 1% dele. Em escala você precisa ir mais fundo — Hotel, TouristDestination, LocalBusiness, Review, AggregateRating, objetos Offer aninhados, BreadcrumbList. Cada tipo tem propriedades obrigatórias e recomendadas, e a interpretação do Google sobre "recomendado" é basicamente "obrigatório se você quer o rich result".Schema.org vocabulary is vast. Most people use about 1% of it. At scale you need to go deeper -- Hotel,TouristDestination,LocalBusiness,Review,AggregateRating, nested Offer objects,BreadcrumbList. Each type has required and recommended properties, and Google's interpretation of "recommended" is basically "required if you want the rich result."

A regra fundamental com a qual trabalho: um `@type` primário por página, com tipos aninhados conforme necessário. Não coloque cinco @type values esperando que um grudar. Escolha o tipo mais específico que se encaixa, depois aninhe tipos de suporte dentro dele.one primary `@type` per page, with nested types as needed.Don't stack five@type values hoping one sticks. Pick the most specific type that fits, then nest supporting types inside it.

---

A Arquitetura Que Realmente Usamos

Para o cliente de viagens, acabamos com um sistema de três camadas. Não elegante em termos de diagrama de quadro branco, mas funcionou.

Camada 1: Classes de Schema no Nível de Template (PHP)

Cada tipo de conteúdo ganhou sua própria classe PHP responsável por construir seu array de schema. HotelSchemaBuilder, DestinationSchemaBuilder, TourSchemaBuilder — você entende o padrão. Cada classe puxava de campos customizados ACF Pro, dados do WooCommerce onde aplicável, e alguns valores computados (como calcular aggregateRating a partir de um sistema de reviews baseado em CPT).HotelSchemaBuilder,DestinationSchemaBuilder,TourSchemaBuilder -- you get the idea. Each class pulled from ACF Pro custom fields, WooCommerce data where applicable, and a few computed values (like calculating aggregateRating from a CPT-based review system).

A saída de cada classe era um array PHP simples. Sem JSON ainda. Apenas dados.

Isso importa porque significa que você consegue fazer unit test da lógica de dados separadamente da serialização. Eu gostaria de ter feito isso desde o primeiro dia desse projeto. Não fiz. Isso nos custou cerca de dois dias de debugging em staging quando ratingValue estava retornando uma string em vez de um float e o validador do Google estava silenciosamente ignorando o bloco aggregateRating inteiro.ratingValue was returning a string instead of a float and Google's validator was silently ignoring the whole aggregateRating block.

Camada 2: Um Gerenciador de Schema Central

Uma única classe SchemaManager, hookada em wp_head, era responsável por:SchemaManager class, hooked into wp_head, was responsible for:

  1. Determinar qual classe de builder invocar com base no template/tipo de post atual
  2. Mesclando entidades em todo o site (o gráfico Organization, WebSite com SearchAction, BreadcrumbList)Organization graph,WebSite with SearchAction,BreadcrumbList)
  3. Codificando o array final como JSON com JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODEJSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
  4. Envolvendo-o em uma tag <script type="application/ld+json"> e fazendo echo<script type="application/ld+json">tag and echoing it

A lógica de breadcrumb foi a parte mais complicada. Os destinos tinham uma hierarquia de três níveis: Region → Country → City. Fazer o BreadcrumbList refletir isso dinamicamente, sem hardcoding, significava percorrer ancestrais de post no tempo de renderização. Lento, se você não tomar cuidado. Cacheavamos os arrays de breadcrumb por ID de post em um transient com TTL de 24 horas. Isso reduziu a sobrecarga a negligenciável.BreadcrumbList to reflect that dynamically, without hardcoding anything, meant traversing post ancestors at render time. Slow, if you're not careful. We cached the breadcrumb arrays per post ID in a transient with a 24-hour TTL. That brought the overhead down to negligible.

Camada 3: Validação e Monitoramento

Gerar schema é o primeiro passo. Saber quando ele quebra é o segundo, e a maioria dos times pula isso inteiramente.

Configuramos uma propriedade no Google Search Console e observamos o relatório de Rich Results semanalmente. Mas isso é reativo — o GSC te diz sobre erros depois que o Google já rastreou a página. Para verificações proativas, rodávamos SchemaApp em um crawl das 2 mil páginas mais importantes mensalmente. Isso evidencia erros em nível de propriedade que o relatório do GSC obscurece.after Google has crawled the page. For proactive checks, we ran SchemaApp on a crawl of the top 2,000 pages monthly. It surfaces property-level errors that the GSC report obscures.

Além disso: o Rich Results Test do Google tem uma API. Escrevemos um pequeno script que acessaria a API com uma amostra aleatória de 50 URLs todas as noites e registraria qualquer falha de validação. Seguro barato.Google's Rich Results Test has an API. We wrote a small script that would hit the API with a random sample of 50 URLs nightly and log any validation failures. Cheap insurance.

---

Tratando Dados Dinâmicos Sem Matar o Desempenho

É aqui que a maioria das implementações em escala desaba. Schema que referencia dados live -- preço, disponibilidade, contagem de reviews -- tem que estar sempre fresco. Mas regenerar JSON-LD em cada carregamento de página para 91.000 páginas não é de graça.

Minha abordagem, e refini isso em talvez uma dúzia de sites grandes desde então:

Cache agressivamente, invalide inteligentemente.

Para páginas de hotel, o schema blob era armazenado como post meta -- uma string JSON-LD serializada -- e regenerado apenas quando:

  • O post em si era atualizado
  • Uma nova avaliação era submetida para esse post
  • O campo customizado de preço foi alterado (conectavamos isso à ação save_post do ACF para isso)save_post action for this)

Tudo o mais servia a string cacheada. Rápido demais. E porque os hooks de invalidação eram específicos, o schema permanecia preciso.

Uma coisa que cometi errado inicialmente: cacheavava a tag <script> completa, incluindo os elementos de abertura e fechamento. Depois precisavamos mudar a URL @context para um tipo de conteúdo. Tivemos que invalidar todo cache. Agora cacheo apenas a string JSON e a envuelvo no tempo de renderização. Cinco minutos de código extra, economizaram uma hora de confusão.<script>tag, including the opening and closing elements. Then we needed to change the@context URL for one content type. Had to bust every cache entry. Now I cache only the JSON string and wrap it at render time. Five minutes of extra code, saved an hour of head-scratching.

E quanto aos preços em tempo real?

Para preços de tour que mudavam várias vezes por dia, adotavamos uma abordagem diferente. O schema base era cacheado, mas o bloco Offer era gerado novo a cada requisição e mesclado antes da serialização. Sim, adicionava uma pequena sobrecarga por requisição. Mas era uma query de banco de dados por carregamento de página, não doze. Tradeoff aceitável.Offer block was generated fresh at request time and merged in before serialisation. Yes, it added a small overhead per request. But it was one database query per page load, not twelve. Acceptable trade-off.

---

Escalabilidade para Múltiplos Sites: A Perspectiva Seahawk

Seahawk construiu mais de 12.000 sites, e implementação de schema aparece em uma parcela significativa deles. O cliente de viagens foi um caso extremo. Mas os mesmos princípios arquiteturais se aplicam se você está fazendo 91.000 páginas ou 4.000.

O padrão reutilizável que cheguei a adotar é um pequeno plugin WordPress interno -- chamamos de seahawk-schema-core -- que fornece o scaffolding do manager/builder sem nenhuma lógica específica de tipo de conteúdo. Projetos de clientes o estendem com suas próprias builder classes. Sem dependências de plugin para a lógica principal de schema. Sem risco de uma atualização de plugin de terceiros quebrar a presença de rich results inteira do site.seahawk-schema-core -- that provides the manager/builder scaffolding without any content-type-specific logic. Client projects extend it with their own builder classes. No plugin dependencies for the core schema logic. No risk of a third-party plugin update blowing up a site's entire rich results presence.

Esse último ponto é mais real do que as pessoas admitem. Já vi atualizações do Rank Math quebrarem silenciosamente custom schema overrides. Não porque Rank Math é ruim -- não é -- mas porque quando você está customizando output no nível que um site grande requer, você está operando fora do que o plugin foi projetado para lidar. Seja dono do código, seja dono do perfil de risco.

---

Testes Nessa Escala: Um Checklist Prático

Você não pode testar manualmente 91.000 URLs. Então você testa inteligentemente.

  1. Amostra por tipo de template. Pegue 10 URLs por tipo de conteúdo. Teste essas. Se o builder está correto para uma página de hotel, está correto para todas as 3.000 páginas de hotel (a menos que tenha dados ruins -- mais sobre isso abaixo).Pick 10 URLs per content type. Test those. If the builder is correct for one hotel page, it's correct for all 3,000 hotel pages (unless there's bad data -- more on that below).
  2. Teste casos extremos especificamente. Páginas sem avaliações. Páginas com campos customizados incompletos. Páginas com caracteres especiais em títulos (&,", caracteres acentuados). A serialização JSON consome muitos desses, mas nem todos.Pages with no reviews. Pages with incomplete custom fields. Pages with special characters in titles (&,", accented characters). JSON serialisation eats a lot of these, but not all of them.
  3. Execute um crawl completo de dados estruturados com Screaming Frog. O Screaming Frog SEO Spider tem um modo de extração de dados estruturados que vai puxar e validar JSON-LD de todas as URLs que ele rastreia. Exporte os erros, agrupe por tipo de template, corrija na fonte.The Screaming Frog SEO Spider has a structured data extraction mode that'll pull and validate JSON-LD from every URL it crawls. Export the errors, group by template type, fix at the source.
  4. Monitore a aba Enhancements do GSC. Configure um alerta de threshold -- se itens válidos caem mais de 5% semana a semana, algo quebrou. Aja dentro de 48 horas.Set a threshold alert -- if valid items drop by more than 5% week-over-week, something broke. Act within 48 hours.
  5. Spot-check após cada deployment. Mesmo se o código de schema não mudou. Database migrations, atualizações de plugin, mudanças de tema -- qualquer uma delas pode introduzir problemas de dados upstream que corrompem a saída de schema.Even if the schema code didn't change. Database migrations, plugin updates, theme changes -- any of them can introduce upstream data issues that corrupt schema output.

Dados Ruins São o Assassino Silencioso

O site de travel tinha um content team de doze pessoas em três países. Algumas destination pages tinham HTML malformado no campo description -- colado do Word, presumivelmente. Quando esse campo alimentava a propriedade schema description, o JSON era tecnicamente válido mas a description incluía entidades&nbsp;e tags<span>soltas. Google ignorou a propriedade. Adicionamos um passo de sanitisation em toda builder class que remove tags e decodifica HTML entities antes do valor chegar ao array de schema. Resolveu permanentemente.description field -- pasted from Word, presumably. When that field fed into the schema description property, the JSON was technically valid but the description included&nbsp;entities and stray<span>tags. Google ignored the property. We added a sanitisation step in every builder class that strips tags and decodes HTML entities before the value hits the schema array. Solved it permanently.

---

O Entity Graph: Não Ignore

Uma coisa que separa um trabalho medíocre de schema de um technical SEO genuinamente bom é o entity graph -- especificamente, as entidades sitewide Organization e WebSite que devem aparecer em toda página e linkar tudo junto.Organization and WebSite entities that should appear on every page and link everything together.

A maioria dos sites tem essas, mal feitas. Name, URL, talvez um logo. O tipo Organization completo suporta links sameAs para sua entrada Wikidata, perfis sociais e outras fontes autoritárias. Essa interligação cruzada é como Google constrói confiança de que sua entidade Organization no seu Knowledge Graph é a mesma entidade aparecendo no seu page schema.Organization type supports sameAs links to your Wikidata entry, social profiles, and other authoritative sources. That cross-linking is how Google builds confidence that your Organization entity in its Knowledge Graph is the same entity appearing in your page schema.

Para o cliente de viagens, construímos o bloco Organization com:Organization block with:

  • sameAs apontando para o perfil Crunchbase deles, página LinkedIn e um stub Wikipedia que tinham pointing to their Crunchbase profile, LinkedIn page, and a Wikipedia stub they had
  • contactPoint com informações estruturadas de telefone e departamento with structured phone and department info
  • foundingDate e numberOfEmployees (range aproximada -- essa é informação pública de qualquer forma) and numberOfEmployees(rough range -- this is public info anyway)

Isso moveu rankings da noite para o dia? Não. Schema quase nunca faz isso isoladamente. Mas é infraestrutura. Você constrói uma vez, corretamente, e isso se agrega com o tempo.

---

FAQ

Quanto tempo leva para implementar schema nessa escala?

Para o site de viagens de 91 mil páginas, a implementação completa -- arquitetura, classes do builder, camada de cache, testes, configuração de monitoramento do GSC -- levou cerca de seis semanas com dois desenvolvedores. Parece bastante. Mas metade desse tempo foi auditoria da qualidade de dados existentes, não escrita de código de schema. Se seus dados estão limpos, você consegue se mover mais rápido.

Devo usar um plugin ou construir algo customizado para sites grandes?

Para qualquer coisa com menos de algumas centenas de páginas, um plugin é genuinamente adequado. O módulo de schema do Rank Math é sólido e o bloco de schema customizado te dá flexibilidade razoável. Acima de alguns milhares de páginas com múltiplos tipos de conteúdo distintos, eu construiria customizado toda vez. O controle vale o custo de desenvolvimento.

Qual é o erro de schema mais comum em escala?

Falta de aggregateRating quando avaliações existem -- ou incluir quando não existem. Google é rigoroso com isso. Se seu schema afirma um aggregateRating de 4,7 de 843 avaliações e um usuário chega na página e não vê avaliações, isso é uma ação manual esperando para acontecer. Lógica condicional nas suas classes do builder é inegociável.aggregateRating when reviews exist -- or including it when they don't. Google is strict about this. If your schema claims an aggregateRating of 4.7 from 843 reviews and a user lands on the page and sees no reviews, that's a manual action waiting to happen. Conditional logic in your builder classes is non-negotiable.

Schema melhora rankings diretamente?

Diretamente? Provavelmente não muito para a maioria dos tipos de consulta. O que faz é desbloquear rich results -- estrelas de classificação, dropdowns de FAQ, snippets de avaliações, breadcrumbs no SERP -- e essas features melhoram click-through rates de forma mensurável. O cliente de viagens viu um aumento de CTR de 22% em páginas de hotel em quatro meses de implementação completa. Isso alimenta sinais de engajamento, que afetam rankings. Então: indiretamente, sim. Substancialmente.

Quais ferramentas você realmente usa dia a dia para trabalho com schema?

Screaming Frog para auditoria em nível de crawl. Google's Rich Results Test para spot-checks. Schema Markup Validator em validator.schema.org para validação em nível de propriedade. E honestamente, a documentação do Schema.org em si -- tenho a página do tipo Hotel e um punhado de outras marcadas e consulto constantemente. Nenhuma ferramenta de subscrição sofisticada necessária.validator.schema.org for property-level validation. And honestly, the Schema.org documentation itself -- I have the Hotel type page and a handful of others bookmarked and I refer to them constantly. No fancy subscription tool needed.

---

Schema em escala é um daqueles problemas que parece um problema de plugin até você estar dentro dele e perceber que é na verdade um problema de arquitetura de software disfarçado de roupagem SEO. Acerte o data model. Cache inteligentemente. Valide incansavelmente. O próprio markup é quase a parte fácil.

< BACK