I'm trying to load/utilise an existing page template (templates/results.php) while making use of the "s" (search) query parameter. For example: example.com/results?s=lorem

Currently, this results in a 404.

I've got a filter using template_includes, however it appears that the page has already been "decided" before that hook/filter (is_search(), is_page('results') are both false) before loading the template. I could just load the template files based on some other parameters, but then I'd have to set the title and other things away from the 404 page.

The pagename and s fields are recognised in the global $wp_query's raw query vars, so it'd just be a case of "deciding" what the query actually is before the template is hit.

Usecase for this being able to show a filtered set of posts for a custom post type "listings". The Listings archive already shows listings, automatically filtered for "open" ones, and optionally filtered for its taxonomy and "s" for any searches. I'd have the results page do the same thing, being able to pick up the same optional query parameters as its archive counterpart.

How can I load a page, and utilise an existing page template while still using the in-built s query parameter?

This answer's solution 1 mentions replacing the default 's' query var with a custom one, aiming to avoid that and hopefully filter/hook in before WordPress decides to search the results page for the 's' string.

How the page/query are currently used:

Template file:


// get_header() and the_post() related parts have already been called in the main template file.

// Temporary query to replace the main one so we can utilise pagination, etc.
global $post;
global $wp_query;

$listing_results_query = new WP_Query( [
    'listing_type'           => get_query_var( 'listing_type' ),
    'pricing'                => get_query_var( 'pricing' ),
    's'                      => get_query_var( 's' ),
    'paged'                  => get_query_var( 'paged', 1 ),
    'post_type'              => 'listing',
    'query_id'               => 'listing-results',
    'listing_status_compare' => '!='
] );

$temp_query = $wp_query;
$wp_query   = NULL;
$wp_query   = $listing_results_query;

<span>Various HTML and get_template_part() calls being used to load the archive</span>

// Reset main query object
$wp_query = NULL;
$wp_query = $temp_query;

Hook/function file (equivalent of functions.php)
I've cut some checks/other query parts out to simplify it for here. At the point of calling this hook (pre_get_posts), $query->is_404 is already set to true.

function wpse_390935_modify_listing_archive_query( $query ) {
    $is_listing_archive_results = ! is_admin() && ( $query->query['query_id'] ?? false ) === 'listing-results';

    if ( $is_listing_archive_results ) {
        $page_no        = $query->query['paged'] ?? 1;
        $listing_type   = $query->query['listing_type'] ?? '';
        $pricing        = $query->query['pricing'] ?? '';
        $search         = $query->query['s'] ?? '';
        $status_compare = $query->query['listing_status_compare'] ?? '=';
        $meta_query     = [];

        // Search
        if ( $search ) {
            $query->set( 's', sanitize_text_field( $search ) );

        // Other custom field/taxonomy query bits
add_action( 'pre_get_posts', 'wpse_390935_modify_listing_archive_query', 10, 1 );
  • Are you working on a theme? If so, it would probably be easiest just to set up the search.php template file (which is loaded automatically by template hierarchy conventions) to load your file. Orrrrr... maybe I've misinterpreted the question.
    – bosco
    Commented Jun 24, 2021 at 2:28
  • @bosco On a custom theme. The results page shows a bunch of listing CPT posts, which can be filtered by the CPT's taxonomy and then 's'. The main listing CPT archive shows the posts (auto filtered for "open" ones), and the results page shows the same thing but ones which are "closed'. Just realised I did the wrong url in the post, will pop some extra info in there too Commented Jun 24, 2021 at 2:37
  • Ahhh gotchya. Tricky one - need to try and prevent WP_Query from parsing s into the main query args. That or maybe a rewrite to a different query var, so it appears as s to the end user but is something else internally.
    – bosco
    Commented Jun 24, 2021 at 3:44
  • How does your page template make use of the s query var? Do you use a secondary WP_Query or get_posts() or some such in there?
    – bosco
    Commented Jun 24, 2021 at 17:33
  • I've got a new WP_Query on the page which temporarily replaces the main one (replaces for most of the actual template part). That WP_Query has a custom query ID which gets picked up in a pre_get_posts hook, as well as sending query vars for s, pricing (a custom field) and listing_type (custom taxonomy) - both these other two work fine. All values are picked up by get_query_var when used as construction args in the WP_Query. Edit: I'll put what I've got in the question - might make a bit more sense Commented Jun 24, 2021 at 23:26

1 Answer 1


Custom Query Var

Moving s to a custom query variable when a page is explicitly requested should be simple enough:

function wpse390935_set_page_search_qv( $wp ) {
  $qvs = $wp->query_vars;

  if( empty( $qvs['s'] ) || ( empty( $qvs['pagename'] ) && empty( $qvs['page_id'] ) ) )

  $wp->query_vars['page_search'] = $qvs['s'];
  unset( $wp->query_vars['s'] );
add_action( 'parse_query', 'wpse390935_set_page_search_qv' );

This prevents it from getting factored into the main query while, leaves it accessible later via get_query_var( 'page_search' ), and continues to present the variable as s to end-users.

Stashing the Query Var

If it absolutely needs to remain in s internally, then you could use a bit of a hack and remove s from the request's query_vars using a parse_request action hook, then add it back again after the query executes.

I've used a singleton class here just to hold the s value in between hooks.

class WPSE390935_Search_Param_Passthrough {
  private static $instance = null;

  private $search = '';

  public static function get_instance() {
    if( is_null( self::$instance ) )
      self::$instance = new self();

    return self::$instance;

  private function __construct() {
    add_action( 'parse_request', [ $this, 'stash_query_var' ], 2 );

  public function is_page_search() {
    if( is_admin() )
      return false;

    if( did_action( 'parse_request' ) > 0 )
      return ! empty( $this->search );
    throw new Error( 'Too early to determine is_page_search() condition.' );

  public function restore_query_var() {
    set_query_var( 's', $this->search );

  public function stash_query_var( $wp ) {
    $qvs = $wp->query_vars;

    if( ( empty( $qvs['pagename'] ) && empty( $qvs['page_id'] ) ) || empty( $qvs['s'] ) )
    $this->search = $qvs['s'];

    unset( $wp->query_vars['s'] );

    add_action( 'wp', [ $this, 'restore_query_var' ], 2 );

function is_page_search() {
  return WPSE390935_Search_Param_Passthrough::get_instance()->is_page_search();


I'd strongly recommend not adding the search value back to the main query, and instead just retrieving it from the class later when needed. This way the state of the main query's query vars would not misrepresent what the main query actually is.

To a similar end, I've also provided an alternate conditional predicate in the form of is_page_search(). While you could set $wp_query->is_search = true in the restore_query_var() method, I would be concerned about any side-effects which that might have. Both in core and any plugin depending on the core predicate.

  • Thanks, opted for the singleton method. Never thought of keeping it that way, very nice. I take it this means there's no simple way to "reprogram" what WP does when it builds the page with the 's' and 'pagename' query params then, guess I was hoping for the easy way out with a hook. Anyway, tested and works exactly as intended, I've called the class just before building the WP_Query on the template so the main query and other parts aren't affected - don't think any part of the main query would do anything with the 's' query var after get_header() has been called Commented Jun 25, 2021 at 1:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.