cswni/spotlight
Spotlight is a Laravel package that adds fast, keyboard-driven search to your application. Register searchable resources, query across models, and surface results through a clean, customizable interface, with sensible defaults and full control over ranking and display.
composer require cswni/spotlight:^1.0. This is the first stable tag, so pin the major version explicitly rather than relying on dev-main or a loose constraint — you want a predictable surface while the 1.x line settles.php artisan vendor:publish --provider="Cswni\Spotlight\SpotlightServiceProvider" without a --tag first. That lists every publishable group (config, assets, migrations, views) in one pass, so you know what the package expects to own before you start overriding things.config/spotlight.php end to end. In packages of this shape the config file is the API — sources, key bindings, result limits, and UI toggles usually live there rather than in a fluent builder. Read it before writing a single line of integration code.register() bindings and container singletons you can swap() in tests, plus any route or Blade/Livewire component registration. That tells you the extension points and whether the front end is server-rendered or asset-driven.@stack/component include actually lands in your layout. This is the single most common "it doesn't work" report on first install.Source registration. Two idioms are worth checking for on day one: a config-array style ('sources' => [Model::class, ...]) and a programmatic style called from a service provider (Spotlight::register(...)). Prefer the config array for static, always-on sources and the programmatic call for anything that needs runtime context (the current tenant, the authenticated user, feature flags). Don't mix both for the same source — you'll get duplicates that are annoying to trace.
Grouped result sets. Structure output by source rather than as one flat list. Users navigate a palette by category ("Customers", "Orders", "Commands"), and grouping also gives you a natural place to cap results per source so one noisy model can't crowd out everything else.
Authorization belongs in the source, not the UI. Filter by Gate/policy or a tenant scope inside the source closure. Hiding results client-side leaks titles and URLs through the JSON payload — assume the response is fully readable by anyone who can hit the endpoint.
Action-per-result. Model the result as {title, subtitle, url} for navigation and a separate action for anything mutating. Keep mutating actions behind a confirmation and a POST route; if the package's default action handler performs a redirect from a GET, add a form-based confirm step rather than trusting the shortcut.
Front-end integration. If the UI is Livewire/Alpine, mount the component in your persistent layout (not a page-level view) so the shortcut works everywhere, and check that the package's JS is registered once — double-inclusion from both a @stack push and a published asset is a classic source of duplicate event listeners.
Testing. Bind a fake source in setUp() via the container rather than hitting real models; assert on the JSON/Inertia payload shape, not on rendered markup. Add one browser-level test for the shortcut, because the payload tests won't catch a missing asset include.
config/spotlight.php, run php artisan config:clear (or config:cache in the deploy step). A stale cached config is the number one cause of "my new source isn't showing up."--provider=... first to enumerate tags. Guessing --tag=spotlight-assets when it's actually spotlight-frontend wastes more time than it saves.$model->relation->name will fire a query per row. with() eagerly, and cap results before hydrating, not after.throttle middleware behind it; server-side throttling also protects you from accidental LIKE '%x%' scans on large tables.LIKE search. If sources query your own tables, add the index or move to a full-text/whereFullText path early. A palette that takes 800ms feels broken even when it's correct.php artisan route:list | grep -i spotlight to confirm the endpoint exists — a missing route usually means the provider wasn't auto-discovered (check bootstrap/providers.php / config/app.php if discovery is disabled).register() (a resolver, a source registry, a result formatter) can be swap()ed in a service provider — that's your cleanest hook for tenant-scoped sources and for deterministic tests, without forking the package.orWhere or a missing whereNull('deleted_at') leaking soft-deleted rows into a palette.How can I help you explore Laravel packages today?