Skip to content
Coding Script or NotebookGoogle ColabFree

Blog Architecture Audit: Click Depth, Orphan Pages and Hub Coverage (Python)

ForInternal linkingContent audits

By Lazarina Stoy· Updated 24 Oct 2026

Blogs grow sideways. New posts push older ones deeper, categories fill unevenly, and some pages end up with no links pointing to them at all. This free Python script audits how your blog, or your whole site, is actually connected, and turns the results into a prioritised fix list. It is the only architecture tool in the library.

The five questions it answers

  1. Click depth. How many clicks from the homepage is each page, and which pages are deeper than three?
  2. URL depth vs click depth. Which pages look shallow in their URL but are buried in the links?
  3. Orphans. Which sitemap URLs have no internal links, which can’t be reached from the homepage, and which posts have one contextual (in-content) link or fewer?
  4. Category balance. Which sections are thin (fewer than three posts) or dominant (more than 40% of posts)?
  5. Hub coverage. Does each category or hub page link to all the posts in its folder, or only the latest few?

It also counts tag, author, date-archive, pagination and parameter URLs found in the crawl, so you can see how much archive clutter you have.

Who it’s for

Content-site owners and SEOs restructuring a blog, planning topic clusters, or tidying up after years of publishing.

How to use it

  1. Install the requirements locally, or in Google Colab with !pip install.
  2. Sitemap mode: point it at your XML sitemap or sitemap index. It reads every sitemap URL, then crawls from the homepage following normal links on your host, counting nav, header and footer links separately from in-content links.
  3. Or Screaming Frog mode: give it the Internal (HTML) export and, for hub coverage and the contextual split, the All Inlinks export. Add your sitemap to find orphans.
  4. Adjust the options if your URLs are flat: --post-pattern and --hub-pattern tell it which URLs are posts and hubs.
  5. Read summary.md, then work through the CSVs.

What you get

pages.csv (depths, inlinks and flags per page), orphans.csv, sections.csv, hubs.csv and a Markdown summary printed to the console.

Limits

  • It doesn’t render JavaScript, so links injected client-side are missed. Use Screaming Frog with rendering for those sites.
  • Click depth counts navigation links, which is how search engines discover pages but can make a site look shallower than it feels to readers. Use the contextual inlinks column for the in-content picture.
  • Orphan detection depends on your sitemap: pages missing from both the sitemap and your links stay invisible.
  • Free to run. Be polite with the crawl delay and only crawl sites you own or have permission to crawl.

I discuss what the results mean, and the structure I recommend, in Blog and Site Architecture for SEO and Conversions in the AI Search Era.

What’s included

  • Click depth from the homepage for every page, with pages deeper than three clicks flagged
  • URL depth vs click depth, to find pages that look shallow but are buried
  • Orphan pages, unreachable pages and posts with one contextual link or fewer
  • Category balance (thin and dominant sections) and hub coverage per category
  • Archive hygiene counts and a summary.md report
. Disclosure menus (APG pattern), hover intent, mobile dialog with focus trap, announcement dismiss, skip-link target, newsletter status message. No dependencies. */ (function () { 'use strict'; var d = document;/* ---------- skip link target: #main, else the page's
, else the first block after the header */ (function () { var mains = d.querySelectorAll('main'); if (mains.length > 1) { // Our post-content
plus another one: drop the landmark from ours. var ours = d.querySelector('main[data-mlfc-main]'); if (ours) { ours.setAttribute('role', 'none'); ours.removeAttribute('id'); } } var skip = d.querySelector('.mlfc-skip'); if (!skip) return; var t = d.getElementById('main'); if (!t || (t.getAttribute('role') === 'none')) { t = d.querySelector('main:not([role="none"]), [role="main"]'); if (!t) { var hd = skip.closest('.wp-block-template-part') || skip.closest('.mlfc-hdw'); t = hd && hd.nextElementSibling; } if (!t) return; if (!t.id) t.id = 'main'; skip.setAttribute('href', '#' + t.id); } if (!t.hasAttribute('tabindex')) t.setAttribute('tabindex', '-1'); skip.addEventListener('click', function () { setTimeout(function () { t.focus({ preventScroll: false }); }, 0); }); })();/* ---------- announcement dismiss */ d.querySelectorAll('[data-mlfc-ann]').forEach(function (bar) { var btn = bar.querySelector('[data-mlfc-ann-close]'); if (!btn) return; btn.addEventListener('click', function () { try { localStorage.setItem(bar.getAttribute('data-mlfc-ann'), '1'); } catch (e) {} var next = d.querySelector('.mlfc-logo'); bar.hidden = true; if (next) next.focus(); }); });/* ---------- disclosure dropdowns */ var items = [].slice.call(d.querySelectorAll('.mlfc-hdw [data-mlfc-dd]')); var fine = window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches; function btnOf(it) { return it.querySelector('button[aria-controls]'); } function panelOf(it) { return d.getElementById(btnOf(it).getAttribute('aria-controls')); } function isOpen(it) { return btnOf(it).getAttribute('aria-expanded') === 'true'; } function setOpen(it, open) { var b = btnOf(it), p = panelOf(it); if (!b || !p) return; b.setAttribute('aria-expanded', open ? 'true' : 'false'); p.hidden = !open; if (open) { // keep the panel inside the viewport p.style.left = ''; p.style.right = ''; var r = p.getBoundingClientRect(); if (r.right > window.innerWidth - 12) { p.style.left = 'auto'; p.style.right = '0'; } } } function closeAll(except) { items.forEach(function (it) { if (it !== except && isOpen(it)) setOpen(it, false); }); }items.forEach(function (it) { var b = btnOf(it), tIn, tOut; if (!b) return; b.addEventListener('click', function () { clearTimeout(tIn); clearTimeout(tOut); var open = !isOpen(it); closeAll(it); setOpen(it, open); }); if (fine) { // Hover intent: open after 300 ms, close 400 ms after the pointer leaves trigger + panel. it.addEventListener('mouseenter', function () { clearTimeout(tOut); tIn = setTimeout(function () { closeAll(it); setOpen(it, true); }, 300); }); it.addEventListener('mouseleave', function () { clearTimeout(tIn); tOut = setTimeout(function () { setOpen(it, false); }, 400); }); } it.addEventListener('focusout', function (e) { if (e.relatedTarget && !it.contains(e.relatedTarget)) setOpen(it, false); }); it.addEventListener('keydown', function (e) { if (e.key === 'Escape' && isOpen(it)) { e.stopPropagation(); setOpen(it, false); b.focus(); } }); }); d.addEventListener('click', function (e) { items.forEach(function (it) { if (isOpen(it) && !it.contains(e.target)) setOpen(it, false); }); }); d.addEventListener('keydown', function (e) { if (e.key !== 'Escape') return; items.forEach(function (it) { if (isOpen(it)) { setOpen(it, false); btnOf(it).focus(); } }); });/* ---------- mobile menu dialog */ var menuBtn = d.querySelector('.mlfc-menu-btn'); var dlg = d.getElementById('mlfc-mnav'); if (menuBtn && dlg) { var FOC = 'a[href],button:not([disabled]),input,select,textarea,[tabindex]:not([tabindex="-1"])'; var openMenu = function () { dlg.hidden = false; menuBtn.setAttribute('aria-expanded', 'true'); d.body.classList.add('mlfc-lock'); var c = dlg.querySelector('[data-mlfc-close]'); if (c) c.focus(); }; var closeMenu = function (restore) { if (dlg.hidden) return; dlg.hidden = true; menuBtn.setAttribute('aria-expanded', 'false'); d.body.classList.remove('mlfc-lock'); if (restore !== false) menuBtn.focus(); }; menuBtn.addEventListener('click', function () { dlg.hidden ? openMenu() : closeMenu(); }); dlg.querySelector('[data-mlfc-close]').addEventListener('click', function () { closeMenu(); }); dlg.addEventListener('keydown', function (e) { if (e.key === 'Escape') { e.preventDefault(); closeMenu(); return; } if (e.key !== 'Tab') return; var f = [].filter.call(dlg.querySelectorAll(FOC), function (el) { return el.offsetParent !== null || el === d.activeElement; }); if (!f.length) return; var first = f[0], last = f[f.length - 1]; if (e.shiftKey && d.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && d.activeElement === last) { e.preventDefault(); first.focus(); } }); // In-page links (e.g. #courses) close the menu first. dlg.addEventListener('click', function (e) { var a = e.target.closest && e.target.closest('a[href^="#"]'); if (a) closeMenu(false); }); var mq = window.matchMedia('(min-width: 1024px)'); var onMq = function () { if (mq.matches) closeMenu(false); }; if (mq.addEventListener) mq.addEventListener('change', onMq); else if (mq.addListener) mq.addListener(onMq); }/* ---------- newsletter: plain POST to Kit in a new tab (same pattern as mlfa2_newsletter()); say so in the live region */ d.querySelectorAll('form[data-mlfc-nl]').forEach(function (form) { var msg = form.querySelector('.mlfc-nl__msg'); form.addEventListener('submit', function () { if (msg) msg.textContent = 'Thanks. Kit opens in a new tab: confirm your subscription there and check your inbox.'; }); }); })();