Skip to content

Releases: Shepdesign-LLC/hooked-on-facets

Hooked on Facets 1.1.1

Choose a tag to compare

@github-actions github-actions released this 21 Sep 12:22
39000ff

What's Changed

  • feat(routing): pretty faceted URLs — namespaced /filter/ paths, canonical + 301 SEO layer by @Shepdesign in #42
  • fix(activator): stop dbDelta from choking on inline SQL comments in the index schema by @Shepdesign in #43
  • fix(deps): patch undici and postcss advisories (6 Dependabot alerts) by @Shepdesign in #54
  • chore(deps)(deps): bump the js-minor-and-patch group across 1 directory with 3 updates by @dependabot[bot] in #53
  • chore(ci): ignore jsdom and vitest majors until the Node 22 floor lands by @Shepdesign in #55
  • chore(ci): bump DavidAnson/markdownlint-cli2-action from 24 to 24.1.0 by @dependabot[bot] in #46
  • chore(deps)(deps): bump @vitest/mocker and vitest by @dependabot[bot] in #58
  • chore(ci): bump DavidAnson/markdownlint-cli2-action from 24.1.0 to 24.2.0 by @dependabot[bot] in #62
  • chore(release): cut 1.1.1 — WooCommerce 11 readiness + pretty URLs by @Shepdesign in #61

Full Changelog: v1.1.0...v1.1.1

Hooked on Facets 1.1.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 07:40
58d66c7

What's Changed

  • chore: release polish — build script, docs, i18n template by @Shepdesign in #17
  • docs: version-control the wiki in /docs and make it accurate for 1.0.0 by @Shepdesign in #18
  • chore: add GPL-2.0 LICENSE file by @Shepdesign in #19
  • docs: point README documentation links to the wiki by @Shepdesign in #20
  • chore(ci): bump actions/checkout from 6 to 7 by @dependabot[bot] in #22
  • chore(deps)(deps-dev): bump the js-minor-and-patch group with 2 updates by @dependabot[bot] in #21
  • chore(deps)(deps): bump undici from 7.25.0 to 7.28.0 by @dependabot[bot] in #25
  • chore(ci): bump actions/cache from 5 to 6 by @dependabot[bot] in #24
  • chore(deps)(deps-dev): bump vitest from 4.1.8 to 4.1.9 in the js-minor-and-patch group by @dependabot[bot] in #23
  • chore(ci): bump DavidAnson/markdownlint-cli2-action from 23 to 24 by @dependabot[bot] in #27
  • fix(security): stop indexing unpublished posts; harden public AI/visual-DNA endpoints by @Shepdesign in #28
  • chore(deps)(deps-dev): bump the js-minor-and-patch group with 2 updates by @dependabot[bot] in #26
  • chore(deps)(deps-dev): bump the js-minor-and-patch group with 2 updates by @dependabot[bot] in #29
  • chore(deps)(deps-dev): bump vite from 8.1.3 to 8.1.4 in the js-minor-and-patch group by @dependabot[bot] in #30
  • fix(security): rate-limit /visual-dna; anchor the rate limiter's fixed window by @Shepdesign in #31
  • feat(dist): free/premium edition split and WordPress.org submission prep by @Shepdesign in #33
  • chore(ci): bump actions/setup-node from 6 to 7 by @dependabot[bot] in #32
  • fix(facets): visual-dna palette reads the swatch term-meta key by @Shepdesign in #35
  • chore(deps)(deps): bump the js-minor-and-patch group with 2 updates by @dependabot[bot] in #34
  • docs: publish the ratified free/Pro split and fix comparison-page claims by @Shepdesign in #36
  • feat!: extract the signature facets and licensing into the Pro add-on by @Shepdesign in #37
  • fix(facets): stop author display rules from defeating the hidden attribute by @Shepdesign in #38
  • ci: build releases via bin/build-release.sh and read the header version by @Shepdesign in #39
  • chore(release): 1.1.0 — the free/Pro split ships by @Shepdesign in #40
  • fix(build): chown node-container output so CI runners can strip maps and verify by @Shepdesign in #41

Full Changelog: v1.0.0...v1.1.0

Hooked on Facets 1.0.1

Choose a tag to compare

@Shepdesign Shepdesign released this 21 Jul 07:20

Maintenance release on the 1.0 line (branch hotfix/1.0.1, based on v1.0.0).

Fixed

  • Elements toggled with the HTML hidden attribute (Visual DNA result row and palette, eyedropper button, swiper done-card) could stay visible: author display rules beat the browser's default [hidden] { display: none }. The public stylesheet now ships a scoped, cascade-final guard, with a regression test that models the cascade from the parsed stylesheet. (#38)
  • The Visual DNA color map read the legacy swatch_color term-meta key, so palettes saved through the swatch fields UI never matched. (#35)

The attached zip is the production build (bin/build-release.sh) deployed to hookedonfacets.com.

Hooked on Facets 1.0.0

Choose a tag to compare

@github-actions github-actions released this 04 Jun 04:22
a1aa612

What's Changed

  • chore(release): prep 1.0.0 — docs, branding, i18n, distribution readiness by @Shepdesign in #15
  • ci: auto-publish releases and sync the wiki from /docs by @Shepdesign in #16

Full Changelog: https://github.com/Shepdesign/hooked-on-facets/commits/v1.0.0

v0.13.1-alpha — counts query optimization

Choose a tag to compare

@Shepdesign Shepdesign released this 03 Jun 21:12

A focused performance patch on top of v0.13.0-alpha.

Changed

Drill-down counts query — 7.5× faster

The counts behind the /filter endpoint grouped by (facet_value, facet_display) — two 191-char VARCHARs — which bloated the aggregation temp table. Since the value→display mapping is 1:1 (slug→name, ID→title, or value === display for meta), the query now GROUP BY facet_value alone and takes the display via MIN(facet_display):

  • Identical output — byte-for-byte the same buckets (verified on the live 100k stack: same 21 category buckets, same counts).
  • 454ms → 60ms on the counts query, taking resolve() p95 from ~465ms → ~63ms uncached on 100k products.
  • COUNT(DISTINCT object_id) is kept (not COUNT(*)) so a multi-row-per-object meta facet still counts each object once.

Together with the v0.13.0 result-set cache, the /filter path is now fast on both cold (~63ms) and repeat (sub-ms) hits. SQL shape is locked by a unit test.

Note: EXPLAIN reported both the old and new queries as "Using temporary; Using filesort" — only wall-clock measurement exposed the 7.5× gap. Measure, don't trust the plan.

Verification

  • PHP: 121 tests / 269 assertions; php -l clean
  • JS: 35 tests; build clean · Markdown: lint clean
  • Live stack: identical buckets confirmed; resolve() re-benchmarked at ~63ms p95

Upgrade

git pull && composer install && npm install

No schema change, no reindex, no behavior change — purely a faster query.

Full changelog: CHANGELOG.md

v0.13.0-alpha — enhancements: SEO, analytics, resolver cache

Choose a tag to compare

@Shepdesign Shepdesign released this 03 Jun 21:02

The first round of enhancements on top of the feature-complete plugin. Three additions, each verified end-to-end on the live 100k-product stack.

What's new

SEO for filtered pages

Faceted URLs bloat crawl budget and spawn near-duplicate pages, and general SEO plugins don't understand the ?hof[*] query shape — so HOF owns the faceted signals via a new SeoManager:

  • rel=canonical to the filter-stripped base URL (deferred when Yoast / Rank Math / AIOSEO / SEOPress is active, to avoid a double tag).
  • noindex,follow once a configurable number of facets are stacked (default 2 — single-facet landing pages stay indexable while the combinatorial long tail doesn't), emitted via the core wp_robots filter so it composes with other plugins.
  • An active-filters title suffix so an indexed single-facet page reads meaningfully in search results.

Settings persist to hof_seo, exposed via a new admin SEO screen and GET|POST /seo-settings. Decision logic is pure and unit-tested (12 cases).

Analytics dashboard

The telemetry recorder now captures per-facet/value usage and zero-result filter combinations — one signal per /filter action, buffered in memory and flushed at shutdown (with caps), so it never adds request latency. snapshot() gains p50/p95/p99 resolver latency.

The admin Dashboard renders most-used facets (usage bars), "filters that find nothing," the latency percentiles, and a dead-facets callout (configured but never applied by a shopper). Covered by 6 tests.

Resolver result-set cache

resolve() and resolve_ids() now cache their output in the object cache, keyed by (index version, filter state). The Indexer bumps hof_index_version on every write, so invalidation is O(1) — a bump orphans every stale key.

Benchmarking on the live 100k stack drove the scope: resolve_ids() was already ~8ms p95, but resolve() — IDs + N drill-down counts behind /filter — was ~465ms, the real bottleneck. Cached repeat hits are sub-millisecond. Most effective with a persistent object cache (Redis / Memcached); kill switch hof_resolver_cache_enabled. Covered by 4 tests.

Deliberately not done: LIMIT/OFFSET pushdown into resolve_ids() (unsafe — QueryHook needs the full ID set for post__in) and mysqli_poll parallel legs (deferred per the Phase-1 note).

Verification

  • PHP: 120 tests / 265 assertions; php -l clean
  • JS: 35 tests; production Vite build clean
  • Markdown: lint clean
  • Live stack: cache 465ms → sub-ms on repeat; version invalidation confirmed on reindex_object; SEO canonical/robots/title verified

Upgrade

git pull && composer install && npm install

No schema change (hof_db_version 1.2.0). No reindex needed. All three features are additive — SEO defaults are conservative, the cache is transparent and version-invalidated, analytics capture is passive.

Full changelog: CHANGELOG.md

v0.12.0-alpha — wow-kit complete: spin wheel, saved bin, matrix + AND resolver + multisite

Choose a tag to compare

@Shepdesign Shepdesign released this 29 May 03:28

The feature-complete milestone. The two designed-but-unbuilt "wow-kit" facets ship, the matrix returns on real intersection semantics, and the plugin goes multisite-aware. Every planned facet (16 types), source (WooCommerce, ACF, Meta Box, Pods), and capability is now in.

What's new

Spin-the-wheel facet (spin_the_wheel)

A gamified single-select picker: a cosmetic conic-gradient dial over a real, keyboard- and screen-reader-accessible radiogroup. Spin lands on a random value (or pick one directly); it degrades to a plain single-select with JS off. Same hof[<name>]=value URL shape as radio, so the resolver treats it identically. Covered by spin.test.js (6 cases).

Saved-bin facet (saved_bin)

A drag-and-drop / click comparison bin. Shoppers pin items — via the new [hof_bin_button id="…"] shortcode or any draggable data-hof-bin-add element — into a per-site localStorage bin, then a Show only saved toggle filters results to the bin. It's the only facet that filters by object ID rather than an index value, riding a new reserved resolver key _bin_ids (a plain ID intersection, parallel to Visual DNA's _visual_ids). Covered by bin.test.js (8 cases).

AND-within-facet resolver

A facet can now require an object to carry every selected value, not just one (settings.match = 'all', an any/all control on multi-value facets in the admin). It composes naturally with the existing INTERSECT engine: instead of one IN-list leg, the facet emits one single-value equality leg per value, and INTERSECT ANDs them — each a covering-index scan with no DISTINCT, so AND mode is at least as fast as OR.

Intersection matrix (matrix) — un-retired

The Venn/UpSet matrix returns. It was originally shelved for an OR/AND semantic mismatch and unreadable selection state; it's back on the new AND-within-facet semantics (which it defaults to), paired with an explicit selected-state dot, a per-row count bar, and the active-filters chip strip — the readability gap the first version lacked.

Multisite support

The index table and facet options were already per-blog, so this is a lifecycle concern: Activator::activate( $network_wide ) installs across every existing site on network activation, install_new_site() (on wp_initialize_site) seeds tables for sites added later while HOF is network-active, the plugins_loaded auto-heal self-installs any blog whose schema is behind, and uninstall.php runs its opt-in cleanup per site (checking the removal gate per site, so one site opting in never wipes a sibling). Now validated on a live WP_ALLOW_MULTISITE network — stable.

Verification

  • PHP: 98 tests / 221 assertions; php -l clean across the tree
  • JS: 35 tests (refresh, swiper, spin, bin); production Vite build clean
  • Markdown: lint clean
  • Live stack (MySQL 8, 100k+ indexed rows): the AND resolver returned AND = 150 = exact co-occurrence count (vs OR = 10,000), and all three new server-rendered facets render without warnings

Known limitations (deferred, gated on a live third-party environment)

  • Breakdance native placement — Element-Studio bundle format needs a live Breakdance install to author + validate; placement works today via [hof_facet].
  • Divi Visual Builder — the native module is authored; its VB render path needs a live Divi install to validate. Theme Builder (main-query) placement already works.
  • Pods table / wp_podsrel storage — the indexer reads postmeta; the meta_in_use() gate cleanly skips table-stored Pods fields rather than mis-suggesting them.
  • Time-of-day faceting — intentionally not a facet type; a raw seconds-range is poor UX and a dedicated clock picker isn't warranted yet.

Upgrade

git pull && composer install && npm install

No schema change since v0.11.0-alpha (hof_db_version 1.2.0). No reindex needed. New facets and the AND mode are opt-in per facet; existing configs are unaffected.


Full changelog: CHANGELOG.md

v0.11.0-alpha — custom-field sources: ACF, Meta Box & Pods

Choose a tag to compare

@Shepdesign Shepdesign released this 29 May 03:29

The custom-field source line lands: HOF now inspects ACF, Meta Box, and Pods fields and one-click-suggests facet configs — backed by an indexer that resolves ID-references to human labels and normalizes dates. Also includes the Divi bridge and an Action Scheduler-backed background reindex.

What's new

Source integrations — suggestion providers

Each integration inspects its plugin's registered fields and proposes ready-to-use facet configs over GET /integrations/{woocommerce,acf,metabox,pods}/suggest, gated by an is_active() check and a shared meta_in_use() data filter so only fields with real data are offered.

  • ACF — maps every meaningful field type to a display: select→dropdown, multi-select/checkbox→checkbox, radio/button_group→radio, true_false→toggle, number/range→range, text/textarea/email/url→search, taxonomy (Save Terms on)→taxonomy facet, relationship/post_object→resolve='post', user→resolve='user', Save-Terms-off taxonomy→resolve='term', date_picker/date_time_picker→date_range.
  • Meta Box — reads the meta_box registry; scalar/select/date/taxonomy types plus post/user (resolve) and taxonomy_advanced (comma-separated term IDs → resolve='term').
  • Pods — post-type Pod fields, including pick relationships mapped by pick_object (post→post, user→user, taxonomy→term). The meta_in_use() gate cleanly skips table-stored Pods fields this indexer can't serve.

Indexer — the resolve mechanism

  • ID → label resolution for meta facets whose values are IDs: 'post' (post title, via wp_posts), 'user' (display name, wp_users), and 'term' (term name + term_id, wp_terms) — resolved in one batched query per facet per reindex batch.
  • Serialized-array adapter — explodes ACF checkbox / multi-select arrays into one bucket per scalar value.
  • Comma-splitextract_ids() splits comma-separated ID strings so Meta Box taxonomy_advanced lands (a no-op for the plain-ID cases).
  • ACF date normalizationdate_picker's compact Ymd is parsed to a UTC-midnight epoch (round-trip validated), so date_range facets scale correctly.
  • Action Scheduler-backed background reindex — chunked, self-chaining full rebuilds under the hooked-on-facets group (with a wp_cron fallback), so large catalogs reindex without blocking a request.

Divi bridge

Divi core ships no scoped per-loop query filter, so the bridge splits Divi's two surfaces: Theme Builder archive/index templates run the main query (already filtered through QueryHook), and a Blog module on a page uses the global hof_divi_query_args() helper from the developer's own scoped pre_get_posts. Placement gets a native ET_Builder_Module (slug et_pb_hof_facet); the [hof_facet] shortcode remains a fallback.

Known limitations (deferred)

  • Divi Visual Builder — the native module is authored; its VB server-render path needs validation against a live Divi install.
  • Pods table / wp_podsrel storage, ACF/Meta Box time-of-day fields — deferred (see plan.md).

Upgrade

git pull && composer install && npm install

Schema bump to hof_db_version 1.2.0 runs automatically on load (the plugins_loaded auto-heal). A reindex picks up newly-suggested fields; suggestion providers are additive and never alter existing facets.


Full changelog: CHANGELOG.md

v0.10.0-alpha — third page builder bridge: Breakdance

Choose a tag to compare

@Shepdesign Shepdesign released this 26 May 06:38

Third page-builder bridge. HOF now filters Breakdance Post Loop Builder loops too — though Breakdance forced a different shape than its predecessors.

What's new

Breakdance bridge

Breakdance is the exception. Elementor gave us elementor/query/{id} and Bricks gave us bricks/posts/query_vars — scoped, documented filters to bind a specific loop. Breakdance documents no equivalent: its Post Loop Builder is customized in the builder, and the only PHP injection point is the user-authored Array Query. So the binding is a documented recipe, not a hook.

Loop binding. Set a Post Loop Builder's Query type to Array Query and return the HOF helper, passing your loop's own args:

return hof_breakdance_query_args( [
    'post_type'      => 'product',
    'posts_per_page' => 12,
] );

When the request carries ?hof[*] filters, the returned args gain a post__in of the matching IDs (or [0] when nothing matches, so the loop shows "no results" rather than everything). With no HOF filters active, your base args are returned unchanged — HOF wins on post__in; you keep control of post type, ordering, and per-page.

Placement. Drop the existing [hof_facet] shortcode into a Breakdance Shortcode/Code element.

The logic lives in Breakdance::query_args() (unit-testable with a mocked resolver); the global hof_breakdance_query_args() is a thin delegate, loaded unconditionally from register_hooks() (an inert function definition until Array Query calls it). 7 PHPUnit cases; full suite 27/27.

Known limitations

  • No native placement element yet. Breakdance elements are Element-Studio directory bundles registered via registerSaveLocation(), not a registerable PHP class. Authoring that format needs validation against a live Breakdance install, so it's deferred — placement uses the shortcode for now.
  • One manual step per loop. Unlike Elementor (Query ID) and Bricks (CSS class), the Array Query recipe means pasting one line into each loop you want filtered. That's the cost of Breakdance lacking a scoped query hook; if it ships one, the binding moves there and this recipe becomes a fallback.

Upgrade

git pull && composer install && npm install

No DB changes. No reindex needed. The helper is inert until invoked from a Breakdance Array Query — safe to deploy on non-Breakdance sites.

v0.9.0-alpha — second page builder bridge: Bricks

Choose a tag to compare

@Shepdesign Shepdesign released this 26 May 06:21

Second page builder bridge lands. HOF now filters Bricks Builder query loops the same way it filters Gutenberg Query Loops and Elementor Loop Grids — explicit, opt-in, no magic detection. Inert when Bricks isn't installed.

What's new

Bricks bridge

Two halves, both behind a single Bootable service that no-ops cleanly when Bricks isn't active.

Facet placement element. Drop the new Hooked Facet element anywhere on a Bricks page and set the slug of a facet you created in the HOF admin. Mirror of the hof/facet Gutenberg block, the [hof_facet] shortcode, and the Elementor widget — all four surfaces render via the same Renderer service, so markup is identical no matter where the facet sits.

Loop binding via CSS class. On the query-loop element you want filtered (Style → CSS classes), add the class hof. HOF hooks bricks/posts/query_vars and applies post__in directly from the URL's ?hof[*] state. A class (rather than the element's CSS ID) is the binding key because it's repeatable across loops and doesn't force a unique HTML id. Multiple bound loops or a different convention:

add_filter( 'hof_bricks_query_ids', fn() => [ 'shop', 'recipes' ] );

Why the boot timing differs from Elementor: Bricks ships as a theme, loaded on after_setup_theme — after HOF's plugins_loaded:5 boot — so \Bricks\Elements doesn't exist yet when the bridge wires up. The passive bricks/posts/query_vars filter registers at boot regardless (it's inert until Bricks fires it during a render); element registration defers to init:11 (after Bricks registers its own elements) and gates on the class being present. If Bricks isn't active, that callback returns early — no element, no cost.

Ships with 11 PHPUnit cases: hook registration, element registration, CSS-class matching (incl. multi-class strings and array tolerance), the URL-filter no-op paths, post__in application, the [0] no-results sentinel, telemetry recording, and the filterable binding list.

Minor: CI

Removed the advanced CodeQL workflow (.github/workflows/codeql.yml) in favor of the repo's CodeQL default setup. The advanced config's SARIF upload was rejected on every code-touching PR — "CodeQL analyses from advanced configurations cannot be processed when the default setup is enabled" — leaving a persistent (non-blocking) red check. Default setup already scans javascript-typescript and actions. To keep the broader security-extended query suites the old workflow ran, switch default setup's query suite to extended in Settings → Code security.

Upgrade

git pull && composer install && npm install

No DB changes. No reindex needed. The bridge is inert if Bricks isn't loaded — safe to deploy on non-Bricks sites.

Not yet verified on a live Bricks install — the _cssClasses binding is fully unit-tested but hasn't been smoke-tested against a running Bricks query loop. Worth a quick check before relying on it in production.