Laravel 13-tutorial #8: een zoekmachine voor je blog
geverifieerd op 2 september 2026 · 5 min
Een formulier in GET, een query where('title', 'like', "%$terme%") en een view volstaan. Let op: onder SQLite negeert LIKE hoofdletters alleen bij ASCII. Zoeken op “demonstration” zonder accent vindt “démonstration” niet, terwijl MySQL en MariaDB dat wel doen.
Een zoekmachine bestaat uit drie stukken: een formulier dat de zoekterm verstuurt, een methode die de database bevraagt en een view die de resultaten toont. Dit hoofdstuk zet ze in elkaar en meet daarna wat die zoekfunctie kan, en wat niet.
Het formulier
Het staat al in de layout uit hoofdstuk 5, boven aan elke pagina:
<form action="{{ route('search') }}" method="GET" role="search" class="flex gap-2">
<label for="q" class="sr-only">Rechercher</label>
<input id="q" type="search" name="q" value="{{ request('q') }}"
placeholder="Rechercher…"
class="rounded border border-gray-300 px-3 py-1.5 text-sm">
<button type="submit" class="rounded bg-gray-900 px-3 py-1.5 text-sm text-white">Go</button>
</form>Drie details doen ertoe. De methode is GET en niet POST: zoeken wijzigt niets, en het resulterende adres blijft deelbaar en indexeerbaar. Het attribuut value="{{ request('q') }}" houdt de ingetypte term vast na het versturen. Het <label> is visueel verborgen, maar wordt wel voorgelezen door schermlezers.
Oudere versies van deze tutorial zetten {{ csrf_field() }} in dit formulier. Dat is nutteloos en schadelijk: CSRF-bescherming geldt alleen voor requests die de staat van de server wijzigen, en het token belandt zo zichtbaar in de URL. Laravel controleert het token op een GET-request trouwens nooit.
De route
Route::get('/recherche', [PostController::class, 'search'])->name('search');De zoekmethode
use Illuminate\Http\Request;
public function search(Request $request): View
{
$validated = $request->validate([
'q' => ['nullable', 'string', 'max:100'],
]);
$key = trim($validated['q'] ?? '');
$posts = Post::published()
->when($key !== '', fn ($query) => $query->where(
fn ($q) => $q->where('title', 'like', '%'.$key.'%')
->orWhere('content', 'like', '%'.$key.'%')
))
->with(['category', 'user'])
->latest('published_at')
->paginate(5)
->withQueryString();
return view('search', [
'key' => $key,
'posts' => $posts,
'categories' => Category::withCount('posts')->orderBy('name')->get(),
'tags' => Tag::orderBy('name')->get(),
'recentPosts' => Post::published()->latest('published_at')->take(5)->get(),
]);
}Vier punten verdienen aandacht.
De validatie. validate() weigert een zoekterm van meer dan honderd tekens of van een onverwacht type. Zonder validatie kan een bezoeker ?q[]=x versturen en een PHP-fout uitlokken door een array door te geven waar een string wordt verwacht.
Het groeperen van de voorwaarden. De anonieme functie die aan where() wordt doorgegeven, zet de twee orWhere-voorwaarden tussen haakjes in de gegenereerde SQL. Zonder die functie wordt de query is_published = 1 AND title LIKE … OR content LIKE …, en door de voorrang van OR komen concepten met een passende inhoud alsnog boven water. Een sluipende bug, die je pas ziet zodra er een concept in de database staat.
when(). De voorwaarde wordt alleen toegepast als de term niet leeg is. Een lege zoekopdracht toont dus alle artikelen in plaats van een lege pagina.
withQueryString(). Zonder die aanroep verliezen de paginatielinks de parameter q, en toont pagina 2 alle artikelen in plaats van de resultaten.
De resultatenview
@extends('layouts.app')
@section('title', 'Recherche : '.$key)
@section('content')
<h1 class="mb-6 text-2xl font-bold">
Résultats pour « {{ $key }} »
<span class="text-base font-normal text-gray-500">({{ $posts->total() }})</span>
</h1>
@include('partials.posts-list')
@endsection$posts->total() geeft het aantal resultaten over alle pagina’s, niet alleen op de pagina die je ziet.
Wat deze zoekfunctie kan
Het gedrag van LIKE hangt af van de database, niet van Laravel. Het verschil is groot en je kent het beter voor je een site live zet. Twaalf artikelen met de titel “Article de démonstration” zijn op 2 september 2026 op beide engines bevraagd.
| Gezochte term | SQLite 3.46.1 | MariaDB 11.8.9 (utf8mb4_unicode_ci) |
|---|---|---|
démonstration | vindt | vindt |
demonstration (zonder accent) | vindt niets | vindt |
DÉMONSTRATION (hoofdletters met accent) | vindt niets | vindt |
ARTICLE (hoofdletters zonder accent) | vindt | vindt |
Met andere woorden: onder SQLite negeert LIKE hoofdletters uitsluitend bij ASCII-tekens. Zodra er een accent in het spel komt, wordt de vergelijking weer strikt en levert een zoekopdracht zonder accent niets op. Dat is een beperking van de standaardimplementatie van SQLite, gedocumenteerd door het project zelf.
MariaDB en MySQL kennen dat probleem niet met een collation utf8mb4_unicode_ci of utf8mb4_general_ci: de vergelijking negeert zowel hoofdletters als accenten.
De omwegen
Blijf je bij SQLite en telt dit punt voor je project, dan zijn er drie opties, van de eenvoudigste naar de stevigste.
- Een kopie zonder accenten opslaan. Voeg een kolom
title_searchtoe die je bij het opslaan vult metStr::ascii($title), en zoek daarin nadat je dezelfde bewerking op de ingetypte term hebt losgelaten. - Overstappen op MySQL of PostgreSQL in productie. Dat is toch de gebruikelijke keuze zodra een site verkeer krijgt.
- Een echte zoekmachine gebruiken. Laravel Scout koppelt de applicatie aan Meilisearch, Typesense of Algolia. Die gaan om met accenten, typefouten en relevantie, en dat zal
LIKEnooit doen.
De bewuste beperkingen
Deze zoekfunctie is met opzet eenvoudig, en je moet weten wat ze niet doet.
- Ze vergeeft geen enkele typefout: “larvel” vindt “Laravel” niet.
- Ze sorteert niet op relevantie. Een artikel waarvan de titel exact overeenkomt, komt op dezelfde hoogte als een artikel dat de term één keer in de inhoud noemt, want er wordt op datum gesorteerd.
- Ze zoekt in de ruwe HTML van de inhoud. Zoeken op
stronghaalt elk artikel met vetgedrukte tekst naar boven. LIKE '%terme%'kan geen index gebruiken. Op een paar duizend artikelen merk je er niets van, daarboven vertraagt de query evenredig met de omvang van de tabel.
Voor een persoonlijke blog volstaat dit. Daarboven is Scout de volgende stap.
Het laatste hoofdstuk voegt paginatie en gerelateerde artikelen toe.