Auditor de Jerarquía de Títulos

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

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 o del og:title.</p> <p>Cómo usarlo:</p> <ul> <li>Entrada HTML: pega el marcado para un análisis estable</li> <li>URL de página: inspecciona la estructura publicada</li> <li>Comparar con títulos meta: contrasta el heading principal con <code><title></code> y <code>og:title</code></li> <li>Mostrar sugerencias: añade una recomendación de nivel o reemplazo por nodo</li> </ul> <p>Qué destaca:</p> <ul> <li>Árbol visual de h1-h6</li> <li>Distribución de severidad tipo heatmap</li> <li>Nivel sugerido para cada heading</li> <li>Múltiples h1 y saltos de nivel</li> <li>Etiquetas de UI disfrazadas de heading</li> <li>Desalineación entre h1 y 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> Resultados de ejemplo </h3> <span class="text-xs text-amber-700">1 Ejemplos</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 una landing con saltos de nivel y h1 duplicados</h4> <p class="text-xs text-gray-500 mt-1">Revisa el árbol de títulos, detecta h1 duplicados y ve qué nodos deberían bajar de nivel o dejar de ser headings.</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" > Cargar ejemplo </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 de 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 con 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 sugerencias </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"> Procesar </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="/es" class="hover:text-blue-600">Home</a></li><li class="flex items-center gap-2"><span>/</span><a href="/es/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">Datos clave</h2> <dl class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div> <dt class="text-xs uppercase tracking-wide text-gray-500">Categoría</dt> <dd class="mt-1 text-sm font-medium text-gray-900">Seguridad y validación</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 salida</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 muestras</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 disponible</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">Resumen</h2> <p class="text-gray-700 leading-7" itemprop="description">El Auditor de Jerarquía de Títulos es una herramienta diseñada para analizar la estructura semántica de encabezados (h1-h6) en sus páginas web, ya sea pegando el código HTML directamente o ingresando una URL activa. Permite identificar de forma rápida errores críticos de SEO y accesibilidad, como saltos de nivel, múltiples etiquetas h1, encabezados vacíos o discrepancias con los títulos meta y etiquetas Open Graph.</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">Cuándo usarlo</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>Al realizar auditorías de SEO técnico para asegurar que los motores de búsqueda indexen correctamente la estructura del contenido.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Durante el desarrollo o rediseño de plantillas web para evitar que elementos visuales de la interfaz de usuario se etiqueten erróneamente como encabezados.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Al optimizar la accesibilidad web garantizando que los lectores de pantalla naveguen por un árbol de títulos lógico y ordenado.</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">Cómo 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>Introduzca el código fuente HTML en el área de texto o proporcione la URL de la página web que desea analizar.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Configure las opciones de análisis, decidiendo si desea comparar el encabezado principal con las etiquetas meta y si prefiere activar las sugerencias automáticas de corrección.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Ejecute la auditoría para generar un árbol visual interactivo que resalta la distribución de severidad de los errores encontrados.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Revise las advertencias específicas de saltos de nivel, etiquetas h1 duplicadas y las recomendaciones de reemplazo para cada nodo.</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">Detección de saltos de nivel y encabezados vacíos en plantillas de blogs o gestores de contenido.</div><div class="rounded-lg bg-slate-50 border border-slate-200 p-4 text-sm text-gray-700">Comparación del encabezado H1 principal con las etiquetas de título SEO y Open Graph para asegurar la coherencia del contenido.</div><div class="rounded-lg bg-slate-50 border border-slate-200 p-4 text-sm text-gray-700">Identificación de elementos visuales de la interfaz de usuario que se han maquetado incorrectamente con etiquetas H2-H6.</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">Ejemplos</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. Corrección de jerarquía en una página de aterrizaje</h4> <span class="shrink-0 rounded-full bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700">Especialista en SEO Técnico</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">Una landing page de producto no está posicionando bien y se sospecha de problemas en la estructura de etiquetas HTML.</dd> </div> <div> <dt class="font-medium text-gray-900">Problema</dt> <dd class="mt-1 text-gray-700 leading-6">La página tiene dos etiquetas H1 y salta directamente de H1 a H4 en la sección de características.</dd> </div> <div> <dt class="font-medium text-gray-900">Cómo usarlo</dt> <dd class="mt-1 text-gray-700 leading-6">Pegue el código HTML de la landing page en el cuadro de texto, active la comparación con títulos meta y las sugerencias de corrección.</dd> </div> <div> <dt class="font-medium text-gray-900">Configuración de ejemplo</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>Producto</h1><h1>Comprar</h1><h4>Características</h4></body></html>", compareWithMetadataTitles: true, 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">El auditor genera un árbol visual que marca en rojo el H1 duplicado y sugiere cambiar los nodos H4 a H2 para mantener la coherencia semántica.</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. Auditoría de URL en vivo para control de calidad</h4> <span class="shrink-0 rounded-full bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700">Desarrollador Front-end</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">Se acaba de desplegar una nueva sección de preguntas frecuentes en producción y se requiere validar la accesibilidad.</dd> </div> <div> <dt class="font-medium text-gray-900">Problema</dt> <dd class="mt-1 text-gray-700 leading-6">Los lectores de pantalla reportan una navegación confusa debido a posibles encabezados vacíos o desordenados.</dd> </div> <div> <dt class="font-medium text-gray-900">Cómo usarlo</dt> <dd class="mt-1 text-gray-700 leading-6">Introduzca la URL de la sección de preguntas frecuentes en el campo correspondiente y ejecute el análisis.</dd> </div> <div> <dt class="font-medium text-gray-900">Configuración de ejemplo</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://example.com/faq", 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">Se detectan tres etiquetas H3 vacías utilizadas para espaciado visual y se recomienda reemplazarlas por clases CSS de margen.</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">Probar con muestras</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="/es/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 Nombrados de Regex</div> <div class="text-sm text-gray-600 mt-1">Colección de patrones de regex que usan grupos de captura nombrados para extraer datos estructurados de texto. Los grupos nombrados hacen que los patrones sean más legibles y mantenibles asignando nombres significativos a las 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="/es/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">Ejemplos Contentful CMS Basado en la Nube</div> <div class="text-sm text-gray-600 mt-1">Ejemplos completos de Contentful cubriendo modelado de contenido, integración de API, webhooks y patrones de integración 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="/es/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">Muestras de HTML con Imágenes</div> <div class="text-sm text-gray-600 mt-1">Muestras de código fuente HTML con imágenes para probar la extracción</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="/es/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">Muestras JWT</div> <div class="text-sm text-gray-600 mt-1">Ejemplos completos de JWT desde la estructura básica de tokens hasta implementaciones de seguridad avanzadas</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="/es/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">Herramientas de extracción, limpieza y exportación HTML a Markdown/PDF</div> <div class="text-sm text-gray-600 mt-1">Compara herramientas de limpieza HTML, extracción de atributos, extracción de imágenes, HTML a Markdown y HTML a PDF en un solo hub para flujos de conversión de contenido web.</div> </a> <a href="/es/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">Herramientas de auditoria de accesibilidad web y color accesible</div> <div class="text-sm text-gray-600 mt-1">Revisa problemas WCAG, orden de lector de pantalla, fallos de contraste, riesgo de vision cromatica y elecciones de color accesible en un solo hub para flujos de accesibilidad web.</div> </a> <a href="/es/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">Herramientas de codificacion y conversion de audio</div> <div class="text-sm text-gray-600 mt-1">Compara conversion de formatos de audio, cambios de bitrate, conversion de frecuencia de muestreo, cambio de codec y exportacion en un solo hub.</div> </a> <a href="/es/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">Herramientas de conversion de formatos de imagen y exportacion animada</div> <div class="text-sm text-gray-600 mt-1">Compara convertidores de imagen para JPG, PNG, GIF, AVIF, WebP, TIFF, ICO, base64 y salidas pensadas para animacion en un solo 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">Herramientas relacionadas</h3> <div class="grid grid-cols-1 md:grid-cols-2 gap-3"> <a href="/es/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 lector de pantalla</div> <div class="text-sm text-gray-600 mt-1">Simula el orden de lectura y la semantica hablada de un lector a partir de una URL o HTML</div> </a> <a href="/es/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">Extractor de Atributos HTML</div> <div class="text-sm text-gray-600 mt-1">Extrae atributos especificados (href, src, data-*, etc.) del contenido HTML con soporte de filtrado de nombres de etiquetas</div> </a> <a href="/es/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">Extractor de Fuentes de Imagen</div> <div class="text-sm text-gray-600 mt-1">Extrae URLs de imagen (atributos src) del código fuente HTML. Admite imágenes de carga diferida y atributos srcset.</div> </a> <a href="/es/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">Extractor Meta Tags</div> <div class="text-sm text-gray-600 mt-1">Extrae y analiza meta etiquetas, Open Graph, Twitter Cards y datos estructurados de páginas web</div> </a> <a href="/es/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">Eliminador de Etiquetas HTML</div> <div class="text-sm text-gray-600 mt-1">Elimina etiquetas HTML del código y extrae contenido de texto limpio</div> </a> <a href="/es/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">Extractor de tablas PDF a CSV/JSON</div> <div class="text-sm text-gray-600 mt-1">Extrae tablas de PDF con OpenDataLoader y las exporta como JSON estructurado, CSV o HTML</div> </a> <a href="/es/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">Extractor de imagenes y captions PDF</div> <div class="text-sm text-gray-600 mt-1">Extrae imagenes PDF, empareja captions cercanos y genera un indice HTML navegable</div> </a> <a href="/es/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 accesibilidad</div> <div class="text-sm text-gray-600 mt-1">Detecta problemas comunes de WCAG 2.1 en HTML, paginas o imagenes de diseno y devuelve sugerencias accionables</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">Preguntas frecuentes</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">¿Por qué es perjudicial tener múltiples etiquetas H1 en una página?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Confunde a los motores de búsqueda sobre cuál es el tema principal y diluye la relevancia semántica del contenido.</p> </details> <details class="rounded-lg border border-gray-200 p-4 bg-gray-50"> <summary class="font-medium text-gray-900 cursor-pointer">¿Qué significa un salto de nivel en los encabezados?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Ocurre cuando se pasa de un nivel a otro sin seguir el orden lógico, por ejemplo, saltar de un H1 directamente a un H3.</p> </details> <details class="rounded-lg border border-gray-200 p-4 bg-gray-50"> <summary class="font-medium text-gray-900 cursor-pointer">¿Cómo detecta la herramienta los encabezados usados solo por estilo?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Identifica etiquetas de encabezado que contienen textos cortos de interfaz de usuario o botones que no estructuran contenido real.</p> </details> <details class="rounded-lg border border-gray-200 p-4 bg-gray-50"> <summary class="font-medium text-gray-900 cursor-pointer">¿Es necesario que el H1 coincida exactamente con el título meta?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">No es obligatorio que sean idénticos, pero una desconexión total confunde al usuario y afecta negativamente al SEO.</p> </details> <details class="rounded-lg border border-gray-200 p-4 bg-gray-50"> <summary class="font-medium text-gray-900 cursor-pointer">¿Puedo auditar páginas que aún no están publicadas en internet?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Sí, copiando y pegando directamente el código HTML en el campo de entrada de texto de la herramienta.</p> </details> </div> </section> </div> <script type="application/ld+json">{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿Por qué es perjudicial tener múltiples etiquetas H1 en una página?","acceptedAnswer":{"@type":"Answer","text":"Confunde a los motores de búsqueda sobre cuál es el tema principal y diluye la relevancia semántica del contenido."}},{"@type":"Question","name":"¿Qué significa un salto de nivel en los encabezados?","acceptedAnswer":{"@type":"Answer","text":"Ocurre cuando se pasa de un nivel a otro sin seguir el orden lógico, por ejemplo, saltar de un H1 directamente a un H3."}},{"@type":"Question","name":"¿Cómo detecta la herramienta los encabezados usados solo por estilo?","acceptedAnswer":{"@type":"Answer","text":"Identifica etiquetas de encabezado que contienen textos cortos de interfaz de usuario o botones que no estructuran contenido real."}},{"@type":"Question","name":"¿Es necesario que el H1 coincida exactamente con el título meta?","acceptedAnswer":{"@type":"Answer","text":"No es obligatorio que sean idénticos, pero una desconexión total confunde al usuario y afecta negativamente al SEO."}},{"@type":"Question","name":"¿Puedo auditar páginas que aún no están publicadas en internet?","acceptedAnswer":{"@type":"Answer","text":"Sí, copiando y pegando directamente el código HTML en el campo de entrada de texto de la herramienta."}}]}</script> <script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://elysiatools.com/es"},{"@type":"ListItem","position":2,"name":"Tools","item":"https://elysiatools.com/es/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">Documentación de la 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">Punto final de la solicitud</h3> <div class="bg-gray-800 rounded p-3 font-mono text-sm text-white"> POST /es/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 de la solicitud</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">Nombre del 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">Descripción</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">No</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">No</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">No</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">No</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 respuesta</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">Documentación 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"> Agregue este herramienta a su configuración 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 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", "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>Puede encadenar múltiples herramientas, por ejemplo: `https://elysiatools.com/mcp/sse?toolId=png-to-webp,jpg-to-webp,gif-to-webp`, máximo 20 herramientas.</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> Si encuentra algún problema, por favor, póngase en contacto con nosotros en 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: es is injected by the server-side renderSharedScripts function const currentLang = 'es'; // 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('/es/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="/es/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('es'); // Initialize tool form const toolData = {"id":"heading-hierarchy-auditor","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","category":"Validation","keywords":["jerarquía 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 de 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 con títulos meta","default":true,"validate":{"def":{"type":"optional","innerType":{"def":{"type":"boolean"},"type":"boolean"}},"type":"optional"},"options":[]},{"type":"checkbox","name":"showFixSuggestions","label":"Mostrar sugerencias","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, 'es'); 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: "/es/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>