Kapitel 8 von 9

Laravel-13-Tutorial #8: eine Suchfunktion für den Blog

geprüft am 2 September 2026 · 5 Min.

Schnelle Antwort

Ein Formular per GET, eine Abfrage where('title', 'like', "%$terme%") und eine View genügen. Achtung: unter SQLite ignoriert LIKE die Groß- und Kleinschreibung nur bei ASCII. Die Suche nach „demonstration“ ohne Akzent findet „démonstration“ nicht, während MySQL und MariaDB damit klarkommen.

Am Ende dieses Kapitels, hat dein Blog eine funktionierende Suche, und du weißt genau, was sie findet und was ihr entgeht.

Eine Suche besteht aus drei Teilen: einem Formular, das die Anfrage abschickt, einer Methode, die die Datenbank befragt, und einer View, die die Treffer anzeigt. Dieses Kapitel setzt sie zusammen und misst dann, was diese Suche kann, und was nicht.

Das Formular

Es steckt schon im Layout aus Kapitel 5, oben auf jeder Seite:

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>

Drei Details zählen. Die Methode ist GET, nicht POST: die Suche ändert nichts, und die entstehende Adresse bleibt teilbar und indexierbar. Das Attribut value="{{ request('q') }}" hält den eingegebenen Begriff nach dem Absenden fest. Das <label> ist optisch versteckt, wird aber von Screenreadern vorgelesen.

Kein CSRF-Token in einem GET-Formular

Ältere Fassungen dieses Tutorials setzten {{ csrf_field() }} in dieses Formular. Das ist nutzlos und sogar schädlich: der CSRF-Schutz gilt nur für Requests, die den Zustand des Servers ändern, und das Token landet sichtbar in der URL. Laravel prüft das Token bei einem GET-Request ohnehin nie.

Die Route

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

Die Suchmethode

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 Punkte verdienen Aufmerksamkeit.

Die Validierung. validate() weist eine Anfrage mit mehr als hundert Zeichen oder von unerwartetem Typ ab. Ohne sie kann ein Besucher ?q[]=x schicken und einen PHP-Fehler auslösen, weil ein Array ankommt, wo ein String erwartet wird.

Das Gruppieren der Bedingungen. Die anonyme Funktion, die an where() übergeben wird, setzt die beiden orWhere im erzeugten SQL in Klammern. Ohne sie wird daraus is_published = 1 AND title LIKE … OR content LIKE …, und die Priorität von OR holt Entwürfe nach oben, deren Inhalt passt. Ein leiser Bug, der erst auffällt, sobald ein Entwurf in der Datenbank liegt.

when(). Die Bedingung greift nur, wenn der Begriff nicht leer ist. Eine leere Suche zeigt also alle Artikel statt einer leeren Seite.

withQueryString(). Ohne diesen Aufruf verlieren die Pagination-Links den Parameter q, und Seite 2 zeigt alle Artikel statt der Treffer.

Die Ergebnis-View

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() liefert die Anzahl der Treffer über alle Seiten hinweg, nicht nur auf der angezeigten.

Was diese Suche kann

Wie sich LIKE verhält, hängt an der Datenbank, nicht an Laravel. Der Unterschied ist deutlich und sollte bekannt sein, bevor eine Site online geht. Zwölf Artikel mit dem Titel „Article de démonstration“ wurden am 2. September 2026 auf beiden Engines abgefragt.

Suchbegriff SQLite 3.46.1 MariaDB 11.8.9 (utf8mb4_unicode_ci)
démonstration findet findet
demonstration (ohne Akzent) findet nichts findet
DÉMONSTRATION (Großbuchstaben mit Akzent) findet nichts findet
ARTICLE (Großbuchstaben ohne Akzent) findet findet

Anders gesagt: unter SQLite ignoriert LIKE die Groß- und Kleinschreibung nur bei ASCII-Zeichen. Sobald ein Akzent im Spiel ist, wird der Vergleich wieder strikt, und eine Suche ohne Akzent liefert nichts. Das ist eine Grenze der Standardimplementierung von SQLite, vom Projekt selbst dokumentiert.

MariaDB und MySQL kennen dieses Problem mit einer Collation utf8mb4_unicode_ci oder utf8mb4_general_ci nicht: der Vergleich ignoriert sowohl die Schreibweise als auch die Akzente.

Die Auswege

Wenn du bei SQLite bleibst und dir das Thema wichtig ist, gibt es drei Möglichkeiten, von der einfachsten zur solidesten.

  • Eine Kopie ohne Akzente speichern. Leg eine Spalte title_search an, die beim Speichern mit Str::ascii($title) gefüllt wird, und such darin, nachdem du dieselbe Umwandlung auf den eingegebenen Begriff angewendet hast.
  • In der Produktion auf MySQL oder PostgreSQL wechseln. Das ist ohnehin die übliche Wahl, sobald eine Site Traffic bekommt.
  • Eine dedizierte Suchmaschine einsetzen. Laravel Scout hängt die Anwendung an Meilisearch, Typesense oder Algolia. Die kümmern sich um Akzente, Tippfehler und Relevanz, was LIKE nie leisten wird.

Die bewusst in Kauf genommenen Grenzen

Diese Suche ist absichtlich einfach gehalten, und man sollte wissen, was sie nicht tut.

  • Sie verzeiht keinen Tippfehler: „larvel“ findet „Laravel“ nicht.
  • Sie sortiert nicht nach Relevanz. Ein Artikel, dessen Titel exakt passt, steht gleichauf mit einem, der den Begriff einmal im Text erwähnt, denn sortiert wird nach Datum.
  • Sie sucht im rohen HTML des Inhalts. Eine Suche nach strong holt alle Artikel hoch, die fett gesetzten Text enthalten.
  • LIKE '%terme%' kann keinen Index nutzen. Bei ein paar tausend Artikeln merkst du davon nichts, darüber hinaus wird die Abfrage proportional zur Tabellengröße langsamer.

Für einen persönlichen Blog reicht das. Darüber hinaus ist Scout der nächste Schritt.

Das letzte Kapitel ergänzt die Pagination und die ähnlichen Artikel.

Häufige Fehler

Ein CSRF-Token in einem GET-Formular Nutzlos und schädlich: der CSRF-Schutz gilt nur für Requests, die den Zustand ändern, und das Token landet sichtbar in der URL. Laravel prüft es bei einem GET nie.
Entwürfe tauchen in den Treffern auf where('is_published', 1)->where(...)->orWhere(...) erzeugt SQL, in dem das OR über das AND gewinnt. Gruppiere die beiden orWhere in einer anonymen Funktion, die du an where() übergibst.
Seite 2 der Treffer verliert den Suchbegriff Ergänze withQueryString() nach paginate(), sonst vergessen die Pagination-Links den Parameter q.
Eine Suche ohne Akzent liefert nichts Verhalten von SQLite, nicht von Laravel. Speichere mit Str::ascii() eine Kopie ohne Akzente, wechsle zu MySQL oder nimm Laravel Scout.
Newsletter

Neue Tests, Tutorials und Projekte, per E-Mail.

Reproduzierbare Tests, versionierter Code, datierte Ergebnisse. Niemals Spam.