Tutorial Laravel 13 #5: página de inicio con Vite y Tailwind
verificado el 7 septiembre 2026 · 7 min
Laravel 13 trae Vite y Tailwind CSS por defecto. Lanza npm install y luego npm run dev, e inserta @vite(['resources/css/app.css', 'resources/js/app.js']) en tu plantilla. Sin uno de esos dos comandos, la página falla con «Vite manifest not found».
Este capítulo construye el armazón visual del sitio: una plantilla común, una página de inicio y la cadena de compilación de las hojas de estilo. Los artículos todavía no existen, así que la página mostrará una lista vacía: el capítulo 6 la llenará.
Vite y Tailwind, incluidos con Laravel
Laravel 13 llega con Vite para compilar los assets y Tailwind CSS para los estilos. No hay nada que instalar del lado de PHP: package.json ya los declara.
Las versiones anteriores de este tutorial cargaban Bootstrap 4.5 y jQuery desde un CDN. Ese enfoque tiene dos defectos: añade tres peticiones a servidores de terceros en cada página e ignora por completo el instrumental que Laravel trae de serie. Tailwind, compilado por Vite, produce un único archivo CSS que solo contiene las clases realmente usadas.
Instala las dependencias JavaScript, una sola vez:
npm installDespués arranca el servidor de desarrollo de Vite, en un segundo terminal, junto a php artisan serve:
npm run devVite vigila tus archivos y refresca el navegador en cada cambio. Para generar los archivos definitivos, por ejemplo antes de una puesta en producción:
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.43sSi la página devuelve este error, con la ruta absoluta de tu proyecto:
Vite manifest not found at: /chemin/vers/blog/public/build/manifest.jsones que no se ha ejecutado ni npm run dev ni npm run build. La directiva @vite necesita uno de los dos. Es el error más frecuente a estas alturas.
La plantilla del sitio
Crea resources/views/layouts/app.blade.php. Es el armazón que van a extender todas las páginas públicas, según el mecanismo de herencia descrito en el 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>Hay tres puntos que merecen atención. @vite([...]) inserta las etiquetas hacia los archivos compilados, apuntando al servidor de desarrollo o a los archivos construidos según el contexto. El formulario de búsqueda usa route('search'), una ruta que declarará el capítulo 8, hasta entonces, déjalo de lado o comenta la línea. Y el <label class="sr-only"> sigue siendo legible para los lectores de pantalla pese a ser invisible.
La barra lateral
Crea resources/views/partials/sidebar.blade.php. Mostrará las categorías, las etiquetas y los artículos recientes, todos servidos por el 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>La lista de artículos
Esta vista la incluirán la página de inicio, las páginas de categoría y de etiqueta, y la página de resultados. Escribirla una vez evita tener que corregirla cinco.
@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>
@endforelseEl operador ?-> evita un error si la relación no está: $post->user?->name devuelve null en lugar de reventar cuando se ha borrado al autor. translatedFormat escribe la fecha en el idioma de la aplicación, a diferencia de format, que se queda en inglés.
La página de inicio
@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')
@endsectionEl controlador
El controlador de la portada todavía no puede consultar modelos, porque no existen. Para comprobar la plantilla desde ya, pásale colecciones vacías:
<?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');Recarga la página: la cabecera, la barra lateral y el pie de página aparecen, con «Aucun article pour le moment» en el centro. La estructura aguanta, solo faltan los datos.
El capítulo siguiente crea las tablas, los modelos y las pantallas del back-office que llenarán esta página.