pollora / meilifacets
Faceted search, filtering and suggestions powered by Meilisearch for Pollora projects.
Requires
- php: ^8.4
- amphibee/meiliscout: dev-feat/meilifacets
- illuminate/console: ^12.0 || ^13.0
- illuminate/contracts: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
- illuminate/view: ^12.0 || ^13.0
- league/uri: ^7.5
Requires (Dev)
- ext-dom: *
- driftingly/rector-laravel: ^2.6
- laravel/pint: ^1.30
- phpunit/phpunit: ^12.5
- rector/rector: ^2.6
Suggests
- ext-intl: Orders facet values as the site language does; without it, DisplayOrder::Name falls back to a byte comparison that misplaces accented labels.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-25 13:31:04 UTC
README
Faceted search for Pollora projects, powered by Meilisearch. The server renders the first page with the filters in the URL already applied; every filter, sort and page change after that is a single request from the browser to Meilisearch, with no WordPress in the loop.
Why
A filtered WordPress listing is a full page load for every click. Measured locally on a 76-product catalogue: the whole search, facet counts included, takes 3 ms in the engine; the page around it takes about 350 ms. Moving the listing to the engine turns each filter into the first number instead of the second — while WordPress keeps the URLs, the routing and the SEO.
Principles
- The browser talks to Meilisearch directly. No PHP proxy and no fallback. The search key is
therefore public, so the index only lets it read what a card needs:
IDandcard. - The URL is the state. WooCommerce's own paths are kept, filters travel as query vars, and the
server applies them on first render. A filtered, sorted or paginated view is served
noindex, follow, without a canonical. - Behaviour in the module, appearance in the theme. Every view is overridable. The only thing a
theme must keep is the set of
data-meilihooks the client binds to — never a class. - MeiliScout indexes, MeiliFacets queries. The module adds its fields —
facets,card,price— to MeiliScout's documents through a filter, and declares its index settings through an extended indexable. - What changes from one shop to the next sits behind a contract with a default. A project with nothing to declare gets a working product listing; a project that binds its own wins.
- Prices are indexed as the shop displays them, taxes included or not, at the shop's address. The module reads them through WooCommerce and never recomputes one.
Extension points
Container bindings, set in a project service provider's register().
| Contract | Default | Decides |
|---|---|---|
ProductFacets |
category and brand | the taxonomies a shop is browsed by, their labels, order and limits |
ProductSorts |
price ↑↓, newest, and on sale when the facets declare a price filter | the sorts on offer |
CardProjector |
title, link, image, WooCommerce price | what a document carries to paint a card |
IndexAttributes |
WooCommerce price and stock fields | the attributes the index declares filterable, sortable and readable |
FacetCounter |
disjunctive multi-search |
how facet counts are computed on first render |
SearchEngine |
Meilisearch client | how searches are sent |
Listing |
the product listing, when WooCommerce is active | what a listing declares — discovered, never registered |
$this->app->scoped(ProductFacets::class, CatalogueFacets::class);
| Filter | Passed | Purpose |
|---|---|---|
meilifacets/script_fetchpriority |
string |
Load the client at another priority than low. |
Settings — browser connection, URL parameter names, image size, readable fields, when a search
fires — live in the project's config/meilifacets.php. Each one, and what cannot be configured, is
in docs/configuration.md.
Requirements
- PHP 8.4 or later
- Pollora on Laravel 12 or 13
- MeiliScout, branch
feat/meilifacetsuntil it is merged - A Meilisearch server above 1.10.3 — the exact minimum has not been measured
- WooCommerce 9.8 or later for the product listing and its prices; the module runs without it
ext-intl, suggested: without it, facet values ordered by name fall back to a byte comparison
Developed against WordPress 6.9.5, WooCommerce 11.0.1 and Meilisearch 1.53.1.
Installation
The module is not published as a package yet. It sits in Modules/MeiliFacets/, is enabled in
modules_statuses.json, and the project's merge-plugin merges its composer.json, MeiliScout
included. docs/installation.md has the step-by-step.
MeiliScout indexes and serves the first render from its own environment; the index is always
called posts:
MEILI_HOST=http://meilisearch:7700 MEILI_KEY=<master key, for indexing> MEILI_SEARCH_KEY=<search-only key>
The module's settings live in the project's config/meilifacets.php. Publish the commented
starting point, which holds every key with its default:
php artisan vendor:publish --tag=meilifacets-config
Two of its keys are required: the address and the key the browser uses. The module never reads them from the environment — the published file bridges them:
'browser' => [ 'url' => env('MEILI_PUBLIC_URL'), // with its scheme, or the client stays off 'key' => env('MEILI_SEARCH_KEY'), // search-only, never the master key ],
Without them the page is still served in full, but no script loads and no filter answers — and nothing says so. The two addresses differ on purpose: one is reached by PHP, the other by the visitor's browser. The other keys — URL parameter names, image size, readable fields, when a search fires — are described in docs/configuration.md.
The stylesheet and the client are copied into public/ on every deployment:
php artisan module:publish MeiliFacets
Usage
On a WooCommerce project there is nothing to declare: the module provides the product listing. Components go anywhere in the template and share a single search.
<x-meilifacets::listing> <x-meilifacets::active-filters /> <x-meilifacets::sort /> <x-meilifacets::reset /> <x-meilifacets::facets /> <x-meilifacets::results /> <x-meilifacets::pagination /> </x-meilifacets::listing>
Any other content gets a listing by implementing Listing; the class is discovered on its own.
Overriding the markup
The module looks for its views in the active theme first. A file in
<theme>/resources/views/modules/meilifacets/components/ replaces one view; the others keep
following the module's updates.
Tags, classes and styles belong to the theme. The data-meili hooks do not: when a required hook
is missing, or the contract version on the listing root no longer matches the client's, the client
does not start — the page stays as the server rendered it, and the console names what is missing.
The hooks are listed in docs/architecture.md.
Before going to production
php artisan meilifacets:check-parameterschecks the URL parameter names against WordPress query vars, the names WooCommerce reads, and what the proxy strips.- The production engine must be upgraded past 1.10.3.
- WP-Cron has to run, or the index drifts from the catalogue without a sign (docs/configuration.md).
Development
Run from the module's root, on the host — not in ddev. A node_modules only serves the system that
installed it, and the module keeps one.
npm install composer install composer check # Pint, ESLint, tsc, Rector, standalone PHP tests, client tests, bundle up to date composer test # standalone PHP tests only (Unit suite) npm test # client tests only composer build # rebuild resources/assets/dist/listing.js after changing the client
The Feature tests render Blade views and need an application, so they run from the host
project:
ddev exec vendor/bin/phpunit --testsuite Modules
Requires Node 24.12 or later.
Documentation
The design documents, in French, are in docs/:
installation, configuration,
architecture, price filter,
decisions, known pitfalls,
delivery batches and the review register, where every finding,
question and decision has a stable number.
Contributing
Commits follow Conventional Commits, on a single line.
Before opening a pull request, composer check and the Modules suite should both pass. The
working rules — what to check before writing, and when a point is done — are in
CLAUDE.md.
No release has been tagged yet.
License
GPL-2.0-or-later.