Auditor für Überschriftenhierarchie

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

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, und og:title.</p> <p>So verwendest du das Tool:</p> <ul> <li>HTML-Eingabe: Markup für eine stabile Analyse einfügen</li> <li>Seiten-URL: veröffentlichte Struktur prüfen</li> <li>Mit Metadaten-Titeln vergleichen: gleicht die Hauptüberschrift mit <code><title></code> und <code>og:title</code> ab</li> <li>Korrekturhinweise anzeigen: ergänzt eine empfohlene Ebene oder Ersetzung pro Knoten</li> </ul> <p>Der Bericht zeigt:</p> <ul> <li>Einen visuellen h1-h6-Baum</li> <li>Eine Heatmap-artige Schwereverteilung</li> <li>Die empfohlene Ebene für jede Überschrift</li> <li>Mehrere h1 und Ebenensprünge</li> <li>Als Heading verkleidete UI-Labels</li> <li>Abweichungen zwischen h1 und Meta-Titeln</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> Beispielergebnisse </h3> <span class="text-xs text-amber-700">1 Beispiele</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">Landingpage mit Sprüngen und doppelten h1 prüfen</h4> <p class="text-xs text-gray-500 mt-1">Zeigt den Überschriftenbaum, erkennt doppelte h1 und empfiehlt, welche Knoten zu h2 oder zu Nicht-Heading-Text werden sollten.</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" > Beispiel laden </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"> Eingabeparameter anzeigen </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"> HTML-Eingabe </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"> Seiten-URL </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"> Mit Metadaten-Titeln vergleichen </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"> Korrekturhinweise anzeigen </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"> Verarbeiten </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="/de" class="hover:text-blue-600">Home</a></li><li class="flex items-center gap-2"><span>/</span><a href="/de/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">Wichtige Fakten</h2> <dl class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div> <dt class="text-xs uppercase tracking-wide text-gray-500">Kategorie</dt> <dd class="mt-1 text-sm font-medium text-gray-900">Sicherheit & Validierung</dd> </div> <div> <dt class="text-xs uppercase tracking-wide text-gray-500">Eingabetypen</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">Ausgabetyp</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">Sample-Abdeckung</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 verfügbar</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">Überblick</h2> <p class="text-gray-700 leading-7" itemprop="description">Der Auditor für Überschriftenhierarchie analysiert die semantische Struktur Ihrer Webseite anhand von eingefügtem HTML-Code oder einer Live-URL, um Fehler wie übersprungene Überschriftenebenen, mehrfache h1-Tags und Abweichungen zu Meta-Titeln aufzudecken.</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">Wann verwenden</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>Vor dem Live-Gang einer neuen Webseite, um die Barrierefreiheit und SEO-Konformität der Überschriftenstruktur zu validieren.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Bei der Optimierung bestehender Seiten, wenn Diskrepanzen zwischen dem Haupt-h1-Tag und den Meta-Titeln vermutet werden.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Zur Bereinigung von HTML-Templates, in denen CSS-Klassen fälschlicherweise als Überschriften-Tags missbraucht wurden.</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">So funktioniert es</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>Fügen Sie den rohen HTML-Code in das Eingabefeld ein oder geben Sie die URL einer veröffentlichten Webseite an.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Aktivieren Sie optional den Abgleich mit Metadaten-Titeln und die Anzeige von konkreten Korrekturhinweisen.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Das Tool analysiert die semantische Hierarchie der h1-h6-Knoten und vergleicht sie mit den Dokument-Titeln.</span></li><li class="flex items-start gap-2"><span class="text-blue-600 mt-0.5">•</span><span>Sie erhalten einen visuellen Überschriftenbaum inklusive einer Heatmap zur Fehlerpriorisierung und konkreten Empfehlungen für jeden Knoten.</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">Anwendungsfälle</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">Überprüfung von Blog-Artikeln auf logische Strukturierung vor der Veröffentlichung.</div><div class="rounded-lg bg-slate-50 border border-slate-200 p-4 text-sm text-gray-700">SEO-Audit von Kunden-Websites zur Identifikation von fehlerhaften Überschriften-Hierarchien und doppelten h1-Tags.</div><div class="rounded-lg bg-slate-50 border border-slate-200 p-4 text-sm text-gray-700">Bereinigung von UI-Templates, um sicherzustellen, dass Design-Elemente keine semantischen Überschriften-Tags missbrauchen.</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">Beispiele</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. Audit einer unstrukturierten Landingpage</h4> <span class="shrink-0 rounded-full bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700">SEO-Spezialist</span> </div> <dl class="space-y-4 text-sm"> <div> <dt class="font-medium text-gray-900">Hintergrund</dt> <dd class="mt-1 text-gray-700 leading-6">Eine neu gestaltete Landingpage rankt schlecht. Der Verdacht liegt nahe, dass die Überschriftenstruktur fehlerhaft ist.</dd> </div> <div> <dt class="font-medium text-gray-900">Problem</dt> <dd class="mt-1 text-gray-700 leading-6">Die Seite enthält mehrere h1-Tags, springt direkt von h1 auf h4, und UI-Elemente sind fälschlicherweise als h3 deklariert.</dd> </div> <div> <dt class="font-medium text-gray-900">Verwendung</dt> <dd class="mt-1 text-gray-700 leading-6">Fügen Sie den HTML-Code der Landingpage in das Feld 'HTML-Eingabe' ein, aktivieren Sie 'Mit Metadaten-Titeln vergleichen' sowie 'Korrekturhinweise anzeigen' und starten Sie die Analyse.</dd> </div> <div> <dt class="font-medium text-gray-900">Beispielkonfiguration</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>Startseite</h1><h1>Produktübersicht</h1><h4>Features</h4><h3>In den Warenkorb (h3)</h3></body></html>", compareWithMetadataTitles: true, showFixSuggestions: true</code></pre> </dd> </div> <div> <dt class="font-medium text-gray-900">Ergebnis</dt> <dd class="mt-1 text-gray-700 leading-6">Das Tool liefert einen visuellen Baum, der die doppelten h1-Tags rot markiert, den Sprung auf h4 bemängelt und empfiehlt, das UI-Element in ein Standard-div umzuwandeln.</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. Überprüfung einer Live-Produktseite</h4> <span class="shrink-0 rounded-full bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700">Webentwickler</span> </div> <dl class="space-y-4 text-sm"> <div> <dt class="font-medium text-gray-900">Hintergrund</dt> <dd class="mt-1 text-gray-700 leading-6">Vor dem Relaunch soll die Live-Produktseite auf Barrierefreiheit und korrekte semantische Struktur geprüft werden.</dd> </div> <div> <dt class="font-medium text-gray-900">Problem</dt> <dd class="mt-1 text-gray-700 leading-6">Es ist unklar, ob die Überschriftenhierarchie logisch aufgebaut ist und ob der h1-Tag mit dem Meta-Titel übereinstimmt.</dd> </div> <div> <dt class="font-medium text-gray-900">Verwendung</dt> <dd class="mt-1 text-gray-700 leading-6">Geben Sie die URL der Produktseite im Feld 'Seiten-URL' ein und aktivieren Sie den Metadaten-Vergleich.</dd> </div> <div> <dt class="font-medium text-gray-900">Beispielkonfiguration</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/produkt", compareWithMetadataTitles: true, showFixSuggestions: true</code></pre> </dd> </div> <div> <dt class="font-medium text-gray-900">Ergebnis</dt> <dd class="mt-1 text-gray-700 leading-6">Der Bericht zeigt eine saubere Hierarchie, warnt jedoch vor einer Abweichung zwischen dem h1-Tag ('Jetzt kaufen') und dem Meta-Titel ('Premium Analytics Software kaufen').</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">Mit Samples testen</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="/de/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">Benannte Capture-Gruppen von Regex</div> <div class="text-sm text-gray-600 mt-1">Sammlung von Regex-Mustern, die benannte Capture-Gruppen verwenden, um strukturierte Daten aus Text zu extrahieren. Benannte Gruppen machen Muster lesbarer und wartbarer, indem sie aussagekräftige Namen den erfassten Teilen zuweisen.</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="/de/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">Contentful Cloud-basierte CMS Beispiele</div> <div class="text-sm text-gray-600 mt-1">Umfassende Contentful Beispiele mit Content Modeling, API Integration, Webhooks und Frontend Integration Patterns</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="/de/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">HTML mit Bildern Proben</div> <div class="text-sm text-gray-600 mt-1">HTML-Quellcodeproben mit Bildern zum Testen der Extraktion</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="/de/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">JWT-Beispiele</div> <div class="text-sm text-gray-600 mt-1">Umfassende JWT-Beispiele von der grundlegenden Token-Struktur bis zu fortgeschrittenen Sicherheitsimplementierungen</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">Verwandte Hubs</h3> <div class="grid grid-cols-1 md:grid-cols-2 gap-3"> <a href="/de/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">HTML-Extraktions-, Bereinigungs- und Markdown/PDF-Export-Tools</div> <div class="text-sm text-gray-600 mt-1">Vergleichen Sie HTML-Bereinigung, Attribut- und Bildquellen-Extraktion, HTML-zu-Markdown und HTML-zu-PDF in einem Hub für Web-Content-Konvertierungs-Workflows.</div> </a> <a href="/de/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">Tools fuer Web-Accessibility-Audits und lesbare Farbpruefung</div> <div class="text-sm text-gray-600 mt-1">Pruefen Sie WCAG-Probleme, Screenreader-Reihenfolge, Kontrastfehler, Risiken bei Farbsehschwaechen und zugaengliche Farbwahl in einem Hub fuer Web-Accessibility-Workflows.</div> </a> <a href="/de/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">Tools fur Audio-Encoding und Formatkonvertierung</div> <div class="text-sm text-gray-600 mt-1">Vergleiche Audioformat-Konvertierung, Bitratenanderungen, Abtastraten-Konvertierung, Codec-Wechsel und Export-Tools in einem Hub.</div> </a> <a href="/de/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">Tools fur Bildformat-Konvertierung und animierten Export</div> <div class="text-sm text-gray-600 mt-1">Vergleiche Bildkonverter fur JPG, PNG, GIF, AVIF, WebP, TIFF, ICO, base64 und animationsgeeignete Exporte in einem 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">Verwandte Tools</h3> <div class="grid grid-cols-1 md:grid-cols-2 gap-3"> <a href="/de/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">Screen-Reader-Simulation-Tester</div> <div class="text-sm text-gray-600 mt-1">Simuliert die Vorlesereihenfolge und gesprochene Semantik eines Screenreaders aus URL oder HTML</div> </a> <a href="/de/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">HTML-Attribut-Extraktor</div> <div class="text-sm text-gray-600 mt-1">Extrahiert angegebene Attribute (href, src, data-*, etc.) aus HTML-Inhalten mit Unterstützung für Tag-Name-Filterung</div> </a> <a href="/de/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">Bildquellen-Extraktor</div> <div class="text-sm text-gray-600 mt-1">Extrahieren Sie Bild-URLs (src-Attribute) aus HTML-Quellcode. Unterstützt lazy-loading Bilder und srcset-Attribute.</div> </a> <a href="/de/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">Meta-Tag-Extraktor</div> <div class="text-sm text-gray-600 mt-1">Extrahiert und analysiert Meta-Tags, Open Graph, Twitter Cards und strukturierte Daten von Webseiten</div> </a> <a href="/de/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">HTML-Tag-Entferner</div> <div class="text-sm text-gray-600 mt-1">Entfernt HTML-Tags aus dem Code und extrahiert sauberen Textinhalt</div> </a> <a href="/de/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">PDF-Tabellenextraktor zu CSV/JSON</div> <div class="text-sm text-gray-600 mt-1">Extrahiert Tabellen aus PDFs mit OpenDataLoader und exportiert sie als JSON, CSV oder HTML</div> </a> <a href="/de/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">PDF-Bild- und Caption-Extraktor</div> <div class="text-sm text-gray-600 mt-1">Extrahiert PDF-Bilder, ordnet nahe Captions zu und erstellt einen durchsuchbaren HTML-Index</div> </a> <a href="/de/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">Barrierefreiheitspruefer</div> <div class="text-sm text-gray-600 mt-1">Erkennt haeufige WCAG-2.1-Probleme in HTML, Seiten oder Designbildern und liefert direkt umsetzbare Hinweise</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">Warum warnt das Tool vor mehreren h1-Überschriften?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Ein einziges h1-Tag definiert das Hauptthema einer Seite. Mehrere h1-Tags können Suchmaschinen verwirren und die semantische Struktur schwächen.</p> </details> <details class="rounded-lg border border-gray-200 p-4 bg-gray-50"> <summary class="font-medium text-gray-900 cursor-pointer">Was bedeutet ein Ebenensprung in der Hierarchie?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Ein Ebenensprung liegt vor, wenn beispielsweise direkt auf ein h1-Tag ein h3-Tag folgt, ohne dass dazwischen ein h2-Tag zur Strukturierung genutzt wird.</p> </details> <details class="rounded-lg border border-gray-200 p-4 bg-gray-50"> <summary class="font-medium text-gray-900 cursor-pointer">Kann ich auch passwortgeschützte Seiten über die URL prüfen?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Nein. Für passwortgeschützte Seiten kopieren Sie bitte den gerenderten HTML-Quelltext und fügen ihn direkt in das HTML-Eingabefeld ein.</p> </details> <details class="rounded-lg border border-gray-200 p-4 bg-gray-50"> <summary class="font-medium text-gray-900 cursor-pointer">Wie hilft der Vergleich mit Meta-Titeln?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Er stellt sicher, dass die Hauptüberschrift (h1) thematisch mit dem Title-Tag und dem og:title übereinstimmt, was für SEO und Social Sharing wichtig ist.</p> </details> <details class="rounded-lg border border-gray-200 p-4 bg-gray-50"> <summary class="font-medium text-gray-900 cursor-pointer">Was sind dekorative Headings?</summary> <p class="text-sm text-gray-700 mt-3 leading-6">Das sind Textabschnitte wie Button-Beschriftungen oder UI-Labels, die fälschlicherweise als h-Tags formatiert wurden, um ein bestimmtes Design zu erzielen.</p> </details> </div> </section> </div> <script type="application/ld+json">{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Warum warnt das Tool vor mehreren h1-Überschriften?","acceptedAnswer":{"@type":"Answer","text":"Ein einziges h1-Tag definiert das Hauptthema einer Seite. Mehrere h1-Tags können Suchmaschinen verwirren und die semantische Struktur schwächen."}},{"@type":"Question","name":"Was bedeutet ein Ebenensprung in der Hierarchie?","acceptedAnswer":{"@type":"Answer","text":"Ein Ebenensprung liegt vor, wenn beispielsweise direkt auf ein h1-Tag ein h3-Tag folgt, ohne dass dazwischen ein h2-Tag zur Strukturierung genutzt wird."}},{"@type":"Question","name":"Kann ich auch passwortgeschützte Seiten über die URL prüfen?","acceptedAnswer":{"@type":"Answer","text":"Nein. Für passwortgeschützte Seiten kopieren Sie bitte den gerenderten HTML-Quelltext und fügen ihn direkt in das HTML-Eingabefeld ein."}},{"@type":"Question","name":"Wie hilft der Vergleich mit Meta-Titeln?","acceptedAnswer":{"@type":"Answer","text":"Er stellt sicher, dass die Hauptüberschrift (h1) thematisch mit dem Title-Tag und dem og:title übereinstimmt, was für SEO und Social Sharing wichtig ist."}},{"@type":"Question","name":"Was sind dekorative Headings?","acceptedAnswer":{"@type":"Answer","text":"Das sind Textabschnitte wie Button-Beschriftungen oder UI-Labels, die fälschlicherweise als h-Tags formatiert wurden, um ein bestimmtes Design zu erzielen."}}]}</script> <script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://elysiatools.com/de"},{"@type":"ListItem","position":2,"name":"Tools","item":"https://elysiatools.com/de/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">API-Dokumentation</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">Request-Endpunkt</h3> <div class="bg-gray-800 rounded p-3 font-mono text-sm text-white"> POST /de/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">Request-Parameter</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">Parameter-Name</th> <th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Typ</th> <th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Erforderlich</th> <th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Beschreibung</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">Nein</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">Nein</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">Nein</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">Nein</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">Antwortformat</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">MCP-Dokumentation</h2> <div class="space-y-4"> <div class="bg-gray-50 rounded-lg p-4"> <p class="text-sm text-gray-600 mb-4"> Fügen Sie dieses Tool zu Ihrer MCP-Server-Konfiguration hinzu: </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": "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", "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>Sie können mehrere Tools verketten, z.B.: `https://elysiatools.com/mcp/sse?toolId=png-to-webp,jpg-to-webp,gif-to-webp`, maximal 20 Tools.</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> Wenn Sie auf Probleme stoßen, kontaktieren Sie uns bitte bei 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: de is injected by the server-side renderSharedScripts function const currentLang = 'de'; // 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('/de/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="/de/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('de'); // Initialize tool form const toolData = {"id":"heading-hierarchy-auditor","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","category":"Validation","keywords":["überschriftenhierarchie","semantik","html","seo"],"resultType":"html","featured":false,"lastUpdated":"2026-06-09T00:00:00.000Z","options":[{"type":"textarea","name":"htmlInput","label":"HTML-Eingabe","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":"Seiten-URL","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":"Mit Metadaten-Titeln vergleichen","default":true,"validate":{"def":{"type":"optional","innerType":{"def":{"type":"boolean"},"type":"boolean"}},"type":"optional"},"options":[]},{"type":"checkbox","name":"showFixSuggestions","label":"Korrekturhinweise anzeigen","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, 'de'); 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: "/de/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>