Destino Dev

Blog

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