Pular para o conteúdo
Navegar na documentação
Nesta página

Adicionar interações estáticas

BlogBlade gera HTML estático. Interações opcionais podem ser adicionadas com um arquivo JavaScript em Content/assets/js/, sem introduzir servidor, framework ou um runtime obrigatório. O conteúdo e os controles devem continuar compreensíveis quando JavaScript estiver desabilitado.

Convenção de carregamento

Crie um arquivo, por exemplo Content/assets/js/interactions.js. O build publica o arquivo com hash; nunca escreva o nome final diretamente no template:

@if ($assets->has('js/interactions.js'))
    <script type="module" src="{{ $siteUrl->asset($assets->path('js/interactions.js')) }}"></script>
@endif

Para manter o comportamento previsível, cada receita deve limitar sua busca a um elemento raiz ([data-interaction]), registrar listeners de forma idempotente e devolver uma função de limpeza quando for inicializada novamente. Um módulo pequeno pode expor este formato:

export function init(root = document) {
  // listeners e estado da interação
  return () => {
    // remove listeners e desfaz estado temporário
  };
}

const cleanup = init();

Não é necessário combinar todas as receitas em um único módulo. Use arquivos separados quando páginas diferentes precisarem de interações diferentes.

Tema claro e escuro

O HTML deve declarar uma preferência inicial e oferecer um botão que funcione com teclado. O estado pode ser persistido em localStorage, mas a página não deve depender dele para renderizar:

<button type="button" data-theme-toggle aria-pressed="false">
  Alternar tema
</button>
const button = document.querySelector('[data-theme-toggle]');
if (button) {
  const apply = (theme) => {
    document.documentElement.dataset.theme = theme;
    button.setAttribute('aria-pressed', String(theme === 'dark'));
  };
  const stored = localStorage.getItem('theme');
  apply(stored || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'));
  button.addEventListener('click', () => {
    const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
    localStorage.setItem('theme', next);
    apply(next);
  });
}

Defina as cores para [data-theme="dark"] no CSS e preserve contraste. O conteúdo continua disponível se o script não carregar; somente a preferência manual deixa de ser aplicada.

Sumário navegável

Para uma página com títulos, gere no Blade uma lista de links para IDs estáveis. O navegador já oferece a navegação sem JavaScript:

<nav aria-label="Sumário">
    <ol>
        @foreach ($documentHeadings as $heading)
            <li><a href="#{{ $heading->id }}">{{ $heading->text }}</a></li>
        @endforeach
    </ol>
</nav>

Use JavaScript apenas para aprimoramentos, como destacar o título visível com IntersectionObserver. Não esconda títulos nem impeça que os links âncora funcionem quando JavaScript estiver desabilitado.

Filtros client-side simples

Renderize todos os itens no HTML e acrescente atributos de dados ao critério:

<label>Filtrar <input type="search" data-filter-input="posts"></label>
<ul data-filter-list="posts">
  <li data-filter-value="blade,mdx"><a href="/postagens/blade/">Blade no conteúdo</a></li>
</ul>
<p data-filter-empty="posts" hidden>Nenhum resultado.</p>

O script pode alternar hidden nos itens e na mensagem vazia. O filtro é um atalho: sem JavaScript, a lista completa permanece visível e utilizável. Use aria-live="polite" para anunciar a quantidade filtrada se isso for relevante.

Use um botão explícito, com texto que descreva a ação, e mantenha uma alternativa manual (window.prompt ou o endereço visível) quando a Clipboard API não estiver disponível:

<button type="button" data-copy-link="/artigos/exemplo/">
  Copiar link
</button>
<span data-copy-status aria-live="polite"></span>
document.querySelectorAll('[data-copy-link]').forEach((button) => {
  button.addEventListener('click', async () => {
    const url = new URL(button.dataset.copyLink, window.location.href).href;
    const status = document.querySelector('[data-copy-status]');
    try {
      await navigator.clipboard.writeText(url);
      if (status) status.textContent = 'Link copiado.';
    } catch {
      window.prompt('Copie este link:', url);
    }
  });
});

O botão é um aprimoramento; ele não deve ser a única forma de descobrir ou compartilhar a URL. Em páginas sensíveis, considere também o contexto seguro exigido pela Clipboard API e não anuncie sucesso antes da operação terminar.

Checklist de acessibilidade

  • Prefira elementos semânticos (button, nav, label) a div clicáveis.
  • Garanta foco visível e operação completa pelo teclado.
  • Use aria-live somente para mudanças de estado relevantes e breves.
  • Respeite prefers-reduced-motion em transições e rolagens suaves.
  • Teste cada página com JavaScript desabilitado: conteúdo, links e formulários ainda devem funcionar.

Consulte Gerenciar assets para a publicação com hash e Blade no Pocket Banana para as variáveis $assets e $siteUrl.