Files
Portfolio-Game/docs/implementation-artifacts/1-4-layouts-routing-transitions-page.md
skycel ec1ae92799 🎉 Init monorepo Nuxt 4 + Laravel 12 (Story 1.1)
Setup complet de l'infrastructure projet :
- Frontend Nuxt 4 (SSR, TypeScript, i18n, Pinia, TailwindCSS)
- Backend Laravel 12 API-only avec middleware X-API-Key et CORS
- Design tokens (sky-dark, sky-accent, sky-text) et polices (Merriweather, Inter)
- Documentation planning et implementation artifacts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 02:08:56 +01:00

15 KiB

Story 1.4: Layouts, routing et transitions de page

Status: ready-for-dev

Story

As a visiteur, I want une navigation fluide entre les pages avec des transitions immersives, so that l'expérience ressemble à un changement de zone, pas à un rechargement.

Acceptance Criteria

  1. Given la structure de pages Nuxt 4 (app/pages/) When le visiteur navigue entre les pages Then les transitions de page sont animées (fade + slide) via pageTransition dans nuxt.config.ts
  2. And la navigation utilise <NuxtLink> pour l'hydration SPA (pas de rechargement)
  3. And le layout par défaut (default.vue) inclut le header avec barre de progression (placeholder) et sélecteur de langue
  4. And un layout minimal.vue existe pour le mode express
  5. And le scrollBehavior est personnalisé (smooth scroll, retour position sauvegardée)
  6. And prefers-reduced-motion désactive les animations de transition via media query CSS
  7. And une page 404 (error.vue) bilingue est en place
  8. And les meta tags SEO dynamiques fonctionnent via useHead() et useSeoMeta()
  9. And le favicon est configuré

Tasks / Subtasks

  • Task 1: Structure des pages Nuxt 4 (AC: #1, #2)

    • Créer la structure frontend/app/pages/ avec les pages de base :
      • index.vue (landing page - placeholder)
      • projets/index.vue (liste projets - placeholder)
      • projets/[slug].vue (détail projet - placeholder)
      • competences.vue (skills - placeholder)
      • temoignages.vue (testimonials - placeholder)
      • parcours.vue (journey - placeholder)
      • contact.vue (contact - placeholder)
      • resume.vue (mode express - placeholder)
    • Vérifier que le routing fonctionne avec les URLs localisées (Story 1.3)
  • Task 2: Layout default.vue (AC: #3)

    • Créer frontend/app/layouts/default.vue
    • Inclure le composant AppHeader (à créer)
    • Inclure le slot <slot /> pour le contenu de page
    • Inclure le composant AppFooter (à créer)
    • Ajouter le wrapper pour les transitions de page
  • Task 3: Composant AppHeader (AC: #3)

    • Créer frontend/app/components/layout/AppHeader.vue
    • Navigation principale avec liens localisés (localePath())
    • Placeholder pour la barre de progression (implémentée en Epic 3)
    • Intégrer le LanguageSwitcher (Story 1.3)
    • Logo/nom du site cliquable vers accueil
    • Version mobile : hamburger menu ou navigation adaptée
    • Sticky header avec fond semi-transparent sur scroll
  • Task 4: Composant AppFooter (AC: #3)

    • Créer frontend/app/components/layout/AppFooter.vue
    • Liens sociaux (GitHub, LinkedIn, etc.) - configurables via runtimeConfig
    • Copyright avec année dynamique
    • Liens secondaires (mentions légales si nécessaire)
    • Style cohérent avec sky-dark / sky-text
  • Task 5: Layout minimal.vue (AC: #4)

    • Créer frontend/app/layouts/minimal.vue
    • Header simplifié (logo + retour vers aventure)
    • Pas de barre de progression
    • Footer minimaliste
    • Utilisé pour /resume et potentiellement d'autres pages express
  • Task 6: Transitions de page (AC: #1, #6)

    • Configurer pageTransition dans nuxt.config.ts :
      app: {
        pageTransition: { name: 'page', mode: 'out-in' }
      }
      
    • Créer les styles CSS pour la transition page dans assets/css/transitions.css
    • Animation : fade-in/out + léger slide vertical (effet "changement de zone")
    • Durée : 300-400ms
    • Respecter prefers-reduced-motion : transition instantanée si activé
  • Task 7: CSS des transitions (AC: #1, #6)

    • Créer frontend/app/assets/css/transitions.css
    • Classes .page-enter-active, .page-leave-active
    • Classes .page-enter-from, .page-leave-to
    • Media query @media (prefers-reduced-motion: reduce) pour désactiver
    • Importer dans nuxt.config.ts via css: ['~/assets/css/transitions.css']
  • Task 8: Scroll behavior personnalisé (AC: #5)

    • Créer frontend/app/router.options.ts pour personnaliser le router
    • scrollBehavior : smooth scroll vers le haut pour nouvelle page
    • Sauvegarder et restaurer la position pour navigation back/forward
    • Gestion des ancres (#section) avec smooth scroll
  • Task 9: Page d'erreur 404 (AC: #7)

    • Créer frontend/app/error.vue
    • Message d'erreur bilingue via $t('error.404')
    • Style immersif cohérent avec le thème (le narrateur pourrait commenter)
    • Bouton retour vers l'accueil (localePath('/'))
    • Gérer différents codes d'erreur (404, 500, etc.)
  • Task 10: Meta tags SEO dynamiques (AC: #8)

    • Créer composable frontend/app/composables/useSeo.ts
    • Méthode setPageMeta({ title, description, image }) utilisant useHead() et useSeoMeta()
    • Inclure Open Graph tags (og:title, og:description, og:image, og:url)
    • Inclure Twitter Card tags
    • Utiliser dans chaque page avec des valeurs traduites
  • Task 11: Favicon et assets statiques (AC: #9)

    • Ajouter favicon dans frontend/public/favicon.ico
    • Ajouter favicon PNG 192x192 et 512x512 pour PWA
    • Configurer dans nuxt.config.ts via app.head.link
    • Optionnel : apple-touch-icon pour iOS
  • Task 12: Validation finale (AC: tous)

    • Navigation entre toutes les pages sans rechargement
    • Transitions visibles et fluides
    • prefers-reduced-motion respecté (tester dans DevTools)
    • Header sticky avec langue switcher fonctionnel
    • Layout minimal sur /resume
    • Page 404 accessible via URL invalide
    • Meta tags visibles dans le code source HTML
    • Favicon affiché dans l'onglet du navigateur

Dev Notes

Structure des layouts et pages

frontend/app/
├── layouts/
│   ├── default.vue          # Layout principal (header, footer, transitions)
│   └── minimal.vue          # Layout simplifié (mode express)
├── pages/
│   ├── index.vue            # Landing page
│   ├── projets/
│   │   ├── index.vue        # Liste des projets
│   │   └── [slug].vue       # Détail projet
│   ├── competences.vue      # Page compétences
│   ├── temoignages.vue      # Page témoignages
│   ├── parcours.vue         # Page parcours
│   ├── contact.vue          # Page contact
│   └── resume.vue           # Mode express (layout minimal)
├── components/
│   └── layout/
│       ├── AppHeader.vue    # Header avec navigation
│       └── AppFooter.vue    # Footer
├── error.vue                # Page d'erreur globale
└── assets/
    └── css/
        └── transitions.css  # Styles des transitions

Configuration nuxt.config.ts pour les transitions

// frontend/nuxt.config.ts
export default defineNuxtConfig({
  // ... autres config

  app: {
    head: {
      link: [
        { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
        { rel: 'icon', type: 'image/png', sizes: '192x192', href: '/favicon-192.png' },
        { rel: 'apple-touch-icon', href: '/apple-touch-icon.png' },
      ],
    },
    pageTransition: {
      name: 'page',
      mode: 'out-in',
    },
    layoutTransition: {
      name: 'layout',
      mode: 'out-in',
    },
  },

  css: [
    '~/assets/css/transitions.css',
  ],
})

CSS des transitions de page

/* frontend/app/assets/css/transitions.css */

/* Transition de page - effet "changement de zone" */
.page-enter-active,
.page-leave-active {
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.page-enter-from {
  opacity: 0;
  transform: translateY(10px);
}

.page-leave-to {
  opacity: 0;
  transform: translateY(-10px);
}

/* Transition de layout */
.layout-enter-active,
.layout-leave-active {
  transition: opacity 0.2s ease;
}

.layout-enter-from,
.layout-leave-to {
  opacity: 0;
}

/* Respect de prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
  .page-enter-active,
  .page-leave-active,
  .layout-enter-active,
  .layout-leave-active {
    transition: none;
  }

  .page-enter-from,
  .page-leave-to,
  .layout-enter-from,
  .layout-leave-to {
    opacity: 1;
    transform: none;
  }
}

Router options pour scroll behavior

// frontend/app/router.options.ts
import type { RouterConfig } from '@nuxt/schema'

export default <RouterConfig>{
  scrollBehavior(to, from, savedPosition) {
    // Si on revient en arrière, restaurer la position
    if (savedPosition) {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(savedPosition)
        }, 350) // Attendre la fin de la transition
      })
    }

    // Si on a une ancre, scroll vers l'ancre
    if (to.hash) {
      return {
        el: to.hash,
        behavior: 'smooth',
        top: 80, // Offset pour le header sticky
      }
    }

    // Sinon, scroll en haut
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve({ top: 0, behavior: 'smooth' })
      }, 350)
    })
  },
}

Layout default.vue

<!-- frontend/app/layouts/default.vue -->
<template>
  <div class="min-h-screen bg-sky-dark text-sky-text flex flex-col">
    <AppHeader />

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

    <AppFooter />
  </div>
</template>

<script setup lang="ts">
// Layout par défaut avec header, contenu, footer
</script>

Layout minimal.vue

<!-- frontend/app/layouts/minimal.vue -->
<template>
  <div class="min-h-screen bg-sky-dark text-sky-text flex flex-col">
    <header class="p-4 flex justify-between items-center">
      <NuxtLink :to="localePath('/')" class="text-sky-accent font-ui font-bold">
        Skycel
      </NuxtLink>
      <NuxtLink :to="localePath('/')" class="text-sky-text/70 hover:text-sky-accent text-sm">
        {{ $t('common.back_to_adventure') }}
      </NuxtLink>
    </header>

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

    <footer class="p-4 text-center text-sky-text/50 text-sm">
      © {{ new Date().getFullYear() }} Célian
    </footer>
  </div>
</template>

<script setup lang="ts">
const localePath = useLocalePath()
</script>

Composable useSeo

// frontend/app/composables/useSeo.ts
interface SeoOptions {
  title: string
  description?: string
  image?: string
  url?: string
}

export const useSeo = () => {
  const config = useRuntimeConfig()
  const route = useRoute()
  const { locale } = useI18n()

  const setPageMeta = (options: SeoOptions) => {
    const fullUrl = `${config.public.siteUrl}${route.fullPath}`
    const imageUrl = options.image || `${config.public.siteUrl}/og-image.jpg`

    useHead({
      title: options.title,
      htmlAttrs: {
        lang: locale.value,
      },
    })

    useSeoMeta({
      title: options.title,
      description: options.description,
      ogTitle: options.title,
      ogDescription: options.description,
      ogImage: imageUrl,
      ogUrl: options.url || fullUrl,
      ogLocale: locale.value === 'fr' ? 'fr_FR' : 'en_US',
      twitterCard: 'summary_large_image',
      twitterTitle: options.title,
      twitterDescription: options.description,
      twitterImage: imageUrl,
    })
  }

  return { setPageMeta }
}

Page d'erreur

<!-- frontend/app/error.vue -->
<template>
  <div class="min-h-screen bg-sky-dark text-sky-text flex flex-col items-center justify-center p-8">
    <h1 class="text-6xl font-bold text-sky-accent mb-4">
      {{ error?.statusCode || 500 }}
    </h1>

    <p class="text-xl font-narrative mb-8 text-center max-w-md">
      {{ errorMessage }}
    </p>

    <NuxtLink
      :to="localePath('/')"
      class="px-6 py-3 bg-sky-accent text-sky-dark font-ui font-semibold rounded-lg hover:bg-sky-accent-hover transition-colors"
    >
      {{ $t('common.back_home') }}
    </NuxtLink>
  </div>
</template>

<script setup lang="ts">
const props = defineProps<{
  error: {
    statusCode: number
    message: string
  }
}>()

const { t } = useI18n()
const localePath = useLocalePath()

const errorMessage = computed(() => {
  if (props.error?.statusCode === 404) {
    return t('error.404')
  }
  return t('error.generic')
})
</script>

Traductions à ajouter (i18n)

// Ajouter dans frontend/i18n/fr.json
{
  "common": {
    "back_home": "Retour à l'accueil",
    "back_to_adventure": "Retour à l'aventure"
  },
  "error": {
    "404": "Oups ! Cette page semble s'être perdue dans les méandres du code...",
    "generic": "Une erreur inattendue s'est produite. Le Bug enquête..."
  }
}

Dépendances avec Stories précédentes

Cette story DÉPEND de :

  • Story 1.1 : Nuxt 4 initialisé avec TailwindCSS et design tokens
  • Story 1.3 : Système i18n configuré, LanguageSwitcher créé

Cette story PRÉPARE pour :

  • Story 1.5 : Landing page utilisera le layout default
  • Story 1.6 : Store Pinia intégrera la barre de progression dans AppHeader
  • Story 1.7 : Page résumé utilisera le layout minimal
  • Epic 2-4 : Toutes les pages utiliseront ces layouts

Project Structure Notes

Fichiers à créer :

frontend/app/
├── layouts/
│   ├── default.vue              # CRÉER
│   └── minimal.vue              # CRÉER
├── pages/
│   ├── index.vue                # CRÉER (placeholder)
│   ├── projets/
│   │   ├── index.vue            # CRÉER (placeholder)
│   │   └── [slug].vue           # CRÉER (placeholder)
│   ├── competences.vue          # CRÉER (placeholder)
│   ├── temoignages.vue          # CRÉER (placeholder)
│   ├── parcours.vue             # CRÉER (placeholder)
│   ├── contact.vue              # CRÉER (placeholder)
│   └── resume.vue               # CRÉER (placeholder)
├── components/
│   └── layout/
│       ├── AppHeader.vue        # CRÉER
│       └── AppFooter.vue        # CRÉER
├── composables/
│   └── useSeo.ts                # CRÉER
├── error.vue                    # CRÉER
├── router.options.ts            # CRÉER
└── assets/
    └── css/
        └── transitions.css      # CRÉER

Fichiers à modifier :

frontend/
├── nuxt.config.ts               # MODIFIER (transitions, css, head)
├── i18n/fr.json                 # MODIFIER (ajouter traductions)
└── i18n/en.json                 # MODIFIER (ajouter traductions)

References

  • [Source: docs/planning-artifacts/architecture.md#Frontend-Architecture]
  • [Source: docs/planning-artifacts/architecture.md#Transitions-et-animations]
  • [Source: docs/planning-artifacts/ux-design-specification.md#Navigation]
  • [Source: docs/planning-artifacts/epics.md#Story-1.4]
  • [Source: docs/prd-gamification.md#FR2]
  • [Source: docs/prd-gamification.md#NFR6]

Technical Requirements

Requirement Value Source
Page transitions fade + slide FR2
Reduced motion Required NFR6, WCAG AA
Sticky header Yes UX Design
SEO meta tags Required NFR5
Layout switching default / minimal Architecture

Dev Agent Record

Agent Model Used

{{agent_model_name_version}}

Debug Log References

Completion Notes List

Change Log

Date Change Author
2026-02-03 Story créée avec contexte complet SM Agent

File List