Kurs Laravel 13 #5: strona główna z Vite i Tailwindem
zweryfikowano 7 września 2026 · 6 min
Laravel 13 dostarcza Vite i Tailwind CSS domyślnie. Uruchom npm install, potem npm run dev, i wstaw @vite(['resources/css/app.css', 'resources/js/app.js']) do swojego szablonu. Bez jednej z tych dwóch komend strona kończy się błędem „Vite manifest not found”.
Ten rozdział buduje wizualny szkielet serwisu: wspólny szablon, stronę główną i cały łańcuch kompilacji arkuszy stylów. Artykułów jeszcze nie ma, więc strona wyświetli pustą listę, wypełni ją rozdział 6.
Vite i Tailwind, dostarczane z Laravelem
Laravel 13 przychodzi z Vite do kompilowania zasobów i Tailwind CSS do stylów. Po stronie PHP nie ma nic do instalowania: package.json już je deklaruje.
Wcześniejsze wersje tego kursu wczytywały Bootstrapa 4.5 i jQuery z CDN-u. To podejście ma dwie wady: dokłada trzy żądania do obcych serwerów na każdej stronie i całkowicie pomija narzędzia, które Laravel już daje. Tailwind kompilowany przez Vite produkuje jeden plik CSS, zawierający wyłącznie realnie użyte klasy.
Zainstaluj zależności JavaScriptu, raz na projekt:
npm installNastępnie uruchom serwer deweloperski Vite, w drugim terminalu, obok php artisan serve:
npm run devVite obserwuje twoje pliki i odświeża przeglądarkę przy każdej zmianie. Żeby wyprodukować pliki końcowe, na przykład przed wdrożeniem:
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.43sJeśli strona zwraca ten błąd, z bezwzględną ścieżką twojego projektu:
Vite manifest not found at: /chemin/vers/blog/public/build/manifest.jsonto znaczy, że nie uruchomiłeś ani npm run dev, ani npm run build. Dyrektywa @vite potrzebuje jednego z nich. To najczęstszy błąd na tym etapie.
Szablon serwisu
Utwórz resources/views/layouts/app.blade.php. To szkielet, który rozszerzą wszystkie publiczne strony, zgodnie z mechanizmem dziedziczenia opisanym w rozdziale 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>Trzy rzeczy zasługują tu na uwagę. @vite([...]) wstawia znaczniki wskazujące skompilowane pliki, kierując je na serwer deweloperski albo na zbudowane pliki, zależnie od kontekstu. Formularz wyszukiwania korzysta z route('search'), trasy, którą zadeklaruje rozdział 8, do tego czasu zostaw go w spokoju albo zakomentuj tę linię. Wreszcie <label class="sr-only"> pozostaje czytelny dla czytników ekranu, będąc jednocześnie niewidocznym.
Pasek boczny
Utwórz resources/views/partials/sidebar.blade.php. Wyświetli kategorie, tagi i najnowsze artykuły, wszystko dostarczone przez kontroler.
<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>Lista artykułów
Ten widok będzie dołączany przez stronę główną, strony kategorii i tagów oraz stronę wyników wyszukiwania. Napisany raz, nie wymaga poprawiania w pięciu miejscach.
@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>
@endforelseOperator ?-> chroni przed błędem, gdy relacji brakuje: $post->user?->name zwraca null, zamiast wywalić stronę, kiedy autor został usunięty. translatedFormat zapisuje datę w języku aplikacji, w odróżnieniu od format, które zostaje przy angielskim.
Strona główna
@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')
@endsectionKontroler
Kontroler strony głównej nie może jeszcze odpytywać modeli, bo te nie istnieją. Żeby sprawdzić szablon już teraz, przekaż mu puste kolekcje:
<?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');Przeładuj stronę: nagłówek, pasek boczny i stopka są na miejscu, a pośrodku widnieje „Aucun article pour le moment”. Struktura się trzyma, brakuje tylko danych.
Następny rozdział tworzy tabele, modele i ekrany zaplecza, które wypełnią tę stronę.