Laravel-13-Tutorial #8: eine Suchfunktion für den Blog
geprüft am 2 September 2026 · 5 Min.
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.
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:
<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.
Ä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
Route::get('/recherche', [PostController::class, 'search'])->name('search');Die Suchmethode
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
@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_searchan, die beim Speichern mitStr::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
LIKEnie 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
strongholt 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.