Services Free Blog Audit Start a Blog

We Linked to Our Best Page 25 Times. The Live HTML Showed Zero of Them.

Quick answer: We published our best guide on 23 June. Over the next three weeks, 25 posts on this site linked to it. Every one of those links was real, saved in the database, and visible in the editor. None of them reached the browser. A single line in a 106-rule link-rewrite map was quietly rewriting every link to the new page so it pointed at the old page instead, at render time, on every page load. The page sat there for 24 days looking like an orphan. The rule was correct when it was written. It became destructive the day we published a page at that URL, and nothing warned us.

The symptom that made no sense

We were fixing a cannibalisation problem. Two posts targeted “how to start a blog”: an older one and a newer, better one. The newer one had zero internal links pointing at it. The older, weaker one had 80. The fix looked trivial: repoint the links.

So we repointed them. The API returned 200. We reloaded the page. The old link was still there.

We checked the database. The new link was saved correctly. We reloaded again. Old link.

Every instinct says cache. We purged the page cache. No change. We purged the object cache. No change. We added a query string to bypass the cache entirely, because our page cache skips URLs with query strings. Still the old link.

That is the moment worth paying attention to. If a purge does not fix it, it was never a cache. Something was actively rewriting the link on the way out.

The Raw vs Rendered Check

WordPress stores your post content in one form and serves it in another. Between the two sits the_content, a filter that every plugin, theme and code snippet on your site is invited to modify. Most of what you see on a live page was not typed by you.

The REST API will hand you both versions in a single response, which makes the diagnosis about sixty seconds of work. Logged in as an admin, open this URL on your own site:

/wp-json/wp/v2/posts/POST_ID?context=edit&_fields=content

You get back two things. content.raw is what is in your database, which is what you wrote. content.rendered is what your visitors and Google actually receive. Search both for the link you are chasing.

  • Link is wrong in raw: your edit did not save. Look at your editor or your API call.
  • Link is right in raw and right in rendered, but wrong on the live page: now it is a cache, or something in your theme’s template.
  • Link is right in raw and wrong in rendered: a filter is rewriting it. Purging will never fix this. Go and find the filter.

That third line is the one nobody checks, because the page looks broken in exactly the way a cache looks broken. We spent twenty minutes purging things before running a check that took one minute.

The line of code

Ours was in a code snippet written during a permalink migration. When this site moved its posts from root-level URLs to category URLs, hundreds of internal links across the archive still pointed at the old paths. Rather than edit every post, the snippet did the sensible thing: it hooked the_content and rewrote old paths to new ones on the fly. 106 rules, one per moved URL.

Rule 29 read:

'/blog-setup/how-to-start-a-blog/'  =>  '/blog-setup/start-a-blog/',

On the day it was written, that rule was correct and useful. /blog-setup/how-to-start-a-blog/ was a dead URL. Anyone linking to it was linking to a 404, and the rule quietly sent them to the live post instead. It did its job for months.

Then, on 23 June, we published a new guide at /blog-setup/how-to-start-a-blog/. That URL stopped being dead. The rule did not know or care. It kept doing exactly what it was told: take every link to that path and point it somewhere else.

The page was not blocked. It was not noindexed. It was in the sitemap, it returned 200, it was self-canonical, and you could read it perfectly well if you typed the address. It simply could not be linked to from its own website.

What it actually cost

We went back through the revision history of every post that links to that page to find out when the links were written. The answer is unflattering.

The damage, in numbers

Posts linking to the page in stored content25
Those links that survived to the browser0
Date of the first erased link23 June 2026
Days live and unlinkable24
Rules in the rewrite map106
Rules that were actually harmful1

The first erased link was written on the day the page was published. Eight more followed on 25 June, twelve on 13 July. Three separate rounds of internal linking work, including a dedicated campaign to fix orphaned pages, all of them adding links that were deleted before anyone could see them. Each round we looked at the rendered output, concluded the page had no links, and added more.

One rule out of 106. That ratio is the uncomfortable part. The map was 99% fine.

Redirect maps have a shelf life

The general lesson is not “don’t use rewrite rules”. It is that a rule encodes an assumption about a URL, and assumptions expire.

Every rule in a map like this quietly asserts: nothing lives at this path, and nothing ever will. That is true on the day you write it. It stays true right up until someone publishes something there, which is exactly what you will do if the path is a good one, because the reason it was a good URL in the first place has not changed. Our rule targeted /blog-setup/how-to-start-a-blog/. Of course we eventually published a guide about how to start a blog at that address. It was the obvious slug.

The rules most likely to turn on you are the ones pointing at your best URLs.

The audit: test your map against your own site

The check is a set intersection, and it takes a minute. Take the list of paths your rules rewrite from. Take the list of URLs that currently exist on your site. Any path in both lists is a live page whose inbound links are being redirected somewhere else.

In our case that produced exactly one result, which was the whole problem. Run it on your own map:

// 1. every live permalink on the site
const live = new Set(allPosts.map(p => new URL(p.link).pathname));

// 2. every 'from' path in your rewrite map
// 3. anything in both is a landmine
map.filter(rule => live.has(rule.from));

This applies to more than hand-rolled snippets. Redirect plugins, “broken link fixer” tools, search-and-replace utilities and legacy .htaccess blocks all encode the same expiring assumption. If a tool on your site rewrites URLs based on a list somebody wrote once, that list has a shelf life.

The habit worth building is smaller than an audit: before you publish at a URL that used to be something else, grep your redirect map for it. A migrated site’s best slugs are precisely the ones sitting in that map.

What this post does not say

This is one incident on one site, not a study. We have no evidence about how common this is, and we are not going to pretend otherwise. We also cannot show you a traffic recovery, because this site does not rank well enough for 24 days of internal links to have moved anything measurable. There is no dramatic graph at the end of this story.

What we can say is narrower and, we think, more useful: the failure was invisible to every check we habitually run. It did not show up in Search Console. Nothing was broken. The links existed. The page worked. Our internal link audit read the rendered HTML, which is what Google reads, and correctly reported zero inbound links. The audit was right. It just could not tell us that the links had been written and then deleted, which is a different problem with a different fix.

An orphan page and a page whose links are being erased look identical from the outside. Only raw versus rendered tells them apart.

Frequently asked questions

Would Google have seen the links?

No. Googlebot receives the same rendered HTML your readers do. Our stored content is irrelevant to it. As far as Google was concerned, 25 links pointed at the old post and none at the new one.

Why didn’t the page cache purge fix it?

Because caching was never involved. The rewrite ran on every render, cached or not. A purge just meant the filter ran again and produced the same wrong output, slightly slower. This is why the purge result is diagnostic: a fix that changes nothing is telling you your model of the problem is wrong.

How do I find which filter is doing it?

Search your snippets, plugins and theme for the_content alongside str_replace or preg_replace, then search that code for the URL slug you are chasing. Ours was found by searching 27 active snippets for the string “start-a-blog” and checking which of them also registered a content filter. That narrowed 27 candidates to one in a few seconds.

Should I delete my rewrite map?

Not necessarily, but understand what it is: a permanent runtime patch over a one-time problem. The durable fix is to update the links in your content once and use server-level redirects for anything you cannot reach. A filter that rewrites links forever is a bill you keep paying, and as we found, it can start charging interest.

Does this affect external links to the page?

No. The rewrite only touched links inside our own post content. Anyone linking to us from another site reached the page normally. The damage was entirely self-inflicted and entirely internal, which is why it survived so long: nothing external was broken enough to complain.

Related reading

Blogging Titan

Written by

Blogging Titan Team

Blogging Titan is an independent team of bloggers documenting what actually grows a blog in the AI search era. We have been building, ranking, and monetizing WordPress sites since 2017, and every guide on this site is based on strategies and tools we have tested ourselves. Want a second pair of eyes on your blog? Request a free blog audit or start with the 2026 playbook.

Blogging Titan » Blog SEO » We Linked to Our Best Page 25 Times. The Live HTML Showed Zero of Them.