Hoofdstuk 8 van 9

Laravel 13-tutorial #8: een zoekmachine voor je blog

geverifieerd op 2 september 2026 · 5 min

Kort antwoord

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.

Aan het einde van dit hoofdstuk, heeft je blog een werkende zoekfunctie, en weet je precies wat ze vindt en wat ze mist.

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:

resources/views/layouts/app.blade.php
<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.

Geen CSRF-token op een GET-formulier

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

routes/web.php
Route::get('/recherche', [PostController::class, 'search'])->name('search');

De zoekmethode

app/Http/Controllers/PostController.php
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

resources/views/search.blade.php
@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_search toe die je bij het opslaan vult met Str::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 LIKE nooit 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 strong haalt 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.

Veelgemaakte fouten

Een CSRF-token in een GET-formulier Nutteloos en schadelijk: CSRF-bescherming geldt alleen voor requests die de staat wijzigen, en het token belandt zichtbaar in de URL. Laravel controleert het op een GET nooit.
Concepten tussen de resultaten where('is_published', 1)->where(...)->orWhere(...) levert SQL op waarin het OR het wint van het AND. Groepeer de twee orWhere in een anonieme functie die je aan where() doorgeeft.
Pagina 2 van de resultaten verliest de zoekterm Zet withQueryString() achter paginate(), anders vergeten de paginatielinks de parameter q.
Zoeken zonder accent levert niets op Gedrag van SQLite, niet van Laravel. Sla een kopie zonder accenten op met Str::ascii(), stap over op MySQL, of gebruik Laravel Scout.
Nieuwsbrief

Nieuwe tests, tutorials en projecten, per e-mail.

Reproduceerbare tests, geversioneerde code, gedateerde resultaten. Nooit spam.