Tutorial Laravel 13 #5: página inicial com Vite e Tailwind
verificado a 7 Setembro 2026 · 7 min
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».
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.
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:
npm installDepois arranca o servidor de desenvolvimento do Vite, num segundo terminal, ao lado do php artisan serve:
npm run devO 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:
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.43sSe a página devolver este erro, com o caminho absoluto do teu projeto:
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.
<!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.
<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.
@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>
@endforelseO 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
@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')
@endsectionO 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:
<?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');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.