D
P
0
← All articles Baca dalam Bahasa Indonesia

WordPress & PHP in Production

Searching an Author Name Returns 1 Result While Four New Posts Stay Invisible? The Theme Had No `search.php`

· · 7 min read
Searching an Author Name Returns 1 Result While Four New Posts Stay Invisible? The Theme Had No `search.php`

The client email carried three observations about the backend, and one of them read like this: recently published articles do not show up right away on the homepage, in the latest list, or in search. Three surfaces inside a single complaint sentence, and they did not turn out to share one cause.

The homepage half was settled earlier, one round of work before this one, and without a single line of code. The cause was not cache. This theme's Customizer holds an author allowlist for the feeds, and that allowlist already had a few users ticked but not the writer who was complaining. The moment even one user is ticked, only the authors on that list appear in the feeds. The four newest posts, published back to back within a span of a dozen minutes on the same day, were silently excluded from the homepage even though they were the most recent ones there. I curled the front page and looked for those titles: zero matches. The fix lived entirely in settings, untick every user in the feed allowlist and then tick that writer in the blog page allowlist. That round shipped no code at all.

What was left was search, and that is where I let myself feel finished too early.

First diagnosis: WordPress search SQL has no idea who wrote the post

The next round is where I picked up the search report. The symptom: searching the writer's first name returned exactly one result, a single article that happened to mention the name in its body text, while the four newest articles did not appear at all.

The explanation I held at the time was reasonable and, as far as the SQL goes, correct. WordPress default search only scans post_title, post_content, and post_excerpt. The byline lives in the WordPress user account, not in the body text, so that query could never reach it.

So I wrote a posts_search filter that wraps the default search clause in an OR group: posts whose post_author matches a user whose name contains the keyword, or posts whose byline meta contains the keyword.

AND ( {WP default search clause} OR (
    wp_posts.post_author IN ({user ids matching display_name/login/nicename})
    OR wp_posts.ID IN ({post ids with _theme_byline LIKE %keyword%})
))

The filter is gated by four conditions: not is_admin(), is_main_query(), is_search(), and a keyword of at least two characters counted with mb_strlen. The user lookup runs through get_users with a wildcard pattern, three search columns, IDs only, capped at 50. The byline lookup is a direct $wpdb LIKE against the byline meta, with esc_like and LIMIT 200.

$user_ids = get_users( [
    'search'         => '*' . $s . '*',
    'search_columns' => [ 'display_name', 'user_login', 'user_nicename' ],
    'fields'         => 'ID',
    'number'         => 50,
] );

This filter travelled with three other items from the same round inside one new file in the inc folder, around 290 lines, plus a one line require_once in functions.php. The upload order cannot be reversed: the inc file first, then functions.php. If functions.php goes up first, the require_once points at a file that is not there yet, wp-admin dies with a fatal, and the only way back is SFTP.

My notes that day said that searching the author name now returned every story that writer authored plus every story carrying the name in the byline meta. I wrote that sentence from reading the code, not from opening the site.

Verifying in the browser flipped the story

Once the PHP filter was live I opened the site through Playwright MCP, and that verification surfaced three gaps the PHP-only audit could not see.

The most basic gap: this theme had no search.php at the theme root. Its structure assumed every render went through front-page.php, single-*.php, page-templates/*.php, and the taxonomy template, while index.php was only a placeholder rendering a single H1. There was a search template under page-templates, a client side searchable index, but it only fired when it was manually assigned to the /search/ page. The header search box submits to the native ?s= URL, and that URL, through the WordPress template hierarchy, landed on broken results. In my notes, this is what sat behind the "only one result" report from the client.

So the SQL filter I shipped first was wrapping the query for a page that never rendered any results at all. The clause was right, the results never reached the screen.

The same gap explains another symptom on that site. This theme had no page.php either, so static pages whose content was complete in the database fell into the same placeholder and looked empty.

The third gap sat inside the client side search index itself. Its byline field read the byline postmeta only. The four posts from that writer had no byline meta, because the account is the post_author, so the JS scorer in the search box returned zero for the name. The single template already carried the right fallback, byline meta when present and the WordPress author name when not, but the index builder never mirrored it.

The fix

Day two of that round shipped four files. A search.php at the theme root, four lines and nothing else. A new page.php of around 38 lines for static pages. A search index template part extracted from the old page template, and that page template itself trimmed from about 265 lines down to 14.

<?php
// search.php at theme root, picked up automatically for ?s= URLs
get_header();
get_template_part( 'template-parts/search-list' );
get_footer();

Inside that template part, the three loops that fill the index (news, interviews, blog) use one byline resolve helper with a fallback to the author display name, mirroring the pattern already present in the single template. The search input is populated from get_search_query() so a ?s= deep link filters immediately on page load, since render() is already called at the end of initSearch().

$resolve_byline = function ( $byline, $post_author ) {
    return $byline ?: get_the_author_meta( 'display_name', $post_author );
};
<input type="search" name="s" value="<?php echo esc_attr( get_search_query() ); ?>">

All three fixes went up without a code rollback and without manual editor intervention. I cleared the WP Rocket cache through Playwright MCP with one click on the clear and preload button, and its notice recorded the time. The final verification ran server side without the cache bypass parameter: the search URL carrying that author name returned a page of over 800 KB, the input already filled with the name, the index holding a bit over a hundred items, and the JS filter rendering four results the moment the page loaded. Four, the same count as the articles that had gone missing.

Why a cache purge shipped along with it

One more item from the same round touched the homepage side of that complaint. After the allowlist was opened up, the posts from that writer should have appeared on the homepage immediately, and they did not. My guess at the time: WP Rocket auto purges the URL of a post when it is saved, but the homepage cache rebuild can lag behind, and the homepage cache that existed was generated before publish with a TTL that had not expired. That is a guess, not something I measured.

So I added a defensive purge, one function hooked to save_post_post, save_post_story, publish_post, and publish_story at priority 30. It calls rocket_clean_home(), rocket_clean_post( $post_id ), and rocket_clean_files() with an explicit list of the archive surfaces the post likely changed: /, /search/, the news listing page, and each term archive the post belongs to.

The lesson

A PHP audit reads the code that is there. It does not see the file that is missing, and the WordPress template hierarchy works precisely through missing files: no search.php, no page.php, everything falls back to index.php. The posts_search filter was not wrong, it just answered a question the page had never been asked.

The round before had already handed me the rule, and I broke it in this one. For a bug only a visitor can see, curl the raw HTML as an anonymous user, because a logged in admin bypasses the cache. In that earlier round I had also guessed at a root cause without verifying it, got pushed back on, then moved to curl based diagnosis and only then found the real culprit. This time the shape was different but the pattern was identical: a conclusion written from the code, before the page was ever opened. Now, when a client complaint reads "search only gives one result", my first step is not reading the query. It is opening the ?s= URL they used and looking at which template actually answers it.