Laravel-13-Tutorial #5: Startseite mit Vite und Tailwind
geprüft am 7 September 2026 · 6 Min.
Laravel 13 bringt Vite und Tailwind CSS von Haus aus mit. Führe npm install und dann npm run dev aus und setz @vite(['resources/css/app.css', 'resources/js/app.js']) in dein Layout. Ohne einen dieser beiden Befehle scheitert die Seite mit „Vite manifest not found“.
Dieses Kapitel baut das visuelle Gerüst der Site: ein gemeinsames Layout, eine Startseite und die Build-Kette für die Stylesheets. Artikel gibt es noch keine, die Seite zeigt also eine leere Liste, Kapitel 6 füllt sie.
Vite und Tailwind sind bei Laravel dabei
Laravel 13 kommt mit Vite für die Assets und Tailwind CSS für die Styles. Auf PHP-Seite ist nichts zu installieren: package.json deklariert beides bereits.
Die früheren Fassungen dieses Tutorials luden Bootstrap 4.5 und jQuery von einem CDN. Das hat zwei Nachteile: es kostet auf jeder Seite drei Requests an fremde Server, und es ignoriert das Tooling, das Laravel mitbringt. Tailwind, von Vite kompiliert, erzeugt eine einzige CSS-Datei, die nur die tatsächlich benutzten Klassen enthält.
Installier die JavaScript-Abhängigkeiten, einmalig:
npm installStarte dann den Vite-Entwicklungsserver in einem zweiten Terminal, neben php artisan serve:
npm run devVite überwacht deine Dateien und lädt den Browser bei jeder Änderung neu. Für die endgültigen Dateien, etwa vor einem Deployment:
npm run buildpublic/build/assets/instrument-sans-500-normal-Z6ESRlEs.woff 21.65 kB
public/build/assets/instrument-sans-600-normal-B9e8oLYv.woff 21.67 kB
public/build/assets/fonts-C9MNnjVw.css 2.35 kB │ gzip: 0.38 kB
public/build/assets/app-D8N0vhr5.css 61.08 kB │ gzip: 12.65 kB
public/build/assets/app-BvRk9kiK.js 0.00 kB │ gzip: 0.02 kB
✓ built in 1.43sWenn die Seite diesen Fehler mit dem absoluten Pfad deines Projekts zurückgibt:
Vite manifest not found at: /chemin/vers/blog/public/build/manifest.jsondann wurde weder npm run dev noch npm run build ausgeführt. Die Direktive @vite braucht eines von beiden. Das ist an dieser Stelle der häufigste Fehler.
Das Layout der Site
Leg resources/views/layouts/app.blade.php an. Das ist das Gerüst, das alle öffentlichen Seiten erweitern, nach dem Vererbungsprinzip aus Kapitel 3.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title', config('app.name'))</title>
<meta name="description" content="@yield('description', 'Un blog construit avec Laravel 13.')">
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-gray-50 text-gray-900 antialiased">
<header class="border-b bg-white">
<div class="mx-auto flex max-w-5xl items-center justify-between gap-4 px-4 py-4">
<a href="http://%20route('home')%20" class="text-lg font-semibold">{{ config('app.name') }}</a>
<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>
</div>
</header>
<main class="mx-auto grid max-w-5xl gap-8 px-4 py-8 md:grid-cols-3">
<div class="md:col-span-2">
@yield('content')
</div>
@include('partials.sidebar')
</main>
<footer class="border-t bg-white">
<div class="mx-auto max-w-5xl px-4 py-6 text-sm text-gray-500">
{{ config('app.name') }} — tutoriel Laravel 13.
</div>
</footer>
</body>
</html>Drei Punkte verdienen Aufmerksamkeit. @vite([...]) fügt die Tags zu den kompilierten Dateien ein und zeigt je nach Kontext auf den Entwicklungsserver oder auf den Build. Das Suchformular nutzt route('search'), eine Route, die erst Kapitel 8 deklariert, bis dahin lässt du es weg oder kommentierst die Zeile aus. Und <label class="sr-only"> bleibt für Screenreader lesbar, ohne sichtbar zu sein.
Die Sidebar
Leg resources/views/partials/sidebar.blade.php an. Sie zeigt die Kategorien, die Tags und die neuesten Artikel, alle vom Controller geliefert.
<aside class="space-y-8">
<section>
<h2 class="mb-2 font-semibold">Catégories</h2>
<ul class="space-y-1 text-sm">
@foreach ($categories as $category)
<li>
<a href="http://%20route('category',%20$category)%20" class="text-blue-700 hover:underline">
{{ $category->name }}
</a>
<span class="text-gray-400">({{ $category->posts_count }})</span>
</li>
@endforeach
</ul>
</section>
<section>
<h2 class="mb-2 font-semibold">Étiquettes</h2>
<ul class="flex flex-wrap gap-2 text-sm">
@foreach ($tags as $tag)
<li>
<a href="http://%20route('tag',%20$tag)%20" class="rounded bg-gray-200 px-2 py-0.5 hover:bg-gray-300">
{{ $tag->name }}
</a>
</li>
@endforeach
</ul>
</section>
<section>
<h2 class="mb-2 font-semibold">Articles récents</h2>
<ul class="space-y-1 text-sm">
@foreach ($recentPosts as $recent)
<li>
<a href="http://%20route('post',%20$recent)%20" class="text-blue-700 hover:underline">{{ $recent->title }}</a>
</li>
@endforeach
</ul>
</section>
</aside>Die Artikelliste
Diese View wird von der Startseite, den Kategorie- und Tag-Seiten und der Ergebnisseite eingebunden. Einmal geschrieben heißt: nicht fünfmal korrigiert.
@forelse ($posts as $post)
<article class="mb-6 rounded border bg-white p-4">
<h2 class="text-xl font-semibold">
<a href="http://%20route('post',%20$post)%20" class="hover:underline">{{ $post->title }}</a>
</h2>
<p class="mt-1 text-sm text-gray-500">
{{ $post->user?->name }} —
<a href="http://%20route('category',%20$post-category)%20" class="hover:underline">{{ $post->category?->name }}</a>
@if ($post->published_at) — {{ $post->published_at->translatedFormat('j F Y') }} @endif
</p>
<p class="mt-2 text-gray-700">{{ $post->excerpt ?: Str::limit(strip_tags($post->content), 160) }}</p>
<a href="http://%20route('post',%20$post)%20" class="mt-3 inline-block text-sm text-blue-700 hover:underline">Lire la suite</a>
</article>
@empty
<p class="text-gray-600">Aucun article pour le moment.</p>
@endforelseDer Operator ?-> verhindert einen Fehler, wenn die Relation fehlt: $post->user?->name liefert null, statt abzustürzen, wenn der Autor gelöscht wurde. translatedFormat schreibt das Datum in der Sprache der Anwendung, anders als format, das bei Englisch bleibt.
Die Startseite
@extends('layouts.app')
@section('title', config('app.name'))
@section('content')
@if ($featuredPosts->isNotEmpty())
<section class="mb-8">
<h1 class="mb-3 text-2xl font-bold">À la une</h1>
<ul class="space-y-1">
@foreach ($featuredPosts as $featured)
<li>
<a href="http://%20route('post',%20$featured)%20" class="text-blue-700 hover:underline">{{ $featured->title }}</a>
</li>
@endforeach
</ul>
</section>
@endif
<h2 class="mb-4 text-xl font-bold">Derniers articles</h2>
@include('partials.posts-list')
@endsectionDer Controller
Der Controller der Startseite kann noch keine Modelle abfragen, es gibt sie nicht. Damit du das Layout schon jetzt prüfen kannst, gibst du ihm leere Collections mit:
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Collection;
use Illuminate\View\View;
class IndexController extends Controller
{
public function __invoke(): View
{
return view('home', [
'posts' => new Collection(),
'featuredPosts' => new Collection(),
'categories' => new Collection(),
'tags' => new Collection(),
'recentPosts' => new Collection(),
]);
}
}<?php
use App\Http\Controllers\IndexController;
use Illuminate\Support\Facades\Route;
Route::get('/', IndexController::class)->name('home');Lad die Seite neu: Header, Sidebar und Footer erscheinen, in der Mitte steht „Aucun article pour le moment“. Die Struktur steht, es fehlen nur die Daten.
Das nächste Kapitel legt die Tabellen, die Modelle und die Backend-Screens an, die diese Seite füllen.