PHPackages                             matheusmarnt/scoutify - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. matheusmarnt/scoutify

ActiveLibrary[Testing &amp; Quality](/categories/testing)

matheusmarnt/scoutify
=====================

⌘K global search modal for Laravel — multi-model Livewire UI powered by Scout

v2.3.3(1mo ago)1255↓100%MITPHPPHP ^8.2CI failing

Since Apr 27Pushed 1mo agoCompare

[ Source](https://github.com/matheusmarnt/scoutify)[ Packagist](https://packagist.org/packages/matheusmarnt/scoutify)[ Docs](https://matheusmarnt.github.io/scoutify/)[ GitHub Sponsors](https://github.com/matheusmarnt)[ RSS](/packages/matheusmarnt-scoutify/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (10)Dependencies (20)Versions (95)Used By (0)

 [![Scoutify](art/scoutify.png)](art/scoutify.png)

 [![Latest Version on Packagist](https://camo.githubusercontent.com/62a7c17c6bcd1fb0cb4439647f3431b34c93972d6f8118ebb4e674a08eccdd6b/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d6174686575736d61726e742f73636f75746966792e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/matheusmarnt/scoutify) [![Tests](https://camo.githubusercontent.com/46e3c135dc9504e5f37921732e91d0a3b788b7c7ba995c49e4e561543c2bbeb7/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6d6174686575736d61726e742f73636f75746966792f74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/matheusmarnt/scoutify/actions?query=workflow%3Atests+branch%3Amain) [![Code Style](https://camo.githubusercontent.com/fe64de22597777ab28f704aeef3bb42a87e7ffcd679188747aedc2441932a35b/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6d6174686575736d61726e742f73636f75746966792f70696e742e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652b7374796c65267374796c653d666c61742d737175617265)](https://github.com/matheusmarnt/scoutify/actions?query=workflow%3Apint+branch%3Amain) [![License](https://camo.githubusercontent.com/c090e080484e2a2bc766446291d04437db823929042bf614b26a1643660ddf6f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d627269676874677265656e3f7374796c653d666c61742d737175617265)](LICENSE.md) [![Laravel](https://camo.githubusercontent.com/1d8ee10c1921564e5a904292037b307b1f6607305c56bb20c9e5d5d07fd3d51c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d3131253743313225374331332d4646324432303f7374796c653d666c61742d737175617265266c6f676f3d6c61726176656c266c6f676f436f6c6f723d7768697465)](https://laravel.com) [![Livewire](https://camo.githubusercontent.com/4500bb19e4fbadde33dc9692f268c45222283802a762610fa4fea1a9ef534afb/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c697665776972652d33253743342d4642373041393f7374796c653d666c61742d737175617265)](https://livewire.laravel.com) [![Scout](https://camo.githubusercontent.com/f0d41a74083b409b2d0b735c086696e059dd9795ab505f0798a4799dad5ca9dc/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53636f75742d313125374331322d4646324432303f7374796c653d666c61742d737175617265266c6f676f3d6c61726176656c266c6f676f436f6c6f723d7768697465)](https://laravel.com/docs/scout) [![Docs](https://camo.githubusercontent.com/1a63b178ff0c80f823f6f9abe810c533625520dbcd9b875ca83fe43177c8264a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646f63732d6f6e6c696e652d3763336165643f7374796c653d666c61742d737175617265)](https://matheusmarnt.github.io/scoutify/)

Scoutify
========

[](#scoutify)

⌘K global search modal for Laravel — multi-model Livewire UI powered by Scout.

> 📘 **Documentation**:

Drops a production-ready ⌘K search experience into any Laravel application. Register Eloquent models, choose a Scout driver, and ship a keyboard-triggered modal that queries multiple model types simultaneously, groups results by type, and persists recent search history to session.

Features
--------

[](#features)

- **Livewire modal** — keyboard-triggered (`⌘K` / `Ctrl+K`) global search dialog
- **Zero-config discovery** — models under `app/Models/` using `Searchable` are auto-detected at boot
- **Grouped results** — results organised by model type with section headers and color tokens
- **Multiple drivers** — Meilisearch, Algolia, Typesense, or Database
- **Accent-insensitive highlight** — diacritic-free queries (`padrao`) match and highlight accented text (`Padrão`) via NFD normalization
- **Auto-discovered subtitles** — models with `description`, `subtitle`, `excerpt`, `summary`, `bio`, or `body` attributes surface them as result subtitles automatically; HTML is sanitized to plain text before display, so CMS fields render cleanly without escaped tags
- **Query hook** — per-model `globalSearchBuilder()` for custom filters, scopes, or infix matching
- **Recent searches** — configurable history, persisted to session
- **i18n** — ships with `pt_BR`, `en`, and `es` translations
- **Dark mode** — full dark mode support out of the box
- **WCAG AA** — accessible markup with focus management and keyboard navigation
- **Any blade-icons pack** — `globalSearchIcon()` accepts any icon name from any [Blade Icons](https://github.com/blade-ui-kit/blade-icons) pack installed via Composer (e.g. `ri-*`, `tabler-*`, `mdi-*`); fully-qualified names are auto-detected by matching against all registered pack prefixes and passed through as-is; short names fall back to the registered prefix (`heroicon-o-` by default; override via `Scoutify::types()->iconPrefix()` in a service provider)
- **File preview &amp; download** — models implementing `HasGlobalSearchPreview` expose an inline file preview pane inside the modal. PDFs, images, and videos render natively; any other type falls back to an external-link/download button. Download is opt-in and dispatches a `scoutify:download` browser event you can handle with a single listener
- **Tailwind v4** — utility classes inlined, override via the fluent theme API

Quick Start
-----------

[](#quick-start)

```
composer require matheusmarnt/scoutify
php artisan scoutify:install
```

This will:

1. Prompt for a Scout driver (`meilisearch`, `algolia`, or `typesense`)
2. Install the driver's Composer packages
3. Publish `config/scoutify.php` and `config/scout.php`
4. Set `SCOUT_DRIVER` in `.env`

Registering Models
------------------

[](#registering-models)

Make your Eloquent models globally searchable:

```
php artisan scoutify:searchable
```

The command discovers Eloquent models under `app/Models/`, prompts you to pick which to register (or pass `--all`), and **automatically edits each chosen model file** to:

1. Import `Matheusmarnt\Scoutify\Concerns\Searchable` and `Matheusmarnt\Scoutify\Contracts\GloballySearchable`
2. Add `implements GloballySearchable` to the class declaration
3. Insert `use Searchable;` as the first statement in the class body

The command then rebuilds the type manifest so models appear in the UI immediately.

The `Searchable` trait provides sensible defaults for every interface method. Override as needed:

```
public function globalSearchTitle(): string      { return $this->title; }
public function globalSearchSubtitle(): ?string  { return $this->author; }
public function globalSearchUrl(): string        { return route('articles.show', $this); }

public static function globalSearchGroup(): string  { return 'Articles'; }
public static function globalSearchLabel(): string  { return 'Articles'; }  // UI chip label
public static function globalSearchIcon(): string   { return 'heroicon-o-document-text'; }
public static function globalSearchColor(): string  { return 'blue'; }
```

> **Icon packs:** `globalSearchIcon()` accepts any icon name supported by [Blade Icons](https://github.com/blade-ui-kit/blade-icons). Fully-qualified names are auto-detected by matching against **all packs registered via Composer service providers** — not just those declared in `config/blade-icons.php`. Install any pack and use its prefix directly:
>
> ```
> composer require andreiio/blade-remix-icon        # ri-*
> composer require ricard0liveira/blade-tabler-icons  # tabler-*
> ```
>
>
>
> ```
> public static function globalSearchIcon(): string { return 'ri-customer-service-2-fill'; }
> public static function globalSearchIcon(): string { return 'tabler-home'; }
> ```
>
>
>
> Short names (e.g. `user`) get the registered prefix prepended (`heroicon-o-` by default). Override in a service provider:
>
> ```
> use Matheusmarnt\Scoutify\Facades\Scoutify;
>
> Scoutify::types()->iconPrefix('ri-');
> ```

> **`globalSearchSubtitle()` auto-discovery:** if your model has a `description`, `subtitle`, `excerpt`, `summary`, `bio`, or `body` attribute, the trait returns it automatically — HTML is sanitized to plain text (tags stripped, entities decoded, whitespace collapsed) then truncated to 150 chars. Override only when you need custom logic or a different field.

Use `--dry-run` to preview edits without touching files:

```
php artisan scoutify:searchable --dry-run
```

Then import your models into the Scout index:

```
php artisan scoutify:import
```

Add to your layout:

```
{{-- Desktop trigger: pill with label + ⌘K badge, visible on lg+ --}}

{{-- Mobile trigger: 44×44 px icon-only button, hidden on lg+ --}}

{{-- Modal: must be at root layout level, AFTER {{ $slot }} --}}
{{ $slot }}

```

> **Modal placement:** `` must live at the root of your layout, **outside any collapsible or conditionally-rendered container** (sidebar, drawer, off-canvas nav, etc.). Livewire does not initialise components inside collapsed containers — placing the modal inside a collapsed sidebar means it will not mount until the sidebar is opened, causing the trigger to appear broken. The trigger component (``) can go anywhere.

Customizing the Scout Query
---------------------------

[](#customizing-the-scout-query)

Override `globalSearchBuilder()` on any model to apply custom filters, scopes, or driver-specific options:

```
use Laravel\Scout\Builder;

public function globalSearchBuilder(Builder $builder, string $query): Builder
{
    return $builder->where('published', true);
}
```

> **Meilisearch note:** Meilisearch uses word-boundary prefix search. Substrings that are not word-prefixes (e.g. `"ano"` inside `"Mariano"`) return no results. If you need substring (infix) matching, override `globalSearchBuilder()` to configure Meilisearch's `attributesToSearchOn` or switch to the `database` driver which uses `LIKE`-based search.

Opening the Modal Programmatically
----------------------------------

[](#opening-the-modal-programmatically)

Any element can open Scoutify without the official trigger component.

**Alpine (recommended):**

```
Search
```

**Plain JS / any context:**

```
window.dispatchEvent(new CustomEvent('scoutify:open'))
```

**Inside a Livewire component:**

```
Search
```

> **Do not use** `wire:click="$dispatch('scoutify:open')"` on plain Blade elements — outside a Livewire component tree, Livewire.js never initialises those directives.

Visibility Gating (Authorization)
---------------------------------

[](#visibility-gating-authorization)

By default, Scoutify is **secure-by-default**:

- **Guests:** cannot see results (always denied).
- **Authenticated users:** can see results if they pass a registered policy check for `view` (e.g. `Gate::check('view', $record)`). If no policy exists for the model, authenticated users are allowed by default.

To customize this behavior per model, implement the `HasGlobalSearchVisibility` contract and use the fluent `VisibilityRule` builder:

```
use Matheusmarnt\Scoutify\Authorization\VisibilityRule;
use Matheusmarnt\Scoutify\Contracts\HasGlobalSearchVisibility;

class Article extends Model implements GloballySearchable, HasGlobalSearchVisibility
{
    use Searchable;

    public function globalSearchVisibility(): VisibilityRule
    {
        return VisibilityRule::make()
            ->visibleToGuests()                  // expose to non-authenticated visitors
            ->orWhenAuthenticated()              // OR when authenticated +
                ->policy('view')                 //   passes registered policy
                ->orPermission('view-articles')  //   OR has Spatie permission
                ->orRole('admin')                //   OR has Spatie role
                ->orAttribute('is_active');      //   OR has boolean attribute true
    }
}
```

### Supported Rules

[](#supported-rules)

RuleDescription`->visibleToGuests()`Allows guests to see results from this model.`->policy(ability, ...args)`Checks `Gate::check(ability, $record, ...args)`.`->permission(name)`Checks Spatie `hasPermissionTo()`. Supports array for multiple.`->role(name)`Checks Spatie `hasRole()`. Supports array for multiple.`->attribute(name, expected)`Compares `$record->name` with `expected` (default `true`).`->using(Closure)`Custom logic: `fn($record, $user) => bool`.Use `->mode(VisibilityMode::All)` to require **all** rules to pass (logical AND) instead of any (logical OR).

> **Spatie Integration:** `->permission()` and `->role()` require `spatie/laravel-permission`. Scoutify detects it automatically and fails closed if the package is missing when these rules are used.

### Global Configuration

[](#global-configuration)

Customize the default behavior in `config/scoutify.php`:

```
'authorization' => [
    'default' => 'secure',          // secure | permissive | gate-only
    'gate_ability' => 'view',       // ability used for policy/gate checks
],
```

- `secure` (default): Guest denied, Auth checks gate if policy/gate exists, else allow.
- `permissive`: Everyone allowed.
- `gate-only`: Everyone (including guest if gate closure allows) must pass gate check; fails closed if gate/policy is missing.

File Preview &amp; Download
---------------------------

[](#file-preview--download)

Any model can expose an inline file preview pane inside the search modal by implementing `HasGlobalSearchPreview`:

```
use Matheusmarnt\Scoutify\Contracts\HasGlobalSearchPreview;
use Matheusmarnt\Scoutify\Support\PreviewDto;

class Document extends Model implements GloballySearchable, HasGlobalSearchPreview
{
    use Searchable;

    public function globalSearchPreview(): ?PreviewDto
    {
        // Storage-based file (disk + path)
        return PreviewDto::fromDisk(
            disk: 'documents',
            path: $this->file_path,
            filename: $this->original_name,  // optional; defaults to basename($path)
        );

        // OR: external / CDN URL
        // return PreviewDto::fromUrl('https://cdn.example.com/file.pdf');
    }
}
```

### How it works

[](#how-it-works)

- **PDFs, images, and videos** render inline inside the preview pane.
- **Other types** show a fallback with an external-link button.
- **Authorization** reuses the same `GlobalSearchAuthorizer` rules as search results — the record must be visible to the current user.
- **Signed route** (`scoutify.preview.stream`) is auto-registered. No manual route publishing needed.
- **Temporary URLs** — if the disk supports them (e.g. S3 with pre-signed URLs), Scoutify uses them directly; otherwise it streams through the signed route.
- **Keyboard accessible** — `Tab` / `Shift+Tab` cycle focus between the search input and the Preview / Download buttons on the active row. `Enter` on a focused button activates it without navigating to the record's route. Opening the preview auto-focuses the Back button; `Esc` closes the pane.

### Download

[](#download)

Implement the download by listening to the `scoutify:download` browser event:

```
window.addEventListener('scoutify:download', (e) => {
    const a = document.createElement('a');
    a.href = e.detail.url;
    a.download = e.detail.filename ?? '';
    a.click();
});
```

### `PreviewDto` reference

[](#previewdto-reference)

Factory methodWhen to use`PreviewDto::fromDisk(disk, path, ...)`File lives on a Laravel filesystem disk`PreviewDto::fromUrl(url, ...)`File is already a publicly-accessible URLOptional parameters: `mime`, `filename`, `sizeBytes`, `view` (custom Blade view), `ttl` (signed URL TTL in seconds, default 3600).

Commands
--------

[](#commands)

CommandDescription`scoutify:install`Install driver packages, publish config, configure backend`scoutify:doctor`Verify driver config and backend connectivity`scoutify:searchable`Register models as globally searchable and rebuild manifest`scoutify:rebuild`Rebuild the type manifest from `app/Models/``scoutify:import`Import registered models into Scout index`scoutify:flush`Flush registered models from Scout index`scoutify:sync`Flush then re-importAI Assistance
-------------

[](#ai-assistance)

Scoutify ships a two-tier AI documentation mechanism so any AI assistant can access current, version-pinned documentation and scaffold correct PHP code.

**Layer 1 — static files (any AI client, zero install):**

```
https://matheusmarnt.github.io/scoutify/llms.txt
https://matheusmarnt.github.io/scoutify/llms-full.txt

```

**Layer 2 — MCP server (Claude Code, Cursor, Codex, Gemini, Windsurf, Copilot, Cline):**

```
# Claude Code
claude mcp add scoutify -- npx -y @matheusmarnt/scoutify-mcp

# All other MCP clients — add to your mcpServers config:
# { "command": "npx", "args": ["-y", "@matheusmarnt/scoutify-mcp"] }
```

The MCP server exposes 8 tools: `search_docs`, `get_page`, `list_pages`, `get_antipatterns`, `scaffold_searchable_model`, `scaffold_visibility_rule`, `scaffold_theme_config`, `validate_snippet`.

→ [Full AI integration guide](https://matheusmarnt.github.io/scoutify/getting-started/ai-assistance/)

Upgrading
---------

[](#upgrading)

Moving from v1.x to v2.x requires updating your `composer.json` constraint and removing legacy config keys **before** running `composer update`. Skipping this order causes a `RuntimeException` crash in the `post-update-cmd` step.

→ [v1.x → v2.0 upgrade guide](https://matheusmarnt.github.io/scoutify/upgrading/v2/)

Documentation
-------------

[](#documentation)

- [Installation guide](docs/installation.md) — step-by-step setup, model registration, Tailwind config, customization
- [Production deployment](docs/production.md) — per-driver production configuration (Meilisearch, Algolia, Typesense, Database)
- [Upgrade guide](docs/upgrade.md) — v1.x → v2.0 migration steps

Testing
-------

[](#testing)

```
composer test
```

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

License
-------

[](#license)

MIT — see [LICENSE](LICENSE.md).

###  Health Score

50

—

FairBetter than 95% of packages

Maintenance94

Actively maintained with recent releases

Popularity18

Limited adoption so far

Community19

Small or concentrated contributor base

Maturity62

Established project with proven stability

 Bus Factor2

2 contributors hold 50%+ of commits

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

53

Last Release

31d ago

Major Versions

v1.15.2 → v2.0.02026-05-09

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/105521310?v=4)[Matheus Mariano — Eng. Software | IA &amp; GovTech](/maintainers/matheusmarnt)[@matheusmarnt](https://github.com/matheusmarnt)

---

Top Contributors

[![freekmurze](https://avatars.githubusercontent.com/u/483853?v=4)](https://github.com/freekmurze "freekmurze (388 commits)")[![matheusmarnt](https://avatars.githubusercontent.com/u/105521310?v=4)](https://github.com/matheusmarnt "matheusmarnt (166 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (74 commits)")[![mvdnbrk](https://avatars.githubusercontent.com/u/802681?v=4)](https://github.com/mvdnbrk "mvdnbrk (46 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (44 commits)")[![Nielsvanpach](https://avatars.githubusercontent.com/u/10651054?v=4)](https://github.com/Nielsvanpach "Nielsvanpach (23 commits)")[![pforret](https://avatars.githubusercontent.com/u/474312?v=4)](https://github.com/pforret "pforret (16 commits)")[![sebastiandedeyne](https://avatars.githubusercontent.com/u/1561079?v=4)](https://github.com/sebastiandedeyne "sebastiandedeyne (14 commits)")[![AlexVanderbist](https://avatars.githubusercontent.com/u/6287961?v=4)](https://github.com/AlexVanderbist "AlexVanderbist (12 commits)")[![patinthehat](https://avatars.githubusercontent.com/u/5508707?v=4)](https://github.com/patinthehat "patinthehat (10 commits)")[![riasvdv](https://avatars.githubusercontent.com/u/3626559?v=4)](https://github.com/riasvdv "riasvdv (10 commits)")[![AdrianMrn](https://avatars.githubusercontent.com/u/12762044?v=4)](https://github.com/AdrianMrn "AdrianMrn (8 commits)")[![crynobone](https://avatars.githubusercontent.com/u/172966?v=4)](https://github.com/crynobone "crynobone (8 commits)")[![irfanm96](https://avatars.githubusercontent.com/u/42065936?v=4)](https://github.com/irfanm96 "irfanm96 (5 commits)")[![thecaliskan](https://avatars.githubusercontent.com/u/13554944?v=4)](https://github.com/thecaliskan "thecaliskan (5 commits)")[![IGedeon](https://avatars.githubusercontent.com/u/694313?v=4)](https://github.com/IGedeon "IGedeon (4 commits)")[![abenerd](https://avatars.githubusercontent.com/u/7523903?v=4)](https://github.com/abenerd "abenerd (3 commits)")[![jessarcher](https://avatars.githubusercontent.com/u/4977161?v=4)](https://github.com/jessarcher "jessarcher (3 commits)")[![koossaayy](https://avatars.githubusercontent.com/u/6431084?v=4)](https://github.com/koossaayy "koossaayy (3 commits)")[![lloricode](https://avatars.githubusercontent.com/u/8251344?v=4)](https://github.com/lloricode "lloricode (3 commits)")

---

Tags

algoliaglobal-searchlaravellaravel-packagelaravel-scoutlivewiremeilisearchpestphpsearchtailwindcsstypesensesearchlaravellivewirescout

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/matheusmarnt-scoutify/health.svg)

```
[![Health](https://phpackages.com/badges/matheusmarnt-scoutify/health.svg)](https://phpackages.com/packages/matheusmarnt-scoutify)
```

###  Alternatives

[dedoc/scramble

Automatic generation of API documentation for Laravel applications.

2.1k9.9M87](/packages/dedoc-scramble)[filament/support

Core helper methods and foundation code for all Filament packages.

2328.3M213](/packages/filament-support)[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[wnx/laravel-backup-restore

A package to restore database backups made with spatie/laravel-backup.

210389.8k2](/packages/wnx-laravel-backup-restore)[nativephp/desktop

NativePHP for Desktop

37833.6k8](/packages/nativephp-desktop)[lunarstorm/laravel-ddd

A Laravel toolkit for Domain Driven Design patterns

18476.4k](/packages/lunarstorm-laravel-ddd)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
