Objectif
Créer un blog minimaliste avec Blade : layout partagé, composants réutilisables (carte article, alerte, navigation), formulaire de création avec validation, et affichage des erreurs.
| Concept | Mis en pratique |
|---|---|
| Layout | @extends / @section / @yield |
| Composant | x-alert, x-article-card, x-nav-link |
| Directives | @forelse, @auth, @can, @error |
| Formulaire | @csrf, old(), validation + messages |
| Stack | @push('scripts') sur la page d'édition |
Consignes
- Créer
resources/views/layouts/app.blade.phpavec@stack('styles')et@stack('scripts') - Créer le composant
x-alertavec proptype(success/error/info) et couleurs correspondantes - Créer le composant
x-article-cardacceptant:article="$article" - Page
/articles:@forelseavec message si vide, flash success après création - Page
/articles/create: formulaire avecold()+@errorsur chaque champ - Utiliser
@authpour afficher le bouton "Créer" et@canpour "Modifier/Supprimer"
Astuce : utilise
session('success') dans le layout pour afficher automatiquement les messages flash sur toutes les pages.
Structure attendue
resources/views/
├── layouts/
│ └── app.blade.php # Layout principal
├── components/
│ ├── alert.blade.php # Composant alerte
│ ├── article-card.blade.php # Carte article
│ └── nav-link.blade.php # Lien de navigation actif
├── articles/
│ ├── index.blade.php # Liste des articles
│ ├── show.blade.php # Détail
│ ├── create.blade.php # Formulaire création
│ └── edit.blade.php # Formulaire édition
└── partials/
└── navbar.blade.php # Navigation
Solution commentée
{{-- layouts/app.blade.php --}}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>@yield('title', 'Blog Laravel') — MonBlog</title>
<link rel="stylesheet" href="/css/app.css">
@stack('styles')
</head>
<body class="bg-gray-50">
@include('partials.navbar')
<main class="container mx-auto py-8 px-4">
{{-- Flash messages globaux --}}
@if(session('success'))
<x-alert type="success">{{ session('success') }}</x-alert>
@endif
@if(session('error'))
<x-alert type="error">{{ session('error') }}</x-alert>
@endif
@yield('content')
</main>
<script src="/js/app.js"></script>
@stack('scripts')
</body>
</html>
{{-- components/alert.blade.php --}}
@props(['type' => 'info', 'dismissible' => false])
@php
$styles = match($type) {
'success' => 'bg-green-50 border-green-400 text-green-800',
'error' => 'bg-red-50 border-red-400 text-red-800',
'warning' => 'bg-yellow-50 border-yellow-400 text-yellow-800',
default => 'bg-blue-50 border-blue-400 text-blue-800',
};
$icon = match($type) {
'success' => '✅',
'error' => '❌',
'warning' => '⚠️',
default => 'ℹ️',
};
@endphp
<div {{ $attributes->merge(['class' => "border-l-4 p-4 mb-4 rounded {$styles}"]) }}
role="alert" x-data x-show="true">
<div class="flex justify-between items-start">
<span>{{ $icon }} {{ $slot }}</span>
@if($dismissible)
<button @click="$el.closest('[role=alert]').remove()"
class="ml-4 font-bold opacity-70 hover:opacity-100">✕</button>
@endif
</div>
</div>
{{-- components/article-card.blade.php --}}
@props(['article'])
<div class="bg-white rounded-lg shadow p-6 hover:shadow-md transition">
<div class="flex justify-between items-start">
<h3 class="text-lg font-semibold">
<a href="{{ route('articles.show', $article) }}"
class="hover:text-red-600">
{{ $article->title }}
</a>
</h3>
<span class="text-xs px-2 py-1 rounded
{{ $article->status === 'published' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600' }}">
{{ $article->status }}
</span>
</div>
<p class="text-gray-600 mt-2 text-sm">{{ $article->excerpt }}</p>
<div class="mt-4 flex items-center justify-between text-sm text-gray-500">
<span>Par {{ $article->author->name }}</span>
<span>{{ $article->created_at->diffForHumans() }}</span>
</div>
@can('update', $article)
<div class="mt-4 flex gap-2">
<a href="{{ route('articles.edit', $article) }}"
class="text-sm text-blue-600 hover:underline">Modifier</a>
<form action="{{ route('articles.destroy', $article) }}" method="POST"
onsubmit="return confirm('Supprimer ?')">
@csrf @method('DELETE')
<button class="text-sm text-red-600 hover:underline">Supprimer</button>
</form>
</div>
@endcan
</div>
{{-- articles/index.blade.php --}}
@extends('layouts.app')
@section('title', 'Tous les articles')
@section('content')
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">Articles</h1>
@auth
<a href="{{ route('articles.create') }}"
class="btn btn-laravel">+ Nouvel article</a>
@endauth
</div>
@forelse($articles as $article)
<x-article-card :article="$article" class="mb-4" />
@empty
<x-alert type="info">
Aucun article pour le moment.
@auth Soyez le premier à <a href="{{ route('articles.create') }}">publier</a> ! @endauth
</x-alert>
@endforelse
{{-- Pagination --}}
<div class="mt-6">{{ $articles->links() }}</div>
@endsection
{{-- articles/create.blade.php --}}
@extends('layouts.app')
@section('title', 'Créer un article')
@section('content')
<div class="max-w-2xl mx-auto">
<h1 class="text-2xl font-bold mb-6">Créer un article</h1>
<form action="{{ route('articles.store') }}" method="POST" class="space-y-4">
@csrf
<div>
<label class="block text-sm font-medium mb-1">Titre *</label>
<input type="text" name="title"
value="{{ old('title') }}"
class="w-full border rounded px-3 py-2
@error('title') border-red-500 @enderror">
@error('title')
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label class="block text-sm font-medium mb-1">Contenu *</label>
<textarea name="body" rows="8"
class="w-full border rounded px-3 py-2
@error('body') border-red-500 @enderror">
{{ old('body') }}
</textarea>
@error('body')
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label class="block text-sm font-medium mb-1">Statut</label>
<select name="status" class="border rounded px-3 py-2">
@foreach(['draft' => 'Brouillon', 'published' => 'Publié'] as $value => $label)
<option value="{{ $value }}"
{{ old('status', 'draft') === $value ? 'selected' : '' }}>
{{ $label }}
</option>
@endforeach
</select>
</div>
<div class="flex gap-3">
<button type="submit" class="btn btn-laravel">Créer</button>
<a href="{{ route('articles.index') }}" class="btn">Annuler</a>
</div>
</form>
</div>
@endsection
@push('scripts')
<script>
// Confirmation avant de quitter si le formulaire est modifié
let formDirty = false;
document.querySelector('form').addEventListener('input', () => formDirty = true);
window.addEventListener('beforeunload', e => {
if (formDirty) e.preventDefault();
});
</script>
@endpush