{"id":48,"date":"2026-05-02T00:15:55","date_gmt":"2026-05-02T00:15:55","guid":{"rendered":"https:\/\/docs.fasterthemes.com\/best-classifieds-wordpress-theme\/2026\/05\/02\/live-search\/"},"modified":"2026-05-02T00:15:55","modified_gmt":"2026-05-02T00:15:55","slug":"live-search","status":"publish","type":"post","link":"https:\/\/docs.fasterthemes.com\/best-classifieds-wordpress-theme\/2026\/05\/02\/live-search\/","title":{"rendered":"Live Search"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\">Live Search<\/h1>\n\n\n<p>The hero search dropdown is one of the most-used parts of the site \u2014 a polished, debounced live-search experience with smart category icons. This page explains how it works, what&#8217;s customizable, and how to extend it.<\/p>\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\"><p><strong>Free feature<\/strong> \u2014 no Pro license required. Built into the base theme.<\/p><\/blockquote>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n<h2 class=\"wp-block-heading\">How it works<\/h2>\n\n\n<svg viewBox=\"0 0 720 280\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"max-width:100%;height:auto;display:block;margin:1rem auto;font-family:system-ui,sans-serif;\">\n  <rect x=\"20\" y=\"20\" width=\"220\" height=\"60\" rx=\"6\" fill=\"#FAFAF7\" stroke=\"#1E40AF\"\/>\n  <text x=\"130\" y=\"44\" text-anchor=\"middle\" font-size=\"12\" font-weight=\"600\">User types in hero search<\/text>\n  <text x=\"130\" y=\"62\" text-anchor=\"middle\" font-size=\"10\" fill=\"#5a6f84\">Debounced 200ms after last keystroke<\/text>\n\n  <line x1=\"240\" y1=\"50\" x2=\"280\" y2=\"50\" stroke=\"#94a3b8\" stroke-width=\"1.5\"\/>\n  <polygon points=\"276,46 286,50 276,54\" fill=\"#94a3b8\"\/>\n\n  <rect x=\"280\" y=\"20\" width=\"220\" height=\"60\" rx=\"6\" fill=\"#FAFAF7\" stroke=\"#1E40AF\"\/>\n  <text x=\"390\" y=\"44\" text-anchor=\"middle\" font-size=\"12\" font-weight=\"600\">REST API call<\/text>\n  <text x=\"390\" y=\"62\" text-anchor=\"middle\" font-size=\"10\" fill=\"#5a6f84\">\/wp-json\/best-classifieds\/v1\/search?q=&#8230;<\/text>\n\n  <line x1=\"500\" y1=\"50\" x2=\"540\" y2=\"50\" stroke=\"#94a3b8\" stroke-width=\"1.5\"\/>\n  <polygon points=\"536,46 546,50 536,54\" fill=\"#94a3b8\"\/>\n\n  <rect x=\"540\" y=\"20\" width=\"160\" height=\"60\" rx=\"6\" fill=\"#FAFAF7\" stroke=\"#1E40AF\"\/>\n  <text x=\"620\" y=\"44\" text-anchor=\"middle\" font-size=\"12\" font-weight=\"600\">WP_Query<\/text>\n  <text x=\"620\" y=\"62\" text-anchor=\"middle\" font-size=\"10\" fill=\"#5a6f84\">listings + categories<\/text>\n\n  <line x1=\"620\" y1=\"80\" x2=\"620\" y2=\"110\" stroke=\"#94a3b8\" stroke-width=\"1.5\"\/>\n  <polygon points=\"616,106 620,116 624,106\" fill=\"#94a3b8\"\/>\n\n  <rect x=\"500\" y=\"120\" width=\"200\" height=\"60\" rx=\"6\" fill=\"#FAFAF7\" stroke=\"#10b981\"\/>\n  <text x=\"600\" y=\"144\" text-anchor=\"middle\" font-size=\"12\" font-weight=\"600\">Smart icon assigned<\/text>\n  <text x=\"600\" y=\"162\" text-anchor=\"middle\" font-size=\"10\" fill=\"#5a6f84\">category \u2192 keyword \u2192 fallback<\/text>\n\n  <line x1=\"500\" y1=\"150\" x2=\"460\" y2=\"150\" stroke=\"#94a3b8\" stroke-width=\"1.5\"\/>\n  <polygon points=\"464,146 454,150 464,154\" fill=\"#94a3b8\"\/>\n\n  <rect x=\"240\" y=\"120\" width=\"220\" height=\"60\" rx=\"6\" fill=\"#FAFAF7\" stroke=\"#10b981\"\/>\n  <text x=\"350\" y=\"144\" text-anchor=\"middle\" font-size=\"12\" font-weight=\"600\">JSON response<\/text>\n  <text x=\"350\" y=\"162\" text-anchor=\"middle\" font-size=\"10\" fill=\"#5a6f84\">8 listings + 4 categories<\/text>\n\n  <line x1=\"240\" y1=\"150\" x2=\"200\" y2=\"150\" stroke=\"#94a3b8\" stroke-width=\"1.5\"\/>\n  <polygon points=\"204,146 194,150 204,154\" fill=\"#94a3b8\"\/>\n\n  <rect x=\"20\" y=\"120\" width=\"180\" height=\"60\" rx=\"6\" fill=\"#FAFAF7\" stroke=\"#10b981\"\/>\n  <text x=\"110\" y=\"144\" text-anchor=\"middle\" font-size=\"12\" font-weight=\"600\">Render dropdown<\/text>\n  <text x=\"110\" y=\"162\" text-anchor=\"middle\" font-size=\"10\" fill=\"#5a6f84\">highlight matches, pills<\/text>\n\n  <rect x=\"20\" y=\"220\" width=\"680\" height=\"40\" rx=\"6\" fill=\"#fff\" stroke=\"#cbd5e1\"\/>\n  <text x=\"360\" y=\"245\" text-anchor=\"middle\" font-size=\"11\" fill=\"#5a6f84\">Keyboard nav: \u2193\/\u2191 to walk results \u00b7 Enter to open \u00b7 Esc to close \u00b7 Click outside to dismiss<\/text>\n<\/svg>\n\n\n<p>The dropdown is a single-state machine \u2014 at any time, exactly one of <code>loading<\/code>, <code>empty<\/code>, or <code>results<\/code> is shown. This prevents the &#8220;spinner + empty state + results stacked&#8221; bug some search UIs have.<\/p>\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n<h2 class=\"wp-block-heading\">REST endpoint<\/h2>\n\n\n<p><code>GET \/wp-json\/best-classifieds\/v1\/search?q={query}<\/code><\/p>\n\n\n<figure class=\"wp-block-table\"><table>\n<thead><tr>\n<th>Param<\/th>\n<th>Type<\/th>\n<th>Required<\/th>\n<th>Description<\/th>\n<\/tr><\/thead><tbody>\n<tr>\n<td><code>q<\/code><\/td>\n<td>string<\/td>\n<td>Yes<\/td>\n<td>Search query, minimum 2 characters<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n<h3 class=\"wp-block-heading\">Response<\/h3>\n\n\n<pre class=\"wp-block-code\"><code>{\n  &quot;q&quot;: &quot;honda&quot;,\n  &quot;listings&quot;: [\n    {\n      &quot;id&quot;: 42,\n      &quot;title&quot;: &quot;2018 Honda Civic Sport \u2014 One Owner&quot;,\n      &quot;url&quot;: &quot;https:\/\/example.com\/listing\/honda-civic\/&quot;,\n      &quot;icon&quot;: &quot;ti-car&quot;,\n      &quot;excerpt&quot;: &quot;Bought new from Austin Honda in 2018, single owner...&quot;,\n      &quot;location&quot;: &quot;Austin, TX&quot;,\n      &quot;price&quot;: &quot;$14,800&quot;\n    }\n  ],\n  &quot;categories&quot;: [\n    {\n      &quot;id&quot;: 12,\n      &quot;name&quot;: &quot;Vehicles&quot;,\n      &quot;url&quot;: &quot;https:\/\/example.com\/category\/vehicles\/&quot;,\n      &quot;icon&quot;: &quot;ti-car&quot;,\n      &quot;count&quot;: 8\n    }\n  ]\n}<\/code><\/pre>\n\n\n<p>The endpoint is <strong>public<\/strong> (<code>__return_true<\/code> permission callback). No auth required.<\/p>\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n<h2 class=\"wp-block-heading\">Smart icon assignment<\/h2>\n\n\n<p>Each listing in the response gets an <code>icon<\/code> field \u2014 a Tabler icon class. The matching logic in <code>best_classifieds_smart_icon_for_post()<\/code>:<\/p>\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Category-based<\/strong> (highest priority): If the listing belongs to a category with a defined icon (Vehicles \u2192 <code>ti-car<\/code>), that&#8217;s the icon.<\/li>\n<li><strong>Keyword scan<\/strong>: If no category icon was found, the title is scanned against a keyword map: &#8220;Honda&#8221; \/ &#8220;Civic&#8221; \/ &#8220;BMW&#8221; \u2192 <code>ti-car<\/code>, &#8220;MacBook&#8221; \/ &#8220;iPhone&#8221; \u2192 <code>ti-device-laptop<\/code>, etc.<\/li>\n<li><strong>Generic fallback<\/strong>: <code>ti-tag<\/code>.<\/li>\n<\/ol>\n\n\n<p>The keyword map is in <code>inc\/template-functions.php<\/code>. To add your own keyword groups via a filter:<\/p>\n\n\n<pre class=\"wp-block-code\"><code>add_filter( &#039;best_classifieds_smart_icon_keywords&#039;, function( $map ) {\n    $map[&#039;boats|yacht|sailboat|kayak&#039;] = &#039;ti-sailboat&#039;;\n    $map[&#039;camera|lens|tripod|gopro&#039;]   = &#039;ti-camera&#039;;\n    return $map;\n} );<\/code><\/pre>\n\n\n<p>Pattern: <code>&#039;pattern1|pattern2|pattern3&#039; =&gt; &#039;ti-iconname&#039;<\/code>. Patterns are word-boundary regex, case-insensitive.<\/p>\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n<h2 class=\"wp-block-heading\">Frontend behavior<\/h2>\n\n\n<h3 class=\"wp-block-heading\">Debouncing<\/h3>\n\n\n<p>The input listener has a 200ms debounce. Typing rapidly fires only one request after you pause. Set the timer if you want \u2014 search for <code>debounceTimer = setTimeout(... 200);<\/code> in <code>live-search.js<\/code>.<\/p>\n\n\n<h3 class=\"wp-block-heading\">Stale response handling<\/h3>\n\n\n<p>If the user types faster than the network responds, an older request can return after a newer one. The JS guards against this:<\/p>\n\n\n<pre class=\"wp-block-code\"><code>if (input.value.trim() !== query) return; \/\/ stale response<\/code><\/pre>\n\n\n<p>The discarded response is silently dropped. Only the latest query&#8217;s results render.<\/p>\n\n\n<h3 class=\"wp-block-heading\">Keyboard navigation<\/h3>\n\n\n<figure class=\"wp-block-table\"><table>\n<thead><tr>\n<th>Key<\/th>\n<th>Action<\/th>\n<\/tr><\/thead><tbody>\n<tr>\n<td><code>\u2193<\/code> \/ <code>\u2191<\/code><\/td>\n<td>Move active result up\/down<\/td>\n<\/tr>\n<tr>\n<td><code>Enter<\/code><\/td>\n<td>Open the active result; if none active, submit form normally (full search results page)<\/td>\n<\/tr>\n<tr>\n<td><code>Esc<\/code><\/td>\n<td>Close the dropdown<\/td>\n<\/tr>\n<tr>\n<td><code>Tab<\/code><\/td>\n<td>Standard browser tab navigation<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n<h3 class=\"wp-block-heading\">Highlighting<\/h3>\n\n\n<p>The query is wrapped in <code>&lt;mark&gt;<\/code> tags around matches in the title and excerpt. The <code>&lt;mark&gt;<\/code> is styled in the accent color (gold by default) so matches pop visually.<\/p>\n\n\n<p>The highlight respects HTML escaping \u2014 <code>&lt;<\/code> in the title is escaped before the regex runs, so no XSS surface.<\/p>\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n<h2 class=\"wp-block-heading\">Customizing what shows up<\/h2>\n\n\n<h3 class=\"wp-block-heading\">Limit search to specific post types<\/h3>\n\n\n<p>By default the endpoint queries <code>best_classifieds_get_listing_post_type()<\/code> (which is <code>post<\/code> for the free theme, <code>listing<\/code> for Pro CPT mode). To search additional post types:<\/p>\n\n\n<pre class=\"wp-block-code\"><code>add_filter( &#039;best_classifieds_search_post_types&#039;, function( $types ) {\n    return array( &#039;post&#039;, &#039;page&#039;, &#039;product&#039; ); \/\/ include pages and WooCommerce products\n} );<\/code><\/pre>\n\n\n<h3 class=\"wp-block-heading\">Change the result count<\/h3>\n\n\n<p>Default is 8 listings + 4 categories. To change:<\/p>\n\n\n<pre class=\"wp-block-code\"><code>add_filter( &#039;best_classifieds_search_listings_limit&#039;, function() { return 5; } );\nadd_filter( &#039;best_classifieds_search_categories_limit&#039;, function() { return 6; } );<\/code><\/pre>\n\n\n<h3 class=\"wp-block-heading\">Add custom fields to the response<\/h3>\n\n\n<p>The <code>excerpt<\/code>, <code>location<\/code>, and <code>price<\/code> fields are emitted; to add more (e.g. condition, mileage, salary range):<\/p>\n\n\n<pre class=\"wp-block-code\"><code>add_filter( &#039;best_classifieds_search_result_fields&#039;, function( $fields, $post_id ) {\n    $fields[&#039;condition&#039;] = get_post_meta( $post_id, &#039;_bc_condition&#039;, true );\n    return $fields;\n}, 10, 2 );<\/code><\/pre>\n\n\n<p>The frontend JS doesn&#8217;t render these by default. To show them in the dropdown, you&#8217;d also extend <code>live-search.js<\/code> (or override it from a child theme).<\/p>\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n<h2 class=\"wp-block-heading\">Performance considerations<\/h2>\n\n\n<p>The endpoint runs a <code>WP_Query<\/code> with <code>s={query}<\/code> \u2014 which is WordPress&#8217;s default LIKE-based search. For sites with 10,000+ listings, you&#8217;ll want to plug in a better search engine:<\/p>\n\n\n<ul class=\"wp-block-list\">\n<li><strong>ElasticPress<\/strong> \u2014 works automatically; it intercepts WP_Query and routes to Elasticsearch<\/li>\n<li><strong>SearchWP<\/strong> \u2014 paid plugin with great relevance tuning<\/li>\n<li><strong>Algolia<\/strong> \u2014 replace the endpoint entirely with an Algolia-powered alternative<\/li>\n<\/ul>\n\n\n<p>To replace the entire endpoint with your own:<\/p>\n\n\n<pre class=\"wp-block-code\"><code>remove_action( &#039;rest_api_init&#039;, &#039;best_classifieds_register_search_endpoint&#039; );\n\nadd_action( &#039;rest_api_init&#039;, function() {\n    register_rest_route( &#039;best-classifieds\/v1&#039;, &#039;\/search&#039;, array(\n        &#039;methods&#039;             =&gt; &#039;GET&#039;,\n        &#039;permission_callback&#039; =&gt; &#039;__return_true&#039;,\n        &#039;callback&#039;            =&gt; &#039;my_custom_search_callback&#039;,\n    ) );\n} );<\/code><\/pre>\n\n\n<p>Return the same JSON shape and the frontend JS will keep working unchanged.<\/p>\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n<h2 class=\"wp-block-heading\">Disabling live search<\/h2>\n\n\n<p>If you want a plain old form post (no dropdown):<\/p>\n\n\n<pre class=\"wp-block-code\"><code>add_filter( &#039;best_classifieds_enable_live_search&#039;, &#039;__return_false&#039; );<\/code><\/pre>\n\n\n<p>The form still works \u2014 it just submits to the standard search page on Enter, no live results.<\/p>\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n<h2 class=\"wp-block-heading\">Mobile behavior<\/h2>\n\n\n<p>The dropdown adapts to the small viewport:<\/p>\n\n\n<ul class=\"wp-block-list\">\n<li>Full-width (extends past form edges via <code>left: -8px; right: -8px<\/code>)<\/li>\n<li>Max height <code>65vh<\/code> so it doesn&#8217;t crowd the keyboard<\/li>\n<li>Excerpt is hidden (just title + meta to save space)<\/li>\n<li>Touch targets minimum 44px<\/li>\n<\/ul>\n\n\n<p>The search input is <code>font-size: 16px<\/code> to prevent iOS auto-zooming on focus \u2014 a small detail that makes the experience feel native.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The hero search dropdown is one of the most-used parts of the site \u2014 a polished, debounced live-search experience with smart category icons. This page explains how it\u2026<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-48","post","type-post","status-publish","format-standard","hentry","category-using-best-classifieds"],"_links":{"self":[{"href":"https:\/\/docs.fasterthemes.com\/best-classifieds-wordpress-theme\/wp-json\/wp\/v2\/posts\/48","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/docs.fasterthemes.com\/best-classifieds-wordpress-theme\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/docs.fasterthemes.com\/best-classifieds-wordpress-theme\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/docs.fasterthemes.com\/best-classifieds-wordpress-theme\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/docs.fasterthemes.com\/best-classifieds-wordpress-theme\/wp-json\/wp\/v2\/comments?post=48"}],"version-history":[{"count":0,"href":"https:\/\/docs.fasterthemes.com\/best-classifieds-wordpress-theme\/wp-json\/wp\/v2\/posts\/48\/revisions"}],"wp:attachment":[{"href":"https:\/\/docs.fasterthemes.com\/best-classifieds-wordpress-theme\/wp-json\/wp\/v2\/media?parent=48"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/docs.fasterthemes.com\/best-classifieds-wordpress-theme\/wp-json\/wp\/v2\/categories?post=48"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/docs.fasterthemes.com\/best-classifieds-wordpress-theme\/wp-json\/wp\/v2\/tags?post=48"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}