I exported all one hundred published posts on this blog and counted the internal links inside the article bodies. The answer was zero.
Not “a few”. Not “not enough”. Across a hundred posts and about 66,000 words of article text, not one sentence linked to another article on the same site. Forty-three posts had a “Related Articles” strip appended underneath, three links each, anchored on the destination’s title and sitting below a promotional box where nobody was still reading. The remaining fifty-seven had nothing at all.
This post is the job of fixing that, written while I was doing it. It is not a tutorial about a plugin. It is what I gave a language model, what it got right, what it got confidently wrong, and the one check at the end that almost nobody runs.
Get the posts out of WordPress first
You cannot map a site you are reading one post at a time in wp-admin. The first step is a flat export you can grep.
WordPress ships a REST API that will hand you everything without a plugin. /wp-json/wp/v2/posts?per_page=100&status=publish&_fields=id,slug,title,date,link,content returns the lot as JSON; page through it if you have more than a hundred. I saved that to a file on disk, alongside one HTML file per post, and from then on every question was a script rather than an afternoon of clicking.
The questions worth asking of that file immediately, before any AI is involved:
- How many posts have at least one link to another post, counting only links that appear before the footer blocks?
- Which posts have never been linked to by anything? Those are your orphans, and they are usually the ones you forgot you published.
- What is the word count of each post, counting the article only and not the boilerplate? Mine ranged from 111 to 1,292 words, and the short end told me which “posts” were really placeholders.
- Which headings repeat across the site? Eighty-seven of my hundred posts had a heading called “Frequently asked questions”, which says a lot about how they were produced.
For writing the changes back, the same API works in reverse, but you need to authenticate. An application password from your user profile is the clean way to do it from a script. I have also done it from the browser by taking the REST nonce that wp-admin exposes and posting to the same endpoints, which works but is fiddly and only sensible for a one-off. Either way, take a full backup of files and database before you write anything.
What I gave the model, and what I asked for
I did not paste a hundred articles into a chat window. I built a small table, one row per post, with seven columns: ID, slug, title, publication date, word count, current category, and a one-line summary of what the post is actually about, which is often not what the title claims.
Then the request, roughly: group these hundred posts into five or six clusters by reader intent rather than by keyword; name a pillar post for each cluster and say why; flag any pair of posts that are covering the same ground; and for each post, propose three to five links to other posts in the list, with a suggested anchor phrase and a one-line reason for each link.
Two instructions in that prompt did most of the work. “By reader intent rather than by keyword” stops the model from putting every title containing the word “tools” into one bucket. “A one-line reason for each link” makes the weak suggestions visible immediately: when the reason reads “both posts are about AI”, there is no reason.
The output arrived in about a minute and was genuinely useful as a first draft. It was also wrong in ways I would not have caught if I had trusted it.
Where the map was wrong
Three failure modes, all of them the same underlying problem: the model was reading titles and summaries, not the site.
It clustered by vocabulary. Anything with “affiliate” in the title went into one group, including a post about mistakes I had made on this blog, which belongs with the other first-hand accounts and not with the explainers. Anything with “tools” went into another, regardless of whether the post was about writing, images or code.
It proposed links to posts that were about to disappear. This is the one that would have done real damage. I was in the middle of taking seventy-five of the hundred posts down, most of them merged into a smaller set of guides, so most of the suggested destinations were URLs that would shortly return a redirect. The model had no way of knowing that, because I had not told it. Once I passed it the list of survivors only, the suggestions got dramatically better, which is the general lesson: the constraint you forget to state is the one that produces the confident mistake.
It over-linked the pillars. Left alone, it pointed nearly everything at two or three big posts, because that is what the pattern of “cluster” content in its training data looks like. A pillar that receives forty links from posts that barely mention it is not authority, it is a sitemap with extra steps. I cut those back to links that a reader following the sentence would actually want.
What I kept was the shape. Five clusters, one pillar each, and the honest observation that two of my categories existed only because a category had to be picked at import time.
| Cluster | What the reader wants from it | Pillar post |
|---|---|---|
| Building websites with AI | Make the thing exist, then keep it running | Starting a blog, step by step |
| AI tools | Which one for which job, and what it costs | Which chatbot for which task |
| Blogging and SEO | Get published work found and connected | Writing with AI without filler |
| Blog monetisation | How the money side works, including the parts that fail | Affiliate marketing, explained honestly |
| Freelancing and solo business | Using AI in paid work without selling raw output | What AI can and cannot earn you |
Anchors a person would actually write
The anchor text the model suggested was nearly always the target’s exact title, or a bare keyword phrase. Both are wrong for different reasons. Exact titles read like a directory listing. Bare keyword phrases read like they were placed for a crawler, which they were.
What I use instead is a short description of what the reader will find there, written to fit the sentence it lives in. Not “read our AI writing tools guide” but something like “the three writing jobs I hand to a model every week”. The anchor should survive being read aloud. If the sentence stops making sense when you remove the link, the link was load-bearing filler.
My working rules, after doing this across every post that survived:
- Three to five links per post, inside the prose, never in a box at the end.
- Never in the first paragraph. A link there is an invitation to leave before the reader has a reason to stay.
- Never two links pointing at the same destination in one post.
- Every link points at the final URL. Not a redirect, not a category archive, not a tag.
- If I cannot write a reason for the link in the sentence itself, the link does not go in.
The check at the end that nobody runs
Here is where this particular job gets interesting, because I was rewiring the links at the same time as taking most of the site down. Sixty-four old URLs get a 301 to their new home. Eleven more have no honest equivalent and return 410, because redirecting a deleted post about a subject you no longer cover to a vaguely related page is a small lie told to a crawler.
So the last step is mechanical and it is the one I would not skip again: extract every internal link from the rewritten posts, and check each target against the redirect table and against the list of published slugs. Any link that hits a redirect gets rewritten to the destination. Any link to a slug that is not in the published list is a broken link I created myself, ten minutes ago, while fixing the site.
That check caught several in my case, all from the same cause: I had written links against the map before the map was final. Redirect chains inside your own site are not fatal, but they are unforced, they slow crawling down, and they are trivially avoidable once you have the export on disk.
What I have not proved
I cannot tell you this improved my rankings, and anyone who tells you their internal linking pass lifted traffic by a specific percentage is describing a correlation they did not isolate.
Here is my actual position: fifteen clicks from Google search in the quarter that ended on 19 September 2026, at an average position of 67. There is no traffic here to improve yet, and the internal linking pass happened at the same time as a rewrite, a round of merges and a change of focus. Even if numbers move, I will not be able to attribute them to this. What I can say is narrower and still worth the afternoon: the site now has a structure a person can follow, no orphan posts, and no link pointing at a URL that redirects.
The reason I did this work at all is in what the export of a hundred posts actually showed, which is the least comfortable thing on this site. The drafting rules that came out of the same clean-up are in writing with AI without publishing filler, the tools I run on my own two blogs are listed in the stack behind these sites, and once the links are in place, the mechanical pass that gets everything recrawled is the launch checklist I run in Search Console.
If you take one thing from this: get your site out of the CMS and into a file you can count. The AI part took a minute. The counting told me what was wrong, and the editing is what made the result worth publishing.
