winter/wn-search-plugin
Add site-wide search to Winter CMS with this plugin. Index pages and supported content models, run queries through a ready-to-use search component, and render paginated results in your theme. Configurable indexes and providers make integration straightforward and extensible.
Install and update
composer require winter/wn-search-plugin
php artisan winter:up
winter:up runs the plugin's migrations (it creates the shared search_index table) and applies pending updates. Confirm the plugin row exists under Settings → System → Updates before debugging anything else.
Where to look first
Inside plugins/winter/search/:
Plugin.php — registers the search component, the Searchable model behavior, the backend settings page, and any console commands.classes/ — the search manager / driver abstraction. This is the seam you will interact with most.drivers/ — the concrete backends (a database driver ships as the safe default; other drivers may need extra composer packages).components/Search.php + components/search/*.htm — the frontend component and its default markup. Read default.htm first; it is the best documentation of handler names, property names, and partial aliases.behaviors/Searchable.php — how models get indexed.Backend config
Go to Settings → Search (or the plugin's settings page) and pick a driver, set results per page, and note whether indexing is synchronous on save or requires a rebuild. If no settings page appears, the driver is configured in config/ or via .env — check Plugin.php for registerSettings.
First use case
Index one model and wire up one page:
class Post extends Model
{
public $implement = [\Winter\Search\Behaviors\Searchable::class];
// fields to push into the index — verify the exact property/hook
// name against Behaviors/Searchable.php in your installed version
public $searchable = ['title', 'excerpt', 'body'];
}
Then on a CMS page:
[search]
resultsPerPage = 10
==
<form data-request="onSearch" data-request-update="'@results': '#search-results'">
<input type="text" name="q" value="{{ searchTerm }}">
<button type="submit">Search</button>
</form>
<div id="search-results">
{% partial '@results' %}
</div>
Re-save a Post in the backend (or run the index/rebuild command) and search for a word you know is in a title. If you get nothing, the indexing path is the problem, not the query.
Behavior-on-model, index-table-as-source-of-truth. The behavior writes rows into a central index table keyed by model class + record ID. Treat that table as a denormalised projection: it is disposable and rebuildable, never authoritative. Anything you need to filter or display should either live in the index payload or be re-fetched from the source model.
Fluent manager over the component. For non-trivial needs, skip the component and talk to the manager directly in a controller, console command, or a custom component:
$results = Search::driver('database')
->search($term)
->where('model', Post::class) // scope to one model type
->paginate(10);
This composes cleanly: add where() clauses for scoping, then hand the paginator to your own partial. It is the right layer for JSON APIs and for search pages that need more than the stock component offers.
Indexing strategy. Decide early between three modes:
Search UI as a progressive enhancement. The pattern that ages well: a plain GET form that works without JS, plus an onSearch AJAX handler that updates only the results partial. Put the term in the query string (?q=...) so results are shareable and so you can hydrate the input on onStart/page load.
Multi-model search. Keep one index and filter by model class for scoped searches; omit the where('model', ...) and group/annotate by model when you want a unified "site search" results page. Render each result through a small per-model presenter (a toSearchResult() method or a map of model → partial) rather than branching in Twig.
Driver abstraction as a scaling dial. Start on the database driver, keep all query construction behind your own service or component wrapper, and swap drivers later by changing config plus one adapter class. This is the main architectural payoff of the package; don't leak driver-specific query syntax into page templates.
DB::table()->insert(), seeders, and direct SQL all bypass model events and therefore the index. Any workflow that writes outside Eloquent needs an explicit reindex step. Symptom: search works in dev, returns stale/empty results after a deploy that imported content.->where('status', 'published') or post-filter by fetching models and applying scopes) and never rely on the index to enforce access control. Re-fetching the source model for the detail view and re-checking permissions is the safe pattern.LIKE '%term%'. No relevance ranking, and a leading wildcard defeats indexes, so it degrades linearly. Acceptable for a blog; not for tens of thousands of rows. Rank results yourself (title matches before body matches) if ordering matters.MATCH ... AGAINST or Lucene-ish syntax — a stray quote or operator is either a SQL error or an unexpected filter.toSearchableArray() payload.plugins/ — plugin files get overwritten on composer update. Backend-editable component partials (CMS → Partials) are the supported extension point.Winter\Search. October CMS plugins using October\ namespaces won't drop in cleanly; if you're porting, check for October\Rain references and a translations compatibility layer.composer.json, keep a fork or patch set handy, and read the driver source before upgrading — you are effectively your own first line of support here.How can I help you explore Laravel packages today?