Auditeur de Hiérarchie des Titres

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

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 et og:title.</p> <p>Mode d’emploi :</p> <ul> <li>Entrée HTML : collez le code pour une analyse stable</li> <li>URL de page : inspecte la structure publiée</li> <li>Comparer aux titres meta : compare le heading principal avec <code><title></code> et <code>og:title</code></li> <li>Afficher les corrections : ajoute une recommandation de niveau ou de remplacement par nœud</li> </ul> <p>Le rapport met en avant :</p> <ul> <li>Un arbre visuel des h1-h6</li> <li>Une répartition de gravité façon heatmap</li> <li>Le niveau conseillé pour chaque heading</li> <li>Les h1 multiples et les sauts de niveau</li> <li>Les libellés UI déguisés en heading</li> <li>Le décalage entre h1 et titres 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> Exemples de résultats </h3> <span class="text-xs text-amber-700">1 Exemples</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">Auditer une landing avec niveaux sautés et h1 dupliqués</h4> <p class="text-xs text-gray-500 mt-1">Affiche l’arbre des titres, détecte les h1 multiples et suggère quels nœuds doivent devenir h2 ou du texte non structurel.</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" > Charger exemple </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"> Voir paramètres d'entrée </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"> Entree 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 page </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"> Comparer aux titres 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"> Afficher les corrections </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"> Traiter </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="/fr" class="hover:text-blue-600">Home</a></li><li class="flex items-center gap-2"><span>/</span><a href="/fr/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">Points clés</h2> <dl class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div> <dt class="text-xs uppercase tracking-wide text-gray-500">Catégorie</dt> <dd class="mt-1 text-sm font-medium text-gray-900">Sécurité et validation</dd> </div> <div> <dt class="text-xs uppercase tracking-wide text-gray-500">Types d’entrée</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">Type de sortie</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">Couverture des échantillons</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">Vue d’ensemble</h2> <p class="text-gray-700 leading-7" itemprop="description">L'Auditeur de Hiérarchie des Titres analyse la structure sémantique de vos pages web à partir de code HTML collé ou d'une URL afin de détecter les erreurs de balisage SEO et d'accessibilité. Il génère un rapport détaillé mettant en évidence les sauts de niveau, les balises H1 multiples, les titres vides et les écarts de cohérence avec vos balises meta comme le Title et l'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">Quand l’utiliser</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>Lors de l'audit SEO technique d'une page pour valider la structure logique des balises H1 à H6.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Avant la mise en ligne d'un modèle de page pour s'assurer que les éléments d'interface ne sont pas balisés comme des titres.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Pour vérifier la cohérence sémantique entre le titre principal H1 de la page et ses balises meta de référencement.</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">Comment ça marche</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>Collez votre code source HTML dans la zone de texte ou saisissez l'URL de la page web à analyser.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Activez les options pour comparer la structure avec les métadonnées et afficher les suggestions de correction.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Analysez le rapport généré contenant l'arbre visuel des titres et la répartition des erreurs par niveau de gravité.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Appliquez les corrections recommandées pour chaque nœud problématique afin de restaurer une hiérarchie sémantique correcte.</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">Cas d’usage</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">Optimisation du référencement naturel en éliminant les H1 multiples et en alignant le titre principal avec la balise Title.</div><div class="rounded-lg bg-slate-50 border border-slate-200 p-4 text-sm text-gray-700">Audit d'accessibilité numérique pour garantir une navigation logique aux utilisateurs de technologies d'assistance.</div><div class="rounded-lg bg-slate-50 border border-slate-200 p-4 text-sm text-gray-700">Nettoyage du code HTML généré par des constructeurs de page qui abusent des balises Hn à des fins de mise en page.</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">Exemples</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. Correction d'une landing page avec H1 dupliqués</h4> <span class="shrink-0 rounded-full bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700">Intégrateur Web</span> </div> <dl class="space-y-4 text-sm"> <div> <dt class="font-medium text-gray-900">Contexte</dt> <dd class="mt-1 text-gray-700 leading-6">Un intégrateur prépare le déploiement d'une nouvelle page de vente mais soupçonne que le constructeur de page a généré des balises Hn incorrectes.</dd> </div> <div> <dt class="font-medium text-gray-900">Problème</dt> <dd class="mt-1 text-gray-700 leading-6">La page contient plusieurs H1 et passe directement de H1 à H3 pour des raisons de taille de police sur certains blocs.</dd> </div> <div> <dt class="font-medium text-gray-900">Comment l’utiliser</dt> <dd class="mt-1 text-gray-700 leading-6">Coller le code HTML de la landing page dans l'entrée HTML, cocher 'Comparer aux titres meta' et 'Afficher les corrections', puis lancer l'audit.</dd> </div> <div> <dt class="font-medium text-gray-900">Configuration d’exemple</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>Acme</h1><h3>Sous-titre</h3><h1>Produits</h1></body></html>", compareWithMetadataTitles=true, showFixSuggestions=true</code></pre> </dd> </div> <div> <dt class="font-medium text-gray-900">Résultat</dt> <dd class="mt-1 text-gray-700 leading-6">L'outil génère un arbre visuel montrant le H1 dupliqué et suggère de rétrograder le H3 en H2 ou de le remplacer par une classe CSS stylisée.</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. Audit SEO d'un article de blog en ligne</h4> <span class="shrink-0 rounded-full bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700">Rédacteur SEO</span> </div> <dl class="space-y-4 text-sm"> <div> <dt class="font-medium text-gray-900">Contexte</dt> <dd class="mt-1 text-gray-700 leading-6">Un rédacteur souhaite vérifier si un article récemment publié respecte la structure sémantique recommandée et correspond bien aux balises Open Graph.</dd> </div> <div> <dt class="font-medium text-gray-900">Problème</dt> <dd class="mt-1 text-gray-700 leading-6">L'article semble stagner dans les résultats de recherche et pourrait présenter des incohérences entre le titre H1 visible et la balise title.</dd> </div> <div> <dt class="font-medium text-gray-900">Comment l’utiliser</dt> <dd class="mt-1 text-gray-700 leading-6">Saisir l'URL de l'article dans le champ 'URL de page' et activer la comparaison avec les métadonnées.</dd> </div> <div> <dt class="font-medium text-gray-900">Configuration d’exemple</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/blog/article-seo", compareWithMetadataTitles=true, showFixSuggestions=true</code></pre> </dd> </div> <div> <dt class="font-medium text-gray-900">Résultat</dt> <dd class="mt-1 text-gray-700 leading-6">Le rapport met en évidence un écart important entre le H1 et le og:title, et signale un titre H4 vide à la fin de l'article.</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">Tester avec des échantillons</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="/fr/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">Groupes de Capture Nommés Regex</div> <div class="text-sm text-gray-600 mt-1">Collection de modèles regex utilisant des groupes de capture nommés pour extraire des données structurées du texte. Les groupes nommés rendent les modèles plus lisibles et maintenables en assignant des noms significatifs aux parties capturées.</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="/fr/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">Exemples Contentful CMS Cloud-based</div> <div class="text-sm text-gray-600 mt-1">Exemples complets de Contentful couvrant le modelage de contenu, l'intégration d'API, les webhooks et les patterns d'intégration 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="/fr/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">Échantillons de HTML avec Images</div> <div class="text-sm text-gray-600 mt-1">Échantillons de code source HTML avec des images pour tester l'extraction</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="/fr/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">Exemples JWT</div> <div class="text-sm text-gray-600 mt-1">Exemples complets de JWT de la structure de base des tokens aux implémentations de sécurité avancées</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 associés</h3> <div class="grid grid-cols-1 md:grid-cols-2 gap-3"> <a href="/fr/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">Outils d’extraction, de nettoyage et d’export HTML vers Markdown/PDF</div> <div class="text-sm text-gray-600 mt-1">Comparez les outils de nettoyage HTML, d’extraction d’attributs, d’extraction d’images, de conversion HTML vers Markdown et HTML vers PDF dans un hub unique pour les workflows de conversion web.</div> </a> <a href="/fr/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">Outils d audit d accessibilite web et de couleurs accessibles</div> <div class="text-sm text-gray-600 mt-1">Regroupez dans un meme hub les controles WCAG, l ordre de lecture des lecteurs d ecran, les problemes de contraste, les risques lies a la vision des couleurs et le choix de palettes accessibles.</div> </a> <a href="/fr/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">Outils d encodage et de conversion audio</div> <div class="text-sm text-gray-600 mt-1">Comparez la conversion de formats audio, les changements de debit, la conversion de frequence d echantillonnage, les changements de codec et les exports dans un meme hub.</div> </a> <a href="/fr/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">Outils de conversion de formats d image et d export anime</div> <div class="text-sm text-gray-600 mt-1">Comparez les convertisseurs d image pour JPG, PNG, GIF, AVIF, WebP, TIFF, ICO, base64 et les exports orientes animation dans un meme 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">Outils associés</h3> <div class="grid grid-cols-1 md:grid-cols-2 gap-3"> <a href="/fr/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">Testeur de simulation de lecteur decran</div> <div class="text-sm text-gray-600 mt-1">Simule lordre de lecture et la semantique vocalisee dun lecteur decran a partir dune URL ou dun HTML</div> </a> <a href="/fr/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">Extracteur d'Attributs HTML</div> <div class="text-sm text-gray-600 mt-1">Extrait les attributs spécifiés (href, src, data-*, etc.) du contenu HTML avec prise en charge du filtrage par nom de balise</div> </a> <a href="/fr/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">Extracteur de Source d'Image</div> <div class="text-sm text-gray-600 mt-1">Extrayez les URLs d'image (attributs src) du code source HTML. Prend en charge les images en chargement différé et les attributs srcset.</div> </a> <a href="/fr/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">Extracteur Meta Tags</div> <div class="text-sm text-gray-600 mt-1">Extrait et analyse les meta tags, Open Graph, Twitter Cards et données structurées des pages web</div> </a> <a href="/fr/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">Supprimeur de Balises HTML</div> <div class="text-sm text-gray-600 mt-1">Supprime les balises HTML du code et extrait le contenu texte brut</div> </a> <a href="/fr/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">Extracteur de tableaux PDF vers CSV/JSON</div> <div class="text-sm text-gray-600 mt-1">Extrait des tableaux PDF avec OpenDataLoader et les exporte en JSON, CSV ou HTML</div> </a> <a href="/fr/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">Extracteur dimages et captions PDF</div> <div class="text-sm text-gray-600 mt-1">Extrait les images PDF, associe les captions voisines et genere un index HTML navigable</div> </a> <a href="/fr/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">Verificateur d accessibilite</div> <div class="text-sm text-gray-600 mt-1">Detecte les problemes WCAG 2.1 courants dans du HTML, des pages ou des maquettes et fournit des corrections concretes</div> </a> </div> </section> <section id="tool-faq" aria-labelledby="tool-faq-title"> <h3 id="tool-faq-title" class="text-base font-semibold text-gray-900 mb-3">FAQ</h3> <div class="space-y-3"> <details class="rounded-lg border border-gray-200 p-4 bg-gray-50"> <summary class="font-medium text-gray-900 cursor-pointer">Pourquoi est-il problématique de sauter des niveaux de titre ?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Sauter des niveaux (par exemple passer d'un H1 à un H3) perturbe les robots d'indexation et les lecteurs d'écran, ce qui nuit au SEO et à l'accessibilité.</p> </details> <details class="rounded-lg border border-gray-200 p-4 bg-gray-50"> <summary class="font-medium text-gray-900 cursor-pointer">L'outil peut-il analyser une page web déjà publiée ?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Oui, il vous suffit de renseigner l'adresse de la page dans le champ URL pour en extraire et analyser la structure des titres.</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'est-ce que la dérive avec les titres meta ?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Il s'agit de l'écart sémantique ou textuel entre votre titre principal H1 et les balises de métadonnées comme <title> ou og:title.</p> </details> <details class="rounded-lg border border-gray-200 p-4 bg-gray-50"> <summary class="font-medium text-gray-900 cursor-pointer">Comment l'outil identifie-t-il les titres purement décoratifs ?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Il détecte les textes courts ou les libellés d'interface utilisateur qui utilisent des balises Hn uniquement pour appliquer un style visuel.</p> </details> <details class="rounded-lg border border-gray-200 p-4 bg-gray-50"> <summary class="font-medium text-gray-900 cursor-pointer">Puis-je obtenir des suggestions pour corriger ma structure ?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Oui, en cochant l'option correspondante, l'outil propose un niveau de titre recommandé ou une alternative sémantique pour chaque erreur.</p> </details> </div> </section> </div> <script type="application/ld+json">{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Pourquoi est-il problématique de sauter des niveaux de titre ?","acceptedAnswer":{"@type":"Answer","text":"Sauter des niveaux (par exemple passer d'un H1 à un H3) perturbe les robots d'indexation et les lecteurs d'écran, ce qui nuit au SEO et à l'accessibilité."}},{"@type":"Question","name":"L'outil peut-il analyser une page web déjà publiée ?","acceptedAnswer":{"@type":"Answer","text":"Oui, il vous suffit de renseigner l'adresse de la page dans le champ URL pour en extraire et analyser la structure des titres."}},{"@type":"Question","name":"Qu'est-ce que la dérive avec les titres meta ?","acceptedAnswer":{"@type":"Answer","text":"Il s'agit de l'écart sémantique ou textuel entre votre titre principal H1 et les balises de métadonnées comme <title> ou og:title."}},{"@type":"Question","name":"Comment l'outil identifie-t-il les titres purement décoratifs ?","acceptedAnswer":{"@type":"Answer","text":"Il détecte les textes courts ou les libellés d'interface utilisateur qui utilisent des balises Hn uniquement pour appliquer un style visuel."}},{"@type":"Question","name":"Puis-je obtenir des suggestions pour corriger ma structure ?","acceptedAnswer":{"@type":"Answer","text":"Oui, en cochant l'option correspondante, l'outil propose un niveau de titre recommandé ou une alternative sémantique pour chaque erreur."}}]}</script> <script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://elysiatools.com/fr"},{"@type":"ListItem","position":2,"name":"Tools","item":"https://elysiatools.com/fr/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">Documentation de l'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">Point de terminaison de la requête</h3> <div class="bg-gray-800 rounded p-3 font-mono text-sm text-white"> POST /fr/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">Paramètres de la requête</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">Nom du paramètre</th> <th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Type</th> <th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Requis</th> <th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Description</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">Non</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">Non</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">Non</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">Non</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">Format de réponse</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">Documentation 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"> Ajoutez cet outil à votre configuration de serveur 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": "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", "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>Vous pouvez chaîner plusieurs outils, par ex.: `https://elysiatools.com/mcp/sse?toolId=png-to-webp,jpg-to-webp,gif-to-webp`, max 20 outils.</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 vous rencontrez des problèmes, veuillez nous contacter à 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: fr is injected by the server-side renderSharedScripts function const currentLang = 'fr'; // 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('/fr/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="/fr/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('fr'); // Initialize tool form const toolData = {"id":"heading-hierarchy-auditor","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","category":"Validation","keywords":["hiérarchie des titres","sémantique","html","seo"],"resultType":"html","featured":false,"lastUpdated":"2026-06-09T00:00:00.000Z","options":[{"type":"textarea","name":"htmlInput","label":"Entree 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 page","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":"Comparer aux titres meta","default":true,"validate":{"def":{"type":"optional","innerType":{"def":{"type":"boolean"},"type":"boolean"}},"type":"optional"},"options":[]},{"type":"checkbox","name":"showFixSuggestions","label":"Afficher les corrections","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, 'fr'); 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: "/fr/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>