Aller au contenu

La page publique : appliquer sa direction artistique

Temps 1 · séance 5 · ~60 min

Les slides du chapitre (PDF)PDF

  • Déclarer les couleurs et les polices de votre planche comme jetons de Tailwind dans @theme.
  • Charger une police avec bunny() dans vite.config.ts.
  • Construire l'accueil, la grille des projets, la fiche et l'à-propos avec une carte de projet partagée.
  • Écrire les classes pour le téléphone d'abord, puis pour les écrans plus larges.
  • Donner ses dimensions à chaque image et retarder le chargement de celles du bas de page.

1. De la planche aux jetons

Ouvrez votre planche de direction artistique, remplie au chapitre 1.09. Elle fixe une palette, deux polices au plus et une échelle de tailles. Dans le code, chacun de ces choix doit vivre à un seul endroit, pour qu'un changement de couleur se fasse en une ligne.

Tailwind 4 lit ces choix dans resources/css/app.css, dans un bloc @theme. Chaque variable déclarée dans ce bloc est un jeton : une valeur nommée de votre direction artistique, dont Tailwind tire des classes. Ajoutez ce bloc juste après la ligne @plugin '@tailwindcss/typography'; du chapitre 1.15 :

@theme {
    --color-brand: oklch(0.55 0.2 265);
    --font-display: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif;
}

Ce sont les valeurs du portfolio de l'exemple : un indigo, et la police Space Grotesk pour les titres. Remplacez-les par celles de votre planche. Une couleur s'écrit dans n'importe quelle notation de CSS, #b83a22 aussi bien que oklch(…).

Le nom de la variable décide du nom des classes. Une variable --color-xxx crée text-xxx, bg-xxx, border-xxx et les autres classes de couleur. Une variable --font-xxx crée font-xxx. Le bloc ci-dessus donne donc text-brand, bg-brand, hover:text-brand et font-display. Dans le labo, le navigateur calcule oklch(0.55 0.2 265) pour la couleur d'un élément text-brand, et "Space Grotesk", ui-sans-serif, system-ui, sans-serif pour la police d'un titre font-display.

Un nom que le kit n'utilise pas

Votre planche parle d'une couleur d'accent, mais n'appelez pas le jeton accent. Le kit déclare déjà cette couleur plus bas dans app.css, dans son bloc @theme inline :

--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);

Le kit se sert de ce gris clair pour le survol des boutons ghost et outline et pour la barre latérale. Redéfinir accent repeindrait tout le back-office. Les noms primary, secondary, muted, destructive, border et ring sont pris de la même façon.

Donnez à vos jetons un nom que le kit n'utilise pas, comme brand.

Dans app.css, le bloc @theme déclare les jetons --color-brand et --font-display : le premier crée les classes text-brand, bg-brand et hover:text-brand, le second font-display. Plus bas, le bloc @theme inline du kit déclare déjà --color-accent, un nom pris, et le redéfinir repeindrait tout le back-office

Une couleur fixe, même en mode sombre

Un jeton déclaré dans @theme garde sa valeur en mode sombre : --color-brand reste le même indigo sur fond noir. Le kit procède autrement pour ses propres couleurs. Il déclare --accent deux fois, une fois sous :root et une fois sous .dark, puis il relie --color-accent à cette variable dans @theme inline. Si votre planche prévoit une autre couleur sur fond sombre, recopiez cette forme pour brand. Sinon, le bloc @theme ci-dessus suffit.

2. La police

Le kit charge sa police dans vite.config.ts, avec la fonction bunny() du plugin Laravel. Ajoutez la vôtre dans la liste fonts, avec les graisses notées sur votre planche :

fonts: [
    bunny('Instrument Sans', {
        weights: [400, 500, 600],
    }),
    bunny('Space Grotesk', {
        weights: [500, 700],
    }),
],

Quand vous enregistrez ce fichier, Vite redémarre tout seul. Le terminal de composer run dev affiche vite.config.ts changed, restarting server.. puis server restarted.. La police est chargée, et la variable --font-display de @theme lui associe la classe font-display.

bunny() ne demande pas au navigateur du visiteur de contacter Bunny Fonts. Le plugin télécharge les fichiers de police, puis npm run build les copie dans le site compilé. Le HTML du portfolio de l'exemple, compilé, les sert lui-même :

/build/assets/space-grotesk-500-normal-lFbtlQH6.woff2
/build/assets/space-grotesk-700-normal-RjhwGPKo.woff2

Aucune donnée du visiteur ne part donc vers un service de polices. Les mentions légales du chapitre 1.21 n'ont rien à en dire.

bunny('Space Grotesk') dans vite.config.ts : le plugin télécharge les fichiers de police chez Bunny Fonts, puis npm run build les copie dans le site compilé, sous /build/assets/. Le site sert lui-même la police au navigateur du visiteur, et aucune donnée du visiteur ne part vers Bunny Fonts

Au démarrage de Vite, le terminal affiche aussi [laravel:fonts] Optimized font fallbacks require the optional "fontaine" package. Cet avertissement n'a aucun effet sur votre site.

Astuce

Pour remplacer la police du kit au lieu d'en ajouter une, changez bunny('Instrument Sans', …) dans vite.config.ts. Le nom de la famille figure ensuite deux fois dans app.css : --font-sans dans le bloc @theme inline, et --font-sans dans le bloc @layer utilities qui vise body et html. Remplacez-le aux deux endroits.

3. L'accueil et son contrôleur

L'accueil répond à trois questions du lecteur pressé du chapitre 1.01 : qui vous êtes, ce que vous avez construit, comment vous écrire. Il a donc besoin de vos projets publiés, et plus seulement de votre nom.

Depuis le chapitre 1.04, la route / appelle une fonction anonyme. Remplacez-la par un contrôleur. Créez le fichier app/Http/Controllers/HomeController.php :

<?php

namespace App\Http\Controllers;

use App\Models\Project;
use Inertia\Inertia;
use Inertia\Response;

class HomeController extends Controller
{
    public function __invoke(): Response
    {
        $projects = Project::published()
            ->orderBy('position')
            ->take(3)
            ->get()
            ->map(fn (Project $project) => [
                'title' => $project->title,
                'slug' => $project->slug,
                'summary' => $project->summary,
                'cover_url' => $project->cover_url,
            ]);

        return Inertia::render('Home', [
            'author' => 'Nora Benali',
            'projects' => $projects,
        ]);
    }
}

Écrivez votre nom à la place de celui de l'exemple.

Ce contrôleur n'a qu'une méthode, __invoke(). Un contrôleur de ce genre s'appelle invocable : la route le nomme sans préciser de méthode, et Laravel appelle __invoke(). La requête reprend ce que vous connaissez. published() est le scope du chapitre 1.16, take(3) garde les trois premiers projets, et cover_url est l'adresse de l'image calculée au chapitre 1.14. Les props restent explicites, comme au chapitre 1.07.

Dans routes/web.php, remplacez la route / entière par celle-ci, avec use App\Http\Controllers\HomeController; en tête du fichier :

Route::get('/', HomeController::class)->name('home');

Le nom home ne change pas, et le lien de votre nom dans le layout public mène toujours à l'accueil.

4. Une carte par projet

L'accueil et la liste des projets montrent les mêmes cartes. Écrivez-les une fois, dans un composant. Créez resources/js/components/ProjectCard.vue :

<script setup>
import { Link } from '@inertiajs/vue3';
import { Badge } from '@/components/ui/badge';
import { show } from '@/routes/projects';

defineProps({
    project: Object,
});
</script>

<template>
    <article class="overflow-hidden rounded-lg border bg-card">
        <img
            v-if="project.cover_url"
            :src="project.cover_url"
            alt=""
            width="1200"
            height="675"
            loading="lazy"
            class="aspect-video w-full object-cover"
        />
        <div class="space-y-2 p-4">
            <h3 class="font-display text-lg font-bold">
                <Link :href="show(project)" class="hover:text-brand">
                    {{ project.title }}
                </Link>
            </h3>
            <p class="text-sm text-muted-foreground">{{ project.summary }}</p>
            <ul v-if="project.tags" class="flex flex-wrap gap-2">
                <li v-for="tag in project.tags" :key="tag">
                    <Badge variant="secondary">{{ tag }}</Badge>
                </li>
            </ul>
        </div>
    </article>
</template>

La carte reçoit un projet en prop et ne connaît rien d'autre. L'accueil n'envoie pas d'étiquettes, la liste en envoie : v-if="project.tags" n'affiche la liste des badges que si elle existe.

HomeController charge les trois premiers projets publiés avec Project::published()->orderBy('position')->take(3) et les passe à Inertia::render('Home') dans les props author et projects. Home.vue répète avec v-for un composant ProjectCard par projet, et chaque carte reçoit un projet en prop

Trois attributs de l'image comptent pour le visiteur.

  • width et height donnent au navigateur le rapport de l'image avant qu'elle arrive. Il réserve la place, et le texte ne saute pas vers le bas au moment où l'image s'affiche. Ces valeurs donnent le rapport, 1200 sur 675 pour les images de l'exemple, et les classes w-full aspect-video règlent la taille affichée. Dans le labo, sur un écran de 800 px, la carte affiche l'image en 308 × 173 pixels.
  • loading="lazy" demande au navigateur de ne charger l'image qu'au moment où elle approche de la partie visible de la page. Une liste de dix projets ne charge plus dix images à l'ouverture.
  • alt="" dit qu'une image est décorative. Le titre du projet suit l'image et dit déjà ce qu'elle montre. Le chapitre 1.21 revient sur ce choix.

5. Les pages

L'accueil

Remplacez le contenu de resources/js/pages/Home.vue :

<script setup>
import { Head, Link } from '@inertiajs/vue3';
import ProjectCard from '@/components/ProjectCard.vue';
import { Button } from '@/components/ui/button';
import { contact } from '@/routes';
import { index as projectsIndex } from '@/routes/projects';

defineProps({
    author: String,
    projects: Array,
});
</script>

<template>
    <Head title="Accueil" />

    <section class="space-y-4 py-12 md:py-20">
        <p class="font-medium text-brand">Développeuse web</p>
        <h1 class="font-display text-4xl font-bold md:text-6xl">
            Bonjour, je suis {{ author }}
        </h1>
        <p class="max-w-prose text-lg text-muted-foreground">
            Je construis des sites et des applications avec Laravel et Vue.js.
            Voici mes projets récents.
        </p>
    </section>

    <section class="space-y-6">
        <h2 class="font-display text-2xl font-bold">Projets récents</h2>
        <div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
            <ProjectCard
                v-for="project in projects"
                :key="project.slug"
                :project="project"
            />
        </div>
        <Link :href="projectsIndex()" class="inline-block text-brand underline">
            Tous les projets
        </Link>
    </section>

    <section class="my-16 rounded-lg bg-brand p-8 text-white">
        <h2 class="font-display text-2xl font-bold">Un projet en tête ?</h2>
        <p class="mt-2">Écrivez-moi, je réponds sous deux jours.</p>
        <Button as-child variant="secondary" class="mt-4">
            <Link :href="contact()">Me contacter</Link>
        </Button>
    </section>
</template>

L'accueil du portfolio de l'exemple : le nom en grand titre, deux cartes de projet avec image et résumé, et le bloc indigo Un projet en tête ? avec le bouton Me contacter

Les textes sont ceux de l'exemple : écrivez les vôtres, ceux de votre cahier des charges. La page a trois sections, une par question du lecteur. La dernière est un appel à l'action, un bloc qui propose au lecteur l'étape suivante, ici vous écrire.

<Button as-child> donne au lien <Link> l'apparence d'un bouton du kit, sans ajouter de balise button autour. Le blanc sur bg-brand est une nouvelle paire de couleurs. Mesurez son contraste au chapitre 1.21.

La liste et la fiche

Dans resources/js/pages/projects/Index.vue, remplacez la liste brute du chapitre 1.08 par la même grille :

<h1 class="py-8 font-display text-3xl font-bold md:text-4xl">
    Mes projets
</h1>

<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
    <ProjectCard
        v-for="project in projects"
        :key="project.slug"
        :project="project"
    />
</div>

Importez ProjectCard dans le script, comme dans l'accueil. L'import de show n'y sert plus : la carte s'en charge.

Dans la fiche, resources/js/pages/projects/Show.vue, deux éléments changent. L'image de couverture est en haut de la page, visible dès l'ouverture : elle garde width et height, mais pas loading="lazy", qui retarderait l'image la plus importante. Le contenu riche du chapitre 1.15 reçoit la classe prose :

<img
    v-if="project.cover_url"
    :src="project.cover_url"
    alt=""
    width="1200"
    height="675"
    class="aspect-video w-full rounded-lg object-cover"
/>

<div
    class="prose max-w-none dark:prose-invert prose-a:text-brand"
    v-html="project.content"
></div>

prose vient du plugin @tailwindcss/typography, installé au chapitre 1.15. Elle met en forme les titres, paragraphes, listes et liens d'un contenu que vous n'écrivez pas balise par balise. max-w-none retire la largeur maximale que prose impose. dark:prose-invert passe le texte en clair en mode sombre. prose-a:text-brand colore les liens du contenu avec votre jeton : les modificateurs de prose acceptent les couleurs de @theme.

L'à-propos

La page About.vue est faite de paragraphes et d'une liste. La même classe lui évite de styler chaque <p> et chaque <li> :

<article class="prose max-w-none py-8 dark:prose-invert">
    <h1 class="font-display">À propos</h1>
    <p>
        Étudiante en développement web à l'IFOSUP Wavre, je reprends des
        études après dix ans dans la logistique.
    </p>
    <h2 class="font-display">Ce que je sais faire</h2>
    <ul>
        <li>Des applications Laravel avec Vue.js et Inertia.</li>
        <li>Des tests automatisés avec Pest.</li>
    </ul>
</article>

Le texte est celui de l'exemple : gardez celui de votre devoir. font-display sur les titres remplace la police que prose leur donnerait.

6. Mobile d'abord

Ouvrez votre portfolio sur un téléphone, ou dans le mode appareil des outils de développement (Ctrl+Maj+M dans Chrome), en 375 pixels de large. Le menu de PublicLayout.vue du chapitre 1.08 est sur la même ligne que votre nom, et il déborde.

Tailwind se lit mobile d'abord. Une classe sans préfixe s'applique à toutes les largeurs, téléphone compris. Une classe préfixée par sm:, md: ou lg: s'ajoute à partir d'une largeur d'écran : 640, 768 et 1 024 pixels. Vous écrivez donc la mise en page du téléphone d'abord, puis ce qui change sur un écran plus large. La grille des projets suit cette règle : une colonne par défaut, deux à partir de sm:, trois à partir de lg:.

Voici resources/js/layouts/PublicLayout.vue refait sur ce principe :

<script setup>
import { Link } from '@inertiajs/vue3';
import { Toaster } from '@/components/ui/sonner';
import { about, contact, home } from '@/routes';
import { index as projectsIndex } from '@/routes/projects';
</script>

<template>
    <div class="mx-auto flex min-h-screen max-w-5xl flex-col px-4">
        <header
            class="flex flex-col gap-3 py-6 sm:flex-row sm:items-center sm:justify-between"
        >
            <Link :href="home()" class="font-display text-xl font-bold">
                Nora Benali
            </Link>
            <nav class="flex gap-5 text-sm font-medium">
                <Link :href="projectsIndex()" class="hover:text-brand">
                    Projets
                </Link>
                <Link :href="about()" class="hover:text-brand">À propos</Link>
                <Link :href="contact()" class="hover:text-brand">Contact</Link>
            </nav>
        </header>

        <main class="flex-1">
            <slot />
        </main>

        <footer
            class="flex flex-col gap-2 border-t py-6 text-sm text-muted-foreground sm:flex-row sm:justify-between"
        >
            <p>© Nora Benali</p>
        </footer>

        <Toaster />
    </div>
</template>

Gardez votre nom dans l'en-tête et dans le pied de page, à la place de celui de l'exemple. Le pied de page recevra le lien vers les mentions légales au chapitre 1.21.

Sur un téléphone, l'en-tête empile votre nom et le menu en colonne, avec flex flex-col. À partir de sm:, sm:flex-row les remet sur une ligne, et sm:justify-between les écarte. min-h-screen sur le conteneur et flex-1 sur <main> poussent le pied de page en bas de l'écran, même sur une page courte.

Le même accueil en 375 pixels de large : le nom au-dessus du menu, les cartes de projet l'une sous l'autre, sans défilement horizontal

Les classes de l'en-tête, flex flex-col gap-3 py-6 sm:flex-row, lues à deux largeurs : sur un téléphone, flex-col empile votre nom au-dessus du menu ; à partir de 640 pixels, sm:flex-row les met sur une ligne

<Toaster /> est le composant du kit qui affiche les messages flash, vu au chapitre 1.12 dans le back-office. Le layout public ne l'avait pas. Le formulaire de contact du chapitre 1.20 en a besoin pour confirmer l'envoi.

Pour vérifier qu'aucune page ne déborde, tapez cette ligne dans la console des outils de développement, en mode appareil à 375 pixels :

document.documentElement.scrollWidth

La réponse doit être 375. Un nombre plus grand signale un élément plus large que l'écran, et la page défile de côté.

7. Le mode sombre

Le kit gère déjà le mode sombre. resources/views/app.blade.php pose la classe dark sur la balise <html> d'après le cookie appearance, réglé dans la page Réglages › Apparence du back-office. Sans ce cookie, il suit le réglage du système du visiteur. app.css redéfinit alors les couleurs du kit sous .dark.

Vos pages suivent sans rien ajouter tant qu'elles utilisent les classes du kit, comme bg-card, text-muted-foreground et border, et dark:prose-invert pour le contenu riche. Le jeton brand, lui, ne change pas.

Même si votre planche ne prévoit pas de mode sombre, un visiteur dont le système est en mode sombre le reçoit. Vérifiez donc vos pages dans les deux modes. Dans les outils de développement de Chrome, ouvrez le panneau Rendering et choisissez prefers-color-scheme: dark. Un texte text-brand trop sombre sur le fond noir se voit tout de suite.

8. Ce qui casse

Les images ne s'affichent plus

cover_url est construit avec APP_URL, la variable de .env réglée au chapitre 1.04. Si cette adresse n'est pas celle que vous ouvrez dans le navigateur, chaque image pointe vers un serveur qui ne répond pas. Avec Herd ou Laragon, APP_URL=http://portfolio.test.

Une modification de .env sans effet

composer run dev lit .env à son démarrage, et transmet ses valeurs aux processus qu'il lance. Le serveur affiche bien Environment modified. Restarting server.. quand vous enregistrez .env, mais les anciennes valeurs restent en place. Arrêtez composer run dev avec Ctrl+C, puis relancez-le.

9. Pratique : votre direction artistique sur le site (45 min)

Temps annoncé : 45 minutes.

Appliquez votre planche aux pages publiques. Les ressources sont celles du chapitre 1.09 : Fonts In Use, Typewolf, Bunny Fonts, Coolors et le vérificateur de WebAIM. La page « Theme variables » de la documentation de Tailwind liste les familles de jetons, couleurs, polices et tailles comprises.

  • Les couleurs de votre planche sont déclarées dans @theme, sous des noms que le kit n'utilise pas.
  • Vos polices, deux au plus, sont chargées par bunny() dans vite.config.ts et déclarées dans @theme.
  • L'accueil dit qui vous êtes, montre trois projets publiés et mène au contact.
  • L'accueil et la liste utilisent ProjectCard.vue.
  • La fiche affiche le contenu riche avec prose, et l'à-propos aussi.
  • Chaque image a width et height, et chaque image hors du haut de page a loading="lazy".
  • Les tailles de vos titres suivent l'échelle de votre planche.
  • À 375 pixels de large, document.documentElement.scrollWidth vaut 375 sur chaque page publique.
  • Vos pages restent lisibles avec prefers-color-scheme: dark.
  • php artisan test reste vert.

Ce que votre portfolio contient maintenant

Une partie publique à vos couleurs et à vos polices : un accueil qui présente l'auteur, ses trois projets et un appel au contact, une grille de projets, des fiches et un à-propos mis en forme. Le site se lit sur un téléphone, et ses images se chargent sans faire sauter la page.

À retenir

  • Une variable --color-brand ou --font-display déclarée dans @theme crée les classes text-brand, bg-brand ou font-display.
  • Les noms accent, primary ou muted appartiennent déjà au kit, donc vos jetons portent un nom à vous.
  • bunny() dans vite.config.ts charge une police, et npm run build copie ses fichiers dans le site compilé.
  • Une classe sans préfixe vise le téléphone, et sm:, md: ou lg: ajoutent ce qui change sur un écran plus large.
  • Chaque image porte width et height, et loading="lazy" sauf celle du haut de page.