Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Sayt Laravel Package

amplify/sayt

Sayt adds search-as-you-type to Laravel, returning live suggestions as users type. Wire it to your models or query builder, tune matching and ranking, and drop results into any Blade or API response.

View on GitHub
Deep Wiki
Context7

Getting Started

amplify/sayt is a small add-on module for Amplify CMS that adds a search-as-you-type autocomplete endpoint (and the JS widget that calls it) on top of the CMS's content index. It is not a general-purpose search package — it assumes you already have Amplify installed and a populated search index.

Install and wire it up

composer require amplify/sayt

# config + any migrations (morph map / index table, if the build ships one)
php artisan vendor:publish --provider="Amplify\Sayt\SaytServiceProvider"

# build/refresh the index the autocomplete reads from
php artisan sayt:index

Then confirm the route exists before touching any front-end:

php artisan route:list | grep -i sayt
curl -s "https://your-app.test/api/sayt?q=about" -H "Accept: application/json" | jq

Where to look first

  1. config/sayt.php — minimum query length, result limit, debounce hint, indexed content types, cache TTL. This is the file you'll actually change 90% of the time.
  2. vendor/amplify/sayt/src/Http/Controllers/ — the endpoint's request/response shape. Copy this contract into your front-end rather than guessing.
  3. vendor/amplify/sayt/src/Contracts/ — the result transformer interface; the extension point for shaping titles/URLs/excerpts.

First use case — replace the slow submit-and-reload search in the Amplify admin with a typeahead that returns grouped results (Entries, Pages, Media) after two keystrokes, and jumps straight to the editor on selection. That's the whole value proposition: fewer page loads for editors, not a public-facing search engine.

Implementation Patterns

Treat the endpoint as the product, not the widget. The bundled JS is thin; most teams either use it as-is in the admin or throw it away and call the route from their own Alpine/Livewire component. Pin down the JSON contract in a feature test first:

it('returns grouped suggestions', function () {
    $this->getJson('/api/sayt?q=abo')
        ->assertOk()
        ->assertJsonStructure([
            'data' => [['type', 'title', 'url', 'excerpt']],
            'meta' => ['query', 'took_ms'],
        ]);
});

Front-end: debounce + abort the previous request. This is the pattern that makes or breaks perceived latency:

let controller, timer;
input.addEventListener('input', (e) => {
  const q = e.target.value.trim();
  clearTimeout(timer);
  controller?.abort();                       // kill the in-flight request
  if (q.length < 2) return render([]);       // mirror `min_length` in config
  timer = setTimeout(async () => {
    controller = new AbortController();
    const res = await fetch(`/api/sayt?q=${encodeURIComponent(q)}`, {
      signal: controller.signal,
      headers: { Accept: 'application/json' },
    });
    render((await res.json()).data);
  }, 150);                                   // ~150–250ms feels right
});

Keep the index fresh from model events, on the queue, after commit. The single most common bug in this class of package is indexing a record that a transaction later rolls back.

protected static function booted(): void
{
    static::saved(fn ($m) => \Amplify\Sayt\Facades\Sayt::indexModel($m)->afterCommit()->onQueue('search'));
    static::deleted(fn ($m) => \Amplify\Sayt\Facades\Sayt::forgetModel($m)->afterCommit());
}

Scope results to what the user is allowed to see. The autocomplete is an enumeration oracle — a draft entry showing up in an editor's typeahead is a content leak. Apply visibility filters in a custom transformer or a thin wrapper service so every caller gets the same rules:

Sayt::query($term)
    ->types(config('sayt.types'))
    ->whereIn('status', ['published', 'review'])   // or gate() per model
    ->limit(8)
    ->get();

Cache aggressively per locale and per role bucket. Autocomplete traffic is bursty and repetitive (people type the same two prefixes). A 60–300s cache keyed on sha1($q . $locale . $roleBucket) cuts DB work to near zero and is trivially safe to bust on save.

Reindex in CI and in deploys. Add php artisan sayt:index to your deploy script after migrations, and a sayt:index --fresh job when the schema or weighting config changes. Do not rely on the index being right by accident.

Gotchas and Tips

  • Zero stars means you own the maintenance. Pin the exact version in composer.json, read the controller before trusting it, and wrap it behind your own thin service class. That way a fork or a replacement package is a one-file change.
  • Verify the vendor name. The repo is amplify-plugin/sayt-module but Packagist may resolve to amplify/sayt; if composer require fails, check the name field in the upstream composer.json rather than guessing.
  • Auth middleware is easy to strip. If the route ships under web with session auth, an unauthenticated fetch from a different host (or a missing X-CSRF-TOKEN) gives you an empty, silent 419. Decide deliberately: admin-only behind auth + throttle:60,1, or public behind a stricter throttle and a visibility filter.
  • Throttle it. Typeahead is a per-keystroke endpoint; without throttle: a single impatient user can generate hundreds of requests a minute. Rate-limit by IP and user, and log 429s — they mean your debounce is too tight.
  • Stale results after edits. If indexing is queued, an editor saves and immediately searches the new title: it isn't there. Either index synchronously for the admin (onConnection('sync')) or have the widget re-query after save. The afterCommit pattern above prevents ghosts; queue lag needs separate handling.
  • Race conditions on slow networks. Out-of-order responses will overwrite newer results with older ones. AbortController (above) handles most of it; add a sequence number if you can't rely on abort.
  • Minimum-length mismatch. The package's min_length and your JS guard drifting apart yields "why does it fetch but show nothing" bug reports. Read it from config and expose it to the view.
  • Index drift from bulk operations. Model::query()->update([...]) and raw SQL bypass model events, so the index silently desyncs. Any mass update needs a follow-up sayt:index --fresh (or a ->chunkById() loop over save()).
  • Debugging: log the resolved driver, the term, and took_ms at debug level behind a config flag. When results are wrong, the question is always "is it the index or the query?" — sayt:index --dry-run (if available) or a direct query with ->toSql() answers that in one step.
  • Extension points to use instead of forking: the result transformer contract (titles, URLs, excerpts), the config type list, and the cache key builder. If you find yourself editing vendor/, you've picked the wrong seam.
  • Multi-locale: make sure the index is written per-locale and the query passes the active locale; a single flat index will happily return French titles to an English admin.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource