Skip to content

Filter the demo's projects archive - #169

Merged
titouanmathis merged 2 commits into
mainfrom
feat/demo-filters
Aug 28, 2026
Merged

Filter the demo's projects archive#169
titouanmathis merged 2 commits into
mainfrom
feat/demo-filters

Conversation

@titouanmathis

@titouanmathis titouanmathis commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Closes the gap #166 identified after #167 landed: query filters, the query_* helpers and a cached filter URL all existed, and no page in the repository showed them working together.

The demo's projects archive now filters by series and sort order.

It uses no framework feature to filter

project_category is the taxonomy's own query var, and orderby is a public one. There is no query-filters.config.php, no QueryFiltersHook, and no custom parameter.

That is not a shortcut — WP_Query::parse_tax_query() reads the var from a checkbox group's array or from a comma string, sorts the terms and builds one IN clause from either:

$term = $query_vars[ $t->query_var ];
if ( ! is_array( $term ) ) { $term = explode( ',', $term ); … }
sort( $term );
$term = implode( ',', $term );

So ?project_category[]=corridors&project_category[]=osaka and ?project_category=corridors,osaka are the same query, and both are cached by #167 — the first through the drop-in, the second through nginx.

The smoke suite asserts that equivalence against a real WP_Query rather than trusting it, along with an unknown slug returning nothing rather than everything.

It works without JavaScript

A plain method="get" form with a submit button. Fetch and Action from @studiometa/ui enhance it — data-on:change="Fetch.fetch()" — and @studiometa/ui/autoload is already imported, so no JavaScript was added to the theme.

The enhanced request is still cached

Fetch gets no data-option-src, unlike the pagination links beside it. It fetches the whole filtered page and swaps the two elements whose id it finds — the index and the count.

A section response would be smaller and would never be cached: Cache-Control: private, no-store is by design. A filtered page is served from the cache without starting PHP, so the whole page off a warm file beats a fragment off a cold render. This is the Frame-versus-sections decision #166 said had to be made before templates were written; the comment in the template records why.

page-cache.config.php keys the two arguments the form emits, with orderby given as a value list rather than a regex:

cacheQueryArgs: [
    'project_category' => '^[a-z0-9-]+(?:,[a-z0-9-]+)*$',
    'orderby' => ['date', 'title'],
],

Also in here

docs/guide/query-filters.md used terms('genre') in four examples. That function does not exist — Timber provides get_terms(). Fixed, and the guide now says plainly that a registered public taxonomy is filterable with no configuration at all, which is what the demo shows; QueryFiltersConfig is for operators, private vars and non-public taxonomies.

Two bugs found by running it against ddev

Checking the form against the real demo install caught two faults in helpers nothing had exercised. Both are fixed here, with unit tests.

query_contains() could not read the comma spelling. ?project_category=still-life,portrait filtered correctly and showed every checkbox unchecked, because the value was compared as one opaque string. The guide's own "Link Toggle Filter" example had the same bug: it could never mark a term active. Verified in ddev — 0 boxes checked before, 2 after.

query_url_toggle() produced URLs the cache can never key. It handed an array to add_query_arg(), which spells it project_category[0]=still-life. An indexed name is not a keyed argument, so every toggle link was a permanent bypass. It now joins with a comma — and keeps the separator literal, because add_query_arg() percent-encodes it to %2C and % is not a character the cache will put in a filename, so the encoded form bypasses just as surely.

Checked against local ddev

/projects/ 200, 3 rows (the archive paginates at 3 — pre-existing, confirmed by stashing the change)
?project_category[]=still-life 200, 2 rows, 1 box checked
?project_category[]=still-life&project_category[]=portrait 200, 3 rows, 2 boxes checked
?project_category=still-life,portrait 200, 3 rows, 2 boxes checked
?project_category=nope 404 — WordPress's own answer for an unknown term

The smoke assertions were run in the container: 25 passed before this branch, 29 after, so all four new filtering assertions ran and passed. The one pre-existing failure (run.sh imported the uploads fixture) appears in both runs and is an artifact of invoking wp eval-file directly rather than through run.sh.

The checkout used for that testing was restored to a clean tree afterwards, vendor included.

Verification

  • composer test:demo 88 passed, composer lint and composer analyse clean of anything new, docs build with no dead links.
  • php -l on all changed PHP; stylelint back to its pre-existing baseline with nothing added.
  • The end-to-end assertions run in CI's "Integration - Demo in WordPress" job, which is what has to prove the filtering claim.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KZusb9DU8coizq3CDgHEqi

Nothing demonstrated query filters, the query_* helpers and a cached filter URL
together, so the feature existed and no page showed it working.

The form uses only what WordPress already parses: project_category is the
taxonomy's own query var, and WP_Query reads it from a checkbox group's array or
from a comma string alike. No hook, no config, no custom parameter.

Fetch enhances it without taking the no-JS path away, and deliberately fetches
the whole filtered page rather than a section URL — a page is cached and a
section is Cache-Control: no-store, so the whole page comes back without PHP.

Also fix the guide's terms() calls: Timber provides get_terms().

Co-authored-by: Claude <claude@anthropic.com>
@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.67%. Comparing base (66ead81) to head (b642c93).

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #169      +/-   ##
============================================
+ Coverage     85.66%   85.67%   +0.01%     
- Complexity     2535     2537       +2     
============================================
  Files           194      194              
  Lines          6988     6997       +9     
============================================
+ Hits           5986     5995       +9     
  Misses         1002     1002              
Flag Coverage Δ
unittests 85.67% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Checking the demo's filter form against a real WordPress found two faults in
helpers nothing had exercised.

query_contains() read a comma-separated value as one opaque string, so a page
reached by a shared filter link showed every checkbox unchecked — and the Link
Toggle Filter example in the guide could never mark a term active.

query_url_toggle() built its URLs by handing an array to add_query_arg(), which
spells them `genre[0]=rock`. That is a name the page cache cannot key, so every
toggle link was a permanent bypass; joining with a comma and keeping the
separator literal puts them back on the fast path, since `%2C` is no better.

Co-authored-by: Claude <claude@anthropic.com>
@titouanmathis
titouanmathis merged commit 6beacef into main Aug 28, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant