Enter any URL and see your full H1–H6 hierarchy scored for AI extractability. Skipped levels, missing H1s, and ordering issues flagged instantly.
HEADING STRUCTURE EXTRACTOR
GEOLAB.NET
Works on any publicly accessible URL. Full page URL including path recommended.
Fetching page and extracting headings…
How to use this tool
1
Enter the full page URL
Use the complete URL including the path — not just the domain. The tool fetches the live page HTML and extracts all heading tags in document order.
2
Read the heading tree
The tree shows every H1–H6 tag in order. Issues are highlighted: skipped levels (H2 → H4), multiple H1s, missing H1, and headings that are too long to be citable.
3
Understand the extractability score
The score (0–100) reflects how well your heading structure helps AI engines parse and cite content by section. Each issue type deducts points based on its impact on extractability.
4
Fix issues in priority order
Critical issues (missing H1, multiple H1s, severe skipping) should be fixed first. Warning-level issues (minor skips, overly long headings) come second. Each issue has a specific fix suggestion.
5
Re-run after fixing
Once you’ve updated the page, re-run to confirm the score improved. Heading structure changes are picked up by AI crawlers at the next crawl cycle — typically within a few days.
GEO Fundamentals Series
Master Extractability
6 emails on Layer 1 of the GEO Stack — heading structure, paragraph patterns, and the content signals that determine whether AI engines cite your pages.
const API_BASE = ‘https://tools.thegeolab.net’;
const H_COLORS = { H1:’#1B4F8A’, H2:’#0f766e’, H3:’#7c3aed’, H4:’#d97706′, H5:’#dc2626′, H6:’#6b7280′ };
async function runCheck() {
let url = document.getElementById(‘urlInput’).value.trim();
if (!url) return;
if (!url.startsWith(‘http’)) url = ‘https://’ + url;
document.getElementById(‘loading’).classList.add(‘show’);
document.getElementById(‘errorBlock’).classList.remove(‘show’);
document.getElementById(‘resultBlock’).classList.remove(‘show’);
document.getElementById(‘checkBtn’).disabled = true;
try {
let headings = [];
let pageTitle = ”;
let source = ‘api’;
// Try API route first
try {
const res = await fetch(`${API_BASE}/api/tools/heading-extractor`, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({ url }),
signal: AbortSignal.timeout(12000)
});
if (res.ok) {
const data = await res.json();
headings = data.headings || [];
pageTitle = data.pageTitle || ”;
} else {
source = ‘proxy’;
}
} catch {
source = ‘proxy’;
}
// Fallback: fetch via proxy and parse client-side
if (source === ‘proxy’) {
const proxyUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`;
const proxyRes = await fetch(proxyUrl, { signal: AbortSignal.timeout(15000) });
if (!proxyRes.ok) throw new Error(‘Could not fetch page’);
const data = await proxyRes.json();
const html = data.contents || ”;
if (!html) throw new Error(‘Empty response from page’);
const extracted = extractHeadingsFromHtml(html);
headings = extracted.headings;
pageTitle = extracted.title;
}
if (!headings.length) {
showError(‘No heading tags found on this page. The page may require JavaScript to render, or may not be publicly accessible.’);
return;
}
renderResult(url, pageTitle, headings);
} catch (err) {
showError(‘Could not fetch this URL. The page may block external requests or require authentication. (‘ + err.message + ‘)’);
} finally {
document.getElementById(‘loading’).classList.remove(‘show’);
document.getElementById(‘checkBtn’).disabled = false;
}
}
function extractHeadingsFromHtml(html) {
// Strip scripts, styles, nav, footer to reduce noise
html = html.replace(/]*>[sS]*?/gi, ”)
.replace(/]*>[sS]*?/gi, ”)
.replace(//gi, ”)
.replace(//gi, ”);
// Extract title
const titleMatch = html.match(/]*>([sS]*?)/i);
const title = titleMatch ? titleMatch[1].replace(/]+>/g, ”).trim() : ”;
// Extract headings
const headings = [];
const hRegex = /]*>([sS]*?)1>/gi;
let match;
while ((match = hRegex.exec(html)) !== null) {
const level = match[1].toUpperCase();
const text = match[2].replace(/]+>/g, ”).replace(/s+/g, ‘ ‘).trim();
if (text) headings.push({ level, text });
}
return { headings, title };
}
function analyzeHeadings(headings) {
const issues = [];
let score = 100;
// Check for H1
const h1s = headings.filter(h => h.level === ‘H1’);
if (h1s.length === 0) {
issues.push({ type: ‘critical’, msg: ‘No H1 found. Every page must have exactly one H1. AI engines use the H1 as the primary topic signal. Without it, the page has no clear citation anchor.’, flag: ‘NO H1’ });
score -= 30;
} else if (h1s.length > 1) {
issues.push({ type: ‘critical’, msg: `Multiple H1s found (${h1s.length}). Only one H1 is allowed per page. Multiple H1s confuse AI engines about the primary topic. Keep only the most descriptive one.`, flag: ‘MULTIPLE H1’ });
score -= 20;
}
// Check for skipped levels
let prevLevel = 0;
headings.forEach((h, i) => {
const lvl = parseInt(h.level[1]);
if (prevLevel > 0 && lvl > prevLevel + 1) {
issues.push({ type: ‘warning’, msg: `Skipped heading level: ${h.level} after H${prevLevel}. Jumping from H${prevLevel} to H${lvl} breaks the hierarchy. AI engines expect H${prevLevel} → H${prevLevel + 1}. Restructure or change the heading level.`, flag: ‘SKIP’ });
score -= 8;
}
prevLevel = lvl;
});
// Check H1 is first heading
if (headings.length > 0 && headings[0].level !== ‘H1’) {
issues.push({ type: ‘warning’, msg: `First heading is ${headings[0].level}, not H1. The H1 should be the first heading on the page. Content before the H1 may not be attributed to the correct topic by AI engines.`, flag: ‘H1 ORDER’ });
score -= 10;
}
// Check for overly long headings
headings.forEach(h => {
if (h.text.length > 120) {
issues.push({ type: ‘warning’, msg: `Heading too long (${h.text.length} chars): “${h.text.slice(0, 60)}…” Headings over 120 characters are rarely used as citation anchors. Shorten to the core topic phrase.`, flag: ‘LONG’ });
score -= 5;
}
});
// Check heading count
if (headings.length < 3) {
issues.push({ type: 'tip', msg: 'Very few headings. Pages with fewer than 3 headings have limited section-level extractability. Consider adding H2 sections for each major topic covered.’, flag: ‘FEW’ });
score -= 5;
}
// Check if H2s exist for long pages
const h2s = headings.filter(h => h.level === ‘H2’);
if (headings.length > 5 && h2s.length === 0) {
issues.push({ type: ‘warning’, msg: ‘No H2 headings found. A page with many headings but no H2s typically means all sub-sections are at H3 or lower, without clear major section breaks. Add H2s for main topics.’, flag: ‘NO H2’ });
score -= 10;
}
return { issues, score: Math.max(0, score) };
}
function renderResult(url, pageTitle, headings) {
const { issues, score } = analyzeHeadings(headings);
const grade = score >= 85 ? ‘A’ : score >= 70 ? ‘B’ : score >= 50 ? ‘C’ : ‘D’;
const gradeColor = { A:’var(–green)’, B:’#2563eb’, C:’var(–amber)’, D:’var(–red)’ }[grade];
const gradeLabel = { A:’Strong structure’, B:’Good — minor issues’, C:’Needs work’, D:’Critical issues’ }[grade];
const gradeDesc = {
A: ‘Your heading hierarchy is well-structured for AI extractability. Each section is clearly delineated and individually citable.’,
B: ‘Solid structure with a few fixable issues. Addressing the warnings below will improve section-level citation probability.’,
C: ‘Structural issues are reducing extractability. AI engines will struggle to attribute content to the right sections.’,
D: ‘Critical structural problems are significantly suppressing citation probability. Fix these before any other GEO work.’
}[grade];
const h1Count = headings.filter(h => h.level === ‘H1’).length;
const h2Count = headings.filter(h => h.level === ‘H2’).length;
const domain = (() => { try { return new URL(url).hostname; } catch { return url; } })();
const rb = document.getElementById(‘resultBlock’);
rb.innerHTML = `
✓ No structural issues found. Heading hierarchy is well-formed.
`}
`;
rb.classList.add(‘show’);
}
function escHtml(s) { return s.replace(/&/g,’&’).replace(//g,’>’); }
function escAttr(s) { return s.replace(/’/g,”\’”); }
function showError(msg) {
const el = document.getElementById(‘errorBlock’);
el.textContent = ‘⚠️ ‘ + msg;
el.classList.add(‘show’);
}
function downloadReport(url, score, grade) {
const headingRows = […document.querySelectorAll(‘.h-row’)].map(r => {
const tag = r.querySelector(‘.h-tag’)?.textContent || ”;
const text = r.querySelector(‘.h-text’)?.textContent || ”;
const flag = r.querySelector(‘.h-error-flag, .h-issue-flag’)?.textContent || ”;
const indent = ‘ ‘.repeat(Math.max(0, parseInt(tag[1]) – 1));
return `${indent}${tag}: ${text}${flag ? ‘ [‘ + flag + ‘]’ : ”}`;
});
const issueRows = […document.querySelectorAll(‘.issue-text’)].map(el => ‘• ‘ + el.innerText);
const lines = [
`Heading Structure Report — ${url}`,
`Score: ${score}/100 (Grade ${grade})`,
”,
‘HEADING TREE:’,
…headingRows,
”,
…(issueRows.length ? [‘ISSUES:’, …issueRows, ”] : [‘No issues found.’, ”]),
‘Generated by The GEO Lab — thegeolab.net’
];
const blob = new Blob([lines.join(‘n’)], { type: ‘text/plain’ });
const a = document.createElement(‘a’); a.href = URL.createObjectURL(blob);
a.download = `heading-report-${new URL(url).hostname}.txt`; a.click();
}
function resetTool() {
document.getElementById(‘urlInput’).value = ”;
document.getElementById(‘resultBlock’).classList.remove(‘show’);
document.getElementById(‘errorBlock’).classList.remove(‘show’);
}
document.getElementById(‘urlInput’).addEventListener(‘keydown’, e => { if (e.key === ‘Enter’) runCheck(); });
const MC_URL = ‘https://us4.list-manage.com/subscribe/post-json?u=7d9a257f5f6d20c7fc37bcaad&id=970bf73c90&c=?’;
function mcMsg(id,t,c){const e=document.getElementById(id);e.textContent=t;e.className=’cta-msg ‘+c}
function mcSubmit(e){
e.preventDefault();const email=document.getElementById(‘ctaEmail’).value.trim();if(!email)return;
const btn=e.submitter;
if(sessionStorage.getItem(‘geo_headings_captured’)){mcMsg(‘ctaMsg’,’You’re already subscribed!’,’ok’);return}
btn.disabled=true;btn.textContent=’Sending…’;
const s=document.createElement(‘script’);const cb=’mc_cb_’+Date.now();
window[cb]=r=>{delete window[cb];document.head.removeChild(s);
if(r.result===’success’||r.msg.includes(‘already’)){sessionStorage.setItem(‘geo_headings_captured’,’1′);mcMsg(‘ctaMsg’,’✓ Check your inbox — first email is on its way.’,’ok’)}
else{mcMsg(‘ctaMsg’,’Something went wrong — try again.’,’err’);btn.disabled=false;btn.textContent=’Send me the series’}};
s.src=`${MC_URL}&EMAIL=${encodeURIComponent(email)}&FTAG=geo-fundamentals&callback=${cb}`;document.head.appendChild(s)
}
About the Author
Artur Ferreira is the founder of The GEO Lab. He developed the GEO Stack framework and leads research into Generative Engine Optimisation methodologies. Connect on X/Twitter or LinkedIn.