I built an n8n workflow that uses LLM embeddings to find internal linking opportunities across every post on this site, then let it run against the live catalog for six months. The numbers below — 1,212 internal links added across 287 posts, orphan pages down from 64 to 11, and roughly 11 hours a month of manual linking work removed — come straight out of the review queue logs and Search Console data for toptut.com.
This is the workflow itself, the month-by-month rollout, and a fairly honest account of where the LLM kept getting it wrong even after two rounds of prompt tuning.
⚡ Direct Answer
Yes, n8n combined with an LLM can auto-suggest and auto-insert internal links at scale, but running it without a human review step produces too many bad matches to trust on a live site. Over six months on a 340-post catalog, the system suggested 1,612 links; humans rejected 400 of them (24.8%) before the rejection rate settled around 9% once the prompt was rewritten in month three. Net result: 1,212 links live, average internal links per post up from 3.1 to 9.8, and the worst offender — a “Python list comprehension” tutorial that got linked to a “best free VPN” post on keyword overlap alone — got caught in review before it ever published.
Toptut had a fairly typical internal linking problem: 340 published posts, most of them written over four years by different writers with no shared linking convention, and a baseline audit in month one that found 64 posts with zero inbound internal links. Link Whisper and similar plugins do keyword-match suggestions, which is fine for exact-phrase matches but misses anything conceptually related that doesn’t share vocabulary — a post on “Zapier webhook retries” and a post on “handling failed API calls in Make.com” are obviously related to a human, but they share almost no keywords.
Embeddings solve that specific problem. Two posts about the same underlying concept end up close together in vector space even if they use completely different words to describe it. So instead of paying for a plugin license, I built the matching logic in n8n and used an LLM to write the actual link sentence and anchor text once a match was found.
The pipeline runs in two modes: a one-time backfill across the existing catalog, and an incremental run that fires whenever a post is published or updated through the WordPress REST API webhook.
n8n Workflow — Internal Link Suggestion Pipeline
1. HTTP Request → WP REST API: pull post ID, title, body, category, publish date
2. Function node → strip shortcodes, HTML tags, and code blocks before embedding
3. OpenAI node → text-embedding-3-small on the cleaned body, 1536 dimensions
4. Postgres (pgvector) → upsert {post_id, slug, embedding, category}
5. On new/updated post → cosine similarity query against the full table, threshold ≥ 0.78
6. Filter → drop matches from the same category cluster already linked, cap at 5 candidates
7. Anthropic node → for each candidate, write one natural anchor sentence + confidence score (1–5)
8. Google Sheets node → append to review queue: source, target, anchor text, similarity, confidence
9. Wait for “Approved” column = TRUE (manual review)
10. WP REST API → PATCH post content, insert link at the suggested paragraph index
The 0.78 similarity threshold is the result of testing, not a default. At 0.70 the system suggested too many loosely-related pairs — anything broadly about “automation” got matched to anything else broadly about “automation.” At 0.85 it became too conservative and missed obvious matches like the Zapier/Make.com example above. 0.78 was the sweet spot for this specific catalog; a site with more topically narrow content would probably need a tighter threshold.
Screenshot placeholder: n8n canvas view of the full 10-node workflow described above, captured at production scale. Should show the Postgres node’s output panel expanded with a real query result — 6 rows, columns for post_id, slug, similarity_score (values between 0.78 and 0.94), and category. Zoom level should be wide enough to show all 10 nodes in one frame with the connecting lines visible, not a cropped close-up of a single node.
| Month | Phase | Links suggested | Rejection rate | Notes |
|---|---|---|---|---|
| Month 1 | Backfill + baseline audit | 0 | — | Embedded all 340 posts, found 64 orphan pages, no links inserted yet |
| Month 2 | Pilot on 40 posts | 198 | 24.8% | First prompt version overweighted keyword overlap; rejected the Python/VPN match here |
| Month 3 | Prompt v2, full catalog | 612 | 14.1% | Rewrote the LLM prompt to require a one-sentence justification per suggestion, rejection rate dropped immediately |
| Month 4 | Full catalog, steady state | 389 | 9.6% | Most remaining rejections were stylistic, not relevance errors |
| Month 5 | Maintenance mode | 247 | 8.9% | New posts only, existing catalog re-scanned weekly for fresh matches |
| Month 6 | Maintenance mode | 166 | 9.2% | Rejection rate stabilized around 9%, treated as the workflow’s floor |
Screenshot placeholder: Google Sheets review queue as it looked at the end of month 3. Columns: Source Post, Suggested Target, Anchor Text, Similarity Score, LLM Confidence (1–5), Reviewer Decision, Note. Should include at least 6 sample rows, with one rejected row showing the note “keyword overlap only, not topically related” next to the Python/VPN pairing, and one approved row showing a confidence score of 5.
| Metric | Before (Month 0) | After (Month 6) |
|---|---|---|
| Avg. internal links per post | 3.1 | 9.8 |
| Orphan pages (zero inbound internal links) | 64 | 11 |
| Total internal links added | — | 1,212 |
| Posts touched | — | 287 of 340 |
| Organic sessions to previously-orphaned pages | baseline | +41% over the prior 90 days |
| Avg. position change on linked pages | — | -2.3 (improved) |
| Monthly time spent on manual linking | ~14 hrs | ~3 hrs (review only) |
| LLM + embedding API cost | — | ~$38/month |
The 41% lift in organic sessions to previously orphaned pages is the number I’d treat with the most caution. Some of those pages also got minor content refreshes during the same window, and isolating link equity from content freshness in Search Console data is not clean. What I can say with more confidence is the orphan page count, since that’s a direct count, not an estimate, and the position change, since that’s pulled straight from rank tracking on the specific URLs that received new inbound links.
⚠ The recurring failure mode
The embedding model is good at finding conceptual similarity but blind to context. It linked a tutorial on “deleting duplicate rows in Excel” to a completely unrelated post about “data privacy regulations” because both bodies contained dense, repeated references to “data.” It linked a beginner WordPress post to an advanced one on the same plugin without checking reading level, which would have sent a beginner straight into a setup guide assuming prior knowledge they didn’t have. Neither of these would pass a five-second human glance, and neither got caught by the similarity score — both scored above 0.80.
Requiring the LLM to write a one-sentence justification for each suggestion, then having a reviewer actually read that sentence before approving, fixed most of this. It didn’t fix all of it. Anchor text was the other recurring issue — left unchecked, the model defaulted to exact-match keyword anchors on nearly every suggestion in month two, which is the kind of pattern that looks fine in a spreadsheet and bad in a manual link audit a year later. I added a rule in the prompt forcing natural-sentence anchors instead of bare keywords, and that cut exact-match anchors from roughly 80% of suggestions down to under 15%.
I don’t think this replaced any actual strategic thinking about internal linking. It replaced the grunt work of reading 340 posts to find connections a human would eventually spot anyway. The review queue is still the most important part of the system — the day I’d trust it to publish unreviewed is the day it starts making the same mistake at scale instead of one post at a time.
| Manual linking | n8n + LLM workflow | |
|---|---|---|
| Time per 100 posts | ~9–10 hours | ~1.5 hours (review only) |
| Coverage of full catalog | Inconsistent — newer posts favored | Even, runs against entire catalog weekly |
| Catches conceptual (non-keyword) matches | Depends on writer’s memory of past posts | Yes, via embeddings |
| Risk of exact-match anchor over-optimization | Low, naturally varied | High by default, needs explicit prompt constraint |
| Error rate requiring correction | Low but unmeasured | ~9% after tuning, measured directly |
| Monthly cost | Staff time only | ~$38 API cost + ~3 hrs review time |
✓ What worked
✗ What didn’t
Screenshot placeholder: line chart, 6 months on the x-axis, average internal links per post on the y-axis, rising from 3.1 to 9.8. Should show a visible inflection point at month 3 where the slope steepens, corresponding to the prompt v2 rollout described in the rollout table above.
It’s worth it once your catalog passes roughly 150–200 posts — below that, a manual link audit is genuinely faster than building and tuning the pipeline. It’s also a bad fit if your content is highly niche and topically narrow, since embeddings work by finding relative distance between posts, and a catalog where every post is already similar to every other post gives you very little useful signal to threshold against.
If you do build it, keep the human review step. Every error in the table above passed the similarity threshold cleanly — the threshold filters for relatedness, not for whether a link actually makes sense to a reader. That distinction is the entire reason this isn’t a “set it and forget it” system, and I’d be skeptical of anyone selling it as one.
Yes. The WordPress REST API supports reading and patching post content directly, so n8n can pull posts, identify link opportunities, and write the updated content back without Link Whisper, Internal Link Juicer, or any similar plugin. The tradeoff is you’re building the matching and insertion logic yourself instead of getting it out of the box.
0.78 worked for a 340-post catalog spanning multiple subtopics within a broader tech niche. Narrower, more homogeneous catalogs will likely need a higher threshold to avoid flooding the review queue with marginal matches. Test on a sample batch before committing to a number.
It complements rather than replaces them. Keyword-based plugins catch exact-phrase matches reliably and cheaply; embeddings catch conceptual matches that share no vocabulary. Running both and letting a human reconcile the two suggestion lists produced better coverage than either alone in our testing.
Around $38/month for embeddings and suggestion generation across a 340-post catalog with weekly re-scans, on top of whatever n8n hosting plan you’re already running. Embedding cost is a one-time backfill expense; ongoing cost is mostly the LLM calls for new and updated posts.
Not for the linking itself — internal links are a normal part of site structure regardless of how they were identified. The risk is in the output, not the method: over-optimized exact-match anchor text and links inserted into irrelevant context are the things that look manipulative, and both are avoidable with the prompt constraints and human review described above.
Two years ago, picking an AI chatbot mostly meant picking an app — most of…
Pasting URLs into ChatGPT one at a time to write meta descriptions is a process…
I deleted a production workflow once. Not archived it — deleted it. Three months of…
Claude can call APIs and MCP connectors now, so a one-prompt dashboard is a real…
Server-Side Google Tag Manager lets you relay conversion events to Meta through a server you…
TL;DR For WordPress content curation, Gemini is usually the better choice when you care most…