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.
Why it exists
Section titled “Why it exists”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.
Entry points
Section titled “Entry points”| Path | What it does |
|---|---|
wp-content/themes/supplychainschool/search.php | The grouped search results page |
wp-content/themes/supplychainschool/inc/components/fc-search.php | Search block for pages |
wp-content/themes/supplychainschool/partials/search-form.php | The form markup |
wp-content/themes/supplychainschool/partials/cta-search.php | Search CTA |
wp-content/themes/supplychainschool/functions.php:359 | blog_filter_sort() on pre_get_posts |
wp-content/themes/supplychainschool/index.php | Blog index with the sort/topic selects |
wp-content/themes/supplychainschool/assets/js/filter.js | Submits the filter selects |
wp-content/themes/supplychainschool/functions.php:416 | terms_clauses() — post_type support in get_terms() |
wp-content/themes/supplychainschool/inc/cpt.php:490 | Excludes attachments from search |
How it works
Section titled “How it works”Grouped search
Section titled “Grouped search”search.php defines three buckets (:16):
| Key | Label | “none” wording |
|---|---|---|
resources | Resources | resources |
page | Pages | pages |
post | News | news 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 filtering
Section titled “Blog filtering”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—oldgivesorderby=date, ASC; anything else (including absent) givesdate, DESC.topic— adds atax_queryon thetopicstaxonomy 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.
Term counts by post type
Section titled “Term counts by post type”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.
Search exclusions
Section titled “Search exclusions”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]].
Configuration
Section titled “Configuration”- 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.
Invariants and gotchas
Section titled “Invariants and gotchas”$_GET['s']is echoed throughhtmlentities()but not escaped for HTML attributes (search.php:82,:115), andget_search_query()is echoed raw inside a<span>at:61.$past_eventsis only defined inside theresourcesbranch (search.php:88) but is referenced in the shared render loop at:126. It happens to work becauseresourcesis the first bucket in the array and so always initialises it first — reordering the$typesarray at:16would break this with an undefined-variable warning and would stop past-event filtering.- Pagination is computed from the main query, whose
posts_per_pageand 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_countatsearch.php:90counts non-past-event resources, but the counting logic only increments inside theEvent or Workshopbranch and in theelse— read it carefully before changing; the nesting is easy to misread.blog_filter_sort()accepts anysortvalue and silently treats unknown ones as newest-first. No validation, no 404.terms_clauses()is a globalterms_clausesfilter. It short-circuits when$args['post_type']is empty, but any plugin that happens to pass apost_typearg toget_terms()will get this rewritten SQL.
Changing it safely
Section titled “Changing it safely”- New search bucket: add an entry to
$typesinsearch.php:16withlabelandnone. Keepresourcesfirst because of the$past_eventsinitialisation order. Also revisit thefloor(/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_searchfilter is the place, not more buckets. - New filters on the blog index: extend
blog_filter_sort()and add the select toindex.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_Queryobjects rather than one query withpost_typearray. 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]].