Auditor de Hierarquia de Títulos

Audita a estrutura de títulos a partir de uma URL ou HTML colado e detecta saltos de nível, múltiplos h1, uso visual indevido de headings e desalinhamento com títulos meta

Cole HTML ou forneça uma URL para inspecionar a semântica dos títulos em vez da ordem visual. O relatório mostra saltos de nível, múltiplos h1, headings usados só para estilo, headings vazios e se o h1 principal se afasta do e do og:title.</p> <p>Como usar:</p> <ul> <li>Entrada HTML: cole a marcação para uma análise estável</li> <li>URL da página: inspeciona a estrutura publicada</li> <li>Comparar com títulos meta: confronta o heading principal com <code><title></code> e <code>og:title</code></li> <li>Mostrar sugestões: adiciona um nível recomendado ou substituição por nó</li> </ul> <p>O que o relatório destaca:</p> <ul> <li>Árvore visual de h1-h6</li> <li>Distribuição de severidade em estilo heatmap</li> <li>Nível sugerido para cada heading</li> <li>Múltiplos h1 e saltos de nível</li> <li>Rótulos de UI disfarçados de heading</li> <li>Desalinhamento entre h1 e títulos meta</li> </ul></div> </div> <form id="tool-form" class="space-y-6"> <div class="mt-6 p-4 bg-amber-50 border border-amber-200 rounded-lg"> <div class="flex items-center justify-between mb-3"> <h3 class="text-sm font-medium text-amber-900 flex items-center gap-2"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/> </svg> Exemplos de resultados </h3> <span class="text-xs text-amber-700">1 Exemplos</span> </div> <div class="space-y-3"> <div class="example-card bg-white p-3 rounded-md border border-amber-100 hover:border-amber-300 transition-colors"> <div class="flex items-start justify-between mb-2"> <div class="flex-1"> <h4 class="text-sm font-medium text-gray-900">Auditar uma landing com saltos de nível e h1 duplicados</h4> <p class="text-xs text-gray-500 mt-1">Mostra a árvore de títulos, detecta h1 duplicados e indica quais nós deveriam virar h2 ou texto não estrutural.</p> </div> <button type="button" class="load-example-btn ml-2 px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-md transition-colors" data-example-index="0" > Carregar exemplo </button> </div> <!-- 输出预览 --> <div class="example-output-preview mt-2 pt-2 border-t border-gray-100"> <div class="text-xs bg-gray-50 p-2 rounded max-h-20 overflow-auto"><div>Heading hierarchy tree</div></div> </div> <!-- 输入参数展示(可折叠) --> <details class="mt-2"> <summary class="text-xs text-gray-500 cursor-pointer hover:text-gray-700"> Ver parâmetros de entrada </summary> <div class="mt-2 p-2 bg-gray-50 rounded text-xs font-mono"> { "htmlInput": "<html><head><title>Acme Analytics Dashboard</title><meta property=\"og:title\" content=\"Acme Analytics Platform\" /></head><body><main><h1>Acme Analytics</h1><section><h3>Executive summary</h3><div class=\"card\"><a href=\"/demo\"><h2 class=\"btn-title\">BOOK DEMO</h2></a></div><h1>Pricing</h1><h4></h4></section></main></body></html>", "pageUrl": "", "compareWithMetadataTitles": true, "showFixSuggestions": true } </div> </details> </div> </div> </div> <div class="space-y-4 p-4 bg-blue-50 rounded-lg"><div class="space-y-2"><label for="htmlInput" class="block text-sm font-medium text-gray-700"> Entrada HTML </label><textarea id="htmlInput" name="htmlInput" rows="4" placeholder="<main><h1>Pricing</h1><h3>Plans</h3><h2>FAQ</h2></main>" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 min-h-32" ></textarea></div></div><div class="grid grid-cols-1 md:grid-cols-2 gap-4"><div class="space-y-2"><label for="pageUrl" class="block text-sm font-medium text-gray-700"> URL da pagina </label><input type="text" id="pageUrl" name="pageUrl" value="" placeholder="https://example.com" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" ></div><div class="space-y-2"><label for="compareWithMetadataTitles" class="block text-sm font-medium text-gray-700"> Comparar com títulos meta </label><input type="checkbox" id="compareWithMetadataTitles" name="compareWithMetadataTitles" checked class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500" ></div><div class="space-y-2"><label for="showFixSuggestions" class="block text-sm font-medium text-gray-700"> Mostrar correcoes </label><input type="checkbox" id="showFixSuggestions" name="showFixSuggestions" checked class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500" ></div></div> <div> <button type="submit" id="submit-button" class="w-full px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50"> Processar </button> </div> </form> <div id="tool-result"></div> </div> <article class="bg-white rounded-lg shadow-sm p-6 mt-6" itemscope itemtype="https://schema.org/SoftwareApplication"> <nav aria-label="Breadcrumb" class="mb-4 text-sm text-gray-500"> <ol class="flex flex-wrap items-center gap-2"> <li class="flex items-center gap-2"><a href="/pt" class="hover:text-blue-600">Home</a></li><li class="flex items-center gap-2"><span>/</span><a href="/pt/tools" class="hover:text-blue-600">Tools</a></li><li class="flex items-center gap-2"><span>/</span><span class="text-gray-700">Heading Hierarchy Auditor</span></li> </ol> </nav> <div class="space-y-8"> <section aria-labelledby="tool-facts-title" class="rounded-xl border border-slate-200 bg-slate-50 p-5"> <h2 id="tool-facts-title" class="text-base font-semibold text-gray-900 mb-4">Fatos principais</h2> <dl class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div> <dt class="text-xs uppercase tracking-wide text-gray-500">Categoria</dt> <dd class="mt-1 text-sm font-medium text-gray-900">Segurança e validação</dd> </div> <div> <dt class="text-xs uppercase tracking-wide text-gray-500">Tipos de entrada</dt> <dd class="mt-1 text-sm font-medium text-gray-900">textarea, text, checkbox</dd> </div> <div> <dt class="text-xs uppercase tracking-wide text-gray-500">Tipo de saída</dt> <dd class="mt-1 text-sm font-medium text-gray-900">html</dd> </div> <div> <dt class="text-xs uppercase tracking-wide text-gray-500">Cobertura de amostras</dt> <dd class="mt-1 text-sm font-medium text-gray-900">4</dd> </div> <div> <dt class="text-xs uppercase tracking-wide text-gray-500">API disponível</dt> <dd class="mt-1 text-sm font-medium text-gray-900">Yes</dd> </div> </dl> </section> <section id="tool-overview" aria-labelledby="tool-overview-title"> <h2 id="tool-overview-title" class="text-lg font-semibold text-gray-900 mb-3">Visão geral</h2> <p class="text-gray-700 leading-7" itemprop="description">O Auditor de Hierarquia de Títulos analisa a estrutura semântica de tags H1-H6 de uma página web a partir de um código HTML colado ou de uma URL ativa, identificando erros de SEO e acessibilidade como saltos de nível, múltiplos H1 e desalinhamento com metadados.</p> </section> <section class="grid grid-cols-1 md:grid-cols-2 gap-6" aria-label="Usage guidance"> <section aria-labelledby="tool-when-title"> <h3 id="tool-when-title" class="text-base font-semibold text-gray-900 mb-3">Quando usar</h3> <ul class="space-y-2 text-sm text-gray-700"> <li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Durante a auditoria de SEO técnico de um site para garantir que a estrutura de cabeçalhos seja lógica e legível para rastreadores.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Ao validar a acessibilidade de novos templates de página antes de publicá-los em produção.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Para verificar se os títulos principais (H1) estão alinhados com as tags de título e Open Graph da página.</span></li> </ul> </section> <section aria-labelledby="tool-how-title"> <h3 id="tool-how-title" class="text-base font-semibold text-gray-900 mb-3">Como funciona</h3> <ul class="space-y-2 text-sm text-gray-700"> <li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Insira o código HTML bruto no campo de texto ou informe a URL da página que deseja analisar.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Ative as opções para comparar o H1 com os títulos meta (title e og:title) e para exibir sugestões de correção.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Execute a auditoria para gerar uma árvore visual interativa com a distribuição de severidade dos erros encontrados.</span></li> </ul> </section> </section> <section id="tool-use-cases" aria-labelledby="tool-use-cases-title"> <h3 id="tool-use-cases-title" class="text-base font-semibold text-gray-900 mb-3">Casos de uso</h3> <div class="grid grid-cols-1 md:grid-cols-3 gap-3"> <div class="rounded-lg bg-slate-50 border border-slate-200 p-4 text-sm text-gray-700">Identificação de saltos de nível e tags H1 duplicadas em landing pages.</div><div class="rounded-lg bg-slate-50 border border-slate-200 p-4 text-sm text-gray-700">Auditoria rápida de páginas publicadas inserindo diretamente a URL do site.</div><div class="rounded-lg bg-slate-50 border border-slate-200 p-4 text-sm text-gray-700">Correção de elementos de interface (UI) que foram marcados incorretamente como títulos.</div> </div> </section> <section id="tool-examples" aria-labelledby="tool-examples-title"> <h3 id="tool-examples-title" class="text-base font-semibold text-gray-900 mb-3">Exemplos</h3> <div class="space-y-4"> <article class="rounded-xl border border-slate-200 bg-slate-50 p-5"> <div class="flex items-start justify-between gap-3 mb-4"> <h4 class="text-base font-semibold text-gray-900">1. Correção de Landing Page com Múltiplos H1</h4> <span class="shrink-0 rounded-full bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700">Analista de SEO</span> </div> <dl class="space-y-4 text-sm"> <div> <dt class="font-medium text-gray-900">Contexto</dt> <dd class="mt-1 text-gray-700 leading-6">Uma landing page de produto recém-lançada apresenta baixo desempenho de indexação e suspeita-se de problemas na estrutura de títulos.</dd> </div> <div> <dt class="font-medium text-gray-900">Problema</dt> <dd class="mt-1 text-gray-700 leading-6">A página possui dois H1s e pula do H1 diretamente para o H3 em seções de depoimentos.</dd> </div> <div> <dt class="font-medium text-gray-900">Como usar</dt> <dd class="mt-1 text-gray-700 leading-6">Cole o HTML da landing page no campo 'htmlInput' e marque a opção 'showFixSuggestions'.</dd> </div> <div> <dt class="font-medium text-gray-900">Configuração de exemplo</dt> <dd class="mt-2"> <pre class="overflow-x-auto rounded-lg bg-slate-900 p-4 text-xs leading-6 text-slate-100"><code>htmlInput: '<html><body><h1>Produto A</h1><h3>Depoimentos</h3><h1>Compre Agora</h1></body></html>', showFixSuggestions: true</code></pre> </dd> </div> <div> <dt class="font-medium text-gray-900">Resultado</dt> <dd class="mt-1 text-gray-700 leading-6">O relatório aponta o segundo H1 como duplicado e sugere alterar o H3 para H2, normalizando a hierarquia.</dd> </div> </dl> </article> <article class="rounded-xl border border-slate-200 bg-slate-50 p-5"> <div class="flex items-start justify-between gap-3 mb-4"> <h4 class="text-base font-semibold text-gray-900">2. Alinhamento de Metadados em Blog Post</h4> <span class="shrink-0 rounded-full bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700">Redator de Conteúdo</span> </div> <dl class="space-y-4 text-sm"> <div> <dt class="font-medium text-gray-900">Contexto</dt> <dd class="mt-1 text-gray-700 leading-6">Um artigo de blog foi publicado, mas o título visível na página parece diferente do título que aparece nos compartilhamentos sociais.</dd> </div> <div> <dt class="font-medium text-gray-900">Problema</dt> <dd class="mt-1 text-gray-700 leading-6">Verificar se o H1 principal diverge do <title> e do og:title configurados no CMS.</dd> </div> <div> <dt class="font-medium text-gray-900">Como usar</dt> <dd class="mt-1 text-gray-700 leading-6">Insira a URL do artigo no campo 'pageUrl' e ative 'compareWithMetadataTitles'.</dd> </div> <div> <dt class="font-medium text-gray-900">Configuração de exemplo</dt> <dd class="mt-2"> <pre class="overflow-x-auto rounded-lg bg-slate-900 p-4 text-xs leading-6 text-slate-100"><code>pageUrl: 'https://meublog.com/artigo-seo', compareWithMetadataTitles: true</code></pre> </dd> </div> <div> <dt class="font-medium text-gray-900">Resultado</dt> <dd class="mt-1 text-gray-700 leading-6">A ferramenta exibe um alerta mostrando que o H1 ('Guia de SEO') difere do og:title ('Guia Completo de SEO para Iniciantes'), permitindo o ajuste.</dd> </div> </dl> </article> </div> </section> <section id="tool-samples" aria-labelledby="tool-samples-title"> <div class="flex items-center justify-between gap-3 mb-3"> <h3 id="tool-samples-title" class="text-base font-semibold text-gray-900">Testar com amostras</h3> <span class="text-xs text-gray-500">html</span> </div> <div class="grid grid-cols-1 md:grid-cols-2 gap-3"> <a href="/pt/samples/regex-named-groups" class="block rounded-lg border border-gray-200 p-4 hover:border-emerald-300 hover:bg-emerald-50 transition-colors"> <div class="flex items-start justify-between gap-3"> <div> <div class="font-medium text-gray-900">Grupos de Captura Nomeados Regex</div> <div class="text-sm text-gray-600 mt-1">Coleção de padrões regex usando grupos de captura nomeados para extrair dados estruturados de texto. Grupos nomeados tornam os padrões mais legíveis e mantíveis atribuindo nomes significativos às partes capturadas.</div> <div class="text-xs text-gray-500 mt-2">task extract</div> </div> <span class="text-xs px-2 py-1 rounded-full bg-emerald-100 text-emerald-700 whitespace-nowrap">sample</span> </div> </a> <a href="/pt/samples/contentful" class="block rounded-lg border border-gray-200 p-4 hover:border-emerald-300 hover:bg-emerald-50 transition-colors"> <div class="flex items-start justify-between gap-3"> <div> <div class="font-medium text-gray-900">Exemplos Contentful CMS Baseado na Nuvem</div> <div class="text-sm text-gray-600 mt-1">Exemplos abrangentes do Contentful cobrindo modelagem de conteúdo, integração de API, webhooks e padrões de integração frontend</div> <div class="text-xs text-gray-500 mt-2">keywords content</div> </div> <span class="text-xs px-2 py-1 rounded-full bg-emerald-100 text-emerald-700 whitespace-nowrap">sample</span> </div> </a> <a href="/pt/samples/html-with-images" class="block rounded-lg border border-gray-200 p-4 hover:border-emerald-300 hover:bg-emerald-50 transition-colors"> <div class="flex items-start justify-between gap-3"> <div> <div class="font-medium text-gray-900">Amostras de HTML com Imagens</div> <div class="text-sm text-gray-600 mt-1">Amostras de código fonte HTML com imagens para testar extração</div> <div class="text-xs text-gray-500 mt-2">task extract</div> </div> <span class="text-xs px-2 py-1 rounded-full bg-emerald-100 text-emerald-700 whitespace-nowrap">sample</span> </div> </a> <a href="/pt/samples/jwt" class="block rounded-lg border border-gray-200 p-4 hover:border-emerald-300 hover:bg-emerald-50 transition-colors"> <div class="flex items-start justify-between gap-3"> <div> <div class="font-medium text-gray-900">Exemplos JWT</div> <div class="text-sm text-gray-600 mt-1">Exemplos completos de JWT da estrutura básica de tokens às implementações de segurança avançadas</div> <div class="text-xs text-gray-500 mt-2">keywords structure</div> </div> <span class="text-xs px-2 py-1 rounded-full bg-emerald-100 text-emerald-700 whitespace-nowrap">sample</span> </div> </a> </div> </section> <section id="tool-hubs" aria-labelledby="tool-hubs-title"> <h3 id="tool-hubs-title" class="text-base font-semibold text-gray-900 mb-3">Hubs relacionados</h3> <div class="grid grid-cols-1 md:grid-cols-2 gap-3"> <a href="/pt/hubs/html-convert" class="block rounded-lg border border-gray-200 p-4 hover:border-violet-300 hover:bg-violet-50 transition-colors"> <div class="font-medium text-gray-900">Ferramentas de extração, limpeza e exportação de HTML para Markdown/PDF</div> <div class="text-sm text-gray-600 mt-1">Compare ferramentas de limpeza de HTML, extração de atributos, extração de imagens, HTML para Markdown e HTML para PDF em um único hub para fluxos de conversão de conteúdo web.</div> </a> <a href="/pt/hubs/web-accessibility-audits" class="block rounded-lg border border-gray-200 p-4 hover:border-violet-300 hover:bg-violet-50 transition-colors"> <div class="font-medium text-gray-900">Ferramentas de auditoria de acessibilidade web e cores acessiveis</div> <div class="text-sm text-gray-600 mt-1">Revise problemas WCAG, ordem de leitor de tela, falhas de contraste, risco para deficiencia de visao de cores e escolhas de cor acessivel em um unico hub para fluxos de acessibilidade web.</div> </a> <a href="/pt/hubs/audio-convert" class="block rounded-lg border border-gray-200 p-4 hover:border-violet-300 hover:bg-violet-50 transition-colors"> <div class="font-medium text-gray-900">Ferramentas de codificacao e conversao de audio</div> <div class="text-sm text-gray-600 mt-1">Compare conversao de formatos de audio, ajustes de bitrate, conversao de taxa de amostragem, troca de codec e exportacao em um unico hub.</div> </a> <a href="/pt/hubs/image-convert" class="block rounded-lg border border-gray-200 p-4 hover:border-violet-300 hover:bg-violet-50 transition-colors"> <div class="font-medium text-gray-900">Ferramentas de conversao de formatos de imagem e exportacao animada</div> <div class="text-sm text-gray-600 mt-1">Compare conversores de imagem para JPG, PNG, GIF, AVIF, WebP, TIFF, ICO, base64 e saidas voltadas para animacao em um unico hub.</div> </a> </div> </section> <section id="tool-related" aria-labelledby="tool-related-title"> <h3 id="tool-related-title" class="text-base font-semibold text-gray-900 mb-3">Ferramentas relacionadas</h3> <div class="grid grid-cols-1 md:grid-cols-2 gap-3"> <a href="/pt/tools/screen-reader-simulation-tester" class="block rounded-lg border border-gray-200 p-4 hover:border-blue-300 hover:bg-blue-50 transition-colors"> <div class="font-medium text-gray-900">Simulador de leitor de tela</div> <div class="text-sm text-gray-600 mt-1">Simula a ordem de leitura e a semantica falada de um leitor de tela a partir de URL ou HTML</div> </a> <a href="/pt/tools/html-attribute-extractor" class="block rounded-lg border border-gray-200 p-4 hover:border-blue-300 hover:bg-blue-50 transition-colors"> <div class="font-medium text-gray-900">Extrator de Atributos HTML</div> <div class="text-sm text-gray-600 mt-1">Extrai atributos especificados (href, src, data-*, etc.) do conteúdo HTML com suporte para filtragem de nomes de tags</div> </a> <a href="/pt/tools/image-source-extractor" class="block rounded-lg border border-gray-200 p-4 hover:border-blue-300 hover:bg-blue-50 transition-colors"> <div class="font-medium text-gray-900">Extrator de Fonte de Imagem</div> <div class="text-sm text-gray-600 mt-1">Extraia URLs de imagem (atributos src) do código fonte HTML. Suporta imagens de carregamento lento e atributos srcset.</div> </a> <a href="/pt/tools/meta-tag-extractor" class="block rounded-lg border border-gray-200 p-4 hover:border-blue-300 hover:bg-blue-50 transition-colors"> <div class="font-medium text-gray-900">Extrator Meta Tags</div> <div class="text-sm text-gray-600 mt-1">Extrai e analisa meta tags, Open Graph, Twitter Cards e dados estruturados de páginas web</div> </a> <a href="/pt/tools/new-html-tag-stripper" class="block rounded-lg border border-gray-200 p-4 hover:border-blue-300 hover:bg-blue-50 transition-colors"> <div class="font-medium text-gray-900">Removedor de Tags HTML</div> <div class="text-sm text-gray-600 mt-1">Remove tags HTML do código e extrai conteúdo de texto limpo</div> </a> <a href="/pt/tools/pdf-table-extractor-to-csv-json" class="block rounded-lg border border-gray-200 p-4 hover:border-blue-300 hover:bg-blue-50 transition-colors"> <div class="font-medium text-gray-900">Extrator de tabelas PDF para CSV/JSON</div> <div class="text-sm text-gray-600 mt-1">Extrai tabelas de PDF com OpenDataLoader e exporta em JSON, CSV ou HTML</div> </a> <a href="/pt/tools/pdf-image-caption-extractor" class="block rounded-lg border border-gray-200 p-4 hover:border-blue-300 hover:bg-blue-50 transition-colors"> <div class="font-medium text-gray-900">Extrator de imagens e captions PDF</div> <div class="text-sm text-gray-600 mt-1">Extrai imagens PDF, relaciona captions proximas e gera um indice HTML navegavel</div> </a> <a href="/pt/tools/accessibility-checker" class="block rounded-lg border border-gray-200 p-4 hover:border-blue-300 hover:bg-blue-50 transition-colors"> <div class="font-medium text-gray-900">Verificador de acessibilidade</div> <div class="text-sm text-gray-600 mt-1">Detecta problemas comuns de WCAG 2.1 em HTML, paginas ou imagens de design e retorna orientacoes praticas</div> </a> </div> </section> <section id="tool-faq" aria-labelledby="tool-faq-title"> <h3 id="tool-faq-title" class="text-base font-semibold text-gray-900 mb-3">FAQ</h3> <div class="space-y-3"> <details class="rounded-lg border border-gray-200 p-4 bg-gray-50"> <summary class="font-medium text-gray-900 cursor-pointer">O que é um salto de nível na hierarquia de títulos?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Ocorre quando a estrutura pula um nível lógico, como passar de um H1 diretamente para um H3 sem um H2 intermediário.</p> </details> <details class="rounded-lg border border-gray-200 p-4 bg-gray-50"> <summary class="font-medium text-gray-900 cursor-pointer">Por que ter múltiplos H1 pode ser um problema?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Embora o HTML5 permita, múltiplos H1 podem diluir o foco semântico da página e confundir leitores de tela ou motores de busca.</p> </details> <details class="rounded-lg border border-gray-200 p-4 bg-gray-50"> <summary class="font-medium text-gray-900 cursor-pointer">Como a ferramenta compara o H1 com os títulos meta?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Ela confronta o texto do primeiro H1 com o conteúdo das tags <title> e og:title para identificar divergências de conteúdo.</p> </details> <details class="rounded-lg border border-gray-200 p-4 bg-gray-50"> <summary class="font-medium text-gray-900 cursor-pointer">O que são headings usados apenas para estilo?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">São elementos de texto curtos ou botões que usam tags H1-H6 apenas para herdar formatação visual, prejudicando a semântica.</p> </details> <details class="rounded-lg border border-gray-200 p-4 bg-gray-50"> <summary class="font-medium text-gray-900 cursor-pointer">Posso auditar páginas que ainda não estão publicadas?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Sim, basta copiar o código HTML gerado no seu ambiente local e colá-lo diretamente no campo de entrada de HTML.</p> </details> </div> </section> </div> <script type="application/ld+json">{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"O que é um salto de nível na hierarquia de títulos?","acceptedAnswer":{"@type":"Answer","text":"Ocorre quando a estrutura pula um nível lógico, como passar de um H1 diretamente para um H3 sem um H2 intermediário."}},{"@type":"Question","name":"Por que ter múltiplos H1 pode ser um problema?","acceptedAnswer":{"@type":"Answer","text":"Embora o HTML5 permita, múltiplos H1 podem diluir o foco semântico da página e confundir leitores de tela ou motores de busca."}},{"@type":"Question","name":"Como a ferramenta compara o H1 com os títulos meta?","acceptedAnswer":{"@type":"Answer","text":"Ela confronta o texto do primeiro H1 com o conteúdo das tags <title> e og:title para identificar divergências de conteúdo."}},{"@type":"Question","name":"O que são headings usados apenas para estilo?","acceptedAnswer":{"@type":"Answer","text":"São elementos de texto curtos ou botões que usam tags H1-H6 apenas para herdar formatação visual, prejudicando a semântica."}},{"@type":"Question","name":"Posso auditar páginas que ainda não estão publicadas?","acceptedAnswer":{"@type":"Answer","text":"Sim, basta copiar o código HTML gerado no seu ambiente local e colá-lo diretamente no campo de entrada de HTML."}}]}</script> <script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://elysiatools.com/pt"},{"@type":"ListItem","position":2,"name":"Tools","item":"https://elysiatools.com/pt/tools"},{"@type":"ListItem","position":3,"name":"Heading Hierarchy Auditor"}]}</script> </article> <div class="bg-white rounded-lg shadow-sm p-6 mt-6"> <!-- API Documentation --> <div class="mb-6"> <h2 class="text-lg font-semibold text-gray-900 mb-4">Documentação da API</h2> <div class="space-y-4"> <div class="bg-gray-50 rounded-lg p-4"> <h3 class="text-sm font-medium text-gray-900 mb-2">Ponto final da solicitação</h3> <div class="bg-gray-800 rounded p-3 font-mono text-sm text-white"> POST /pt/api/tools/heading-hierarchy-auditor </div> </div> <div class="bg-gray-50 rounded-lg p-4"> <h3 class="text-sm font-medium text-gray-900 mb-2">Parâmetros da solicitação</h3> <div class="overflow-x-auto"> <table class="min-w-full divide-y divide-gray-200"> <thead class="bg-gray-100"> <tr> <th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Nome do parâmetro</th> <th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Tipo</th> <th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Requerido</th> <th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Descrição</th> </tr> </thead> <tbody class="bg-white divide-y divide-gray-200"> <tr> <td class="px-4 py-2 text-sm text-gray-900 font-mono">htmlInput</td> <td class="px-4 py-2 text-sm text-gray-500">textarea</td> <td class="px-4 py-2 text-sm text-gray-500">Não</td> <td class="px-4 py-2 text-sm text-gray-500">-</td> </tr> <tr> <td class="px-4 py-2 text-sm text-gray-900 font-mono">pageUrl</td> <td class="px-4 py-2 text-sm text-gray-500">text</td> <td class="px-4 py-2 text-sm text-gray-500">Não</td> <td class="px-4 py-2 text-sm text-gray-500">-</td> </tr> <tr> <td class="px-4 py-2 text-sm text-gray-900 font-mono">compareWithMetadataTitles</td> <td class="px-4 py-2 text-sm text-gray-500">checkbox</td> <td class="px-4 py-2 text-sm text-gray-500">Não</td> <td class="px-4 py-2 text-sm text-gray-500">-</td> </tr> <tr> <td class="px-4 py-2 text-sm text-gray-900 font-mono">showFixSuggestions</td> <td class="px-4 py-2 text-sm text-gray-500">checkbox</td> <td class="px-4 py-2 text-sm text-gray-500">Não</td> <td class="px-4 py-2 text-sm text-gray-500">-</td> </tr> </tbody> </table> </div> </div> <div class="bg-gray-50 rounded-lg p-4"> <h3 class="text-sm font-medium text-gray-900 mb-2">Formato de resposta</h3> <div class="bg-gray-800 rounded p-3 font-mono text-sm text-white overflow-x-auto"> <pre>{ "result": "<div>Processed HTML content</div>", "error": "Error message (optional)", "message": "Notification message (optional)", "metadata": { "key": "value" } }</pre> </div> <div class="mt-2 text-sm text-gray-600"> <strong>HTML:</strong> HTML </div> </div> </div> </div> </div> <div class="bg-white rounded-lg shadow-sm p-6 mt-6"> <div class="mb-6"> <h2 class="text-lg font-semibold text-gray-900 mb-4">Documentação de MCP</h2> <div class="space-y-4"> <div class="bg-gray-50 rounded-lg p-4"> <p class="text-sm text-gray-600 mb-4"> Adicione este ferramenta à sua configuração de servidor MCP: </p> <div class="bg-gray-800 rounded p-3 font-mono text-sm text-white overflow-x-auto"> <pre>{ "mcpServers": { "elysiatools-heading-hierarchy-auditor": { "name": "heading-hierarchy-auditor", "description": "Audita a estrutura de títulos a partir de uma URL ou HTML colado e detecta saltos de nível, múltiplos h1, uso visual indevido de headings e desalinhamento com títulos meta", "baseUrl": "https://elysiatools.com/mcp/sse?toolId=heading-hierarchy-auditor", "command": "", "args": [], "env": {}, "isActive": true, "type": "sse" } } }</pre> </div> <div class="mt-4 p-3 bg-gray-50 rounded border border-gray-200"> <p class="flex items-start text-sm text-gray-600"> <svg class="w-5 h-5 mr-2 text-gray-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path></svg> <span>Você pode encadear várias ferramentas, ex: `https://elysiatools.com/mcp/sse?toolId=png-to-webp,jpg-to-webp,gif-to-webp`, máx 20 ferramentas.</span> </p> </div> <div class="mt-6 pt-4 border-t border-gray-100"> <p class="flex items-center text-sm text-gray-500"> <svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path></svg> Se você encontrar algum problema, por favor, entre em contato conosco em admin@elysiatools.com </p> </div> </div> </div> </div> </div> </div> </div> </main> <footer class="bg-white border-t border-gray-200 py-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="flex flex-col md:flex-row justify-between items-center"> <div class="text-sm text-gray-500 mb-4 md:mb-0"> <p>© 2026 Elysia Tools. All rights reserved.</p> <p class="mt-1">Contact: <a href="mailto:admin@elysiatools.com" class="text-blue-600 hover:text-blue-800">admin@elysiatools.com</a></p> </div> <div class="flex space-x-6"> <a href="/terms" class="text-sm text-gray-600 hover:text-blue-600 transition-colors"> Terms of Service </a> <a href="/privacy" class="text-sm text-gray-600 hover:text-blue-600 transition-colors"> Privacy Policy </a> </div> </div> </div> </footer> <!-- Navigation & Recent Tools Script --> <script> document.addEventListener('DOMContentLoaded', () => { const mobileMenuBtn = document.getElementById('mobile-menu-btn'); const mobileMenu = document.getElementById('mobile-menu'); if (mobileMenuBtn && mobileMenu) { mobileMenuBtn.addEventListener('click', () => mobileMenu.classList.toggle('hidden')); } const languageBtn = document.getElementById('language-dropdown-btn'); const languageDropdown = document.getElementById('language-dropdown'); if (languageBtn && languageDropdown) { languageBtn.addEventListener('click', (e) => { e.stopPropagation(); languageDropdown.classList.toggle('hidden'); }); document.addEventListener('click', (e) => { const target = e.target; if (target instanceof Node && !languageBtn.contains(target) && !languageDropdown.contains(target)) { languageDropdown.classList.add('hidden'); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') languageDropdown.classList.add('hidden'); }); } // Update language switch links to preserve query parameters const langLinks = document.querySelectorAll('.lang-switch-link'); const currentUrl = new URL(window.location.href); langLinks.forEach((link) => { if (link instanceof HTMLAnchorElement) { const newLang = link.getAttribute('data-lang'); const currentLang = link.getAttribute('data-current-lang'); if (newLang && currentLang) { // Update href to preserve query parameters const pathname = currentUrl.pathname; let newPathname; // Check if pathname starts with a language code const langPattern = /^\/([a-z]{2})(\/|$)/; const match = pathname.match(langPattern); if (match) { // Pathname has language code - replace it newPathname = pathname.replace(langPattern, '/' + newLang + '/'); } else { // Pathname has no language code (e.g., root /) - add it // Remove trailing slash before adding language const cleanPath = pathname.replace(/\/$/, ''); newPathname = cleanPath ? '/' + newLang + cleanPath : '/' + newLang; } currentUrl.pathname = newPathname; link.href = currentUrl.toString(); } } }); const RECENT_KEY = 'recent-tools'; const recentDesktop = document.getElementById('recent-list-desktop'); const recentMobile = document.getElementById('recent-list-mobile'); const recentBtn = document.getElementById('recent-dropdown-btn'); const recentDropdown = document.getElementById('recent-dropdown'); function loadRecentTools() { try { const stored = localStorage.getItem(RECENT_KEY); const list = stored ? JSON.parse(stored) : []; return Array.isArray(list) ? list : []; } catch { return []; } } function renderRecentTools() { const list = loadRecentTools(); const renderList = (container, isMobile = false) => { if (!container) return; if (!list.length) { container.innerHTML = '<p class="px-4 py-2 text-sm text-gray-500">No recent tools</p>'; return; } container.innerHTML = list.map((item) => { // Determine current language from URL or context // Note: pt is injected by the server-side renderSharedScripts function const currentLang = 'pt'; // Try to get translation for current language let name = item.name ? item.name : item.id; let description = item.description || ''; if (item.translations && item.translations[currentLang]) { name = item.translations[currentLang].name || name; description = item.translations[currentLang].description || description; } // Construct URL for current language const href = '/' + currentLang + '/tools/' + item.id; const category = item.category || ''; let html = '<a href="' + href + '" class="block px-4 py-3 hover:bg-gray-50 border-b border-gray-100 last:border-b-0">'; html += '<div class="flex items-center justify-between">'; html += '<div class="flex-1 min-w-0">'; html += '<h4 class="text-sm font-medium text-gray-900 truncate">' + name + '</h4>'; if (description) { html += '<p class="text-xs text-gray-500 mt-1 truncate">' + description + '</p>'; } if (category) { html += '<span class="inline-block mt-1 px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-800">' + category + '</span>'; } html += '</div>'; html += '<svg class="w-4 h-4 text-gray-400 ml-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">'; html += '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>'; html += '</svg>'; html += '</div>'; html += '</a>'; return html; }).join(''); }; renderList(recentDesktop, false); renderList(recentMobile, true); } window.renderRecentTools = renderRecentTools; renderRecentTools(); if (recentBtn && recentDropdown) { recentBtn.addEventListener('click', (e) => { e.stopPropagation(); recentDropdown.classList.toggle('hidden'); }); document.addEventListener('click', (e) => { const target = e.target; if (target instanceof Node && !recentBtn.contains(target) && !recentDropdown.contains(target)) { recentDropdown.classList.add('hidden'); } }); } }); </script> <!-- Global Search Script --> <script> document.addEventListener('DOMContentLoaded', () => { const searchConfigs = [ { inputId: 'global-search', dropdownId: 'search-dropdown-desktop', resultsId: 'search-results-desktop' }, { inputId: 'global-search-mobile', dropdownId: 'search-dropdown-mobile', resultsId: 'search-results-mobile' } ]; searchConfigs.forEach(config => { const input = document.getElementById(config.inputId); const dropdown = document.getElementById(config.dropdownId); const results = document.getElementById(config.resultsId); if (input && dropdown && results) { let searchTimeout; let currentSearchRequest = null; const handleSearch = (raw) => { const searchTerm = (raw || '').trim(); if (searchTerm.length === 0) { dropdown.classList.add('hidden'); return; } if (searchTimeout) clearTimeout(searchTimeout); if (currentSearchRequest) currentSearchRequest.abort(); results.innerHTML = ` <div class="p-4 text-center text-gray-500"> <div class="inline-block animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500"></div> <span class="ml-2">Searching...</span> </div> `; dropdown.classList.remove('hidden'); searchTimeout = setTimeout(async () => { try { currentSearchRequest = new AbortController(); const response = await fetch('/pt/api/search?q=' + encodeURIComponent(searchTerm) + '&limit=10', { signal: currentSearchRequest.signal }); if (!response.ok) throw new Error('Search failed'); const responseData = await response.json(); const tools = Array.isArray(responseData.results) ? responseData.results : []; if (tools.length === 0) { results.innerHTML = ` <div class="p-4 text-center text-gray-500"> No tools found for "${searchTerm}" </div> `; } else { results.innerHTML = tools.map((tool) => ` <a href="/pt/tools/${tool.id}" class="block px-4 py-3 hover:bg-gray-50 border-b border-gray-100 last:border-b-0"> <div class="flex items-center justify-between"> <div class="flex-1"> <h4 class="text-sm font-medium text-gray-900">${tool.name}</h4> <p class="text-xs text-gray-500 mt-1">${tool.description}</p> <span class="inline-block mt-1 px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-800"> ${tool.category} </span> </div> <svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path> </svg> </div> </a> `).join(''); } } catch (error) { if (error.name !== 'AbortError') { console.error('Search error:', error); results.innerHTML = ` <div class="p-4 text-center text-red-500"> Search failed. Please try again. </div> `; } } }, 300); }; input.addEventListener('input', (e) => { const target = e.target; handleSearch(target && target.value ? target.value : ''); }); input.addEventListener('focus', () => { if (results.innerHTML.trim().length > 0) { dropdown.classList.remove('hidden'); } }); // Close dropdown when clicking outside document.addEventListener('click', (e) => { const target = e.target; if (target instanceof Node && !input.contains(target) && !dropdown.contains(target)) { dropdown.classList.add('hidden'); } }); // Close on Escape key input.addEventListener('keydown', (e) => { if (e.key === 'Escape') { dropdown.classList.add('hidden'); input.blur(); } }); } }); }); </script> <script> document.addEventListener('DOMContentLoaded', () => { // Initialize tool navigation function initializeToolNavigation(lang){let searchInput=document.getElementById("tool-search"),searchResultsEl=document.getElementById("tool-search-results"),categoriesNav=document.getElementById("tool-categories"),categoryGroups=document.querySelectorAll(".category-group"),currentPath=window.location.pathname,escapeText=(value)=>String(value||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'");if(searchInput){let searchTimeout,currentSearchRequest=null;searchInput.addEventListener("input",(e)=>{let searchTerm=e.target.value.trim();if(searchTerm.length===0){if(searchResultsEl)searchResultsEl.innerHTML="",searchResultsEl.classList.add("hidden");if(categoriesNav)categoriesNav.classList.remove("hidden");categoryGroups.forEach((group)=>{group.style.display="block"});return}if(searchTimeout)clearTimeout(searchTimeout);if(currentSearchRequest)currentSearchRequest.abort();if(categoriesNav)categoriesNav.classList.add("hidden");if(searchResultsEl)searchResultsEl.innerHTML=` <div class="rounded-xl border border-slate-200 bg-slate-50 px-3 py-3 text-sm text-slate-500"> Searching... </div> `,searchResultsEl.classList.remove("hidden");searchTimeout=setTimeout(async()=>{try{currentSearchRequest=new AbortController;let response=await fetch(`/${lang}/api/search?q=${encodeURIComponent(searchTerm)}&limit=50`,{signal:currentSearchRequest.signal});if(!response.ok)throw new Error("Search failed");let responseData=await response.json(),matchedTools=Array.isArray(responseData.results)?responseData.results:[];if(!searchResultsEl)return;if(matchedTools.length===0){searchResultsEl.innerHTML=` <div class="rounded-xl border border-slate-200 bg-slate-50 px-3 py-3 text-sm text-slate-500"> No tools found for "${escapeText(searchTerm)}" </div> `;return}searchResultsEl.innerHTML=matchedTools.map((tool)=>` <a href="/${lang}/tools/${tool.id}" class="block rounded-xl border border-slate-200 px-3 py-3 hover:border-blue-300 hover:bg-blue-50 transition-colors ${currentPath===`/${lang}/tools/${tool.id}`?"bg-blue-50 border-blue-200":"bg-white"}" > <div class="text-sm font-medium text-slate-900">${escapeText(tool.name)}</div> <div class="mt-1 text-xs text-slate-500">${escapeText(tool.category||"")}</div> </a> `).join("")}catch(error){if(error instanceof Error&&error.name==="AbortError")return;if(console.error("Search error:",error),searchResultsEl)searchResultsEl.innerHTML=` <div class="rounded-xl border border-red-200 bg-red-50 px-3 py-3 text-sm text-red-600"> Search failed. Please try again. </div> `}finally{currentSearchRequest=null}},300)})}let createToolLinkHtml=(category,tool,currentToolPath)=>` <a href="/${lang}/tools/${tool.id}" class="tool-link flex items-start gap-2 px-3 py-2.5 rounded-lg text-sm ${currentToolPath===`/${lang}/tools/${tool.id}`?"bg-blue-50 text-blue-700 font-medium border border-blue-100":"text-gray-700 hover:bg-gray-50"}" data-tool-id="${tool.id}" data-tool-name="${(tool.name||"").toLowerCase()}" data-category="${category}" style="display:flex;" > <span class="mt-1 h-1.5 w-1.5 rounded-full ${currentToolPath===`/${lang}/tools/${tool.id}`?"bg-blue-600":"bg-slate-300"}"></span> <span class="min-w-0 flex-1">${escapeText(tool.name||"")}</span> </a> `,ensureCategoryLoaded=async(category,page,append)=>{let group=document.querySelector(`.category-toggle[data-category="${category}"]`)?.closest(".category-group");if(!group)return;let content=group.querySelector(".category-content"),list=group.querySelector(".category-tools-list"),toggle=group.querySelector(".category-toggle"),moreButton=group.querySelector(".category-more-toggle");if(!content||!list||!toggle)return;let response=await fetch(`/${lang}/api/categories/${encodeURIComponent(category)}?page=${page}&limit=12`);if(!response.ok)throw new Error("Failed to load category tools");let responseData=await response.json(),tools=Array.isArray(responseData.tools)?responseData.tools:[],pagination=responseData.pagination||{current:page,total:page},existingIds=new Set(Array.from(list.querySelectorAll(".tool-link")).map((item)=>item.getAttribute("data-tool-id"))),html2=tools.filter((tool)=>!existingIds.has(tool.id)).map((tool)=>createToolLinkHtml(category,tool,currentPath)).join("");if(!append)list.innerHTML=html2;else list.insertAdjacentHTML("beforeend",html2);if(toggle.setAttribute("data-loaded","true"),toggle.setAttribute("data-page",String(pagination.current)),moreButton){let hasMore=pagination.current<pagination.total;moreButton.style.display=hasMore?"block":"none",moreButton.setAttribute("data-page",String(pagination.current));let total=parseInt(moreButton.getAttribute("data-total")||"0",10),remaining=Math.max(total-pagination.current*12,0),showMoreLabel=moreButton.getAttribute("data-show-more-label")||"Show more";moreButton.textContent=remaining>0?`${showMoreLabel} (${remaining})`:showMoreLabel}};document.querySelectorAll(".category-toggle").forEach((header)=>{header.addEventListener("click",async()=>{let content=header.closest(".category-group")?.querySelector(".category-content"),chevron=header.querySelector(".category-chevron");if(content){if(content.style.display==="none"){if(header.getAttribute("data-loaded")!=="true")try{await ensureCategoryLoaded(header.getAttribute("data-category")||"",1,!1)}catch(error){console.error("Failed to load category tools:",error)}if(content.style.display="block",header.setAttribute("aria-expanded","true"),chevron)chevron.style.transform="rotate(0deg)"}else if(content.style.display="none",header.setAttribute("aria-expanded","false"),chevron)chevron.style.transform="rotate(-90deg)"}})}),document.querySelectorAll(".category-more-toggle").forEach((button)=>{button.addEventListener("click",async()=>{let category=button.getAttribute("data-category"),currentPage=parseInt(button.getAttribute("data-page")||"0",10);if(!category)return;try{await ensureCategoryLoaded(category,currentPage+1,!0)}catch(error){console.error("Failed to load more category tools:",error)}})})} initializeToolNavigation('pt'); // Initialize tool form const toolData = {"id":"heading-hierarchy-auditor","name":"Auditor de Hierarquia de Títulos","description":"Audita a estrutura de títulos a partir de uma URL ou HTML colado e detecta saltos de nível, múltiplos h1, uso visual indevido de headings e desalinhamento com títulos meta","detail":"Cole HTML ou forneça uma URL para inspecionar a semântica dos títulos em vez da ordem visual. O relatório mostra saltos de nível, múltiplos h1, headings usados só para estilo, headings vazios e se o h1 principal se afasta do <title> e do og:title.\n\nComo usar:\n- Entrada HTML: cole a marcação para uma análise estável\n- URL da página: inspeciona a estrutura publicada\n- Comparar com títulos meta: confronta o heading principal com `<title>` e `og:title`\n- Mostrar sugestões: adiciona um nível recomendado ou substituição por nó\n\nO que o relatório destaca:\n- Árvore visual de h1-h6\n- Distribuição de severidade em estilo heatmap\n- Nível sugerido para cada heading\n- Múltiplos h1 e saltos de nível\n- Rótulos de UI disfarçados de heading\n- Desalinhamento entre h1 e títulos meta","category":"Validation","keywords":["hierarquia de títulos","semântica","html","seo"],"resultType":"html","featured":false,"lastUpdated":"2026-06-09T00:00:00.000Z","options":[{"type":"textarea","name":"htmlInput","label":"Entrada HTML","placeholder":"<main><h1>Pricing</h1><h3>Plans</h3><h2>FAQ</h2></main>","primary":true,"validate":{"def":{"type":"union","options":[{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},{"def":{"type":"undefined"},"type":"undefined"}]},"type":"union","options":[{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},{"def":{"type":"undefined"},"type":"undefined"}]},"options":[]},{"type":"text","name":"pageUrl","label":"URL da pagina","placeholder":"https://example.com","validate":{"def":{"type":"union","options":[{"def":{"type":"string","checks":[{"def":{"type":"string","format":"url","check":"string_format","abort":false},"type":"string","format":"url","minLength":null,"maxLength":null}]},"type":"string","format":"url","minLength":null,"maxLength":null},{"def":{"type":"string","checks":[{}]},"type":"string","format":null,"minLength":0,"maxLength":0}]},"type":"union","options":[{"def":{"type":"string","checks":[{"def":{"type":"string","format":"url","check":"string_format","abort":false},"type":"string","format":"url","minLength":null,"maxLength":null}]},"type":"string","format":"url","minLength":null,"maxLength":null},{"def":{"type":"string","checks":[{}]},"type":"string","format":null,"minLength":0,"maxLength":0}]},"options":[]},{"type":"checkbox","name":"compareWithMetadataTitles","label":"Comparar com títulos meta","default":true,"validate":{"def":{"type":"optional","innerType":{"def":{"type":"boolean"},"type":"boolean"}},"type":"optional"},"options":[]},{"type":"checkbox","name":"showFixSuggestions","label":"Mostrar correcoes","default":true,"validate":{"def":{"type":"optional","innerType":{"def":{"type":"boolean"},"type":"boolean"}},"type":"optional"},"options":[]}],"exampleResults":[{"title":"Audit a landing page with skipped heading levels and duplicate h1 tags","description":"Review the outline tree, catch h1 duplication, and see which headings should become h2 or non-heading UI labels.","inputs":{"htmlInput":"<html><head><title>Acme Analytics Dashboard

Acme Analytics

Executive summary

Pricing

","pageUrl":"","compareWithMetadataTitles":true,"showFixSuggestions":true},"output":{"type":"html","content":"
Heading hierarchy tree
"},"translations":{"zh":{"title":"审计存在跳级和重复 h1 的落地页","description":"检查大纲树,识别重复 h1,并查看哪些标题应改成 h2 或改为非标题 UI 文本。"},"es":{"title":"Auditar una landing con saltos de nivel y h1 duplicados","description":"Revisa el árbol de títulos, detecta h1 duplicados y ve qué nodos deberían bajar de nivel o dejar de ser headings."},"ru":{"title":"Проверить лендинг с пропусками уровней и дублирующимися h1","description":"Показывает дерево заголовков, дубликаты h1 и какие узлы стоит понизить или убрать из heading-разметки."},"fr":{"title":"Auditer une landing avec niveaux sautés et h1 dupliqués","description":"Affiche l’arbre des titres, détecte les h1 multiples et suggère quels nœuds doivent devenir h2 ou du texte non structurel."},"de":{"title":"Landingpage mit Sprüngen und doppelten h1 prüfen","description":"Zeigt den Überschriftenbaum, erkennt doppelte h1 und empfiehlt, welche Knoten zu h2 oder zu Nicht-Heading-Text werden sollten."},"pt":{"title":"Auditar uma landing com saltos de nível e h1 duplicados","description":"Mostra a árvore de títulos, detecta h1 duplicados e indica quais nós deveriam virar h2 ou texto não estrutural."}}}],"translations":{"en":{"name":"Heading Hierarchy Auditor","description":"Audit heading structure from a live URL or pasted HTML, then flag skipped levels, multiple h1 usage, style-only heading abuse, and title metadata drift","detail":"Paste HTML or provide a page URL to inspect heading semantics rather than visual reading order. The report surfaces skipped heading levels, multiple h1 nodes, likely style-only heading misuse, empty headings, and whether the main h1 drifts away from the document title or Open Graph title.\n\nHow to use it:\n- HTML Input: paste raw markup for deterministic checks\n- Page URL: fetch a live page when you want to inspect the published structure\n- Compare With Metadata Titles: checks the primary heading against `` and `og:title`\n- Show Fix Suggestions: includes a suggested heading level or replacement strategy for each node\n\nWhat the report highlights:\n- A visual hierarchy tree for h1-h6 nodes\n- Heatmap-like severity distribution across headings\n- Suggested replacement level for each heading node\n- Multiple h1 and skipped-level warnings\n- Likely style-only UI labels disguised as headings\n- Detachment between h1 and metadata titles","keywords":["heading hierarchy","html headings","h1","seo","semantic audit","content structure"],"options":{"HTML Input":"HTML Input","Page URL":"Page URL","Compare With Metadata Titles":"Compare With Metadata Titles","Show Fix Suggestions":"Show Fix Suggestions"}},"zh":{"name":"标题层级可读性审计器","description":"从在线 URL 或粘贴的 HTML 审计标题结构,识别跳级、多重 h1、纯样式化标题滥用,以及与标题元数据的脱节问题","detail":"粘贴 HTML 或提供页面 URL,检查页面的标题语义,而不是视觉阅读顺序。报告会指出标题跳级、多个 h1、疑似仅用于样式的标题、空标题,以及主 h1 是否与文档 title / og:title 脱节。\n\n使用方式:\n- HTML 输入:粘贴源码做稳定分析\n- 页面 URL:抓取线上页面结构\n- 比对标题元数据:检查主标题与 `<title>`、`og:title` 是否一致\n- 显示修复建议:为每个标题节点给出建议级别或替代方案\n\n报告重点:\n- h1-h6 的可视化层级树\n- 按严重级别分布的热力提示\n- 每个标题节点建议改成什么级别\n- 多个 h1 与跳级警告\n- 疑似把 UI 标签误用为标题的情况\n- h1 与元数据标题的脱节","keywords":["标题层级","标题结构","语义","html","seo"],"options":{"HTML Input":"HTML 输入","Page URL":"页面 URL","Compare With Metadata Titles":"比对标题元数据","Show Fix Suggestions":"显示修复建议"}},"es":{"name":"Auditor de Jerarquía de Títulos","description":"Audita la estructura de títulos desde una URL o HTML pegado y detecta saltos de nivel, varios h1, abuso de headings solo visuales y desconexión con los títulos meta","detail":"Pega HTML o proporciona una URL para inspeccionar la semántica de títulos en lugar del orden visual. El informe muestra saltos de nivel, múltiples h1, headings usados solo por estilo, headings vacíos y si el h1 principal se aleja del <title> o del og:title.\n\nCómo usarlo:\n- Entrada HTML: pega el marcado para un análisis estable\n- URL de página: inspecciona la estructura publicada\n- Comparar con títulos meta: contrasta el heading principal con `<title>` y `og:title`\n- Mostrar sugerencias: añade una recomendación de nivel o reemplazo por nodo\n\nQué destaca:\n- Árbol visual de h1-h6\n- Distribución de severidad tipo heatmap\n- Nivel sugerido para cada heading\n- Múltiples h1 y saltos de nivel\n- Etiquetas de UI disfrazadas de heading\n- Desalineación entre h1 y títulos meta","keywords":["jerarquía de títulos","semántica","html","seo"],"options":{"HTML Input":"Entrada HTML","Page URL":"URL de pagina","Compare With Metadata Titles":"Comparar con títulos meta","Show Fix Suggestions":"Mostrar sugerencias"}},"ru":{"name":"Аудитор Иерархии Заголовков","description":"Проверяет структуру заголовков по URL или вставленному HTML и находит пропуски уровней, несколько h1, декоративное злоупотребление headings и расхождение с мета-заголовками","detail":"Вставьте HTML или укажите URL страницы, чтобы проверить семантику заголовков, а не визуальный порядок. Отчёт показывает пропуски уровней, несколько h1, вероятно декоративные headings, пустые заголовки и расхождение основного h1 с <title> и og:title.\n\nКак использовать:\n- HTML ввод: вставьте разметку для стабильной проверки\n- URL страницы: проверьте опубликованную структуру\n- Сравнить с мета-заголовками: сопоставляет основной heading с `<title>` и `og:title`\n- Показать исправления: добавляет рекомендуемый уровень или замену для каждого узла\n\nЧто показывает отчёт:\n- Визуальное дерево h1-h6\n- Распределение серьёзности в формате heatmap\n- Рекомендуемый уровень для каждого heading\n- Несколько h1 и пропуски уровней\n- Вероятное использование heading только ради стиля\n- Расхождение между h1 и мета-заголовками","keywords":["иерархия заголовков","семантика","html","seo"],"options":{"HTML Input":"HTML ввод","Page URL":"URL страницы","Compare With Metadata Titles":"Сравнить с мета-заголовками","Show Fix Suggestions":"Показать исправления"}},"fr":{"name":"Auditeur de Hiérarchie des Titres","description":"Audite la structure des titres depuis une URL ou du HTML collé et détecte les sauts de niveau, les h1 multiples, les headings purement décoratifs et la dérive avec les titres meta","detail":"Collez du HTML ou fournissez une URL pour inspecter la sémantique des titres plutôt que l’ordre visuel. Le rapport signale les sauts de niveau, les h1 multiples, les headings utilisés seulement pour le style, les titres vides et l’écart entre le h1 principal, le <title> et og:title.\n\nMode d’emploi :\n- Entrée HTML : collez le code pour une analyse stable\n- URL de page : inspecte la structure publiée\n- Comparer aux titres meta : compare le heading principal avec `<title>` et `og:title`\n- Afficher les corrections : ajoute une recommandation de niveau ou de remplacement par nœud\n\nLe rapport met en avant :\n- Un arbre visuel des h1-h6\n- Une répartition de gravité façon heatmap\n- Le niveau conseillé pour chaque heading\n- Les h1 multiples et les sauts de niveau\n- Les libellés UI déguisés en heading\n- Le décalage entre h1 et titres meta","keywords":["hiérarchie des titres","sémantique","html","seo"],"options":{"HTML Input":"Entree HTML","Page URL":"URL de page","Compare With Metadata Titles":"Comparer aux titres meta","Show Fix Suggestions":"Afficher les corrections"}},"de":{"name":"Auditor für Überschriftenhierarchie","description":"Prüft die Überschriftenstruktur aus URL oder eingefügtem HTML und erkennt Sprünge, mehrere h1, rein dekorativen Heading-Missbrauch und Abweichungen zu Meta-Titeln","detail":"Füge HTML ein oder gib eine Seiten-URL an, um die semantische Überschriftenstruktur statt der visuellen Leserichtung zu prüfen. Der Bericht zeigt Ebenensprünge, mehrere h1, vermutlich nur dekorative Heading-Nutzung, leere Überschriften und Abweichungen zwischen dem Haupt-h1, <title> und og:title.\n\nSo verwendest du das Tool:\n- HTML-Eingabe: Markup für eine stabile Analyse einfügen\n- Seiten-URL: veröffentlichte Struktur prüfen\n- Mit Metadaten-Titeln vergleichen: gleicht die Hauptüberschrift mit `<title>` und `og:title` ab\n- Korrekturhinweise anzeigen: ergänzt eine empfohlene Ebene oder Ersetzung pro Knoten\n\nDer Bericht zeigt:\n- Einen visuellen h1-h6-Baum\n- Eine Heatmap-artige Schwereverteilung\n- Die empfohlene Ebene für jede Überschrift\n- Mehrere h1 und Ebenensprünge\n- Als Heading verkleidete UI-Labels\n- Abweichungen zwischen h1 und Meta-Titeln","keywords":["überschriftenhierarchie","semantik","html","seo"],"options":{"HTML Input":"HTML-Eingabe","Page URL":"Seiten-URL","Compare With Metadata Titles":"Mit Metadaten-Titeln vergleichen","Show Fix Suggestions":"Korrekturhinweise anzeigen"}},"pt":{"name":"Auditor de Hierarquia de Títulos","description":"Audita a estrutura de títulos a partir de uma URL ou HTML colado e detecta saltos de nível, múltiplos h1, uso visual indevido de headings e desalinhamento com títulos meta","detail":"Cole HTML ou forneça uma URL para inspecionar a semântica dos títulos em vez da ordem visual. O relatório mostra saltos de nível, múltiplos h1, headings usados só para estilo, headings vazios e se o h1 principal se afasta do <title> e do og:title.\n\nComo usar:\n- Entrada HTML: cole a marcação para uma análise estável\n- URL da página: inspeciona a estrutura publicada\n- Comparar com títulos meta: confronta o heading principal com `<title>` e `og:title`\n- Mostrar sugestões: adiciona um nível recomendado ou substituição por nó\n\nO que o relatório destaca:\n- Árvore visual de h1-h6\n- Distribuição de severidade em estilo heatmap\n- Nível sugerido para cada heading\n- Múltiplos h1 e saltos de nível\n- Rótulos de UI disfarçados de heading\n- Desalinhamento entre h1 e títulos meta","keywords":["hierarquia de títulos","semântica","html","seo"],"options":{"HTML Input":"Entrada HTML","Page URL":"URL da pagina","Compare With Metadata Titles":"Comparar com títulos meta","Show Fix Suggestions":"Mostrar correcoes"}}}}; function initializeToolForm(toolData,lang){let loading=!1,result=null,fileUploadStates={};function getCompatiblePrefillField(){let orderedOptions=[...toolData.options||[]].sort((left,right)=>{if(!!left.primary!==!!right.primary)return left.primary?-1:1;return 0});for(let option of orderedOptions){if(!["textarea","text"].includes(option.type))continue;let fieldName=option.name||option.label,field=document.querySelector(`[name="${fieldName}"]`);if(field)return field}return null}function applyStoredSamplePrefill(){let storageKey=`tool-prefill:${toolData.id}`,raw=sessionStorage.getItem(storageKey);if(!raw)return;try{let payload=JSON.parse(decodeURIComponent(raw));if(!payload?.content)return;let field=getCompatiblePrefillField();if(!field)return;field.value=String(payload.content),field.dispatchEvent(new Event("change",{bubbles:!0})),sessionStorage.removeItem(storageKey);let message=lang==="zh"?`\u5DF2\u4ECE sample \u52A0\u8F7D\u5185\u5BB9\uFF1A${payload.sampleItemTitle||payload.sampleName||"sample"}`:`Loaded sample content: ${payload.sampleItemTitle||payload.sampleName||"sample"}`;showNotification(message,"success")}catch(error){sessionStorage.removeItem(storageKey)}}applyStoredSamplePrefill(),document.querySelectorAll(".load-example-btn").forEach((btn)=>{btn.addEventListener("click",(e)=>{let target=e.currentTarget,index=parseInt(target.getAttribute("data-example-index")||"0");loadExample(index,toolData.exampleResults)})});function loadExample(index,examples){if(!examples||!examples[index])return;let example=examples[index],fileUploadRequiredText=lang==="zh"?"file\u7C7B\u578B\u9700\u8981\u81EA\u884C\u4E0A\u4F20":"File-type inputs must be uploaded manually",exampleLoadedText=lang==="zh"?"\u793A\u4F8B\u5DF2\u52A0\u8F7D":"Example loaded successfully",hasFileInputs=!1;Object.entries(example.inputs).forEach(([key,value])=>{let field=document.querySelector(`[name="${key}"]`);if(!field)return;switch(field.type){case"checkbox":field.checked=!!value;break;case"radio":let radio=document.querySelector(`[name="${key}"][value="${value}"]`);if(radio)radio.checked=!0;break;case"select-one":case"select-multiple":if(field.tagName==="SELECT"){let selectField=field;if(Array.isArray(value))Array.from(selectField.options).forEach((opt)=>{opt.selected=value.includes(opt.value)});else selectField.value=String(value)}break;case"file":hasFileInputs=!0;break;default:field.value=String(value)}field.dispatchEvent(new Event("change",{bubbles:!0}))}),showNotification(hasFileInputs?`${exampleLoadedText}\uFF08${fileUploadRequiredText}\uFF09`:exampleLoadedText,"success"),document.getElementById("tool-form")?.scrollIntoView({behavior:"smooth",block:"start"})}function escapeHtml(text){let div=document.createElement("div");return div.textContent=text,div.innerHTML}function copyToClipboard(text){return navigator.clipboard.writeText(text)}function downloadFile(content,filename,contentType="text/plain"){let blob=new Blob([content],{type:contentType}),url=URL.createObjectURL(blob),a=document.createElement("a");a.href=url,a.download=filename,a.click(),URL.revokeObjectURL(url)}function downloadJson(data,filename){let jsonString=JSON.stringify(data,null,2);downloadFile(jsonString,filename,"application/json")}function showNotification(message,type="info"){let notification=document.createElement("div"),bgColor=type==="success"?"bg-green-500":type==="error"?"bg-red-500":"bg-blue-500";notification.className=`fixed top-4 right-4 ${bgColor} text-white px-6 py-3 rounded-md shadow-lg z-50 fade-in`,notification.textContent=message,document.body.appendChild(notification),setTimeout(()=>{notification.remove()},3000)}function updateSubmitButton(){let submitButton2=document.getElementById("submit-button");if(submitButton2)submitButton2.disabled=loading,submitButton2.textContent=loading?"Processing...":"Process"}function generateJsonForm(data,path3){if(data===null||data===void 0)return`<div class="mb-2"> <label class="block text-sm font-medium text-gray-700 mb-1">${path3||"value"}</label> <input type="text" data-path="${path3}" value="" placeholder="null" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> </div>`;if(typeof data==="string")return`<div class="mb-2"> <label class="block text-sm font-medium text-gray-700 mb-1">${path3||"value"} (string)</label> <input type="text" data-path="${path3}" value="${escapeHtml(data)}" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> </div>`;if(typeof data==="number")return`<div class="mb-2"> <label class="block text-sm font-medium text-gray-700 mb-1">${path3||"value"} (number)</label> <input type="number" data-path="${path3}" value="${data}" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> </div>`;if(typeof data==="boolean")return`<div class="mb-2"> <label class="block text-sm font-medium text-gray-700 mb-1">${path3||"value"} (boolean)</label> <select data-path="${path3}" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> <option value="true" ${data?"selected":""}>true</option> <option value="false" ${!data?"selected":""}>false</option> </select> </div>`;if(Array.isArray(data)){let html2=`<div class="mb-4 border border-gray-200 rounded-md p-3"> <div class="flex items-center justify-between mb-2"> <label class="block text-sm font-medium text-gray-700">${path3||"array"} (array[${data.length}])</label> <button onclick="addArrayItem('${path3}')" class="px-2 py-1 bg-green-500 text-white text-xs rounded hover:bg-green-600">Add Item</button> </div>`;return data.forEach((item,index)=>{let itemPath=path3?`${path3}[${index}]`:`[${index}]`;html2+=`<div class="ml-4 mb-2 border-l-2 border-gray-200 pl-3"> <div class="flex items-center justify-between mb-1"> <span class="text-xs text-gray-500">Index ${index}</span> <button onclick="removeArrayItem('${path3}', ${index})" class="px-1 py-0.5 bg-red-500 text-white text-xs rounded hover:bg-red-600">Remove</button> </div> ${generateJsonForm(item,itemPath)} </div>`}),html2+="</div>",html2}if(typeof data==="object"&&data!==null){let html2=`<div class="mb-4 border border-gray-200 rounded-md p-3"> <label class="block text-sm font-medium text-gray-700 mb-2">${path3||"object"} (object)</label>`;return Object.keys(data).forEach((key)=>{let keyPath=path3?`${path3}.${key}`:key;html2+=`<div class="ml-4 mb-2 border-l-2 border-gray-200 pl-3"> <div class="flex items-center justify-between mb-1"> <span class="text-xs text-gray-500 font-mono">${key}</span> <button onclick="removeObjectKey('${path3}', '${key}')" class="px-1 py-0.5 bg-red-500 text-white text-xs rounded hover:bg-red-600">Remove</button> </div> ${generateJsonForm(data[key],keyPath)} </div>`}),html2+=`<button onclick="addObjectKey('${path3}')" class="px-2 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600 mt-2">Add Property</button>`,html2+="</div>",html2}return`<div class="mb-2"> <label class="block text-sm font-medium text-gray-700 mb-1">${path3||"value"}</label> <input type="text" data-path="${path3}" value="${escapeHtml(String(data))}" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> </div>`}function renderResult(result2,resultType){let html2='<div class="mt-8">';if(result2.error&&result2.validationErrors)return html2+='<h3 class="text-lg font-medium text-red-600 mb-4">Validation Errors</h3>',html2+='<div class="space-y-2">',Object.entries(result2.validationErrors).forEach(([field,error])=>{html2+='<div class="p-3 bg-red-50 border border-red-200 rounded-md">',html2+='<p class="text-sm text-red-800">',html2+=`<span class="font-medium">${escapeHtml(field)}:</span> ${escapeHtml(error)}`,html2+="</p>",html2+="</div>"}),html2+="</div>",html2+="</div>",html2;if(result2.data.error)return html2+='<h3 class="text-lg font-medium text-red-600 mb-4">Error</h3>',html2+='<div class="p-4 bg-red-50 border border-red-200 rounded-md">',html2+=`<p class="text-sm text-red-800">${escapeHtml(result2.data.message||result2.data.error||"Unknown error")}</p>`,html2+="</div>",html2+="</div>",html2;switch(html2+='<h3 class="text-lg font-medium text-gray-900 mb-4">Result</h3>',resultType){case"stream":break;case"text":html2+='<div class="space-y-4">',html2+='<div class="p-4 bg-gray-100 rounded-md tool-result-container"><pre class="whitespace-pre-wrap text-sm">'+escapeHtml(result2.data.result)+"</pre></div>",html2+=renderMetadata(result2.data.metadata),html2+='<div class="flex gap-2">',html2+='<button onclick="copyResult()" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">Copy</button>',html2+='<button onclick="downloadResult()" class="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600">Download</button>',html2+="</div></div>";break;case"image":html2+='<div class="space-y-4">',html2+='<img src="'+escapeHtml(result2.data.result)+'" alt="Result" class="max-w-full h-auto rounded-md">',html2+=renderMetadata(result2.data.metadata),html2+='<button onclick="downloadImage()" class="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600">Download Image</button>',html2+="</div>";break;case"json":if(html2+='<div class="space-y-4">',result2.data&&result2.data.original!==void 0&&result2.data.formConfig!==void 0)html2+='<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">',html2+='<div class="space-y-2">',html2+='<h4 class="font-medium text-gray-900">Original JSON</h4>',html2+='<div class="p-4 bg-gray-100 rounded-md tool-result-container">',html2+='<pre class="whitespace-pre-wrap text-sm"><code id="original-json">'+escapeHtml(JSON.stringify(result2.data.parsed||result2.data.original,null,2))+"</code></pre>",html2+="</div>",html2+='<div class="flex gap-2">',html2+='<button onclick="copyOriginalJson()" class="px-3 py-1 bg-blue-500 text-white text-sm rounded hover:bg-blue-600">Copy Original</button>',html2+='<button onclick="copyEditedJson()" class="px-3 py-1 bg-green-500 text-white text-sm rounded hover:bg-green-600">Copy Edited</button>',html2+="</div>",html2+="</div>",html2+='<div class="space-y-2">',html2+='<h4 class="font-medium text-gray-900">Editable Form</h4>',html2+='<div id="json-form-container" class="p-4 bg-white border border-gray-200 rounded-md max-h-96 overflow-y-auto">',html2+=generateJsonForm(result2.data.parsed||result2.data.original,""),html2+="</div>",html2+='<div class="flex gap-2">',html2+='<button onclick="updateJsonFromForm()" class="px-3 py-1 bg-purple-500 text-white text-sm rounded hover:bg-purple-600">Update JSON</button>',html2+='<button onclick="resetForm()" class="px-3 py-1 bg-gray-500 text-white text-sm rounded hover:bg-gray-600">Reset</button>',html2+="</div>",html2+="</div>",html2+="</div>";else html2+='<div class="p-4 bg-gray-100 rounded-md tool-result-container"><pre class="whitespace-pre-wrap text-sm"><code>'+escapeHtml(JSON.stringify(result2.data,null,2))+"</code></pre></div>";html2+='<div class="flex gap-2">',html2+='<button onclick="copyJsonResult()" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">Copy JSON</button>',html2+='<button onclick="downloadJsonResult()" class="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600">Download JSON</button>',html2+="</div></div>";break;case"html":html2+='<div class="space-y-4">',html2+='<div class="p-4 bg-white border border-gray-200 rounded-md tool-result-container">',html2+=result2.data.result,html2+="</div>",html2+=renderMetadata(result2.data.metadata),html2+='<div class="flex gap-2">',html2+='<button onclick="copyHtmlResult()" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">Copy HTML</button>',html2+='<button onclick="downloadHtmlResult()" class="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600">Download HTML</button>',html2+="</div></div>";break;case"file":if(html2+='<div class="space-y-4">',html2+='<div class="p-4 bg-blue-50 border border-blue-200 rounded-md">',html2+='<div class="flex items-center space-x-3">',html2+='<svg class="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">',html2+='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>',html2+="</svg>",html2+="<div>",html2+='<h4 class="text-lg font-medium text-blue-900">'+escapeHtml(result2.data.fileName||"Generated File")+"</h4>",result2.data.size)html2+='<p class="text-sm text-blue-700">Size: '+escapeHtml(result2.data.size/1024+"")+"kb</p>";if(result2.data.contentType)html2+='<p class="text-sm text-blue-700">Type: '+escapeHtml(result2.data.contentType)+"</p>";html2+="</div>",html2+="</div>",html2+="</div>",html2+=renderMetadata(result2.data.metadata),html2+='<div class="flex gap-2">',html2+='<button onclick="downloadFileResult()" class="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 flex items-center space-x-2">',html2+='<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">',html2+='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>',html2+="</svg>",html2+="<span>Download File</span>",html2+="</button>",html2+="</div></div>";break;case"interactive":if(html2+='<div id="interactive-container" data-result class="w-full"></div>',result2.data.styles)html2+="<style>"+result2.data.styles+"</style>";html2+=renderMetadata(result2.data.metadata);break;case"redirect":if(result2.data.result)html2+='<div class="space-y-4">',html2+='<div class="p-4 bg-blue-50 border border-blue-200 rounded-md">',html2+='<div class="flex items-center space-x-3">',html2+='<svg class="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">',html2+='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>',html2+="</svg>",html2+="<div>",html2+='<h4 class="text-lg font-medium text-blue-900">Redirecting to external tool</h4>',html2+='<p class="text-sm text-blue-700 break-all">'+escapeHtml(result2.data.result)+"</p>",html2+="</div>",html2+="</div>",html2+="</div>",html2+=renderMetadata(result2.data.metadata),html2+='<div class="flex gap-2">',html2+='<button onclick="openRedirectLink()" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 flex items-center space-x-2">',html2+='<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">',html2+='<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>',html2+="</svg>",html2+="<span>Open Tool</span>",html2+="</button>",html2+="</div></div>",setTimeout(()=>{window.open(result2.data.result,"_blank")},1000);else html2+='<div class="p-4 bg-red-50 border border-red-200 rounded-md">',html2+='<p class="text-sm text-red-800">No redirect URL provided</p>',html2+="</div>";break;default:html2+='<div class="p-4 bg-gray-100 rounded-md"><p class="text-sm">Result: '+escapeHtml(JSON.stringify(result2.data.result||result2.data))+"</p></div>"}return html2+="</div>",html2}function renderMetadata(metadata){if(!metadata)return"";return'<div class="mt-4 p-4 bg-gray-50 border border-gray-200 rounded-md"><h5 class="text-sm font-medium text-gray-900 mb-2">Metadata</h5><div class="text-sm text-gray-700"><pre class="whitespace-pre-wrap">'+escapeHtml(JSON.stringify(metadata,null,2))+"</pre></div></div>"}function extractFormData(){let form3=document.getElementById("tool-form");if(!form3)return{};let formData=new FormData(form3),values={},checkboxes=form3.querySelectorAll('input[type="checkbox"]'),checkboxValues={};checkboxes.forEach((checkbox)=>{let checkboxElement=checkbox;checkboxValues[checkboxElement.name]=checkboxElement.checked}),form3.querySelectorAll('input[type="hidden"][id$="_path"]').forEach((input)=>{let inputElement=input,fieldName=inputElement.id.replace("_path",""),value=inputElement.value;if(value)try{let parsed=JSON.parse(value);values[fieldName]=parsed}catch{values[fieldName]=value}});let allFormElements=form3.querySelectorAll("input, select, textarea"),processedKeys=new Set;return allFormElements.forEach((element)=>{let formElement=element,name=formElement.name;if(!name||processedKeys.has(name))return;if(formElement.type==="checkbox")return;if(formElement.type==="file")return;if(name.endsWith("_uploaded"))return;if(processedKeys.add(name),formElement.tagName==="SELECT"){let selectElement=formElement;if(selectElement.multiple){let selectedOptions=Array.from(selectElement.selectedOptions);if(selectedOptions.length===0)return;let selectedValues=selectedOptions.map((option)=>option.value.trim());values[name]=selectedValues}else{let value=selectElement.value.trim();if(value==="")return;values[name]=value}}else if(formElement.type==="number"||formElement.type==="range"){let value=formElement.value.trim();if(value==="")return;let numValue=parseFloat(value);values[name]=isNaN(numValue)?value:numValue}else if(formElement.type==="text"||formElement.tagName==="TEXTAREA"){let value=formElement.value.trim();values[name]=value}else{let value=formElement.value.trim();if(value==="")return;values[name]=value}}),Object.assign(values,checkboxValues),values}async function handleSubmit(e){e.preventDefault(),loading=!0,updateSubmitButton();let isStreaming=!1,missingFiles=[];if(toolData.options.forEach((option)=>{if(option.type==="file"&&option.required){let fieldName=option.name||option.label,hasValueInUploadStates=fileUploadStates[fieldName]&&fileUploadStates[fieldName].uploadedFilePaths.length>0,hiddenInput=document.getElementById(`${fieldName}_path`),hasValueInForm=hiddenInput&&hiddenInput.value.trim()!=="";if(!hasValueInUploadStates&&!hasValueInForm)missingFiles.push(option.label)}}),missingFiles.length>0){showNotification(`Please upload required files: ${missingFiles.join(", ")}`,"error"),loading=!1,updateSubmitButton();return}let processedValues=extractFormData();toolData.options.forEach((option)=>{let fieldName=option.name||option.label;if(option.type==="file"&&fileUploadStates[fieldName]&&fileUploadStates[fieldName].uploadedFilePaths.length>0){if(option.fileCount&&option.fileCount>1||option.multipleFiles){if(processedValues[fieldName]=fileUploadStates[fieldName].uploadedFilePaths,fileUploadStates[fieldName].uploadedOriginalNames&&fileUploadStates[fieldName].uploadedOriginalNames.length>0)processedValues[`${fieldName}OriginalNames`]=fileUploadStates[fieldName].uploadedOriginalNames}else if(processedValues[fieldName]=fileUploadStates[fieldName].uploadedFilePaths[0],fileUploadStates[fieldName].uploadedOriginalNames&&fileUploadStates[fieldName].uploadedOriginalNames.length>0)processedValues[`${fieldName}OriginalNames`]=fileUploadStates[fieldName].uploadedOriginalNames}if(option.type==="number"&&typeof processedValues[fieldName]==="string"){let numValue=parseFloat(processedValues[fieldName]);processedValues[fieldName]=isNaN(numValue)?0:numValue}if(option.type==="range"&&typeof processedValues[fieldName]==="string"){let numValue=parseFloat(processedValues[fieldName]);processedValues[fieldName]=isNaN(numValue)?0:numValue}});try{let response=await fetch(`/${lang}/api/tools/${toolData.id}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(processedValues)});if(response.status==429){showNotification("Rate limit exceeded. Please try again later. (429)","error"),loading=!1,updateSubmitButton(),result={data:{error:"Rate limit exceeded. Please try again later. (429)",message:"Rate limit exceeded. Please try again later. (429)"}};return}let contentType=response.headers.get("content-type");if(console.log(contentType),contentType&&contentType.includes("text/event-stream"))isStreaming=!0,result=await handleStreamResponse(response,toolData);else result=await response.json();if(toolData.resultType==="interactive"&&result.data&&result.data.scripts)setTimeout(()=>{try{new Function("inputs",result.data.scripts)(processedValues)}catch(scriptError){console.error("Error executing interactive scripts:",scriptError)}},100)}catch(error){console.error("Error:",error),result={data:{error:"Processing failed",message:"Processing failed"}}}finally{if(loading=!1,isStreaming)updateSubmitButton();else{let toolResult=document.getElementById("tool-result");if(toolResult){let resultHtml=result?renderResult(result,toolData.resultType):"";toolResult.innerHTML=resultHtml}updateSubmitButton()}}}window.copyResult=function(){if(result&&result.data)copyToClipboard(result.data.result).then(()=>{showNotification("Copied to clipboard!","success")})},window.downloadResult=function(){if(result&&result.data)downloadFile(result.data.result,"result.txt","text/plain")},window.copyStreamMarkdown=function(){let text=typeof _streamPendingText==="string"&&_streamPendingText.length>0?_streamPendingText:document.getElementById("stream-content")?.textContent||"";if(text&&text.length>0)copyToClipboard(text).then(()=>{showNotification("Markdown Copied to Clipboard!","success")})},window.downloadStreamTxt=function(){let text=typeof _streamPendingText==="string"&&_streamPendingText.length>0?_streamPendingText:document.getElementById("stream-content")?.textContent||"";if(text&&text.length>0)downloadFile(text,"result.txt","text/plain"),showNotification("TXT Download Started!","success")},window.downloadImage=function(){if(result&&result.data){let a=document.createElement("a");a.href=result.data.result,a.download="result.png",a.click()}},window.copyJsonResult=function(){if(result&&result.data){let jsonString=JSON.stringify(result.data,null,2);copyToClipboard(jsonString).then(()=>{showNotification("JSON copied to clipboard!","success")})}},window.downloadJsonResult=function(){if(result&&result.data)downloadJson(result.data,"result.json")},window.copyHtmlResult=function(){if(result&&result.data)copyToClipboard(result.data.result).then(()=>{showNotification("HTML copied to clipboard!","success")})},window.downloadHtmlResult=function(){if(result&&result.data)downloadFile(result.data.result,"result.html","text/html")},window.downloadFileResult=function(){if(result&&result.data){let filename=result.data.fileName||"download",contentType=result.data.contentType||"application/octet-stream";if(typeof result.data.filePath==="string"&&(result.data.filePath.startsWith("http")||result.data.filePath.startsWith("/"))){let a=document.createElement("a");a.href=result.data.filePath,a.download=filename,document.body.appendChild(a),a.click(),document.body.removeChild(a),showNotification("File download started!","success")}else downloadFile(result.data,filename,contentType)}},window.openRedirectLink=function(){if(result&&result.data&&result.data.result)window.open(result.data.result,"_blank"),showNotification("Opening external tool...","info")},window.copyOriginalJson=function(){if(result&&result.data&&result.data.original)copyToClipboard(result.data.original).then(()=>{showNotification("Original JSON copied to clipboard!","success")})},window.copyEditedJson=function(){let editedJson=document.getElementById("original-json")?.textContent;if(editedJson)copyToClipboard(editedJson).then(()=>{showNotification("Edited JSON copied to clipboard!","success")})},window.updateJsonFromForm=function(){try{let formContainer=document.getElementById("json-form-container");if(!formContainer){showNotification("Form container not found!","error");return}let updatedData=extractJsonFormData(formContainer),originalJsonElement=document.getElementById("original-json");if(originalJsonElement)originalJsonElement.textContent=JSON.stringify(updatedData,null,2);if(result&&result.data)result.data.parsed=updatedData;showNotification("JSON updated successfully!","success")}catch(error){console.error("Error updating JSON:",error),showNotification("Error updating JSON: "+error.message,"error")}},window.resetForm=function(){try{if(result&&result.data&&result.data.original){let formContainer=document.getElementById("json-form-container");if(formContainer){let originalData=typeof result.data.original==="string"?JSON.parse(result.data.original):result.data.original;formContainer.innerHTML=generateJsonForm(originalData,"");let originalJsonElement=document.getElementById("original-json");if(originalJsonElement)originalJsonElement.textContent=JSON.stringify(originalData,null,2);showNotification("Form reset to original values!","success")}}}catch(error){console.error("Error resetting form:",error),showNotification("Error resetting form: "+error.message,"error")}};function extractJsonFormData(container){let inputs=container.querySelectorAll("input, select"),data={};return inputs.forEach((input)=>{let inputElement=input,path3=inputElement.getAttribute("data-path");if(!path3)return;let value=inputElement.value;if(inputElement.type==="number")value=parseFloat(value)||0;else if(inputElement.tagName==="SELECT")value=value==="true";else if(value===""&&inputElement.placeholder==="null")value=null;setNestedValue2(data,path3,value)}),data}function setNestedValue2(obj,path3,value){if(!path3)return;let keys=path3.split(/[\.\[\]]/).filter((key)=>key!==""),current=obj;for(let i=0;i<keys.length-1;i++){let key=keys[i],nextKey=keys[i+1];if(!current[key])current[key]=/^\d+$/.test(nextKey)?[]:{};current=current[key]}let lastKey=keys[keys.length-1];current[lastKey]=value}window.addArrayItem=function(path3){try{let formContainer=document.getElementById("json-form-container");if(!formContainer)return;let currentData=extractJsonFormData(formContainer),arrayData=getNestedValue(currentData,path3)||[];arrayData.push(""),setNestedValue2(currentData,path3,arrayData),formContainer.innerHTML=generateJsonForm(currentData,""),showNotification("Array item added!","success")}catch(error){console.error("Error adding array item:",error),showNotification("Error adding array item","error")}},window.removeArrayItem=function(path3,index){try{let formContainer=document.getElementById("json-form-container");if(!formContainer)return;let currentData=extractJsonFormData(formContainer),arrayData=getNestedValue(currentData,path3);if(Array.isArray(arrayData)&&index>=0&&index<arrayData.length)arrayData.splice(index,1),setNestedValue2(currentData,path3,arrayData),formContainer.innerHTML=generateJsonForm(currentData,""),showNotification("Array item removed!","success")}catch(error){console.error("Error removing array item:",error),showNotification("Error removing array item","error")}},window.addObjectKey=function(path3){let keyName=prompt("Enter property name:");if(!keyName)return;try{let formContainer=document.getElementById("json-form-container");if(!formContainer)return;let currentData=extractJsonFormData(formContainer),objectData=getNestedValue(currentData,path3)||{};objectData[keyName]="",setNestedValue2(currentData,path3,objectData),formContainer.innerHTML=generateJsonForm(currentData,""),showNotification(`Property "${keyName}" added!`,"success")}catch(error){console.error("Error adding object key:",error),showNotification("Error adding property","error")}},window.removeObjectKey=function(path3,key){try{let formContainer=document.getElementById("json-form-container");if(!formContainer)return;let currentData=extractJsonFormData(formContainer),objectData=getNestedValue(currentData,path3);if(objectData&&typeof objectData==="object"&&!Array.isArray(objectData))delete objectData[key],setNestedValue2(currentData,path3,objectData),formContainer.innerHTML=generateJsonForm(currentData,""),showNotification(`Property "${key}" removed!`,"success")}catch(error){console.error("Error removing object key:",error),showNotification("Error removing property","error")}};function getNestedValue(obj,path3){if(!path3)return obj;let keys=path3.split(/[\.\[\]]/).filter((key)=>key!==""),current=obj;for(let key of keys){if(current===null||current===void 0)return;current=current[key]}return current}function initializeDropzoneForFile(container,fieldName,option,toolId){let dropzone=container.querySelector(".dropzone"),fileInput=container.querySelector('input[type="file"]'),fileList=container.querySelector(".file-list"),uploadProgress=container.querySelector(".upload-progress"),progressBar=container.querySelector(".progress-bar"),progressText=container.querySelector(".progress-text"),addMoreButton=container.querySelector(".add-more-files");if(!dropzone||!fileInput||!fileList||!uploadProgress||!progressBar||!progressText){console.error("Dropzone elements not found");return}let multipleFiles=container.dataset.multiple==="true",maxFiles=parseInt(container.dataset.maxFiles||"1");if(!fileUploadStates[fieldName])fileUploadStates[fieldName]={uploadedFiles:[],uploadedFilePaths:[],uploadedOriginalNames:[]};function updateFileDisplay(){let fileListContainer=document.getElementById(`${fieldName}-file-list`);if(!fileListContainer)return;let{uploadedFiles,uploadedFilePaths}=fileUploadStates[fieldName];if(uploadedFiles.length===0){fileList.classList.add("hidden"),container.querySelector(".dropzone-content")?.classList.remove("hidden");let addMoreButton2=container.querySelector(".add-more-files");if(addMoreButton2)addMoreButton2.classList.add("hidden")}else{fileList.classList.remove("hidden"),container.querySelector(".dropzone-content")?.classList.add("hidden"),fileListContainer.innerHTML=uploadedFiles.map((file2,index)=>` <div class="flex items-center justify-between p-3 bg-gray-50 rounded-md"> <div class="flex items-center"> <svg class="h-8 w-8 text-gray-400 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> </svg> <div> <p class="text-sm font-medium text-gray-900">${file2.name}</p> <p class="text-xs text-gray-500">${(file2.size/1024/1024).toFixed(2)} MB</p> </div> </div> <button type="button" class="text-red-500 hover:text-red-700 remove-file" data-index="${index}"> <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> </svg> </button> </div> `).join(""),fileListContainer.querySelectorAll(".remove-file").forEach((button)=>{button.addEventListener("click",(e)=>{e.stopPropagation();let index=parseInt(e.currentTarget.dataset.index||"0");if(fileUploadStates[fieldName].uploadedFiles.splice(index,1),fileUploadStates[fieldName].uploadedFilePaths.splice(index,1),fileUploadStates[fieldName].uploadedOriginalNames.splice(index,1),updateFileDisplay(),updateFormField(),fileUploadStates[fieldName].uploadedFilePaths.length===0)fileInput.value=""})});let addMoreButton2=container.querySelector(".add-more-files");if(addMoreButton2)if(uploadedFiles.length>=maxFiles)addMoreButton2.classList.add("hidden");else addMoreButton2.classList.remove("hidden")}}function updateFormField(){let{uploadedFilePaths,uploadedOriginalNames}=fileUploadStates[fieldName],hiddenInput=document.getElementById(`${fieldName}_path`);if(!hiddenInput)hiddenInput=document.createElement("input"),hiddenInput.type="hidden",hiddenInput.id=`${fieldName}_path`,hiddenInput.name=`${fieldName}_uploaded`,container.appendChild(hiddenInput);if(hiddenInput)if(multipleFiles)hiddenInput.value=JSON.stringify(uploadedFilePaths);else if(uploadedFilePaths.length>0)hiddenInput.value=uploadedFilePaths[0];else hiddenInput.value="";let originalNamesInput=document.getElementById(`${fieldName}_original_names`);if(!originalNamesInput)originalNamesInput=document.createElement("input"),originalNamesInput.type="hidden",originalNamesInput.id=`${fieldName}_original_names`,originalNamesInput.name=`${fieldName}OriginalNames`,container.appendChild(originalNamesInput);if(originalNamesInput)if(multipleFiles)originalNamesInput.value=JSON.stringify(uploadedOriginalNames);else if(uploadedOriginalNames.length>0)originalNamesInput.value=JSON.stringify(uploadedOriginalNames);else originalNamesInput.value=""}async function handleSingleFileUpload(file2){if(option.fileType){let acceptedTypes=option.fileType,fileType2=file2.type;if(console.log("Frontend file validation:",{fileName:file2.name,fileType:fileType2,acceptedTypes,fileSize:file2.size}),!acceptedTypes.some((type)=>{if(type.startsWith("."))return file2.name.toLowerCase().endsWith(type.toLowerCase());else if(type.includes("/*"))return fileType2.startsWith(type.split("/*")[0]);else return fileType2===type})){showNotification(`File type not supported. Supported types: ${option.fileType.join(", ")}`,"error");return}}if(option.fileLimit){if(file2.size>option.fileLimit){showNotification(`File size exceeds limit (${Math.round(option.fileLimit/1024/1024)}MB)`,"error");return}}uploadProgress.classList.remove("hidden"),progressBar.style.width="0%",progressText.textContent="Uploading... 0%";try{let formData=new FormData;formData.append("file",file2);let result2=await new Promise((resolve,reject)=>{let xhr=new XMLHttpRequest;xhr.upload.onprogress=(e)=>{if(e.lengthComputable){let percent=Math.round(e.loaded/e.total*100);progressBar.style.width=`${percent}%`,progressText.textContent=`Uploading... ${percent}%`}},xhr.onload=()=>{if(xhr.status>=200&&xhr.status<300){progressBar.style.width="100%",progressText.textContent="Upload completed";try{let response=JSON.parse(xhr.responseText);resolve(response)}catch(parseError){reject(new Error("Invalid response from server"))}}else reject(new Error(`Upload failed: ${xhr.statusText}`))},xhr.onerror=()=>{reject(new Error("Network error during upload"))},xhr.onabort=()=>{reject(new Error("Upload was aborted"))},xhr.ontimeout=()=>{reject(new Error("Upload timeout"))},xhr.timeout=30000,xhr.open("POST",`/upload/${toolId}`),xhr.send(formData)});if(multipleFiles)fileUploadStates[fieldName].uploadedFiles.push(file2),fileUploadStates[fieldName].uploadedFilePaths.push(result2.filePath||result2.url||file2.name),fileUploadStates[fieldName].uploadedOriginalNames.push(file2.name);else fileUploadStates[fieldName].uploadedFiles=[file2],fileUploadStates[fieldName].uploadedFilePaths=[result2.filePath||result2.url||file2.name],fileUploadStates[fieldName].uploadedOriginalNames=[file2.name];updateFileDisplay(),updateFormField(),showNotification("File uploaded successfully","success"),setTimeout(()=>{uploadProgress.classList.add("hidden")},2000)}catch(error){console.error("File upload error:",error),progressText.textContent="Upload failed",progressBar.style.width="0%",progressBar.classList.add("bg-red-500"),showNotification(error instanceof Error?error.message:"File upload failed","error"),setTimeout(()=>{uploadProgress.classList.add("hidden"),progressBar.classList.remove("bg-red-500")},3000)}}async function handleMultipleFiles(files){let filesArray=Array.from(files);if(fileUploadStates[fieldName].uploadedFiles.length+filesArray.length>maxFiles){showNotification(`Maximum ${maxFiles} files allowed`,"error");return}for(let file2 of filesArray){if(option.fileType){let acceptedTypes=option.fileType,fileType2=file2.type;if(!acceptedTypes.some((type)=>{if(type.startsWith("."))return file2.name.toLowerCase().endsWith(type.toLowerCase());else if(type.includes("/*"))return fileType2.startsWith(type.split("/*")[0]);else return fileType2===type})){showNotification(`File type not supported: ${file2.name}`,"error");continue}}if(option.fileLimit&&file2.size>option.fileLimit){showNotification(`File size exceeds limit: ${file2.name}`,"error");continue}await handleSingleFileUpload(file2)}}if(dropzone.addEventListener("dragover",(e)=>{e.preventDefault(),dropzone.classList.add("border-blue-500","bg-blue-50")}),dropzone.addEventListener("dragleave",(e)=>{e.preventDefault(),dropzone.classList.remove("border-blue-500","bg-blue-50")}),dropzone.addEventListener("drop",(e)=>{e.preventDefault(),dropzone.classList.remove("border-blue-500","bg-blue-50");let files=e.dataTransfer?.files;if(files&&files.length>0)if(multipleFiles)handleMultipleFiles(files);else handleSingleFileUpload(files[0])}),dropzone.addEventListener("click",()=>{if(multipleFiles&&fileUploadStates[fieldName].uploadedFiles.length>=maxFiles){showNotification(`Maximum ${maxFiles} files allowed`,"error");return}fileInput.value="",fileInput.click()}),fileInput.addEventListener("change",(e)=>{let files=e.target.files;if(files&&files.length>0)if(multipleFiles)handleMultipleFiles(files);else handleSingleFileUpload(files[0])}),addMoreButton)addMoreButton.addEventListener("click",(e)=>{if(e.stopPropagation(),fileUploadStates[fieldName].uploadedFiles.length>=maxFiles){showNotification(`Maximum ${maxFiles} files allowed`,"error");return}fileInput.value="",fileInput.click()})}async function handleStreamResponse(response,toolData2){let reader=response.body?.getReader(),decoder=new TextDecoder,fullText="",buffer="";if(_streamRenderScheduled=!1,_streamLastRenderTs=0,_streamLastRenderedLen=0,_streamPendingText="",console.log("start sse"),!reader)throw new Error("Response body is not readable");let resultDiv=document.getElementById("tool-result");if(resultDiv)resultDiv.innerHTML=` <div class="stream-result-container"> <div class="flex items-center mb-4"> <div class="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600 mr-3"></div> <span class="text-gray-600">AI\u6B63\u5728\u5206\u6790\u4E2D\uFF0C\u8BF7\u7A0D\u5019...</span> </div> <div id="stream-content" class="prose max-w-none"></div> </div> `;try{while(!0){let{done,value}=await reader.read();if(done){if(buffer.includes(` `)){let events=buffer.split(/\n\n/);for(let evt of events){let line=evt.trim();if(line.startsWith("data: "))try{let data=JSON.parse(line.slice(6));if(data.type==="stream"&&data.chunk)fullText+=data.chunk,updateStreamDisplay(data.chunk,fullText,toolData2);else if(data.type==="done")finalizeStreamDisplay(fullText,toolData2);else if(data.type==="error")throw new Error(data.error)}catch(parseError){console.error("Error parsing final SSE data:",parseError)}}}break}buffer+=decoder.decode(value,{stream:!0});let delimiterIndex=buffer.indexOf(` `);while(delimiterIndex!==-1){let eventStr=buffer.slice(0,delimiterIndex);buffer=buffer.slice(delimiterIndex+2);let line=eventStr.trim();if(line.startsWith("data: "))try{let data=JSON.parse(line.slice(6));if(data.type==="stream"&&data.chunk)fullText+=data.chunk,updateStreamDisplay(data.chunk,fullText,toolData2);else if(data.type==="done")finalizeStreamDisplay(fullText,toolData2);else if(data.type==="error")throw new Error(data.error)}catch(parseError){console.error("Error parsing SSE data:",parseError)}delimiterIndex=buffer.indexOf(` `)}}}catch(error){throw console.error("Stream reading error:",error),error}return{data:{result:fullText,metadata:{isStream:!0,streamLength:fullText.length}}}}let STREAM_THROTTLE_MS=80,_streamRenderScheduled=!1,_streamLastRenderTs=0,_streamLastRenderedLen=0,_streamPendingText="";function updateStreamDisplay(chunk,fullText,toolData2){if(fullText.length<=_streamLastRenderedLen)return;if(_streamPendingText=fullText,_streamRenderScheduled)return;_streamRenderScheduled=!0;let renderLatest=()=>{let now=typeof performance!=="undefined"&&performance.now?performance.now():Date.now(),elapsed=now-_streamLastRenderTs;if(elapsed<STREAM_THROTTLE_MS){setTimeout(renderLatest,STREAM_THROTTLE_MS-elapsed);return}let streamContent=document.getElementById("stream-content");if(streamContent){if(toolData2&&toolData2.ignoreResultFormat===!0)streamContent.innerHTML=`<pre class="whitespace-pre-wrap text-sm">${_streamPendingText.replace(/</g,"<").replace(/>/g,">")}</pre>`;else{let md=window.markdownit&&typeof window.markdownit.render==="function"?window.markdownit:null;if(md)streamContent.innerHTML=md.render(_streamPendingText);else{let formattedText=_streamPendingText.replace(/\n\n/g,"</p><p>").replace(/\n/g,"<br>").replace(/^/,"<p>").replace(/$/,"</p>");streamContent.innerHTML=formattedText}}streamContent.scrollTop=streamContent.scrollHeight}_streamLastRenderedLen=_streamPendingText.length,_streamLastRenderTs=now,_streamRenderScheduled=!1};if(typeof requestAnimationFrame==="function")requestAnimationFrame(renderLatest);else setTimeout(renderLatest,STREAM_THROTTLE_MS)}function finalizeStreamDisplay(fullText,toolData2){let resultDiv=document.getElementById("tool-result");if(resultDiv){let loadingIndicator=resultDiv.querySelector(".animate-spin");if(loadingIndicator)loadingIndicator.parentElement?.remove();let streamContent=document.getElementById("stream-content");if(streamContent&&fullText.length>_streamLastRenderedLen){if(toolData2&&toolData2.ignoreResultFormat===!0)streamContent.innerHTML=`<pre class="whitespace-pre-wrap text-sm">${fullText.replace(/</g,"<").replace(/>/g,">")}</pre>`;else{let md=window.markdownit&&typeof window.markdownit.render==="function"?window.markdownit:null;if(md)streamContent.innerHTML=md.render(fullText);else{let formattedText=fullText.replace(/\n\n/g,"</p><p>").replace(/\n/g,"<br>").replace(/^/,"<p>").replace(/$/,"</p>");streamContent.innerHTML=formattedText}}streamContent.scrollTop=streamContent.scrollHeight,_streamLastRenderedLen=fullText.length}let streamContainer=resultDiv.querySelector(".stream-result-container");if(streamContainer){let completionDiv=document.createElement("div");completionDiv.className="mt-4 p-3 bg-green-50 border border-green-200 rounded-md",completionDiv.innerHTML=` <div class="flex items-center text-green-800"> <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path> </svg> <span>\u5206\u6790\u5B8C\u6210</span> </div> `,streamContainer.appendChild(completionDiv);let actionsDiv=document.createElement("div");actionsDiv.className="mt-3 flex gap-2",actionsDiv.innerHTML=` <button onclick="copyStreamMarkdown()" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">Copy</button> <button onclick="downloadStreamTxt()" class="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600">Download</button> `,streamContainer.appendChild(actionsDiv)}}}let form2=document.getElementById("tool-form"),submitButton=document.getElementById("submit-button");if(form2&&submitButton)form2.addEventListener("submit",handleSubmit),toolData.options.forEach((option)=>{if(option.type==="file"){let fieldName=option.name||option.label,container=document.querySelector(`[data-field="${fieldName}"]`);if(container)initializeDropzoneForFile(container,fieldName,option,toolData.id)}})} initializeToolForm(toolData, 'pt'); const params = new URLSearchParams(window.location.search); const sampleParam = params.get('sample'); if (sampleParam) { const sampleSection = document.getElementById('tool-samples'); if (sampleSection) { sampleSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } }); </script> <script> try { const tool = { id: "heading-hierarchy-auditor", name: "Heading Hierarchy Auditor", description: "Audit heading structure from a live URL or pasted HTML, then flag skipped levels, multiple h1 usage, style-only heading abuse, and title metadata drift", category: "Validation", path: "/pt/tools/heading-hierarchy-auditor", translations: {"en":{"name":"Heading Hierarchy Auditor","description":"Audit heading structure from a live URL or pasted HTML, then flag skipped levels, multiple h1 usage, style-only heading abuse, and title metadata drift","detail":"Paste HTML or provide a page URL to inspect heading semantics rather than visual reading order. The report surfaces skipped heading levels, multiple h1 nodes, likely style-only heading misuse, empty headings, and whether the main h1 drifts away from the document title or Open Graph title.\n\nHow to use it:\n- HTML Input: paste raw markup for deterministic checks\n- Page URL: fetch a live page when you want to inspect the published structure\n- Compare With Metadata Titles: checks the primary heading against `<title>` and `og:title`\n- Show Fix Suggestions: includes a suggested heading level or replacement strategy for each node\n\nWhat the report highlights:\n- A visual hierarchy tree for h1-h6 nodes\n- Heatmap-like severity distribution across headings\n- Suggested replacement level for each heading node\n- Multiple h1 and skipped-level warnings\n- Likely style-only UI labels disguised as headings\n- Detachment between h1 and metadata titles","keywords":["heading hierarchy","html headings","h1","seo","semantic audit","content structure"],"options":{"HTML Input":"HTML Input","Page URL":"Page URL","Compare With Metadata Titles":"Compare With Metadata Titles","Show Fix Suggestions":"Show Fix Suggestions"}},"zh":{"name":"标题层级可读性审计器","description":"从在线 URL 或粘贴的 HTML 审计标题结构,识别跳级、多重 h1、纯样式化标题滥用,以及与标题元数据的脱节问题","detail":"粘贴 HTML 或提供页面 URL,检查页面的标题语义,而不是视觉阅读顺序。报告会指出标题跳级、多个 h1、疑似仅用于样式的标题、空标题,以及主 h1 是否与文档 title / og:title 脱节。\n\n使用方式:\n- HTML 输入:粘贴源码做稳定分析\n- 页面 URL:抓取线上页面结构\n- 比对标题元数据:检查主标题与 `<title>`、`og:title` 是否一致\n- 显示修复建议:为每个标题节点给出建议级别或替代方案\n\n报告重点:\n- h1-h6 的可视化层级树\n- 按严重级别分布的热力提示\n- 每个标题节点建议改成什么级别\n- 多个 h1 与跳级警告\n- 疑似把 UI 标签误用为标题的情况\n- h1 与元数据标题的脱节","keywords":["标题层级","标题结构","语义","html","seo"],"options":{"HTML Input":"HTML 输入","Page URL":"页面 URL","Compare With Metadata Titles":"比对标题元数据","Show Fix Suggestions":"显示修复建议"}},"es":{"name":"Auditor de Jerarquía de Títulos","description":"Audita la estructura de títulos desde una URL o HTML pegado y detecta saltos de nivel, varios h1, abuso de headings solo visuales y desconexión con los títulos meta","detail":"Pega HTML o proporciona una URL para inspeccionar la semántica de títulos en lugar del orden visual. El informe muestra saltos de nivel, múltiples h1, headings usados solo por estilo, headings vacíos y si el h1 principal se aleja del <title> o del og:title.\n\nCómo usarlo:\n- Entrada HTML: pega el marcado para un análisis estable\n- URL de página: inspecciona la estructura publicada\n- Comparar con títulos meta: contrasta el heading principal con `<title>` y `og:title`\n- Mostrar sugerencias: añade una recomendación de nivel o reemplazo por nodo\n\nQué destaca:\n- Árbol visual de h1-h6\n- Distribución de severidad tipo heatmap\n- Nivel sugerido para cada heading\n- Múltiples h1 y saltos de nivel\n- Etiquetas de UI disfrazadas de heading\n- Desalineación entre h1 y títulos meta","keywords":["jerarquía de títulos","semántica","html","seo"],"options":{"HTML Input":"Entrada HTML","Page URL":"URL de pagina","Compare With Metadata Titles":"Comparar con títulos meta","Show Fix Suggestions":"Mostrar sugerencias"}},"ru":{"name":"Аудитор Иерархии Заголовков","description":"Проверяет структуру заголовков по URL или вставленному HTML и находит пропуски уровней, несколько h1, декоративное злоупотребление headings и расхождение с мета-заголовками","detail":"Вставьте HTML или укажите URL страницы, чтобы проверить семантику заголовков, а не визуальный порядок. Отчёт показывает пропуски уровней, несколько h1, вероятно декоративные headings, пустые заголовки и расхождение основного h1 с <title> и og:title.\n\nКак использовать:\n- HTML ввод: вставьте разметку для стабильной проверки\n- URL страницы: проверьте опубликованную структуру\n- Сравнить с мета-заголовками: сопоставляет основной heading с `<title>` и `og:title`\n- Показать исправления: добавляет рекомендуемый уровень или замену для каждого узла\n\nЧто показывает отчёт:\n- Визуальное дерево h1-h6\n- Распределение серьёзности в формате heatmap\n- Рекомендуемый уровень для каждого heading\n- Несколько h1 и пропуски уровней\n- Вероятное использование heading только ради стиля\n- Расхождение между h1 и мета-заголовками","keywords":["иерархия заголовков","семантика","html","seo"],"options":{"HTML Input":"HTML ввод","Page URL":"URL страницы","Compare With Metadata Titles":"Сравнить с мета-заголовками","Show Fix Suggestions":"Показать исправления"}},"fr":{"name":"Auditeur de Hiérarchie des Titres","description":"Audite la structure des titres depuis une URL ou du HTML collé et détecte les sauts de niveau, les h1 multiples, les headings purement décoratifs et la dérive avec les titres meta","detail":"Collez du HTML ou fournissez une URL pour inspecter la sémantique des titres plutôt que l’ordre visuel. Le rapport signale les sauts de niveau, les h1 multiples, les headings utilisés seulement pour le style, les titres vides et l’écart entre le h1 principal, le <title> et og:title.\n\nMode d’emploi :\n- Entrée HTML : collez le code pour une analyse stable\n- URL de page : inspecte la structure publiée\n- Comparer aux titres meta : compare le heading principal avec `<title>` et `og:title`\n- Afficher les corrections : ajoute une recommandation de niveau ou de remplacement par nœud\n\nLe rapport met en avant :\n- Un arbre visuel des h1-h6\n- Une répartition de gravité façon heatmap\n- Le niveau conseillé pour chaque heading\n- Les h1 multiples et les sauts de niveau\n- Les libellés UI déguisés en heading\n- Le décalage entre h1 et titres meta","keywords":["hiérarchie des titres","sémantique","html","seo"],"options":{"HTML Input":"Entree HTML","Page URL":"URL de page","Compare With Metadata Titles":"Comparer aux titres meta","Show Fix Suggestions":"Afficher les corrections"}},"de":{"name":"Auditor für Überschriftenhierarchie","description":"Prüft die Überschriftenstruktur aus URL oder eingefügtem HTML und erkennt Sprünge, mehrere h1, rein dekorativen Heading-Missbrauch und Abweichungen zu Meta-Titeln","detail":"Füge HTML ein oder gib eine Seiten-URL an, um die semantische Überschriftenstruktur statt der visuellen Leserichtung zu prüfen. Der Bericht zeigt Ebenensprünge, mehrere h1, vermutlich nur dekorative Heading-Nutzung, leere Überschriften und Abweichungen zwischen dem Haupt-h1, <title> und og:title.\n\nSo verwendest du das Tool:\n- HTML-Eingabe: Markup für eine stabile Analyse einfügen\n- Seiten-URL: veröffentlichte Struktur prüfen\n- Mit Metadaten-Titeln vergleichen: gleicht die Hauptüberschrift mit `<title>` und `og:title` ab\n- Korrekturhinweise anzeigen: ergänzt eine empfohlene Ebene oder Ersetzung pro Knoten\n\nDer Bericht zeigt:\n- Einen visuellen h1-h6-Baum\n- Eine Heatmap-artige Schwereverteilung\n- Die empfohlene Ebene für jede Überschrift\n- Mehrere h1 und Ebenensprünge\n- Als Heading verkleidete UI-Labels\n- Abweichungen zwischen h1 und Meta-Titeln","keywords":["überschriftenhierarchie","semantik","html","seo"],"options":{"HTML Input":"HTML-Eingabe","Page URL":"Seiten-URL","Compare With Metadata Titles":"Mit Metadaten-Titeln vergleichen","Show Fix Suggestions":"Korrekturhinweise anzeigen"}},"pt":{"name":"Auditor de Hierarquia de Títulos","description":"Audita a estrutura de títulos a partir de uma URL ou HTML colado e detecta saltos de nível, múltiplos h1, uso visual indevido de headings e desalinhamento com títulos meta","detail":"Cole HTML ou forneça uma URL para inspecionar a semântica dos títulos em vez da ordem visual. O relatório mostra saltos de nível, múltiplos h1, headings usados só para estilo, headings vazios e se o h1 principal se afasta do <title> e do og:title.\n\nComo usar:\n- Entrada HTML: cole a marcação para uma análise estável\n- URL da página: inspeciona a estrutura publicada\n- Comparar com títulos meta: confronta o heading principal com `<title>` e `og:title`\n- Mostrar sugestões: adiciona um nível recomendado ou substituição por nó\n\nO que o relatório destaca:\n- Árvore visual de h1-h6\n- Distribuição de severidade em estilo heatmap\n- Nível sugerido para cada heading\n- Múltiplos h1 e saltos de nível\n- Rótulos de UI disfarçados de heading\n- Desalinhamento entre h1 e títulos meta","keywords":["hierarquia de títulos","semântica","html","seo"],"options":{"HTML Input":"Entrada HTML","Page URL":"URL da pagina","Compare With Metadata Titles":"Comparar com títulos meta","Show Fix Suggestions":"Mostrar correcoes"}}} }; console.log(tool); const RECENT_KEY = 'recent-tools'; const stored = localStorage.getItem(RECENT_KEY); let list = []; try { list = stored ? JSON.parse(stored) : []; if (!Array.isArray(list)) list = []; } catch (e) { list = []; } // Remove if exists (to move to top) list = list.filter(t => t.id !== tool.id); // Add to front list.unshift(tool); // Keep max 10 list = list.slice(0, 10); localStorage.setItem(RECENT_KEY, JSON.stringify(list)); // Update UI if function exists if (window.renderRecentTools) window.renderRecentTools(); } catch (e) { console.error('Failed to update recent tools:', e); } </script> </body> </html>