How to Display Product Categories as Links + Product Grid on Search Results Page

This topic has 4 replies, 2 voices, and was last updated 3 months, 1 weeks ago ago by Jack Richardson

  • Avatar: Nancy
    Nancy
    Participant
    August 7, 2025 at 11:42

    Hi XStore team,

    I’m customizing the search results experience in my WooCommerce store using Ajax Search Pro, in combination with XStore and Elementor. I’m currently trying to achieve a specific layout for the search results page, but I’m running into some limitations, and I’d really appreciate your guidance.

    Context:
    Ajax Search Pro handles the search logic, but it relies entirely on the theme (XStore) to render the search results page.

    In my case, I would like to display both product categories and products on the same search results page, but each in a different layout:

    Categories as a list of text links

    Products in the usual product grid layout

    The challenge:
    The default XStore search results template displays all result types (products, product categories, pages, etc.) using a blog-style layout, which doesn’t work well for WooCommerce products.

    The product archive template (grid layout) displays products beautifully, but excludes categories and other result types.

    What I’m hoping to achieve:
    Display product categories (as links) above or alongside the product results

    Display matching products in the XStore product grid

    Keep both on the same page

    Ideally maintain pagination for the product results

    Why I’m reaching out:
    Since Ajax Search Pro depends on the theme’s templates for rendering search results, I’m turning to you to ask whether this type of split layout is possible within the XStore framework.

    Is there a supported or recommended way in XStore to render different result types in different layouts?

    Can I insert a block (e.g. via shortcode or code hook) to display the categories separately from the product grid?

    Or is there another approach you suggest for achieving this within your system?

    I apologize if this is a less intelligent question or if I’m missing something obvious — I’ve been puzzling over this for a while and may have lost the bigger picture. I truly appreciate your time and any help you can offer.

    Thank you in advance!

    Best regards,
    Nancy

    3 Answers
    Avatar: Jack Richardson
    Jack Richardson
    Support staff
    August 7, 2025 at 13:09

    Hello @Nancy,

    We hope this message finds you well.

    As you are currently using the Elementor builder to create your pages, we would like to inform you that it is possible to utilize our Search Results Page Builder. You can find detailed documentation and instructions at the following link:
    https://www.8theme.com/documentation/xstore/xstore-builders/xstore-search-results-page-builder-with-elementor/

    Within the page content, you may add any of the available widgets such as Searched Products, Searched Posts, and Searched Projects. Additionally, you can incorporate other Elementor widgets, including text areas, static blocks, flip boxes, and more, to further customize your search results page.

    Should you require any further assistance, please do not hesitate to provide us with more details, including relevant screenshots. Kindly share your WordPress admin and FTP access credentials via the private area so we can assist you more effectively.

    Furthermore, we recommend reviewing the following topic, which offers a solution for enabling search functionality for custom post types, in case this is relevant to your needs:
    https://www.8theme.com/topic/is-it-possible-to-make-the-search-for-store-locator-posts/#post-413819

    Best regards,
    Jack Richardson
    The 8Theme Team

    Avatar: Nancy
    Nancy
    Participant
    August 27, 2025 at 12:26

    Sorry for the late response,

    I am using ajaxsearchpro (another plugin for this). Since I’ve adapted it to xstore I want to share the process here:

    this is what I added to functions.php

    // ===== Ajax Search Pro: gedeelde instellingen =====
    if ( ! defined('BG_ASP_INSTANCE_ID') ) {
        define('BG_ASP_INSTANCE_ID', 8);
    }
    /*CHANGE THE 8 TO YOUR INSTANCE ID */
    /* ---------- Shortcodes renderers ---------- */
    add_shortcode('asp_categories', function () {
        global $asp_cats;
        if (empty($asp_cats) || !is_array($asp_cats)) return '';
        ob_start(); ?>
        <ul class="asp-cat-list">
          <?php foreach ($asp_cats as $c):
              $name  = isset($c->title) ? $c->title : '';
              $link  = isset($c->link)  ? $c->link  : '#';
              $thumb = isset($c->image) ? $c->image : '';
          ?>
            <li class="asp-cat-item">
              <a href="<?php echo esc_url($link); ?>">
                <?php if ($thumb): ?><img src="<?php echo esc_url($thumb); ?>" alt="<?php echo esc_attr($name); ?>"><?php endif; ?>
                <span><?php echo esc_html($name); ?></span>
              </a>
            </li>
          <?php endforeach; ?>
        </ul>
        <?php
        return ob_get_clean();
    });
    
    add_shortcode('asp_products', function () {
        global $asp_products;
        if (empty($asp_products) || !is_array($asp_products)) return '';
        ob_start(); ?>
        <div class="asp-products-grid">
          <?php foreach ($asp_products as $p):
              $title = isset($p->title) ? $p->title : '';
              $link  = isset($p->link)  ? $p->link  : '#';
              $img   = isset($p->image) ? $p->image : '';
              $price_html = '';
              if (!empty($p->id) && function_exists('wc_get_product')) {
                $obj = wc_get_product((int)$p->id);
                if ($obj) $price_html = $obj->get_price_html();
              }
          ?>
            <a class="asp-product-card" href="<?php echo esc_url($link); ?>">
              <?php if ($img): ?><img src="<?php echo esc_url($img); ?>" alt="<?php echo esc_attr($title); ?>"><?php endif; ?>
              <h3><?php echo esc_html($title); ?></h3>
              <?php if ($price_html): ?><div class="asp-price"><?php echo wp_kses_post($price_html); ?></div><?php endif; ?>
            </a>
          <?php endforeach; ?>
        </div>
        <?php
        return ob_get_clean();
    });
    
    add_shortcode('asp_pages_posts', function () {
        global $asp_pages;
        if (empty($asp_pages) || !is_array($asp_pages)) return '';
        ob_start(); ?>
        <ul class="asp-pages-list">
          <?php foreach ($asp_pages as $pg):
              $title = isset($pg->title) ? $pg->title : '';
              $link  = isset($pg->link)  ? $pg->link  : '#';
          ?>
            <li><a href="<?php echo esc_url($link); ?>"><?php echo esc_html($title); ?></a></li>
          <?php endforeach; ?>
        </ul>
        <?php
        return ob_get_clean();
    });
    
    add_shortcode('asp_other', function () {
        global $asp_other;
        if (empty($asp_other) || !is_array($asp_other)) return '';
        ob_start(); ?>
        <ul class="asp-other-list">
          <?php foreach ($asp_other as $o):
              $title = isset($o->title) ? $o->title : '';
              $link  = isset($o->link)  ? $o->link  : '#';
          ?>
            <li><a href="<?php echo esc_url($link); ?>"><?php echo esc_html($title); ?></a></li>
          <?php endforeach; ?>
        </ul>
        <?php
        return ob_get_clean();
    });
    
    /* ---------- Resultaat-splitsing voor tabs (client-side ASP run) ---------- */
    add_filter('asp_results', function($results, $search_id){
        if ((int)$search_id !== (int)BG_ASP_INSTANCE_ID) return $results;
        if (!wp_doing_ajax()) return $results;               // alleen live overlay vullen
    
        if (empty($results)) {
            // Niets doen -> niet per ongeluk server-side arrays leegmaken
            return $results;
        }
    
        global $asp_cats, $asp_products, $asp_pages, $asp_other;
        $asp_cats = $asp_products = $asp_pages = $asp_other = [];
    
        foreach ($results as $r) {
            $pt = isset($r->post_type) ? $r->post_type : '';
            $tx = isset($r->taxonomy)  ? $r->taxonomy  : '';
    
            if ($tx === 'product_cat' || $pt === 'product_cat') {
                $asp_cats[] = $r;
            } elseif ($pt === 'product') {
                // optioneel: if (empty($r->image)) { ..thumbnail ophalen.. }
                $asp_products[] = $r;
            } elseif (in_array($pt, ['page','post'], true)) {
                $asp_pages[] = $r;
            } else {
                $asp_other[] = $r;
            }
        }
        return $results;
    }, 10, 2);
    
    /* ---------- DEBUG helper (enkelvoudig!) ---------- */
    $GLOBALS['bg_asp_debug'] = [];
    if ( ! function_exists('bg_dbg') ) {
      function bg_dbg($k, $v){ $GLOBALS['bg_asp_debug'][$k] = $v; }
    }
    
    /* ---------- (Optioneel) suggested phrases cache voor debugging ---------- */
    $GLOBALS['bg_asp_suggested'] = [];
    add_filter('asp_suggested_phrases', function($phrases, $search_id){
        $phrases = is_array($phrases) ? array_values(array_filter(array_map('trim', $phrases))) : [];
        $GLOBALS['bg_asp_suggested'][(int)$search_id] = $phrases;
        return $phrases;
    }, 10, 2);
    
    /* ---------- Kandidaten generator (NL heuristics) ---------- */
    function bg_asp_candidates($orig){
        $w = mb_strtolower(trim((string)$orig));
        $c = [];
        if ($w !== '') $c[] = $w;
    
        // NL heuristics
        if (preg_match('~kosten$~u', $w)) {
            $c[] = preg_replace('~kosten$~u', 'ing', $w);   // bezorgkosten -> bezorging
            $c[] = preg_replace('~kosten$~u', 'en', $w);    // bezorgkosten -> bezorgen
            $c[] = 'verzendkosten';
            $c[] = 'verzending';
        }
        if (strpos($w, 'bezorg') !== false) {
            $c[] = 'bezorging';
            $c[] = 'bezorgen';
        }
    
        // 1e woord als laatste fallback
        $parts = preg_split('~\s+~u', $w, -1, PREG_SPLIT_NO_EMPTY);
        if (!empty($parts)) $c[] = $parts[0];
    
        // Uniek + schoon
        $c = array_values(array_unique(array_filter(array_map('trim', $c))));
        return $c;
    }
    
    /* ---------- Server-side ASP-run (vult óók tabs) ---------- */
    // ===================== Server-side ASP-run (vult óók tabs) =====================
    function bg_asp_run_fallback_query($phrase){
        $phrase  = trim((string)$phrase);
        $out_ids = [];
    
        global $asp_cats, $asp_products, $asp_pages, $asp_other;
        $asp_cats     = is_array($asp_cats) ? $asp_cats : [];
        $asp_products = is_array($asp_products) ? $asp_products : [];
        $asp_pages    = is_array($asp_pages) ? $asp_pages : [];
        $asp_other    = is_array($asp_other) ? $asp_other : [];
    
        try {
            if (class_exists('\\WPDRMS\\ASP\\Query\\SearchQuery')) {
                $sq = new \WPDRMS\ASP\Query\SearchQuery([
                    's'               => $phrase,
                    '_ajax_search'    => false,
                    'posts_per_page'  => 50
                ], (int)BG_ASP_INSTANCE_ID);
    
                $rows = isset($sq->posts) && is_array($sq->posts) ? $sq->posts : [];
    
                foreach ($rows as $res) {
                    $d   = isset($res->asp_data) ? $res->asp_data : (object)[];
                    $id  = isset($d->id) ? (int)$d->id : (isset($res->ID) ? (int)$res->ID : 0);
                    $ct  = isset($d->content_type) ? $d->content_type : '';
                    $pt  = isset($d->post_type)    ? $d->post_type    : ( $id ? get_post_type($id) : '' );
                    $tx  = isset($d->taxonomy)     ? $d->taxonomy     : '';
    
                    // Termen (categorieën)
                    if ($ct === 'term' || $tx === 'product_cat' || $pt === 'product_cat') {
                        $link = ($tx && $id && function_exists('get_term_link')) ? get_term_link($id, $tx) : '#';
                        if (!is_wp_error($link) && $link) $d->link = $link;
    
                        $asp_cats[] = (object)[
                            'id'=>$id,'title'=>$d->title ?? '','link'=>$d->link ?? '#','image'=>$d->image ?? '',
                            'post_type'=>$pt,'taxonomy'=>$tx
                        ];
                        continue;
                    }
    
                    // Producten (robuster): direct product óf variatie → parent
                    $is_product = ($pt === 'product' || $pt === 'product_variation');
                    if (!$is_product && $id) {
                        $gpt = get_post_type($id);
                        $is_product = ($gpt === 'product' || $gpt === 'product_variation');
                        $pt = $pt ?: $gpt;
                    }
    
                    if ($is_product) {
                        // map variatie → parent voor de Woo-loop
                        if (function_exists('wc_get_product')) {
                            $p = wc_get_product($id);
                            if ($p && $p->is_type('variation')) {
                                $parent_id = $p->get_parent_id();
                                if ($parent_id) $id = (int)$parent_id;
                            }
                        }
                        $asp_products[] = (object)[
                            'id'=>$id,'title'=>$d->title ?? ( $id ? get_the_title($id) : '' ),
                            'link'=>$d->link ?? ( $id ? get_permalink($id) : '#' ),
                            'image'=>$d->image ?? '','post_type'=>'product','taxonomy'=>''
                        ];
                        if ($id) $out_ids[] = $id;
                        continue;
                    }
    
                    // Pagina/bericht
                    if (in_array($pt, ['page','post'], true)) {
                        $asp_pages[] = (object)[
                            'id'=>$id,'title'=>$d->title ?? ( $id ? get_the_title($id) : '' ),
                            'link'=>$d->link ?? ( $id ? get_permalink($id) : '#' ),
                            'image'=>$d->image ?? '','post_type'=>$pt,'taxonomy'=>''
                        ];
                        continue;
                    }
    
                    // Overige
                    $asp_other[] = (object)[
                        'id'=>$id,'title'=>$d->title ?? '','link'=>$d->link ?? '#',
                        'image'=>$d->image ?? '','post_type'=>$pt,'taxonomy'=>$tx
                    ];
                }
            }
        } catch (\Throwable $e) { /* optioneel loggen */ }
    
        // Laat Woo de zichtbaarheid bepalen: filter IDs naar zichtbare + unieke
        $out_ids = bg_wc_filter_visible_products($out_ids);
    
        // Eventuele kale WP zoekfallback op producten (optioneel)
        if (empty($out_ids)) {
            $wpq = new WP_Query([
                'post_type'      => 'product',
                'post_status'    => ['publish'],
                's'              => $phrase,
                'posts_per_page' => 50,
                'fields'         => 'ids'
            ]);
            $out_ids = bg_wc_filter_visible_products( is_array($wpq->posts) ? $wpq->posts : [] );
        }
    
        return ['ids' => $out_ids, 'filled' => !empty($out_ids)];
    }
    
    function bg_asp_fill_tabs_from_wd_asp(){
        if (!function_exists('wd_asp')) return;
    
        $results = (isset(wd_asp()->results) && method_exists(wd_asp()->results, 'getResults'))
            ? wd_asp()->results->getResults()
            : [];
    
        if (empty($results) || !is_array($results)) return;
    
        global $asp_cats, $asp_products, $asp_pages, $asp_other;
        $asp_cats     = is_array($asp_cats) ? $asp_cats : [];
        $asp_products = is_array($asp_products) ? $asp_products : [];
        $asp_pages    = is_array($asp_pages) ? $asp_pages : [];
        $asp_other    = is_array($asp_other) ? $asp_other : [];
    
        foreach ($results as $r) {
            $obj = (object)[
                'id'        => isset($r->id) ? (int)$r->id : 0,
                'title'     => isset($r->title) ? $r->title : '',
                'link'      => isset($r->link)  ? $r->link  : '#',
                'image'     => isset($r->image) ? $r->image : '',
                'post_type' => isset($r->post_type) ? $r->post_type : '',
                'taxonomy'  => isset($r->taxonomy)  ? $r->taxonomy  : ''
            ];
    
            if ($obj->taxonomy === 'product_cat' || $obj->post_type === 'product_cat') {
                $asp_cats[] = $obj;
            } elseif ($obj->post_type === 'product') {
                $asp_products[] = $obj;
            } elseif (in_array($obj->post_type, ['page','post'], true)) {
                $asp_pages[] = $obj;
            } else {
                $asp_other[] = $obj;
            }
        }
    }
    function bg_wc_filter_visible_products(array $ids){
        if (!function_exists('wc_get_product')) return array_values(array_unique(array_map('intval',$ids)));
    
        $out = [];
        foreach (array_unique(array_map('intval', $ids)) as $id) {
            $p = wc_get_product($id);
            if (!$p) continue;
    
            // Variatie -> parent
            if ($p->is_type('variation')) {
                $parent_id = $p->get_parent_id();
                if ($parent_id) {
                    $p = wc_get_product($parent_id);
                    if (!$p) continue;
                    $id = $parent_id;
                }
            }
            if (method_exists($p, 'is_visible') ? $p->is_visible() : true) {
                $out[] = $id;
            }
        }
        return array_values(array_unique($out));
    }
    /* ---------- Main hijack van resultatenpagina ---------- */
    add_action('pre_get_posts', function($q){
        if ( is_admin() || !$q->is_main_query() || !is_search() ) return;
        if ((int)($_GET['asp_active'] ?? 0) !== 1 || (int)($_GET['p_asid'] ?? 0) !== (int)BG_ASP_INSTANCE_ID) return;
        if (!function_exists('wd_asp')) return;
    
        unset($GLOBALS['bg_asp_predicted']); // reset melding
        $orig = isset($_GET['s']) ? trim((string)$_GET['s']) : '';
        $q->set('post_type', 'product');
    
        // 1) live ASP results → IDs
        $ids = [];
        if (isset(wd_asp()->results) && method_exists(wd_asp()->results, 'getResults')) {
            foreach ((array) wd_asp()->results->getResults() as $r) {
                if (!empty($r->id) && (($r->post_type ?? '') === 'product' || ($r->post_type ?? '') === 'product_variation')) {
                    $ids[] = (int)$r->id;
                }
            }
        }
        $ids = bg_wc_filter_visible_products($ids);
        if (!empty($ids)) {
            $q->set('post__in', $ids);
            $q->set('orderby', 'post__in');
            if (function_exists('bg_asp_fill_tabs_from_wd_asp')) bg_asp_fill_tabs_from_wd_asp();
            return;
        }
    
        // 2) server-side run op ORIGINELE term
        $base = bg_asp_run_fallback_query($orig);
        if (!empty($base['ids'])) {
            $q->set('post__in', $base['ids']);
            $q->set('orderby', 'post__in');
            return;
        }
    
        // 3) kandidaten (alleen als origineel niets oplevert)
        foreach (bg_asp_candidates($orig) as $cand) {
            if (mb_strtolower($cand) === mb_strtolower($orig)) continue;
            $out = bg_asp_run_fallback_query($cand);
            if (!empty($out['ids'])) {
                $q->set('post__in', $out['ids']);
                $q->set('orderby', 'post__in');
                $GLOBALS['bg_asp_predicted'] = ['original' => $orig, 'used' => $cand];
                return;
            }
        }
    
        // 4) nog niets
        $q->set('post__in', [0]);
    }, 20);
    
    /* ---------- Zoek-LIKE uitzetten als we post__in sturen ---------- */
    add_filter('posts_search', function($search, $q){
        if (
            $q->is_main_query()
            && $q->is_search()
            && $q->get('post_type') === 'product'
            && !empty($q->get('post__in'))
        ) {
            return ''; // Laat post__in leidend zijn
        }
        return $search;
    }, 20, 2);
    // 1) Sla de keyword suggestions op in een global
    add_filter('asp/suggestions/keywords', function($keywords, $phrase){
        global $asp_suggestions;
        $asp_suggestions = $keywords; // eerste suggestie bewaren
        return $keywords;
    }, 10, 2);
    
    // 2) Controleer bij een search.php render of er geen resultaten zijn, en trigger fallback
    add_action('template_redirect', function() {
        if (!is_search()) return;
        if (!defined('BG_ASP_INSTANCE_ID')) return;
    
        global $asp_products, $asp_cats, $asp_pages, $asp_other, $asp_suggestions;
    
        // Safeguard arrays
        $asp_products = is_array($asp_products) ? $asp_products : [];
        $asp_cats     = is_array($asp_cats) ? $asp_cats : [];
        $asp_pages    = is_array($asp_pages) ? $asp_pages : [];
        $asp_other    = is_array($asp_other) ? $asp_other : [];
    
        // Check: geen resultaten in alle tabs
        $total_results = count($asp_products) + count($asp_cats) + count($asp_pages) + count($asp_other);
        if ($total_results > 0) return; // resultaten, niks doen
    
        // Check: hebben we een suggestie?
        if (empty($asp_suggestions) || !is_array($asp_suggestions)) return;
        $first_suggestion = trim($asp_suggestions[0]);
        if (!$first_suggestion) return;
    
        // Redirect naar dezelfde zoekpagina met nieuw zoekwoord
        $url = add_query_arg('s', urlencode($first_suggestion), home_url('/'));
        // Optioneel: forceer ASP active en instance
        $url = add_query_arg([
            'asp_active' => 1,
            'p_asid'     => BG_ASP_INSTANCE_ID
        ], $url);
    
        wp_safe_redirect($url);
        exit;
    });

    I added a search.php:

    <?php
    /**
     * search.php — Tabs met Ajax Search Pro splitsing
     * - Producten-tab: gebruikt standaard Woo/XStore archive loop (met wrappers)
     * - Andere tabs: eenvoudige weergave via gesplitste ASP-globals
     */
    
    defined('ABSPATH') || exit;
    get_header();
    
    if ( ! defined('BG_ASP_INSTANCE_ID') ) define('BG_ASP_INSTANCE_ID', 8);
    
    global $asp_cats, $asp_products, $asp_pages, $asp_other;
    
    // Query markers
    $search_query = get_search_query();
    $p_asid       = isset($_GET['p_asid']) ? (int) $_GET['p_asid'] : 0;
    $asp_active   = isset($_GET['asp_active']) ? (int) $_GET['asp_active'] : 0;
    $use_asp      = ($asp_active === 1 && $p_asid === (int)BG_ASP_INSTANCE_ID);
    
    // Safeguards
    $asp_cats     = is_array($asp_cats) ? $asp_cats : [];
    $asp_products = is_array($asp_products) ? $asp_products : [];
    $asp_pages    = is_array($asp_pages) ? $asp_pages : [];
    $asp_other    = is_array($asp_other) ? $asp_other : [];
    
    // Bepaal welke tabs zichtbaar zijn
    global $wp_query;
    $has_wc_products = $use_asp && isset($wp_query) && (int)$wp_query->post_count > 0;
    $show_products   = $has_wc_products;
    $show_cats       = !empty($asp_cats);
    $show_pages      = !empty($asp_pages);
    $show_other      = !empty($asp_other);
    
    // Eerste actieve tab
    $active_tab = $show_products ? 'products' : ($show_cats ? 'cats' : ($show_pages ? 'pages' : ($show_other ? 'other' : 'none')));
    ?>
    <div class="custom-search-tabs-container">
      <div class="container">
        <div class="search-header">
          <h1 class="search-title">
            <?php echo $search_query
              ? sprintf( esc_html__('Zoekresultaten voor: “%s”', 'xstore-child'), esc_html($search_query) )
              : esc_html__('Zoekresultaten', 'xstore-child'); ?>
          </h1>
          <div class="search-form-wrapper">
            <?php echo do_shortcode('[wpdreams_ajaxsearchpro id='.intval(BG_ASP_INSTANCE_ID).']'); ?>
          </div>
        </div>
    <?php if ( isset($_GET['debug']) && current_user_can('manage_options') ) : ?>
      <pre style="white-space:pre-wrap;background:#111;color:#eee;padding:12px;margin:12px 0;max-height:60vh;overflow:auto;">
    <?php
    global $wp_query, $asp_cats, $asp_products, $asp_pages, $asp_other;
    $dump = [
      'GET'                     => $_GET,
      'BG_ASP_INSTANCE_ID'      => defined('BG_ASP_INSTANCE_ID') ? BG_ASP_INSTANCE_ID : null,
      'use_asp'                 => isset($use_asp) ? $use_asp : null,
      'wp_query->post_count'    => isset($wp_query) ? (int)$wp_query->post_count : null,
      'wp_query->found_posts'   => isset($wp_query) ? (int)$wp_query->found_posts : null,
      'asp_products_count'      => is_array($asp_products) ? count($asp_products) : null,
      'asp_cats_count'          => is_array($asp_cats) ? count($asp_cats) : null,
      'asp_pages_count'         => is_array($asp_pages) ? count($asp_pages) : null,
      'asp_other_count'         => is_array($asp_other) ? count($asp_other) : null,
      'bg_asp_predicted'        => $GLOBALS['bg_asp_predicted'] ?? null,
      'bg_asp_suggested'        => $GLOBALS['bg_asp_suggested'] ?? null,
      'bg_asp_debug'            => $GLOBALS['bg_asp_debug'] ?? null,
      'bg_asp_force_ids'        => $GLOBALS['bg_asp_force_ids'] ?? null,
    ];
    print_r($dump);
    ?>
      </pre>
    <?php endif; ?>
    <?php
     if ( ! empty($GLOBALS['bg_asp_predicted']) ) : ?>
      <p class="asp-predicted-note">
        <?php printf( esc_html__('Geen exacte matches voor “%1$s”. We tonen resultaten voor “%2$s”.', 'xstore-child'),
          esc_html($GLOBALS['bg_asp_predicted']['original']),
          esc_html($GLOBALS['bg_asp_predicted']['used'])
        ); ?>
      </p>
    <?php endif; ?>
    
        <?php if (!$use_asp): ?>
          <p class="asp-inactive-note"><?php esc_html_e('Ajax Search Pro is niet actief voor deze aanvraag (asp_active=1 & p_asid ontbreken).', 'xstore-child'); ?></p>
          <?php get_search_form(); ?>
        <?php else: ?>
    
          <div class="custom-tabs">
            <ul class="tab-titles" role="tablist">
              <?php if ($show_products): ?>
                <li class="tab-title <?php echo ($active_tab==='products'?'active':''); ?>" data-tab="tab-products" role="tab"><?php esc_html_e('Producten','xstore-child'); ?></li>
              <?php endif; ?>
              <?php if ($show_cats): ?>
                <li class="tab-title <?php echo ($active_tab==='cats'?'active':''); ?>" data-tab="tab-cats" role="tab"><?php esc_html_e('Categorieën','xstore-child'); ?></li>
              <?php endif; ?>
              <?php if ($show_pages): ?>
                <li class="tab-title <?php echo ($active_tab==='pages'?'active':''); ?>" data-tab="tab-pages" role="tab"><?php esc_html_e('Pagina’s','xstore-child'); ?></li>
              <?php endif; ?>
              <?php if ($show_other): ?>
                <li class="tab-title <?php echo ($active_tab==='other'?'active':''); ?>" data-tab="tab-other" role="tab"><?php esc_html_e('Overige','xstore-child'); ?></li>
              <?php endif; ?>
            </ul>
    
            <div class="tab-panels">
              <?php if ($show_products): ?>
               <div id="tab-products" class="tab-pane <?php echo ($active_tab==='products'?'active':''); ?>">
      <?php
      // === Render producten o.b.v. ASP-IDs, met Woo pagination ===
      // 1) Verzamel ASP product IDs in ASP-volgorde
      $asp_ids = [];
      foreach ($asp_products as $r) {
          if (!empty($r->id)) $asp_ids[] = (int) $r->id;
      }
    
      // 2) Als er niets is, toon nette no-products
      if (empty($asp_ids)) {
          do_action('woocommerce_before_main_content');
          do_action('woocommerce_no_products_found');
          do_action('woocommerce_after_main_content');
      } else {
          // 3) Paginering en query
          $paged    = max(1, get_query_var('paged') ? (int) get_query_var('paged') : (isset($_GET['paged']) ? (int) $_GET['paged'] : 1));
          $per_page = (int) apply_filters('loop_shop_per_page', 12); // sluit aan op Woo instelling/filter
    
          $args = [
              'post_type'      => 'product',
              'post__in'       => $asp_ids,
              'orderby'        => 'post__in',
              'posts_per_page' => $per_page,
              'paged'          => $paged,
          ];
          $q = new WP_Query($args);
    
          // 4) Woo loop-context en render
          do_action('woocommerce_before_main_content');
    
          // Zorg dat Woo weet dat dit een "search-like" loop is
          if (function_exists('wc_set_loop_prop')) {
              wc_set_loop_prop('is_search', true);
              wc_set_loop_prop('current_page', $paged);
              wc_set_loop_prop('per_page', $per_page);
              wc_set_loop_prop('total', (int) $q->found_posts);
          }
    
          if ($q->have_posts()) {
              // Tijdelijk de global $wp_query wisselen zodat Woo pagination en hooks goed werken
              global $wp_query;
              $old_wp_query = $wp_query;
              $wp_query     = $q;
    
              do_action('woocommerce_before_shop_loop');
              woocommerce_product_loop_start();
    
              while ($q->have_posts()) {
                  $q->the_post();
                  if (get_post_type() !== 'product') continue;
                  wc_get_template_part('content', 'product');
              }
    
              woocommerce_product_loop_end();
              do_action('woocommerce_after_shop_loop'); // pagination etc.
    
              // Herstel globals
              wp_reset_postdata();
              $wp_query = $old_wp_query;
          } else {
              do_action('woocommerce_no_products_found');
          }
    
          do_action('woocommerce_after_main_content');
      }
      ?>
    </div>
              <?php endif; ?>
    
              <?php if ($show_cats): ?>
                <div id="tab-cats" class="tab-pane <?php echo ($active_tab==='cats'?'active':''); ?>">
                  <ul class="asp-cat-list">
                    <?php foreach ($asp_cats as $c):
                      $name  = isset($c->title) ? $c->title : '';
                      $link  = isset($c->link)  ? $c->link  : '#';
                      $thumb = isset($c->image) ? $c->image : '';
                    ?>
                      <li class="asp-cat-item">
                        <a href="<?php echo esc_url($link); ?>">
                          <?php if ($thumb): ?><img src="<?php echo esc_url($thumb); ?>" alt="<?php echo esc_attr($name); ?>"><?php endif; ?>
                          <span><?php echo esc_html($name); ?></span>
                        </a>
                      </li>
                    <?php endforeach; ?>
                  </ul>
                </div>
              <?php endif; ?>
    
              <?php if ($show_pages): ?>
                <div id="tab-pages" class="tab-pane <?php echo ($active_tab==='pages'?'active':''); ?>">
                  <ul class="asp-pages-list">
                    <?php foreach ($asp_pages as $pg): ?>
                      <li><a href="<?php echo esc_url(isset($pg->link)?$pg->link:'#'); ?>"><?php echo esc_html(isset($pg->title)?$pg->title:''); ?></a></li>
                    <?php endforeach; ?>
                  </ul>
                </div>
              <?php endif; ?>
    
              <?php if ($show_other): ?>
                <div id="tab-other" class="tab-pane <?php echo ($active_tab==='other'?'active':''); ?>">
                  <ul class="asp-other-list">
                    <?php foreach ($asp_other as $o): ?>
                      <li><a href="<?php echo esc_url(isset($o->link)?$o->link:'#'); ?>"><?php echo esc_html(isset($o->title)?$o->title:''); ?></a></li>
                    <?php endforeach; ?>
                  </ul>
                </div>
              <?php endif; ?>
            </div>
          </div>
    
        <?php endif; ?>
      </div>
    </div>
    
    <?php
    // Klein tabs-script inline
    add_action('wp_footer', function(){ ?>
    <script>
    (function(){
      var wrap = document.querySelector('.custom-tabs');
      if(!wrap) return;
      var titles = wrap.querySelectorAll('.tab-title');
      var panes  = wrap.querySelectorAll('.tab-pane');
    
      function activate(id){
        titles.forEach(function(t){ t.classList.toggle('active', t.getAttribute('data-tab')===id); });
        panes.forEach(function(p){ p.classList.toggle('active', p.id===id); });
      }
    
      titles.forEach(function(t){
        t.addEventListener('click', function(e){
          e.preventDefault();
          activate(this.getAttribute('data-tab'));
        });
      });
    })();
    </script>
    <?php }, 5);
    
    get_footer();

    And this is the CSS which I added through the xstore > css settings in the frontend

    body.search .sidebar-enabled.sidebar-left {
      display: none !important;
    }
    /* Maak content breder als sidebar weg is */
    body.search .container.sidebar-mobile-off_canvas.content-page {
        max-width: 100% !important;
        padding-left: 15px;
        padding-right: 15px;
    }
    body.custom-search-page .col-md-9.col-md-push-3 {
        width: 100% !important;
        float: none !important;
    }
    body.is-search-page .col-md-3.col-md-pull-9.sidebar-enabled.sidebar.sidebar-left {
        display: none !important;
    }
    .custom-tabs { margin-top: 1.5rem; }
    .custom-tabs .tab-titles { display:flex; gap:1rem; list-style:none; padding:0; margin:0 0 1rem; border-bottom:1px solid rgba(0,0,0,.08); }
    .custom-tabs .tab-title { padding:.5rem 0; cursor:pointer; position:relative; color:inherit; opacity:.7; }
    .custom-tabs .tab-title.active { opacity:1; }
    .custom-tabs .tab-title.active::after {
      content:""; position:absolute; left:0; right:0; bottom:-1px; height:2px; background: currentColor;
    }
    .custom-tabs .tab-panels .tab-pane { display:none; }
    .custom-tabs .tab-panels .tab-pane.active { display:block; }
    
    .asp-cat-list, .asp-pages-list, .asp-other-list { list-style:none; padding:0; margin:0; }
    .asp-cat-item { display:flex; align-items:center; gap:.5rem; margin:0 0 12px; }
    .asp-cat-item img { width:40px; height:40px; object-fit:cover; border-radius:4px; }

    This is also a warning for people who are using searchanise; we were using it for years and years (think about 8 years).
    They have blocked us from using the admin and deleted the package we were using. And forcing us to use a new package that would triple the costs.
    They did that without any warning, so beware if you use them.

    Avatar: Jack Richardson
    Jack Richardson
    Support staff
    August 27, 2025 at 15:26

    Hello @Nancy,

    Thank you very much for your detailed message and for sharing your custom integration of Ajax Search Pro with the XStore theme. We truly appreciate the time and effort you’ve taken to document your implementation and provide valuable insights for the community.

    Your solution is very well-structured and will certainly be helpful for other users looking to achieve similar functionality. We’re glad to see how you’ve extended the theme’s capabilities to suit your specific needs, and we commend your initiative in customizing the search experience with such precision.

    Additionally, thank you for your note regarding Searchanise. We understand how frustrating such unexpected changes can be, especially after long-term use. Your feedback will be helpful for others considering or currently using that service.

    If you have any further questions or need assistance with anything related to XStore, please don’t hesitate to reach out. We’re always here to help.

    Best Regards,
    8Theme’s Team

  • Viewing 4 results - 1 through 4 (of 4 total)

You must be logged in to reply to this topic.Log in/Sign up

We're using our own and third-party cookies to improve your experience and our website. Keep on browsing to accept our cookie policy.