Capítulo 5 de 9

Tutorial Laravel 13 #5: página inicial com Vite e Tailwind

verificado a 7 Setembro 2026 · 7 min

Resposta rápida

O Laravel 13 traz o Vite e o Tailwind CSS por omissão. Corre npm install e depois npm run dev, e insere @vite(['resources/css/app.css', 'resources/js/app.js']) no teu template. Sem um destes dois comandos, a página falha com «Vite manifest not found».

No fim deste capítulo, terás um template de site completo, uma barra lateral, uma lista de artigos reutilizável e uma página inicial que aparece como deve ser.

Este capítulo constrói o esqueleto visual do site: um template comum, uma página inicial e a cadeia de compilação das folhas de estilo. Os artigos ainda não existem, por isso a página vai mostrar uma lista vazia, o capítulo 6 vai enchê-la.

Vite e Tailwind, incluídos no Laravel

O Laravel 13 chega com o Vite para compilar os assets e o Tailwind CSS para os estilos. Nada a instalar do lado do PHP: o package.json já os declara.

Porquê abandonar o Bootstrap em CDN

As versões anteriores deste tutorial carregavam o Bootstrap 4.5 e o jQuery a partir de um CDN. Essa abordagem tem dois defeitos: acrescenta três pedidos a servidores terceiros em cada página e ignora por completo as ferramentas que o Laravel fornece. O Tailwind, compilado pelo Vite, produz um único ficheiro CSS que só contém as classes realmente usadas.

Instala as dependências JavaScript, uma só vez:

bash
npm install

Depois arranca o servidor de desenvolvimento do Vite, num segundo terminal, ao lado do php artisan serve:

bash
npm run dev

O Vite vigia os teus ficheiros e atualiza o browser a cada alteração. Para produzir os ficheiros definitivos, por exemplo antes de uma colocação em linha:

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

Se a página devolver este erro, com o caminho absoluto do teu projeto:

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

é porque nem o npm run dev nem o npm run build foram executados. A diretiva @vite precisa de um dos dois. É o erro mais frequente nesta fase.

O template do site

Cria o resources/views/layouts/app.blade.php. É o esqueleto que todas as páginas públicas vão estender, segundo o mecanismo de herança descrito no capítulo 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>

Três pontos merecem atenção. O @vite([...]) insere as tags para os ficheiros compilados, apontando para o servidor de desenvolvimento ou para os ficheiros construídos, consoante o contexto. O formulário de pesquisa usa o route('search'), uma rota que o capítulo 8 vai declarar, até lá, deixa-o de lado ou comenta a linha. Por fim, o <label class="sr-only"> continua legível para os leitores de ecrã, ao mesmo tempo que fica invisível.

A barra lateral

Cria o resources/views/partials/sidebar.blade.php. Vai mostrar as categorias, as etiquetas e os artigos recentes, todos fornecidos pelo controlador.

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>

A lista de artigos

Esta view será incluída pela página inicial, pelas páginas de categoria e de etiqueta, e pela página de resultados. Escrevê-la uma vez evita corrigi-la cinco vezes.

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

O operador ?-> evita um erro se a relação estiver ausente: $post->user?->name devolve null em vez de rebentar quando o autor foi eliminado. O translatedFormat escreve a data na língua da aplicação, ao contrário do format, que fica em inglês.

A página inicial

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

O controlador

O controlador da página inicial ainda não pode interrogar modelos: eles não existem. Para verificares o template já a seguir, passa-lhe coleções vazias:

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

Recarrega a página: o cabeçalho, a barra lateral e o rodapé aparecem, com «Aucun article pour le moment» ao centro. A estrutura aguenta-se, só faltam os dados.

O capítulo seguinte cria as tabelas, os modelos e os ecrãs do back-office que vão encher esta página.

Erros frequentes

Vite manifest not found at: … Nem o npm run dev nem o npm run build foram executados. A diretiva @vite precisa de um dos dois. É o erro mais frequente do capítulo.
route('search') lança uma exceção A rota de pesquisa só é declarada no capítulo 8. Comenta o formulário ou declara a rota vazia entretanto.
A data aparece em inglês O format() não traduz. Usa o translatedFormat(), que segue a locale da aplicação.
Newsletter

Os novos testes, tutoriais e projetos, por e-mail.

Testes reproduzíveis, código versionado, resultados datados. Nunca spam.