D
P
0
← All articles Baca dalam Bahasa Indonesia

WordPress & PHP in Production

`get_page_by_path('add-property')` Returns `null` for a Nested Child Page? Production Ballooned to 137 Duplicate Pages

· · 5 min read
`get_page_by_path('add-property')` Returns `null` for a Nested Child Page? Production Ballooned to 137 Duplicate Pages

A booking platform I maintain for a client site has a custom plugin that auto-creates its required pages on activation. One day I opened Pages in the admin and the count made me pause: 137 pages. It should have been around 17. The rest, nearly 120 rows, were duplicates: add-property-2, add-property-3, on and on, and the same for bookings-2, bookings-3. The page list looked like a photocopier jammed in the on position.

The curious part was that not every page cloned itself. Only two split: add-property and bookings. Both happen to be child pages of owner-dashboard, where property owners manage their listings. Every top-level page stayed a single copy. Only those two nested children kept breeding.

The dead ends I tried first

My first instinct pointed the wrong way. I assumed someone had been clicking a button, or a rogue cron was looping. So I cleaned up by hand: select every add-property-N and bookings-N, trash them, empty the trash. Pages dropped back to 17. Relief, briefly.

Then the next deploy shipped, I bumped the plugin version, and the duplicates came right back. Not gradually, a fresh pair appeared the instant the version bumped. That was when I stopped blaming people. A bug that reappears at a predictable moment is deterministic, and the only thing that runs on a version bump was my own plugin bootstrap.

The root cause

The plugin has a bootstrap that ensures every required page exists, gated by a PCP_VERSION option. Each time the version constant goes up, it runs once and calls pcp_ensure_page() for each required page. That function is meant to be idempotent: if the page exists, do not create another. Here is how it checked:

$existing = get_page_by_path( $slug ); // $slug = 'add-property'
if ( ! $existing ) {
    wp_insert_post( /* ... new page ... */ );
}

For a top-level page this is correct. get_page_by_path('contact') finds the contact page, the guard trips, no insert. The trouble starts the moment a page has a parent.

add-property and bookings are children of owner-dashboard. In WordPress, a child page's path is not its bare slug, it is the full path through its parent: owner-dashboard/add-property, not add-property. And get_page_by_path() matches against that full path. So:

get_page_by_path( 'add-property' );              // null, even though the page EXISTS
get_page_by_path( 'owner-dashboard/add-property' ); // found

The bare-slug call always returned null. The guard concluded the page did not exist, so wp_insert_post() created another one. Because the slug add-property was already taken under the same parent, WordPress did not reject it, it silently appended a numeric suffix: add-property-2, then add-property-3 on the next bump. Every version bump added one more layer per nested child. That is why only those two pages ballooned, and only on a version bump.

The deceptive part was that nothing errored. get_page_by_path() returning null is valid for a path that does not match, and wp_insert_post() succeeded every time. From PHP's point of view nothing failed. The bug lived in the gap between the path I checked and the path the page actually uses.

The fix

There were two parts, and the order mattered.

First, check existence by the full path, not the bare slug. I gave pcp_ensure_page() a parent parameter and built the right path before the lookup:

function pcp_ensure_page( $slug, $title, $parent_slug = '' ) {
    $path = $parent_slug ? $parent_slug . '/' . $slug : $slug;
    $existing = get_page_by_path( $path );
    if ( $existing ) {
        return $existing->ID; // idempotency now correct for nested children
    }
    return wp_insert_post( /* ... */ );
}

With $parent_slug = 'owner-dashboard', the lookup becomes owner-dashboard/add-property and matches the existing page. The guard does its job, the insert stops. Version bumps no longer produce duplicates.

Second, clean up the hundreds of dupes already piled up. I added pcp_cleanup_duplicate_child_pages($max = 50) into that same version-gated bootstrap. It deletes the accumulated duplicates, scoped tightly: it only targets children named add-property, add-property-2, add-property-3, and so on under the required parent, never touching any other page. A few guardrails I treated as non-negotiable:

Once it ran, production pages fell from 137 to 17, and stayed there through the next version bumps.

The takeaway