v0.7.0-alpha — first page builder bridge: Elementor
Pre-releaseFirst page builder bridge lands. HOF now filters Elementor Loop Grids the same way it filters Gutenberg Query Loops — explicit, opt-in, no magic detection. Brought a PHPUnit harness along with it.
What's new
Elementor bridge
Two halves, both behind a single Bootable service that no-ops cleanly when Elementor isn't installed.
Facet placement widget. Drop the new Hooked Facet widget anywhere on an Elementor page, set the slug of a facet you created in the HOF admin. Mirror of the hof/facet Gutenberg block and the [hof_facet] shortcode — all three surfaces render via the same Renderer service, so markup is identical no matter where the facet sits.
Loop binding via Query ID. On the Loop Grid / Posts / Products widget you want filtered, set Advanced → Query ID to hof. HOF hooks elementor/query/hof and applies post__in directly from the URL's ?hof[*] state. Multiple bound loops or a different naming convention:
add_filter( 'hof_elementor_query_ids', fn() => [ 'shop', 'recipes' ] );Why Query ID instead of piggybacking on pre_get_posts: Elementor calls $query->query($args) after the elementor/query/{id} action fires, and WP_Query::query() resets query_vars from $args — so setting an opt-in flag like hof_facet_target on the query would be discarded. Mutating post__in directly is robust and matches Elementor's own documented pattern.
PHPUnit harness
First PHP test suite. PHPUnit 11 + Brain Monkey + Mockery. No live WordPress — WP functions stub per-test. Run with composer test. Tests live under tests/php/; the bridge ships with 9 tests covering presence-guard, widget registration, the Query ID filter (incl. dedupe + sanitize), bind_query's no-op paths, post__in application, the [0] no-results sentinel, and telemetry recording. CI gates on the suite — the previous composer run-script test --if-it-exists was a no-op due to an invalid flag; that's fixed too.
Minor refactor (testability)
Two narrow interfaces extracted from final services so consumers can mock past them:
HookedOnFacets\Filter\IdResolver— justresolve_ids()HookedOnFacets\Telemetry\LoopHookRecorder— justrecord_loop_hook()
Resolver and Recorder implement them. The DI container still resolves to the concrete classes — only consumer type hints changed. SHEPDESIGN.md documents the convention.
Upgrade
git pull && composer install && npm installNo DB changes. No reindex needed. The bridge is inert if Elementor isn't loaded — safe to deploy on non-Elementor sites.