Rozdział 5 z 9

Kurs Laravel 13 #5: strona główna z Vite i Tailwindem

zweryfikowano 7 września 2026 · 6 min

Szybka odpowiedź

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”.

Na koniec tego rozdziału, będziesz mieć kompletny szablon serwisu, pasek boczny, listę artykułów do wielokrotnego użytku i poprawnie wyświetlaną stronę główną.

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.

Dlaczego znika Bootstrap z CDN-u

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:

bash
npm install

Następnie uruchom serwer deweloperski Vite, w drugim terminalu, obok php artisan serve:

bash
npm run dev

Vite obserwuje twoje pliki i odświeża przeglądarkę przy każdej zmianie. Żeby wyprodukować pliki końcowe, na przykład przed wdrożeniem:

bash
npm run build
code
public/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.43s
Vite Manifest not found

Jeśli strona zwraca ten błąd, z bezwzględną ścieżką twojego projektu:

code
Vite manifest not found at: /chemin/vers/blog/public/build/manifest.json

to 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.

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

resources/views/partials/sidebar.blade.php
<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.

resources/views/partials/posts-list.blade.php
@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>
@endforelse

Operator ?-> 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

resources/views/home.blade.php
@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')
@endsection

Kontroler

Kontroler strony głównej nie może jeszcze odpytywać modeli, bo te nie istnieją. Żeby sprawdzić szablon już teraz, przekaż mu puste kolekcje:

app/Http/Controllers/IndexController.php
<?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(),
        ]);
    }
}
routes/web.php
<?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ę.

Częste błędy

Vite manifest not found at: … Nie uruchomiono ani npm run dev, ani npm run build. Dyrektywa @vite potrzebuje jednego z nich. To najczęstszy błąd w tym rozdziale.
route('search') rzuca wyjątek Trasa wyszukiwania powstaje dopiero w rozdziale 8. Zakomentuj formularz albo zadeklaruj pustą trasę na przeczekanie.
Data wyświetla się po angielsku format() nie tłumaczy. Użyj translatedFormat(), które podąża za locale aplikacji.
Newsletter

Nowe testy, poradniki i projekty, e-mailem.

Powtarzalne testy, wersjonowany kod, datowane wyniki. Nigdy spamu.