Chapter 5 of 9

Laravel 13 tutorial #5: build a home page with Tailwind

verified on 7 September 2026 · 6 min

Quick answer

Laravel 13 ships with Vite and Tailwind CSS. Run npm install then npm run dev, and put @vite(['resources/css/app.css', 'resources/js/app.js']) in your layout. Without one of those two commands, the page fails with “Vite manifest not found”.

By the end of this chapter, you will have a complete site layout, a sidebar, a reusable post list and a home page that renders properly.

This chapter builds the visual skeleton of the site: a shared layout, a home page, and the stylesheet build chain. There are no posts yet, so the page will show an empty list, chapter 6 will fill it.

Vite and Tailwind, shipped with Laravel

Laravel 13 comes with Vite to compile the assets and Tailwind CSS for the styles. Nothing to install on the PHP side: package.json already declares them.

Why Bootstrap from a CDN is gone

Earlier versions of this tutorial loaded Bootstrap 4.5 and jQuery from a CDN. That approach has two flaws: it adds three requests to third-party servers on every page, and it completely ignores the tooling Laravel provides. Tailwind, compiled by Vite, produces a single CSS file that contains only the classes actually used.

Install the JavaScript dependencies, once and for all:

bash
npm install

Then start the Vite development server, in a second terminal, next to php artisan serve:

bash
npm run dev

Vite watches your files and refreshes the browser on every change. To produce the final files, before going live for instance:

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

If the page returns this error, with the absolute path of your project:

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

then neither npm run dev nor npm run build has been run. The @vite directive needs one of the two. It is the most common error at this stage.

The site layout

Create resources/views/layouts/app.blade.php. This is the skeleton every public page will extend, through the inheritance mechanism described in chapter 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>

Three points deserve attention. @vite([...]) inserts the tags pointing at the compiled files, aiming either at the development server or at the built files depending on the context. The search form uses route('search'), a route that chapter 8 will declare, until then, leave it aside or comment the line out. Finally, the <label class="sr-only"> stays readable by screen readers while being invisible on screen.

The sidebar

Create resources/views/partials/sidebar.blade.php. It will list the categories, the tags and the recent posts, all supplied by the controller.

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>

The post list

This view will be included by the home page, by the category and tag pages, and by the results page. Writing it once saves fixing it five times.

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

The ?-> operator avoids an error when the relation is missing: $post->user?->name returns null instead of blowing up once the author has been deleted. translatedFormat writes the date in the language of the application, unlike format, which stays in English.

The home page

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

The controller

The home controller cannot query any models yet, they do not exist. To check the layout straight away, pass it empty collections:

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');

Reload the page: the header, the sidebar and the footer show up, with “Aucun article pour le moment” in the middle. The structure holds, only the data is missing.

The next chapter creates the tables, the models and the back-office screens that will fill this page.

Common errors

Vite manifest not found at: … Neither npm run dev nor npm run build has been run. The @vite directive needs one of the two. It is the most common error in this chapter.
route('search') throws an exception The search route is only declared in chapter 8. Comment the form out, or declare an empty route in the meantime.
The date ignores the application language format() does not translate. Use translatedFormat(), which follows the locale of the application.
Newsletter

New tests, tutorials and projects, by e-mail.

Reproducible tests, versioned code, dated results. Never any spam.