Breadcrumb semântico e acessível com Web Component + JSON-LD — passo a passo
Por John Pires · 20 de março de 2026

Breadcrumb semântico e acessível com Web Component + JSON-LD — passo a passo
Contexto: quando você vai usar isso
Por que breadcrumb importa
Breadcrumbs (migalhas de pão) orientam usuários sobre onde estão dentro da hierarquia de um site. Em sites de documentação, e-commerce ou sites com categorias aninhadas, breadcrumb reduz cliques e ajuda a recuperar contexto. Além disso, um breadcrumb bem implementado melhora a experiência de navegação e contribui para SEO quando refletido corretamente no JSON-LD.
Casos práticos para iniciantes
Use breadcrumb quando sua página fizer sentido dentro de uma hierarquia: por exemplo, Loja > Roupas > Camisetas > Camiseta X. Se a sua aplicação tem rotas aninhadas, breadcrumbs ajudam o usuário a subir na hierarquia sem depender exclusivamente do botão voltar ou do menu principal.
O objetivo deste artigo é mostrar como criar um componente reutilizável, sem depender de frameworks, que gere marcação semântica (nav, ol, li), ofereça acessibilidade (aria-label, aria-current) e publique JSON-LD sincronizado para motores de busca.
Explicação visual com diagramas mentais
Diagrama mental: estrutura HTML visível vs JSON-LD
Imagine duas camadas: a camada visível (HTML) e a camada de SEO (JSON-LD). A camada visível precisa ser acessível — nav com ol e li, links com foco e último item marcado como aria-current='page'. A camada SEO precisa refletir exatamente a mesma ordem e URLs, usando um objeto BreadcrumbList em JSON-LD.
Fluxo de criação do componente
Fluxo simples: 1) Recebe dados (children ou attribute JSON), 2) Renderiza HTML semântico, 3) Injeta/atualiza JSON-LD no <head> ou dentro do componente, 4) Garante acessibilidade e foco. Vamos transformar cada passo em código vivo.
Tip: motores de busca validam e comparam conteúdo visual com JSON-LD. Mantenha-os sincronizados para evitar discrepâncias que penalizem o snippet do site.
Primeiro exemplo simples
O que vamos fazer (visão curta)
Criaremos um Web Component minimal que lê itens passados como elementos filhos (links) e renderiza uma nav com ol. Também geraremos um JSON-LD básico e o injetaremos no document.head. Este exemplo foca em clareza: sem Shadow DOM, para preservar acessibilidade padrão e permitir que o CSS do site estilize o breadcrumb.
Exemplo: componente básico que usa filhos <a>
Contexto: este bloco define o componente. Ele pega todos os elementos <a> dentro do componente e monta uma lista ordenada. Depois cria ou atualiza um <script type='application/ld+json'> no head com o BreadcrumbList correspondente.
<!-- breadcrumb-basic.html: define o componente básico e funcional -->
<script>
class BreadcrumbBasic extends HTMLElement {
connectedCallback() {
// coleta links filhos (usuários colocam <a> como filhos direto)
const links = Array.from(this.querySelectorAll('a'));
// renderiza HTML semântico
const nav = document.createElement('nav');
nav.setAttribute('aria-label', 'Breadcrumb');
const ol = document.createElement('ol');
links.forEach((a, i) => {
const li = document.createElement('li');
if (i === links.length - 1) {
// último item: atual
const span = document.createElement('span');
span.textContent = a.textContent;
span.setAttribute('aria-current', 'page');
li.appendChild(span);
} else {
const clone = a.cloneNode(true);
li.appendChild(clone);
}
ol.appendChild(li);
});
nav.appendChild(ol);
this.innerHTML = '';
this.appendChild(nav);
// Gera JSON-LD e injeta no head
const list = links.map((a, i) => ({
'@type': 'ListItem',
'position': i + 1,
'name': a.textContent.trim(),
'item': a.getAttribute('href')
}));
const scriptId = 'breadcrumb-jsonld-basic';
let script = document.getElementById(scriptId);
const json = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
'itemListElement': list
};
if (!script) {
script = document.createElement('script');
script.id = scriptId;
script.type = 'application/ld+json';
document.head.appendChild(script);
}
script.textContent = JSON.stringify(json);
}
}
customElements.define('breadcrumb-basic', BreadcrumbBasic);
</script>
<!-- uso: coloque links dentro do componente -->
<breadcrumb-basic>
<a href='/loja'>Loja</a>
<a href='/loja/roupas'>Roupas</a>
<a href='/loja/roupas/camisetas'>Camisetas</a>
</breadcrumb-basic>
O que acontece depois: o componente substitui seus filhos por uma nav > ol > li acessível e injeta um script JSON-LD no head com o mesmo conteúdo. Motores de busca poderão ler esse JSON-LD para gerar rich snippets.
Segundo exemplo mais realista
Contexto: integração com rotas e conteúdo dinâmico
Em SPAs ou páginas onde a URL muda via JS (p.ex. pushState), o breadcrumb deve reagir a mudanças de rota. Vamos construir um componente que aceita um atributo data-items='[...]' como JSON ou que observa mudanças no DOM para atualizar JSON-LD dinamicamente. Também vamos preservar foco e permitir estilos via classes.
Exemplo: componente dinâmico que aceita JSON via atributo
Contexto do código: este exemplo mostra como passar dados diretamente como JSON no atributo data-items. O componente analisa o JSON, valida, renderiza a marcação e atualiza o JSON-LD. Suporta atualização via setAttribute e também um método público setItems.
<!-- breadcrumb-dynamic.html: componente que aceita data-items -->
<script>
class BreadcrumbDynamic extends HTMLElement {
constructor() {
super();
this.scriptId = 'breadcrumb-jsonld-dynamic';
}
connectedCallback() {
this.renderFromAttr();
}
static get observedAttributes() { return ['data-items']; }
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'data-items' && oldVal !== newVal) {
this.renderFromAttr();
}
}
setItems(items) { // items: [{name, url}]
this.setAttribute('data-items', JSON.stringify(items));
}
renderFromAttr() {
let items = [];
try {
const raw = this.getAttribute('data-items');
items = raw ? JSON.parse(raw) : [];
} catch (e) {
console.error('data-items must be valid JSON', e);
items = [];
}
const nav = document.createElement('nav');
nav.setAttribute('aria-label', 'Breadcrumb');
const ol = document.createElement('ol');
items.forEach((it, i) => {
const li = document.createElement('li');
if (i === items.length - 1) {
const span = document.createElement('span');
span.textContent = it.name;
span.setAttribute('aria-current', 'page');
li.appendChild(span);
} else {
const a = document.createElement('a');
a.href = it.url;
a.textContent = it.name;
li.appendChild(a);
}
ol.appendChild(li);
});
nav.appendChild(ol);
this.innerHTML = '';
this.appendChild(nav);
// Atualiza JSON-LD
const list = items.map((it, i) => ({
'@type': 'ListItem',
'position': i + 1,
'name': it.name,
'item': it.url
}));
const json = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
'itemListElement': list
};
let script = document.getElementById(this.scriptId);
if (!script) {
script = document.createElement('script');
script.id = this.scriptId;
script.type = 'application/ld+json';
document.head.appendChild(script);
}
script.textContent = JSON.stringify(json);
}
}
customElements.define('breadcrumb-dynamic', BreadcrumbDynamic);
</script>
<!-- uso: diretamente com atributo JSON -->
<breadcrumb-dynamic id='bc'
data-items='[{"name":"Loja","url":"/loja"},{"name":"Roupas","url":"/loja/roupas"},{"name":"Camisetas","url":"/loja/roupas/camisetas"}]'>
</breadcrumb-dynamic>
<script>
// Exemplo de atualização dinâmica (p.ex. após fetch ou rota change)
setTimeout(() => {
const bc = document.getElementById('bc');
bc.setItems([
{ name: 'Loja', url: '/loja' },
{ name: 'Roupas', url: '/loja/roupas' },
{ name: 'Camisetas', url: '/loja/roupas/camisetas' },
{ name: 'Camiseta X', url: window.location.pathname }
]);
}, 2000);
</script>
O que acontece depois: o componente atualiza tanto a interface quanto o JSON-LD quando os dados mudam. Isso mantém a camada visual e a camada de SEO sincronizadas — o ponto crítico para rich snippets confiáveis.
Erros comuns de iniciante e como evitar
Erro 1: HTML visível diferente do JSON-LD
Muitos iniciantes geram um JSON-LD estático e atualizam o breadcrumb visual separadamente, levando a inconsistências. Motores de busca podem ignorar o JSON-LD se ele não corresponder ao que o usuário vê. Sempre derive o JSON-LD a partir dos mesmos dados usados para renderizar o HTML — ou gere os dois a partir de uma única fonte (como em nossos exemplos).
Erro 2: usar elementos não semânticos
Evite checar apenas para aparência. Não use <div> ou <span> sem role apropriado. Use <nav aria-label='Breadcrumb'> e <ol> > <li> para marcar que é uma lista ordenada. O último item deve receber aria-current='page' para indicar posição atual.
Erro 3: esquecer de atualizar JSON-LD em páginas SPA
Em SPAs, a URL muda sem reload. Se você não atualizar o script JSON-LD, os motores de busca verão dados desatualizados. Solução: atualize o conteúdo do <script type='application/ld+json'> ao mudar de rota ou use MutationObserver para detectar mudanças no breadcrumb e sincronizar automaticamente.
Erro 4: JSON inválido ou não escapado
Quando você passa JSON em atributos HTML, caracteres especiais podem quebrar o parser. Use JSON.stringify e atribua programaticamente, ou escape corretamente ao escrever atributos. Evite concatenar strings manualmente.
Dicas rápidas e checklist
- Checklist antes de publicar: HTML semântico presente, último item marcado com
aria-current, JSON-LD refletindo exatamente os mesmos nomes e URLs, script JSON-LD no head ou dentro do componente. - Teste com ferramentas: Rich Results Test do Google para validar JSON-LD, Lighthouse para acessibilidade e validação de navegação.
- Use classes para permitir estilo CSS: o componente não deve impor estilos que quebrem a identidade visual do site.
Prática: sempre compare a saída visível e o JSON-LD com uma inspeção rápida: abra DevTools, verifique o elemento nav e o script application/ld+json — devem expressar a mesma hierarquia.
Conclusão prática
Resumo dos passos práticos
Para entregar um breadcrumb semântico e acessível, siga este fluxo prático: 1) Defina fonte única de dados (children, atributo JSON ou fetch), 2) Renderize nav > ol > li com aria-current no último item, 3) Gere JSON-LD a partir dos mesmos dados e injete/atualize no head, 4) Garanta atualizações em SPAs e trate erros de parsing.
Próximos passos e evolução
Depois de dominar o componente básico, evolua para: suporte a microdados ou RDFa se o projeto pedir, adicionar testes automatizados que validem JSON-LD, e criar variações com Shadow DOM para encapsular estilos (mas lembre-se de validar acessibilidade quando usar Shadow DOM).
Este artigo teve foco em mostrar código funcional e padrões práticos. Reaplique os exemplos no seu projeto, comece pelo componente básico e avance para o dinâmico conforme o seu fluxo de rotas. Monitoramento prático: rode o Rich Results Test e Lighthouse depois das alterações no componente.
Seja explícito: crie testes de integração que verifiquem que o script JSON-LD contém a mesma lista de URLs visíveis. Isso evita regressões e garante que seu breadcrumb cumpra tanto a função de navegação quanto de SEO.
Boa prática final: mantenha o componente pequeno, com API clara (por exemplo: aceitar filhos <a> e/ou atributo data-items) e documentação curta para outros devs do time.
Assuntos: html-semantic, web-components, acessibilidade, seo, json-ld
Leia também
Modal acessível com <dialog>: semântica, foco e fallback passo a passo
Aprenda a criar um modal acessível com <dialog>: semântica correta, gerenciamento de foco, retorno do foco e fallback para navegadores sem suporte. Tutorial…
Skip links e landmarks: navegação por teclado acessível
Aprenda, passo a passo, a implementar skip links e landmarks para tornar páginas HTML navegáveis por teclado. Técnicas simples, exemplos funcionais e…
Validação de formulários acessível com ARIA: role=alert e live regions
Como tornar validação de formulários acessível usando role=alert e live regions: exemplos práticos, padrões simples, foco no primeiro erro e armadilhas a…