Skip to content

Site search and blog filtering

Site search, and the topic/date filtering on the blog index. Search results are grouped into three buckets — Resources, Pages and News — each with its own query, rather than one merged relevance list.

A single blended result list buried the resource library, which is what members actually search for. Splitting by type and giving each a fixed share of the page keeps resources visible, and lets past events be filtered out of the resource bucket.

PathWhat it does
wp-content/themes/supplychainschool/search.phpThe grouped search results page
wp-content/themes/supplychainschool/inc/components/fc-search.phpSearch block for pages
wp-content/themes/supplychainschool/partials/search-form.phpThe form markup
wp-content/themes/supplychainschool/partials/cta-search.phpSearch CTA
wp-content/themes/supplychainschool/functions.php:359blog_filter_sort() on pre_get_posts
wp-content/themes/supplychainschool/index.phpBlog index with the sort/topic selects
wp-content/themes/supplychainschool/assets/js/filter.jsSubmits the filter selects
wp-content/themes/supplychainschool/functions.php:416terms_clauses() — post_type support in get_terms()
wp-content/themes/supplychainschool/inc/cpt.php:490Excludes attachments from search

search.php defines three buckets (:16):

KeyLabel“none” wording
resourcesResourcesresources
pagePagespages
postNewsnews articles

Each gets its own WP_Query with posts_per_page = floor(get_option('posts_per_page') / 3) (:33) and the shared s term. From page 2 onward, buckets with no results are dropped entirely (:48) so later pages are not mostly empty headings.

The resources bucket does an extra pass to hide past events: for each result where the ACF type is Event or Workshop, it compares the time_start timestamp against now and collects past ones into $past_events, which are then skipped in the render loop (:87-133). If every resource result is a past event it prints the “no resources found” message.

Pages and posts render a description from the Yoast meta description (_yoast_wpseo_metadesc) falling back to wp_trim_words(get_the_content(), 30) (:136).

Pagination uses the main query’s max_num_pages (:160), not any of the three bucket queries.

A “Jump to:” list of anchor links to each bucket is built with array_walk (:66).

blog_filter_sort() (functions.php:359) hooks pre_get_posts and only acts on the main query on the blog home. It reads two query params:

  • sort — old gives orderby=date, ASC; anything else (including absent) gives date, DESC.
  • topic — adds a tax_query on the topics taxonomy by slug.

Note it calls $query->set('tax_query', $tax_query) unconditionally (:390), so an empty array is always set. That is harmless for an empty array but means any tax_query set earlier in the request is discarded.

index.php renders the two selects; filter.js submits on change.

terms_clauses() (functions.php:416) adds support for a non-standard post_type argument to get_terms(), rewriting the SQL to join term_relationships and posts and count only terms used by those post types. This is what makes get_terms(['post_type' => 'post', 'taxonomy' => 'topics', 'hide_empty' => true]) in index.php:6 return only topics that actually have posts, rather than topics used by resources too.

exclude_images_from_search_results() (inc/cpt.php:490) sets exclude_from_search on attachment at init. The videos, faqs and partners CPTs are registered with 'exclude_from_search' => true; resources is explicitly false so it is searchable.

Admin-side resource search is separately extended to match post meta so a Moodle cmid finds its resource — inc/admin/resources.php, see [[lms-resource-sync]].

  • Settings → Reading → “Blog pages show at most” divides by three for the per-bucket count. A value not divisible by 3 loses the remainder to floor().
  • Yoast SEO supplies result descriptions.
  • No environment variables.
  • $_GET['s'] is echoed through htmlentities() but not escaped for HTML attributes (search.php:82, :115), and get_search_query() is echoed raw inside a <span> at :61.
  • $past_events is only defined inside the resources branch (search.php:88) but is referenced in the shared render loop at :126. It happens to work because resources is the first bucket in the array and so always initialises it first — reordering the $types array at :16 would break this with an undefined-variable warning and would stop past-event filtering.
  • Pagination is computed from the main query, whose posts_per_page and post type do not match any of the three bucket queries. Page counts can be wrong in either direction.
  • The per-bucket count is floor(per_page / 3). With the WordPress default of 10 that is 3 results per bucket, 9 of 10 slots used.
  • $resource_count at search.php:90 counts non-past-event resources, but the counting logic only increments inside the Event or Workshop branch and in the else — read it carefully before changing; the nesting is easy to misread.
  • blog_filter_sort() accepts any sort value and silently treats unknown ones as newest-first. No validation, no 404.
  • terms_clauses() is a global terms_clauses filter. It short-circuits when $args['post_type'] is empty, but any plugin that happens to pass a post_type arg to get_terms() will get this rewritten SQL.
  • New search bucket: add an entry to $types in search.php:16 with label and none. Keep resources first because of the $past_events initialisation order. Also revisit the floor(/count) division — adding a fourth bucket silently drops each bucket to 2 results on a default install.
  • Relevance ranking is entirely WordPress default. If you need better, a search plugin or a custom posts_search filter is the place, not more buckets.
  • New filters on the blog index: extend blog_filter_sort() and add the select to index.php. Validate the incoming value — the existing code does not, and copying it propagates that.
  • Verify by hand: search a term that matches all three types, on page 1 and page 2; search a term matching only past events and confirm the “no resources found” message; filter the blog index by topic and by oldest.
  • Deliberately not abstracted: three separate WP_Query objects rather than one query with post_type array. The point is a guaranteed slot count per type, which one query cannot give.

None.

Related: [[lms-resource-sync]], [[acf-flexible-content-blocks]], [[site-chrome-and-navigation]].