Live Search
The hero search dropdown is one of the most-used parts of the site — a polished, debounced live-search experience with smart category icons. This page explains how it works, what’s customizable, and how to extend it.
Free feature — no Pro license required. Built into the base theme.
How it works
The dropdown is a single-state machine — at any time, exactly one of loading, empty, or results is shown. This prevents the “spinner + empty state + results stacked” bug some search UIs have.
REST endpoint
GET /wp-json/best-classifieds/v1/search?q={query}
| Param | Type | Required | Description |
|---|---|---|---|
q |
string | Yes | Search query, minimum 2 characters |
Response
{
"q": "honda",
"listings": [
{
"id": 42,
"title": "2018 Honda Civic Sport — One Owner",
"url": "https://example.com/listing/honda-civic/",
"icon": "ti-car",
"excerpt": "Bought new from Austin Honda in 2018, single owner...",
"location": "Austin, TX",
"price": "$14,800"
}
],
"categories": [
{
"id": 12,
"name": "Vehicles",
"url": "https://example.com/category/vehicles/",
"icon": "ti-car",
"count": 8
}
]
}
The endpoint is public (__return_true permission callback). No auth required.
Smart icon assignment
Each listing in the response gets an icon field — a Tabler icon class. The matching logic in best_classifieds_smart_icon_for_post():
- Category-based (highest priority): If the listing belongs to a category with a defined icon (Vehicles →
ti-car), that’s the icon. - Keyword scan: If no category icon was found, the title is scanned against a keyword map: “Honda” / “Civic” / “BMW” →
ti-car, “MacBook” / “iPhone” →ti-device-laptop, etc. - Generic fallback:
ti-tag.
The keyword map is in inc/template-functions.php. To add your own keyword groups via a filter:
add_filter( 'best_classifieds_smart_icon_keywords', function( $map ) {
$map['boats|yacht|sailboat|kayak'] = 'ti-sailboat';
$map['camera|lens|tripod|gopro'] = 'ti-camera';
return $map;
} );
Pattern: 'pattern1|pattern2|pattern3' => 'ti-iconname'. Patterns are word-boundary regex, case-insensitive.
Frontend behavior
Debouncing
The input listener has a 200ms debounce. Typing rapidly fires only one request after you pause. Set the timer if you want — search for debounceTimer = setTimeout(... 200); in live-search.js.
Stale response handling
If the user types faster than the network responds, an older request can return after a newer one. The JS guards against this:
if (input.value.trim() !== query) return; // stale response
The discarded response is silently dropped. Only the latest query’s results render.
Keyboard navigation
| Key | Action |
|---|---|
↓ / ↑ |
Move active result up/down |
Enter |
Open the active result; if none active, submit form normally (full search results page) |
Esc |
Close the dropdown |
Tab |
Standard browser tab navigation |
Highlighting
The query is wrapped in <mark> tags around matches in the title and excerpt. The <mark> is styled in the accent color (gold by default) so matches pop visually.
The highlight respects HTML escaping — < in the title is escaped before the regex runs, so no XSS surface.
Customizing what shows up
Limit search to specific post types
By default the endpoint queries best_classifieds_get_listing_post_type() (which is post for the free theme, listing for Pro CPT mode). To search additional post types:
add_filter( 'best_classifieds_search_post_types', function( $types ) {
return array( 'post', 'page', 'product' ); // include pages and WooCommerce products
} );
Change the result count
Default is 8 listings + 4 categories. To change:
add_filter( 'best_classifieds_search_listings_limit', function() { return 5; } );
add_filter( 'best_classifieds_search_categories_limit', function() { return 6; } );
Add custom fields to the response
The excerpt, location, and price fields are emitted; to add more (e.g. condition, mileage, salary range):
add_filter( 'best_classifieds_search_result_fields', function( $fields, $post_id ) {
$fields['condition'] = get_post_meta( $post_id, '_bc_condition', true );
return $fields;
}, 10, 2 );
The frontend JS doesn’t render these by default. To show them in the dropdown, you’d also extend live-search.js (or override it from a child theme).
Performance considerations
The endpoint runs a WP_Query with s={query} — which is WordPress’s default LIKE-based search. For sites with 10,000+ listings, you’ll want to plug in a better search engine:
- ElasticPress — works automatically; it intercepts WP_Query and routes to Elasticsearch
- SearchWP — paid plugin with great relevance tuning
- Algolia — replace the endpoint entirely with an Algolia-powered alternative
To replace the entire endpoint with your own:
remove_action( 'rest_api_init', 'best_classifieds_register_search_endpoint' );
add_action( 'rest_api_init', function() {
register_rest_route( 'best-classifieds/v1', '/search', array(
'methods' => 'GET',
'permission_callback' => '__return_true',
'callback' => 'my_custom_search_callback',
) );
} );
Return the same JSON shape and the frontend JS will keep working unchanged.
Disabling live search
If you want a plain old form post (no dropdown):
add_filter( 'best_classifieds_enable_live_search', '__return_false' );
The form still works — it just submits to the standard search page on Enter, no live results.
Mobile behavior
The dropdown adapts to the small viewport:
- Full-width (extends past form edges via
left: -8px; right: -8px) - Max height
65vhso it doesn’t crowd the keyboard - Excerpt is hidden (just title + meta to save space)
- Touch targets minimum 44px
The search input is font-size: 16px to prevent iOS auto-zooming on focus — a small detail that makes the experience feel native.