Every WordPress site has a page like this. It was published two years ago, it still exists, and nobody has linked to it since. It sits in the XML sitemap, so it looks accounted for. It has never appeared in a report, because reports are built from links it does not have.
The usual advice is to run an orphan page audit. That advice is right, but the definition behind it is broken. On a WordPress site with a menu, almost nothing is technically an orphan, and the pages that are actually stranded do not show up under the strict definition at all.
The Strict Definition Is Useless on a WordPress Site
Strictly, an orphan page is a page with no internal links pointing to it. No menu item, no footer link, no body link. Nothing.
Almost no WordPress page meets that test. The header and footer link the same handful of pages on every URL, and a typical menu covers the services, the blog, and the contact page. The blog archive lists every post. The strict count on a normal site is close to zero, which is exactly why the audit feels finished before it starts.
The definition also conflicts with what Google actually asks for. Its link guidance puts the requirement in one sentence: “Every page you care about should have a link from at least one other page on your site.” A navigation link satisfies the letter of that. It does not satisfy the intent, because a menu link is identical on every page and says nothing about what the destination covers.
So use a working definition instead: a page is an effective orphan when no contextual link points to it from a page a reader would plausibly be reading. Contextual means a link inside body content, placed because the destination is relevant to the sentence around it. It is not a Google term. It is the version you can act on.
There is a second form worth naming. A page can have inbound links and still be effectively unreachable, because every link to it comes from an archive, a tag page, or another page that is itself buried. The links exist. The path does not. That is why counting inbound links and measuring reachability are two different audits, and why this piece does the second one.
Why Pages Become Orphans
Orphans are rarely created on purpose. They are the residue of five ordinary events.
1. A migration moved the content and not the links. A site changes theme, host, or permalink structure, and the posts arrive intact while the contextual links between them point at URLs that no longer exist. The old links may redirect, which hides the damage: a redirect is a working link for a reader and a weaker one for discovery.
2. A hub was rewritten. The page that used to collect ten articles is now a service page, or its slug changed, or it was replaced by a newer overview. The ten articles still exist, still rank for their own queries, and lost their only parent.
3. The menu was restructured. A page is dropped from the navigation because the menu was getting long. It stays published because nobody unpublishes anything. Now it has no inbound links at all, and the only record of it is in the sitemap.
4. A landing page was built for ads. Thank-you pages, gated download pages, and campaign pages are often built to be reached from one ad and nothing else. They are legitimately unlinked. They also should not be indexed, which is a different decision than deleting them.
5. Content was split or merged. A long article becomes three shorter ones in a refresh, or three thin ones become one. The redirects get set up for the old URL, and the new ones go live with no links between them, because the links lived in the piece that was replaced.
None of these involve anyone deciding to hide a page. That is what makes the audit necessary: the problem is created by work that looked finished.
The Sitemap Lists Orphans, Which Is the Problem
A sitemap feels like an inventory, and an inventory feels like proof that a page is connected. It is not.
Google’s sitemap documentation separates the two jobs in a single paragraph. On discovery: “If your site’s pages are properly linked, Google can usually discover most of your site. Proper linking means that all pages that you deem important can be reached through some form of navigation, be that your site’s menu or links that you placed on pages.” On limits: “A sitemap helps search engines discover URLs on your site, but it doesn’t guarantee that all the items in your sitemap will be crawled and indexed.”
That second sentence is the one to keep. A sitemap is a list of URLs with metadata. It carries no anchor text and no sentence explaining why one page leads to another, so a page that appears only in the sitemap is listed without being recommended. It can be crawled. It has no reason to be.
The Page indexing report is where this becomes visible. Google’s own diagnostic instruction is blunt: “If the total URL count in this report is much smaller than your site’s page count, then Google isn’t finding pages on your site.” The same page explains why that matters — “Google needs a way to find a page in order to crawl it” — and warns that the example URL list is capped at 1,000 rows, so the report is a sample even when it looks complete.
Read those two documents together and the sitemap’s role becomes clear. It is a discovery aid for pages that are already linked. It is not a substitute for the links, and it is not an audit.
Reachability Is the Measure That Matters
Two questions sound similar and produce different answers.
“How many pages link here?” is the inbound count. It is useful, it is cheap to compute from post_content, and it is the first audit most sites run. Its blind spot is that one link from a page nobody visits is indistinguishable from one link from the homepage.
“How many clicks from the homepage?” is reachability. It measures the path a crawler and a reader actually take. A page that is three links deep is in the graph. A page that is seven links deep, behind a paginated archive and a tag page, is technically in the graph and practically invisible.
Click depth is not a Google ranking metric, and no official documentation gives it a threshold. Treat it as a proxy for two things you can reason about: how much internal context a page receives, and how likely a crawler is to arrive with a fresh crawl budget. The numbers below are rules of thumb for triage, not a scoring system.
| Depth | What it usually means |
|---|---|
| 0–1 | Homepage, or a page in the main navigation |
| 2 | A hub or a major section landing page |
| 3 | A normal article reached from a hub |
| 4 | A supporting page reached from an article |
| 5+ | Buried. Worth a deliberate link even if it is technically reachable |
| unreachable | No path from the homepage at all |
Expect a long tail at depth 5 and 6. That is not a crisis. The tail becomes a problem when a page carrying commercial intent or a page you actively maintain sits in it.
A Breadth-First Crawl You Can Run
The script below starts at the homepage, fetches each page, extracts every same-host <a href>, and records the shortest number of clicks needed to reach each published post or page. Breadth-first order is what makes the number the shortest path: a URL is enqueued the first time it is seen, so it is discovered at the smallest possible depth.
It reads the HTML a visitor receives rather than the post_content field on purpose. An orphan is a page with no path, and paths live in menus, widgets, related-post modules, and theme templates as well as in article bodies. Reading the database would miss all of those and report false orphans.
<?php
/**
* orphan-depth.php — shortest click path from the front page to every published page.
* Run: wp eval-file orphan-depth.php > depth.csv
*/
$home = home_url( '/' );
$host = wp_parse_url( $home, PHP_URL_HOST );
$queue = array( array( $home, 0 ) );
$seen = array( $home => 0 );
$depth = array();
$limit = 3000;
while ( $queue && count( $seen ) < $limit ) {
list( $url, $level ) = array_shift( $queue );
$id = url_to_postid( $url );
if ( $id && ! isset( $depth[ $id ] ) ) {
$depth[ $id ] = $level;
}
if ( $level >= 6 ) {
continue;
}
$response = wp_remote_get( $url, array( 'timeout' => 20 ) );
if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
continue;
}
preg_match_all(
'#<a\s[^>]*href=["\']([^"\']+)["\']#i',
wp_remote_retrieve_body( $response ),
$matches
);
foreach ( $matches[1] as $href ) {
$link = trim( $href );
$link = explode( '#', $link )[0];
$link = explode( '?', $link )[0];
if ( '' === $link || 0 === strpos( $link, '//' ) ) {
continue;
}
if ( 0 === strpos( $link, '/' ) ) {
$link = home_url( $link );
}
if ( $host !== wp_parse_url( $link, PHP_URL_HOST ) ) {
continue;
}
if ( preg_match( '#/(wp-admin|wp-json|wp-login\.php)#', $link ) ) {
continue;
}
if ( preg_match( '#\.(jpg|jpeg|png|gif|svg|webp|pdf|zip|css|js|xml|txt)$#i', $link ) ) {
continue;
}
if ( isset( $seen[ $link ] ) ) {
continue;
}
$seen[ $link ] = $level + 1;
$queue[] = array( $link, $level + 1 );
}
}
$published = get_posts(
array(
'post_type' => array( 'post', 'page' ),
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
)
);
$out = fopen( 'php://stdout', 'w' );
fputcsv( $out, array( 'depth', 'url', 'title' ) );
foreach ( $published as $id ) {
fputcsv(
$out,
array(
isset( $depth[ $id ] ) ? $depth[ $id ] : 'unreachable',
get_permalink( $id ),
get_the_title( $id ),
)
);
}
fclose( $out );
wp eval-file runs the file inside a fully loaded WordPress, so home_url(), url_to_postid(), and wp_remote_get() are already available. url_to_postid() is doing the translation from URL to post ID, which is why the output is keyed to real published content rather than to whatever strings happen to appear in an href.
Five notes on running it:
- It uses loopback HTTP requests. Some managed hosts block requests from the server to its own domain. If every row comes back unreachable, run the script against a local copy or a staging environment where loopback works.
- It stops at 3,000 URLs and depth 6. Those caps keep a run bounded on a normal site. Raise them for a large catalog, and expect the run time to grow with the number of pages, not the number of links.
- It does not honor
rel="nofollow". A nofollowed internal link is still clickable for a reader, so the crawl counts it. Google may treat those links as a weaker signal, which is why a row discovered only through nofollow is evidence of a path rather than proof of one. - It does not normalize trailing slashes. A site that mixes
/pageand/page/spends a few extra requests. That costs time, not accuracy, because both forms resolve to the same post ID. - It reads raw HTML, not the rendered DOM. Links injected by JavaScript after load are missed, which can make a JavaScript-heavy site look deeper than it is. Google renders pages before indexing, as the JavaScript SEO basics explain, so treat those rows as “check manually” rather than “orphaned”.
Read the Output
The CSV sorts however you open it, so sort by the depth column first. Three patterns are worth looking for.
Unreachable rows. These are the true orphans. Check them against the sitemap before touching anything: if a URL is in the sitemap but not in the crawl, the page has no path from the homepage at all.
Depth 5 and 6 rows with commercial intent. A privacy policy at depth 6 is fine. A service page or a product page at depth 6 is a page you are paying to maintain and not letting anyone find.
Depth 1 and 2 rows that should not be there. Utility pages, tag archives, and thin landing pages often sit high because they are in the menu or the footer. Shallow is not automatically good; it means the page is consuming sitewide link equity that a real hub could use.
For a sanity check on coverage, count what WordPress considers published:
wp post list --post_type=post,page --post_status=publish --format=count
If the crawl found substantially fewer URLs than that count, the difference is either unreachable pages or a crawl that stopped early at the cap. Both are worth knowing, and the two are easy to tell apart by looking at the last depth value in the output.
Triage: Five Decisions, Not One
An orphan audit produces a list, and the list is not the deliverable. Each row needs one of five decisions, and choosing the wrong one is how sites end up with more redirects than pages.
| Decision | Use it when | Watch out for |
|---|---|---|
| Link | The page has unique value and a relevant source exists | The source has to be a page a reader would actually be on |
| Merge | Two pages answer the same query and neither is strong | Merge the content first, then redirect the weaker URL |
| Redirect 301 | The page was replaced, and it has backlinks or traffic | Do not redirect unrelated pages to the homepage |
| noindex | The page must exist for users but has no search value | The tag only works if the page stays crawlable |
| Delete | No links, no traffic, no unique information | Check inbound links before unpublishing |
Link is the only decision that adds value without removing anything, and it is usually underused because it requires writing a sentence. The link has to come from a page on the same subject. A link from the newest post to an unrelated old page fixes the count and not the problem.
Merge is for the pair of pages that both answer “how much does shipping cost” in slightly different words. Pick the URL with the better links, move the useful paragraphs into it, and redirect the other. The semantic SEO guide covers why two pages competing for one entity is worse than one page covering it properly.
Redirect is the right answer when the page has been replaced. Google’s redirect documentation is precise about the type: “The 301 and 308 status codes mean that a page has permanently moved to a new location,” and a permanent redirect is “a signal that the redirect target should be canonical.” Use a 302 only when the move is genuinely temporary, because a temporary redirect keeps the old URL eligible to appear in results.
The warning that belongs with this row: do not redirect twenty unrelated orphans to the homepage. That produces a set of URLs that all resolve to a page that does not answer them, which is a worse outcome for a visitor than a 404 with a search box.
noindex is for the page that has a job but not a search audience. Thank-you pages, gated downloads, and internal tools are the standard cases. Keep them out of the sitemap at the same time, and remember that a noindexed page still needs to be crawlable for the tag to be read. The staging guide covers the same tag in a different context, and the ordering rule is identical.
Delete is correct more often than site owners expect. A page with no inbound links, no organic traffic, and no information a reader cannot get elsewhere is not an asset. Before deleting, run the broken link check to catch the one page that links to it, because a 404 is a worse outcome than a thin page.
Fix the Cause Before the Symptom
Cleaning the list takes an afternoon. Keeping it clean takes one rule in the publishing process.
Every new page gets one contextual inbound link before it goes live. Not “when someone remembers”. At publish time, from the hub section that covers the topic. That single habit keeps new content at depth 3 or less and makes the hub do the work it was built for.
Every deletion gets a link check first. Unpublishing a page without checking inbound links is how the next orphan is created. The check is one search in the database or one run of the broken link checker.
Every merge gets a redirect and a link. The redirect handles the old URL. The link handles the new one, from the page that used to point at the old one.
The page structure guide covers the URL side of the same discipline: a page whose URL says /seo/technical/ and whose links describe it as a performance article is a page with two parents and no clear one. Structure and links have to agree, and the SEO basics guide covers the crawl-and-index fundamentals that make the agreement matter.
Verify the Fix, Then Stop
Re-run the script after the links are live. Three outcomes confirm the work:
- The unreachable count falls, and the pages that were unreachable now show a depth of 2 or 3.
- No page moved deeper. A new link that pushes something else further away is a net loss, and it usually means a hub section was replaced rather than extended.
- The redirects return a single hop.
curl -sI https://yourdomain.com/old-page/ | head -n 1should return301and the target should return200, not another redirect.
Then use URL Inspection in Search Console on the pages that changed, and request indexing once. Requesting it before the links are live wastes the request, because Google will recrawl the page and find the same dead end.
Rankings are the wrong thing to watch for the first month. This work changes discovery and internal context, and both take crawl cycles to show up. The number that should move immediately is the count of unreachable pages, and it is the only number the script measures. The ranking guide covers the factors that do move positions; this audit only changes whether a page can be found at all.
Leave a Reply