Internal Link Audit and Topic Clustering Notebook with Embeddings
Good internal linking starts with two questions: which pages are under-linked, and which pages belong together? This free Colab notebook answers both. It audits your current links and anchor text, then uses embeddings to group pages into topic clusters and shortlist the links that are missing inside each one.
Who it’s for
SEOs running internal linking projects on content sites, and anyone planning hub pages who wants the clusters to come from the content itself instead of a guess.
How to use it
- Load the data. Use a Screaming Frog crawl (a custom extraction of your main content plus the All Inlinks export), or run the demo dataset first: about 70 MLforSEO posts pulled through the WordPress REST API.
- Link audit metrics. Inlinks, unique inlinks, in-content inlinks, orphan pages, and broken, nofollow or UTM-tagged internal links.
- Anchor text analysis. Generic, branded, mixed, exact-title and descriptive anchors, anchor length and the most common bigrams and trigrams.
- Embeddings. One vector per page with
BAAI/bge-small-en-v1.5(or embeddings exported from Screaming Frog), and a list of possible duplicates (cosine similarity of 0.95 or more). - Topic clustering. K-means with the number of clusters picked by silhouette score, or agglomerative clustering or HDBSCAN. Clusters are named with c-TF-IDF, pillar candidates are flagged and a 2D map shows the result.
- Bridge pages. Pages close to two clusters, your candidates for links between topics.
- Link opportunities. Similar pages that aren’t linked yet, prioritised so pillars and pages with no in-content inlinks come first, each with an anchor phrase found in the source page’s own text.
- Export CSVs and the cluster map for your audit deliverable.
What it costs
Nothing. The free Colab CPU runtime is enough, there are no API keys, and the embedding model (about 130 MB) downloads on the first run.
Limits
- The demo only sees links in the post body, so its orphans are “pages with no contextual links”. A full crawl gives you menus, footers and link position.
- Similarity thresholds are relative: the notebook uses a percentile of your own site’s similarity scores because different models score differently.
- The suggestions are a shortlist. Read them before you add anything.
Want it on a schedule? The same folder has automate_internal_linking.py for orphan checks, passage-level link suggestions with anchor guardrails, link health and applying approved links to WordPress. For a no-code start, try the Internal Link Opportunities Quick Find sheet. Read the full approach in 10 Ways to Automate Internal Linking with AI and learn the methods in the Introduction to Machine Learning for SEO course.
What’s included
- Link metrics per page: inlinks, in-content inlinks, orphans, broken, nofollow and UTM-tagged internal links
- Anchor text classified as generic, branded, exact-title or descriptive, with top bigrams and trigrams
- Page embeddings, possible duplicates and automatic topic clusters with names, pillar candidates and a 2D map
- Bridge pages between clusters and prioritised link opportunities with an anchor phrase from the source page
- Works on a Screaming Frog crawl or a WordPress demo dataset, no API keys
Where this resource is used on MLforSEO
Walkthroughs on the blog
More internal linking resources
- Coding Script or NotebookBlog Architecture Audit: Click Depth, Orphan Pages and Hub Coverage (Python)
- No-Code TemplateInternal Link Opportunities Quick Find (Google Sheets Template)
- Coding Script or NotebookML-Powered Internal Linking Opportunity Discovery with LinkBERT
- Content audits · No-Code TemplateContent Audit Template: Search Console, GA4 and Crawl to Keep, Update, Merge or Prune (Google Sheets)
- Content audits · ChecklistSEO Site Migration Checklist + Redirect Mapping Workbook
- Content audits · Coding Script or NotebookAI Crawler Access Checker: robots.txt, llms.txt and Bot Rules
See all 4 internal linking resources →See all 16 content audits resources →See all 42 coding script or notebook resources →
