Cours Next.js · Module 3: Styles et optimisation visuelle

Icônes, favicon et métadonnées visuelles dans Next.js 16

16 min de lecture
Dans cette leçon14

À Metropolis Quantum, l'identité visuelle de la ville est soigneusement conçue pour que chaque élément - des logos sur les bâtiments officiels à la signalétique des quartiers, en passant par les applications de la ville - crée une image cohérente. Cette identité visuelle aide habitants et visiteurs à reconnaître les éléments officiels de la ville, instaure la confiance et donne une image professionnelle.

Dans le monde des applications web, les icônes, le favicon et les métadonnées visuelles jouent un rôle similaire. Ils sont essentiels à la reconnaissance de ton application, influencent la première impression des utilisateurs et facilitent l'identification de ton site parmi de nombreux onglets ouverts. Next.js 16 offre de nombreux outils pour gérer ces éléments, que nous découvrirons dans ce chapitre.

L'importance des icônes et des métadonnées visuelles

Favicon et icônes d'application

Le favicon (abréviation de « favorite icon ») est une petite icône affichée dans l'onglet du navigateur, la liste des favoris et l'historique de navigation. C'est le premier élément visuel que les utilisateurs associent à ton application, il vaut donc la peine de veiller à sa qualité et à sa reconnaissabilité.

Dans les applications web modernes, il ne nous faut pas seulement un favicon de base, mais tout un jeu d'icônes de différentes tailles qui seront utilisées dans différents contextes :

  • Les icônes d'onglet de navigateur
  • Les icônes d'application sur les appareils mobiles
  • Les icônes d'application de bureau (PWA)
  • Les icônes dans les résultats de recherche
  • Les icônes de partage sur les réseaux sociaux

Métadonnées visuelles

Les métadonnées visuelles sont des informations qui déterminent comment ta page est présentée lorsqu'elle est partagée sur les réseaux sociaux, les messageries ou les résultats de recherche. Elles comprennent :

  • Le titre de la page
  • La description de la page
  • Les images/visuels d'aperçu
  • Les couleurs du thème
  • Les informations sur l'application (pour les PWA)

Configuration de base du favicon dans Next.js 16

Next.js 16 propose une manière simplifiée de configurer le favicon et les icônes grâce à la Metadata API, disponible depuis Next.js 13.2. Commençons par la configuration de base.

Placer les icônes dans le répertoire public

D'abord, place les fichiers d'icône dans le répertoire public :

1public/
2├── favicon.ico           # Klasyczny favicon (16x16 lub 32x32)
3├── icon.png              # Main icon (can be used in metadata API)
4├── apple-icon.png        # Icon for Apple devices
5└── icons/                # Directory with different icon sizes
6    ├── icon-16x16.png
7    ├── icon-32x32.png
8    ├── icon-192x192.png
9    └── icon-512x512.png

Configurer le favicon de base

La méthode la plus simple consiste à placer un fichier favicon.ico dans le répertoire public. Next.js reconnaîtra automatiquement ce fichier et l'utilisera comme favicon :

1// app/layout.tsx
2export default function RootLayout({
3  children,
4}: {
5  children: React.ReactNode;
6}) {
7  return (
8    <html lang="pl">
9      <body>{children}</body>
10    </html>
11  );
12}

Dans ce cas, tu n'as rien d'autre à faire - le navigateur récupérera automatiquement /favicon.ico.

Configuration via la Metadata API

Next.js 16 offre une configuration avancée des icônes via la Metadata API :

1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5  title: 'Metropolis Quantum',
6  description: 'Official portal of the city of the future',
7  icons: {
8    icon: '/icon.png',         // lub array: [{ url: '/icon-16x16.png', sizes: '16x16' }, ...]
9    shortcut: '/shortcut-icon.png',
10    apple: '/apple-icon.png',  // lub array: [{ url: '/apple-icon.png' }, ...]
11    other: {
12      rel: 'apple-touch-icon-precomposed',
13      url: '/apple-touch-icon-precomposed.png',
14    },
15  },
16};
17
18export default function RootLayout({
19  children,
20}: {
21  children: React.ReactNode;
22}) {
23  return (
24    <html lang="pl">
25      <body>{children}</body>
26    </html>
27  );
28}

Configuration avancée des icônes

Pour les applications professionnelles, il vaut la peine de préparer un jeu complet d'icônes de différentes tailles et formats.

Configuration étendue des icônes

1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5  icons: {
6    icon: [
7      { url: '/icons/icon-16x16.png', sizes: '16x16', type: 'image/png' },
8      { url: '/icons/icon-32x32.png', sizes: '32x32', type: 'image/png' },
9      { url: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' },
10      { url: '/icons/icon-512x512.png', sizes: '512x512', type: 'image/png' },
11    ],
12    shortcut: '/shortcut-icon.png',
13    apple: [
14      { url: '/icons/apple-icon-57x57.png', sizes: '57x57', type: 'image/png' },
15      { url: '/icons/apple-icon-72x72.png', sizes: '72x72', type: 'image/png' },
16      { url: '/icons/apple-icon-114x114.png', sizes: '114x114', type: 'image/png' },
17      { url: '/icons/apple-icon-180x180.png', sizes: '180x180', type: 'image/png' },
18    ],
19  },
20};

Résultat en HTML

La configuration ci-dessus générera les balises suivantes dans la section <head> du document HTML :

1<link rel="icon" href="/icons/icon-16x16.png" sizes="16x16" type="image/png" />
2<link rel="icon" href="/icons/icon-32x32.png" sizes="32x32" type="image/png" />
3<link rel="icon" href="/icons/icon-192x192.png" sizes="192x192" type="image/png" />
4<link rel="icon" href="/icons/icon-512x512.png" sizes="512x512" type="image/png" />
5<link rel="shortcut icon" href="/shortcut-icon.png" />
6<link rel="apple-touch-icon" href="/icons/apple-icon-57x57.png" sizes="57x57" type="image/png" />
7<link rel="apple-touch-icon" href="/icons/apple-icon-72x72.png" sizes="72x72" type="image/png" />
8<link rel="apple-touch-icon" href="/icons/apple-icon-114x114.png" sizes="114x114" type="image/png" />
9<link rel="apple-touch-icon" href="/icons/apple-icon-180x180.png" sizes="180x180" type="image/png" />

Métadonnées Open Graph et Twitter Cards

Les métadonnées Open Graph et Twitter Cards déterminent l'apparence de ta page lorsqu'elle est partagée sur les plateformes sociales. Next.js 16 offre une prise en charge intégrée de ces métadonnées.

Configurer Open Graph

1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5  title: 'Metropolis Quantum',
6  description: 'Official portal of the city of the future',
7  openGraph: {
8    title: 'Metropolis Quantum - City of the Future',
9    description: "Discover tomorrow's technology in the city of Metropolis Quantum",
10    url: 'https://metropolisquantum.pl',
11    siteName: 'Metropolis Quantum',
12    images: [
13      {
14        url: '/images/og-image.jpg', // 1200x630 px taille recommandée
15        width: 1200,
16        height: 630,
17        alt: 'Metropolis Quantum - Panorama of the city of the future',
18      },
19    ],
20    locale: 'pl_PL',
21    type: 'website',
22  },
23};

Configurer les Twitter Cards

1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5  twitter: {
6    card: 'summary_large_image',
7    title: 'Metropolis Quantum - City of the Future',
8    description: "Discover tomorrow's technology in the city of Metropolis Quantum",
9    siteId: '1467726470533754880', // ID konta Twitter
10    creator: '@MetropolisQ',
11    creatorId: '1467726470533754880',
12    images: ['/images/twitter-image.jpg'], // 800x418 px taille recommandée
13  },
14};

Tester les métadonnées Open Graph et Twitter Cards

Après le déploiement, tu peux tester tes métadonnées à l'aide de :

Métadonnées dynamiques

Dans Next.js 16, tu peux générer des métadonnées de façon dynamique en fonction du chemin, des paramètres d'URL ou de données externes.

Métadonnées dynamiques pour les sous-pages

1// app/districts/[id]/page.tsx
2import { Metadata, ResolvingMetadata } from 'next';
3
4interface Props {
5  params: Promise<{ id: string }>;
6  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
7}
8
9export async function generateMetadata(
10  { params, searchParams }: Props,
11  parent: ResolvingMetadata
12): Promise<Metadata> {
13  // Récupérer les données du quartier
14  const districtId = (await params).id;
15  const district = await getDistrictData(districtId);
16
17  // Optionally you can merge with parent metadata
18  const previousImages = (await parent).openGraph?.images || [];
19
20  return {
21    title: `${district.name} | Metropolis Quantum`,
22    description: district.description,
23    openGraph: {
24      images: [
25        {
26          url: `/images/districts/${districtId}.jpg`,
27          width: 1200,
28          height: 630,
29          alt: district.name,
30        },
31        ...previousImages,
32      ],
33    },
34    twitter: {
35      images: [`/images/districts/${districtId}-twitter.jpg`],
36    },
37  };
38}
39
40export default async function DistrictPage({ params }: Props) {
41  // Rendu de la page
42}
43
44// Function fetching district data
45async function getDistrictData(id: string) {
46  // Dans une vraie application : fetch depuis une API ou une base de données
47  return {
48    name: 'Dystrykt Kwantowy',
49    description: 'Le quartier technologiquement le plus avancé de Metropolis Quantum.',
50  };
51}

Manifeste d'application web (Web App Manifest)

Un manifeste d'application web est un fichier JSON qui fournit aux navigateurs et aux appareils mobiles des informations sur ton application. Il est crucial pour les Progressive Web Apps (PWA).

Créer le fichier manifest.json

Place le fichier manifest.json dans le répertoire public :

1{
2  "name": "Metropolis Quantum",
3  "short_name": "MetroQ",
4  "description": "Official portal of the city of the future",
5  "start_url": "/",
6  "display": "standalone",
7  "background_color": "#121212",
8  "theme_color": "#3a86ff",
9  "icons": [
10    {
11      "src": "/icons/icon-192x192.png",
12      "sizes": "192x192",
13      "type": "image/png",
14      "purpose": "any maskable"
15    },
16    {
17      "src": "/icons/icon-512x512.png",
18      "sizes": "512x512",
19      "type": "image/png"
20    }
21  ]
22}

Configurer le manifeste dans Next.js

1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5  // Inne metadane...
6
7  // Manifest pour la PWA
8  manifest: '/manifest.json',
9
10  // Or define the manifest directly:
11  /*
12  manifest: {
13    name: 'Metropolis Quantum',
14    short_name: 'MetroQ',
15    description: 'Official portal of the city of the future',
16    start_url: '/',
17    display: 'standalone',
18    background_color: '#121212',
19    theme_color: '#3a86ff',
20    icons: [
21      {
22        src: '/icons/icon-192x192.png',
23        sizes: '192x192',
24        type: 'image/png',
25        purpose: 'any maskable'
26      },
27      {
28        src: '/icons/icon-512x512.png',
29        sizes: '512x512',
30        type: 'image/png'
31      }
32    ]
33  }
34  */
35};

Autres métadonnées visuelles et fonctionnelles

Thème de couleurs

Tu définis la couleur de thème des navigateurs mobiles dans un export viewport séparé. Le champ themeColor de l'objet metadata est déprécié depuis Next.js 14 :

1// app/layout.tsx
2import type { Viewport } from 'next';
3
4export const viewport: Viewport = {
5  // Theme color for mobile browsers
6  themeColor: [
7    { media: '(prefers-color-scheme: light)', color: '#3a86ff' },
8    { media: '(prefers-color-scheme: dark)', color: '#1e429f' },
9  ],
10
11  // Lub pojedynczy kolor:
12  // themeColor: '#3a86ff',
13};

Prise en charge spécifique à Apple

1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5  // Inne metadane...
6
7  // Paramètres pour Safari sur iOS
8  appleWebApp: {
9    title: 'Metropolis Quantum', // Alternative title for iOS
10    statusBarStyle: 'black-translucent',
11    startupImage: [
12      {
13        url: '/startup/apple-startup-2048x2732.png',
14        media: '(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)',
15      },
16      {
17        url: '/startup/apple-startup-1668x2388.png',
18        media: '(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)',
19      },
20      // More sizes...
21    ],
22  },
23};

Application mobile alternative

1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5  // Inne metadane...
6
7  // Liens vers les applications mobiles
8  alternates: {
9    'android-app': 'android-app://com.metropolisquantum/https/metropolisquantum.pl',
10    'ios-app': 'ios-app://1234567890/https/metropolisquantum.pl',
11  },
12};

Générer les icônes et les métadonnées

Créer manuellement toutes les tailles d'icône peut être fastidieux. Heureusement, il existe des outils qui automatisent ce processus.

Outils de génération d'icônes

  1. Favicon Generator (https://realfavicongenerator.net/) - génère un jeu complet d'icônes à partir d'une seule image
  2. PWA Asset Generator (https://github.com/onderceylan/pwa-asset-generator) - un outil en ligne de commande pour générer les icônes de PWA

Exemple d'utilisation de PWA Asset Generator

1# Instalacja
2npm i -g pwa-asset-generator
3
4# Generowanie ikon
5npx pwa-asset-generator ./logo.svg ./public/icons --manifest ./public/manifest.json --icon-only --favicon

Génération dynamique d'images Open Graph

Il vaut parfois la peine de générer dynamiquement des images Open Graph pour différentes sous-pages. Dans Next.js 16, nous pouvons le faire à l'aide des Route Handlers ou d'un fichier opengraph-image.tsx placé à côté de la page.

Implémentation d'un Route Handler pour les images OG dynamiques

1// app/api/og/route.tsx
2import { ImageResponse } from 'next/og';
3import { NextRequest } from 'next/server';
4
5// No runtime = 'edge' export: in Next.js 16 ImageResponse runs in the default Node.js runtime,
6// and the documentation marks the Edge Runtime as deprecated
7
8export async function GET(request: NextRequest) {
9  const { searchParams } = new URL(request.url);
10
11  // Récupérer les paramètres depuis l’URL
12  const title = searchParams.get('title') || 'Metropolis Quantum';
13  const description = searchParams.get('description') || 'City of the future';
14
15  // Optionnel : récupérer des données supplémentaires depuis une base ou une API
16  // const data = await fetchData();
17
18  // Générer l’image
19  return new ImageResponse(
20    (
21      <div
22        style={{
23          display: 'flex',
24          fontSize: 40,
25          color: 'white',
26          background: 'linear-gradient(to bottom, #3a86ff, #1e429f)',
27          width: '100%',
28          height: '100%',
29          padding: '50px 200px',
30          textAlign: 'center',
31          justifyContent: 'center',
32          alignItems: 'center',
33          flexDirection: 'column',
34        }}
35      >
36        <img
37          src={`${request.nextUrl.origin}/logo.png`}
38          alt="Logo"
39          width="200"
40          height="200"
41        />
42        <h1 style={{ fontSize: 70 }}>{title}</h1>
43        <p style={{ fontSize: 40 }}>{description}</p>
44      </div>
45    ),
46    {
47      width: 1200,
48      height: 630,
49    }
50  );
51}

Utiliser une image OG dynamique dans les métadonnées

1// app/districts/[id]/page.tsx
2import { Metadata } from 'next';
3
4interface Props {
5  params: Promise<{ id: string }>;
6}
7
8export async function generateMetadata({ params }: Props): Promise<Metadata> {
9  const district = await getDistrictData((await params).id);
10
11  const ogImageUrl = new URL(`/api/og`, 'https://metropolisquantum.pl');
12  ogImageUrl.searchParams.append('title', district.name);
13  ogImageUrl.searchParams.append('description', district.description);
14
15  return {
16    title: `${district.name} | Metropolis Quantum`,
17    description: district.description,
18    openGraph: {
19      images: [ogImageUrl.toString()],
20    },
21    twitter: {
22      images: [ogImageUrl.toString()],
23    },
24  };
25}

Données structurées pour le SEO

Les données structurées aident les moteurs de recherche à mieux comprendre le contenu de ta page et peuvent influencer la façon dont les résultats de recherche sont affichés.

Implémentation du JSON-LD

1// app/layout.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5  // Inne metadane...
6};
7
8export default function RootLayout({
9  children,
10}: {
11  children: React.ReactNode;
12}) {
13  return (
14    <html lang="pl">
15      <head>
16        <script
17          type="application/ld+json"
18          dangerouslySetInnerHTML={{
19            __html: JSON.stringify({
20              '@context': 'https://schema.org',
21              '@type': 'Organization',
22              name: 'Metropolis Quantum',
23              url: 'https://metropolisquantum.pl',
24              logo: 'https://metropolisquantum.pl/logo.png',
25              contactPoint: {
26                '@type': 'ContactPoint',
27                telephone: '+48-123-456-789',
28                contactType: 'customer service',
29              },
30              sameAs: [
31                'https://facebook.com/metropolisquantum',
32                'https://twitter.com/metropolisq',
33                'https://instagram.com/metropolisquantum',
34              ],
35            }),
36          }}
37        />
38      </head>
39      <body>{children}</body>
40    </html>
41  );
42}

JSON-LD dynamique pour les sous-pages

1// components/JsonLd.tsx
2interface JsonLdProps {
3  data: any;
4}
5
6export function JsonLd({ data }: JsonLdProps) {
7  return (
8    <script
9      type="application/ld+json"
10      dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
11    />
12  );
13}
1// app/districts/[id]/page.tsx
2import { JsonLd } from '@/components/JsonLd';
3
4export default async function DistrictPage({ params }: { params: Promise<{ id: string }> }) {
5  const district = await getDistrictData((await params).id);
6
7  const jsonLdData = {
8    '@context': 'https://schema.org',
9    '@type': 'Place',
10    name: district.name,
11    description: district.description,
12    address: {
13      '@type': 'PostalAddress',
14      addressLocality: 'Metropolis Quantum',
15      addressRegion: 'MQ',
16      postalCode: '00-000',
17      addressCountry: 'PL',
18    },
19    geo: {
20      '@type': 'GeoCoordinates',
21      latitude: district.coordinates.lat,
22      longitude: district.coordinates.lng,
23    },
24    image: `https://metropolisquantum.pl/images/districts/${(await params).id}.jpg`,
25  };
26
27  return (
28    <div>
29      <JsonLd data={jsonLdData} />
30      <h1>{district.name}</h1>
31      {/* Rest of content */}
32    </div>
33  );
34}

Exemples d'implémentation complète

Métadonnées pour une application e-commerce

1// app/layout.tsx
2import type { Metadata, Viewport } from 'next';
3
4export const metadata: Metadata = {
5  metadataBase: new URL('https://metropolisquantum.pl'),
6  title: {
7    default: 'Metropolis Quantum Store',
8    template: '%s | Metropolis Quantum Store',
9  },
10  description: 'Store with the latest technologies of the future',
11  keywords: ['technology', 'future', 'metropolis', 'quantum', 'store'],
12  authors: [{ name: 'Metropolis Quantum', url: 'https://metropolisquantum.pl' }],
13  creator: 'Metropolis Quantum Team',
14  publisher: 'Metropolis Quantum Corporation',
15
16  // Ikony
17  icons: {
18    icon: [
19      { url: '/icons/icon-16x16.png', sizes: '16x16', type: 'image/png' },
20      { url: '/icons/icon-32x32.png', sizes: '32x32', type: 'image/png' },
21      { url: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' },
22      { url: '/icons/icon-512x512.png', sizes: '512x512', type: 'image/png' },
23    ],
24    apple: [
25      { url: '/icons/apple-icon.png' },
26    ],
27  },
28
29  // Open Graph
30  openGraph: {
31    type: 'website',
32    siteName: 'Metropolis Quantum Store',
33    title: 'Metropolis Quantum Store - Technology of the future today',
34    description: 'Store with the latest technologies of the future from Metropolis Quantum',
35    url: 'https://store.metropolisquantum.pl',
36    images: [
37      {
38        url: '/images/og-default.jpg',
39        width: 1200,
40        height: 630,
41        alt: 'Metropolis Quantum Store',
42      },
43    ],
44    locale: 'pl_PL',
45  },
46
47  // Twitter
48  twitter: {
49    card: 'summary_large_image',
50    title: 'Metropolis Quantum Store',
51    description: 'Technology of the future today',
52    images: ['/images/twitter-image.jpg'],
53    creator: '@MetropolisQ',
54  },
55
56  // Robots
57  robots: {
58    index: true,
59    follow: true,
60    googleBot: {
61      index: true,
62      follow: true,
63      'max-video-preview': -1,
64      'max-image-preview': 'large',
65      'max-snippet': -1,
66    },
67  },
68
69  // Manifest PWA
70  manifest: '/manifest.json',
71
72  // Applications alternatives
73  alternates: {
74    canonical: 'https://store.metropolisquantum.pl',
75    languages: {
76      'en-US': 'https://store.metropolisquantum.pl/en',
77      'de-DE': 'https://store.metropolisquantum.pl/de',
78    },
79    media: {
80      'only screen and (max-width: 640px)': 'https://m.store.metropolisquantum.pl',
81    },
82    types: {
83      'application/rss+xml': 'https://store.metropolisquantum.pl/rss',
84    },
85  },
86
87  // Paramètres pour les applications Apple
88  appleWebApp: {
89    title: 'MQ Store',
90    statusBarStyle: 'black-translucent',
91    capable: true,
92  },
93
94  // Inne
95  formatDetection: {
96    telephone: true,
97    date: false,
98    address: true,
99    email: true,
100    url: true,
101  },
102};
103
104// Couleur de thème - dans un export viewport séparé, car themeColor dans metadata est déprécié
105export const viewport: Viewport = {
106  themeColor: [
107    { media: '(prefers-color-scheme: light)', color: '#3a86ff' },
108    { media: '(prefers-color-scheme: dark)', color: '#1e429f' },
109  ],
110};

Métadonnées pour une page produit

1// app/products/[id]/page.tsx
2import { Metadata } from 'next';
3
4interface Props {
5  params: Promise<{ id: string }>;
6}
7
8export async function generateMetadata({ params }: Props): Promise<Metadata> {
9  const product = await getProductData((await params).id);
10
11  const price = new Intl.NumberFormat('pl-PL', {
12    style: 'currency',
13    currency: 'PLN',
14  }).format(product.price);
15
16  return {
17    title: product.name,
18    description: product.description,
19    openGraph: {
20      title: product.name,
21      description: product.description,
22      type: 'product',
23      images: [
24        {
25          url: product.images[0].url,
26          width: 800,
27          height: 600,
28          alt: product.name,
29        },
30      ],
31      availability: product.inStock ? 'in stock' : 'out of stock',
32      price: {
33        amount: product.price.toString(),
34        currency: 'PLN',
35      },
36    },
37  };
38}
39
40export default async function ProductPage({ params }: Props) {
41  const product = await getProductData((await params).id);
42
43  const jsonLdData = {
44    '@context': 'https://schema.org',
45    '@type': 'Product',
46    name: product.name,
47    description: product.description,
48    image: product.images.map((img) => img.url),
49    offers: {
50      '@type': 'Offer',
51      price: product.price,
52      priceCurrency: 'PLN',
53      availability: product.inStock
54        ? 'https://schema.org/InStock'
55        : 'https://schema.org/OutOfStock',
56      url: `https://store.metropolisquantum.pl/products/${(await params).id}`,
57    },
58    brand: {
59      '@type': 'Brand',
60      name: 'Metropolis Quantum',
61    },
62    aggregateRating: {
63      '@type': 'AggregateRating',
64      ratingValue: product.rating.average,
65      reviewCount: product.rating.count,
66    },
67  };
68
69  return (
70    <div>
71      <script
72        type="application/ld+json"
73        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLdData) }}
74      />
75      <h1>{product.name}</h1>
76      {/* Rest of product page content */}
77    </div>
78  );
79}
80
81async function getProductData(id: string) {
82  // Dans une vraie application : fetch depuis une API ou une base de données
83  return {
84    id,
85    name: 'Kwantowy komunikator MQ-7',
86    description: 'The latest communicator using quantum technology for instant communication over any distance.',
87    price: 4999.99,
88    inStock: true,
89    images: [
90      { url: '/images/products/mq-7-main.jpg' },
91      { url: '/images/products/mq-7-side.jpg' },
92      { url: '/images/products/mq-7-back.jpg' },
93    ],
94    rating: {
95      average: 4.8,
96      count: 156,
97    },
98  };
99}

Bonnes pratiques

  1. Prépare un jeu complet d'icônes - inclus toutes les tailles requises pour les différentes plateformes et appareils
  2. Optimise les images - compresse et optimise toutes les icônes et images Open Graph
  3. Teste sur différentes plateformes - vérifie l'apparence de tes métadonnées sur Facebook, Twitter, WhatsApp et d'autres plateformes
  4. Utilise des noms de fichiers parlants - nomme tes fichiers de façon descriptive (par ex. product-og-image.jpg au lieu de image1.jpg)
  5. Ajoute des données structurées - utilise le JSON-LD pour un meilleur SEO
  6. Personnalise les métadonnées de chaque sous-page - génère des métadonnées dynamiques pour chaque page
  7. N'oublie pas les textes alternatifs - ajoute toujours l'attribut alt aux images
  8. Prends en compte les différents modes (clair/sombre) - adapte les couleurs du thème aux préférences de l'utilisateur
  9. Fournis des icônes au format SVG quand c'est possible - pour une meilleure mise à l'échelle

Déboguer les métadonnées

Outils de test des métadonnées

  1. Facebook Sharing Debugger - https://developers.facebook.com/tools/debug/
  2. Twitter Card Validator - https://cards-dev.twitter.com/validator
  3. LinkedIn Post Inspector - https://www.linkedin.com/post-inspector/
  4. Rich Results Test (Google) - https://search.google.com/test/rich-results

Débogage dans Next.js

Tu peux vérifier les métadonnées générées directement dans le code source HTML de ta page. Vérifie la section <head> dans les outils de développement du navigateur.

Résumé

Les icônes, le favicon et les métadonnées visuelles sont un élément clé de l'identité visuelle de ton application Next.js 16. Tout comme à Metropolis Quantum, où chaque élément visuel construit une image cohérente de la ville, dans ton application des icônes et des métadonnées soigneusement conçues construisent une image professionnelle et augmentent la reconnaissance.

Points clés à retenir :

  1. Utilise la Metadata API de Next.js 16 - elle simplifie la gestion de toutes les métadonnées
  2. Prépare un jeu complet d'icônes - pour les différentes plateformes et appareils
  3. Configure Open Graph et les Twitter Cards - pour une meilleure visibilité sur les réseaux sociaux
  4. Ajoute des données structurées - pour un meilleur SEO et des résultats de recherche plus riches
  5. Génère des métadonnées dynamiques - personnalisées pour chaque sous-page
  6. Teste tes métadonnées - vérifie leur apparence sur différentes plateformes

Dans le prochain chapitre, nous nous concentrerons sur les animations et les transitions dans l'interface utilisateur, qui ajoutent du dynamisme et de l'interactivité aux applications Next.js 16, tout comme les effets holographiques et les animations fluides donnent vie à l'architecture de Metropolis Quantum.

Code de cette leçon : app/page.tsx
1// CSS Animations & Transitions - Cyberpunk Metropolis
2'use client';
3
4export default function AnimationsDemo() {
5  return (
6    <div style={{
7      minHeight: '100vh',
8      background: 'linear-gradient(135deg, #0f0f23, #1a1a2e)',
9      color: '#ffffff',
10      padding: '3rem 2rem'
11    }}>
12      <h1 style={{
13        fontSize: '3rem',
14        textAlign: 'center',
15        background: 'linear-gradient(45deg, #64ffda, #7c4dff)',
16        WebkitBackgroundClip: 'text',
17        WebkitTextFillColor: 'transparent',
18        marginBottom: '3rem',
19        animation: 'fadeIn 1s ease-out'
20      }}>
21        Animations & Transitions
22      </h1>
23
24      {/* Transition Examples */}
25      <section style={{maxWidth: '1200px', margin: '0 auto 3rem', background: 'rgba(255,255,255,0.05)', padding: '2rem', borderRadius: '1rem', border: '1px solid rgba(100,255,218,0.2)'}}>
26        <h2 style={{color: '#64ffda', marginBottom: '2rem'}}>CSS Transitions</h2>
27        <div style={{display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '2rem'}}>
28          <div style={{background: '#64ffda', padding: '2rem', borderRadius: '0.75rem', textAlign: 'center', color: '#0f0f23', fontWeight: 600, transition: 'all 0.3s ease', cursor: 'pointer'}} onMouseEnter={e => e.currentTarget.style.transform = 'translateY(-10px)'} onMouseLeave={e => e.currentTarget.style.transform = 'translateY(0)'}>
29            Hover me!
30          </div>
31          <div style={{background: 'rgba(124,77,255,0.2)', padding: '2rem', borderRadius: '0.75rem', textAlign: 'center', border: '2px solid #7c4dff', color: '#7c4dff', fontWeight: 600, transition: 'all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55)', cursor: 'pointer'}} onMouseEnter={e => { e.currentTarget.style.transform = 'scale(1.1) rotate(5deg)'; e.currentTarget.style.background = 'rgba(124,77,255,0.4)'; }} onMouseLeave={e => { e.currentTarget.style.transform = 'scale(1) rotate(0deg)'; e.currentTarget.style.background = 'rgba(124,77,255,0.2)'; }}>
32            Bounce Effect
33          </div>
34        </div>
35      </section>
36
37      {/* Keyframe Animations */}
38      <section style={{maxWidth: '1200px', margin: '0 auto 3rem', background: 'rgba(255,255,255,0.05)', padding: '2rem', borderRadius: '1rem', border: '1px solid rgba(100,255,218,0.2)'}}>
39        <h2 style={{color: '#64ffda', marginBottom: '2rem'}}>Keyframe Animations</h2>
40
41        <div style={{display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '3rem', textAlign: 'center'}}>
42          <div>
43            <div style={{width: '100px', height: '100px', margin: '0 auto 1rem', background: 'linear-gradient(135deg, #64ffda, #448aff)', borderRadius: '50%', animation: 'pulse 2s ease-in-out infinite'}}></div>
44            <p style={{color: '#b0bec5'}}>Pulse</p>
45          </div>
46
47          <div>
48            <div style={{width: '100px', height: '100px', margin: '0 auto 1rem', background: 'linear-gradient(135deg, #7c4dff, #ff0080)', borderRadius: '0.75rem', animation: 'rotate 3s linear infinite'}}></div>
49            <p style={{color: '#b0bec5'}}>Rotate</p>
50          </div>
51
52          <div>
53            <div style={{width: '100px', height: '100px', margin: '0 auto 1rem', background: 'linear-gradient(135deg, #00ff7f, #ffdc00)', borderRadius: '50% 0', animation: 'bounce 1.5s ease-in-out infinite'}}></div>
54            <p style={{color: '#b0bec5'}}>Bounce</p>
55          </div>
56        </div>
57      </section>
58
59      {/* Neon Glow Animation */}
60      <section style={{maxWidth: '1200px', margin: '0 auto 3rem', background: 'rgba(255,255,255,0.05)', padding: '2rem', borderRadius: '1rem', border: '1px solid rgba(100,255,218,0.2)'}}>
61        <h2 style={{color: '#64ffda', marginBottom: '2rem'}}>Neon Glow Effect</h2>
62        <div style={{textAlign: 'center', padding: '3rem'}}>
63          <h3 style={{fontSize: '4rem', fontWeight: 800, color: '#64ffda', textShadow: '0 0 10px #64ffda, 0 0 20px #64ffda, 0 0 30px #64ffda', animation: 'neonPulse 2s ease-in-out infinite'}}>
64            QUANTUM
65          </h3>
66          <p style={{color: '#b0bec5', marginTop: '1rem', fontSize: '1.25rem', animation: 'fadeIn 2s ease-out'}}>
67            Cyberpunk 2150
68          </p>
69        </div>
70      </section>
71
72      {/* Loading Animations */}
73      <section style={{maxWidth: '1200px', margin: '0 auto 3rem', background: 'rgba(255,255,255,0.05)', padding: '2rem', borderRadius: '1rem', border: '1px solid rgba(100,255,218,0.2)'}}>
74        <h2 style={{color: '#64ffda', marginBottom: '2rem'}}>Loading Animations</h2>
75        <div style={{display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '3rem', textAlign: 'center'}}>
76          <div>
77            <div style={{width: '50px', height: '50px', margin: '0 auto 1rem', border: '4px solid rgba(100,255,218,0.3)', borderTop: '4px solid #64ffda', borderRadius: '50%', animation: 'spin 1s linear infinite'}}></div>
78            <p style={{color: '#b0bec5'}}>Spinner</p>
79          </div>
80
81          <div>
82            <div style={{display: 'flex', gap: '8px', justifyContent: 'center', marginBottom: '1rem'}}>
83              <div style={{width: '12px', height: '12px', background: '#64ffda', borderRadius: '50%', animation: 'dotPulse 1.4s ease-in-out infinite', animationDelay: '0s'}}></div>
84              <div style={{width: '12px', height: '12px', background: '#7c4dff', borderRadius: '50%', animation: 'dotPulse 1.4s ease-in-out infinite', animationDelay: '0.2s'}}></div>
85              <div style={{width: '12px', height: '12px', background: '#ff0080', borderRadius: '50%', animation: 'dotPulse 1.4s ease-in-out infinite', animationDelay: '0.4s'}}></div>
86            </div>
87            <p style={{color: '#b0bec5'}}>Dots</p>
88          </div>
89
90          <div>
91            <div style={{width: '100%', height: '4px', background: 'rgba(100,255,218,0.2)', borderRadius: '2px', overflow: 'hidden', margin: '0 auto 1rem'}}>
92              <div style={{width: '50%', height: '100%', background: 'linear-gradient(90deg, #64ffda, #7c4dff)', borderRadius: '2px', animation: 'progress 2s ease-in-out infinite'}}></div>
93            </div>
94            <p style={{color: '#b0bec5'}}>Progress</p>
95          </div>
96        </div>
97      </section>
98
99      <style jsx global>{`
100        @keyframes fadeIn {
101          from { opacity: 0; transform: translateY(20px); }
102          to { opacity: 1; transform: translateY(0); }
103        }
104
105        @keyframes pulse {
106          0%, 100% { transform: scale(1); opacity: 1; }
107          50% { transform: scale(1.1); opacity: 0.8; }
108        }
109
110        @keyframes rotate {
111          from { transform: rotate(0deg); }
112          to { transform: rotate(360deg); }
113        }
114
115        @keyframes bounce {
116          0%, 100% { transform: translateY(0); }
117          50% { transform: translateY(-30px); }
118        }
119
120        @keyframes neonPulse {
121          0%, 100% { text-shadow: 0 0 10px #64ffda, 0 0 20px #64ffda, 0 0 30px #64ffda; }
122          50% { text-shadow: 0 0 20px #64ffda, 0 0 40px #64ffda, 0 0 60px #64ffda; }
123        }
124
125        @keyframes spin {
126          to { transform: rotate(360deg); }
127        }
128
129        @keyframes dotPulse {
130          0%, 100% { transform: scale(0.8); opacity: 0.5; }
131          50% { transform: scale(1.2); opacity: 1; }
132        }
133
134        @keyframes progress {
135          0% { transform: translateX(-100%); }
136          100% { transform: translateX(200%); }
137        }
138      `}</style>
139    </div>
140  );
141}

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Le mode sombre dans les applications s'implémente le mieux avec :

  2. 2. Les Container Queries CSS permettent :

Voici 2 des 13 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Tri vertical

    Classe dans le bon ordre les étapes de configuration et de chargement des polices dans Next.js (next/font) :

  • Tri vertical

    Classe dans le bon ordre les étapes d'implémentation du mode sombre dans une application Next.js :

  • Tri vertical

    Place les éléments dans le bon ordre : 'use server' → async function → Component

  • Tri vertical

    Place les éléments dans le bon ordre : 'use client' → import { useState } → function

  • Tri vertical

    Place dans l'ordre les étapes de configuration manuelle de Tailwind CSS 3 dans un projet Next.js

Articles utiles