Cours Next.js · Module 8: Déploiement et production
Open Graph et partage sur les réseaux sociaux
Dans cette leçon9
Un partage correct du contenu sur les réseaux sociaux est un élément clé de la stratégie marketing et SEO de toute application moderne. Des protocoles comme Open Graph et Twitter Cards permettent de contrôler l'apparence des liens partagés, ce qui augmente fortement leur attractivité et leur efficacité. Dans ce chapitre, tu vas apprendre à optimiser ton application Next.js pour le partage sur les réseaux sociaux.
Qu'est-ce que le protocole Open Graph ?
Le protocole Open Graph (OGP) est une technologie développée par Facebook qui permet à n'importe quelle page web de devenir un objet riche sur les réseaux sociaux. Quand un utilisateur partage un lien vers ta page sur Facebook, LinkedIn ou d'autres réseaux sociaux, le protocole OG détermine quelles informations et quels éléments visuels doivent être affichés.
Les balises Open Graph de base
L'ensemble de base des balises Open Graph comprend :
- og:title - le titre du contenu
- og:type - le type de contenu (par ex. website, article, product)
- og:image - l'URL de l'image qui doit représenter le contenu
- og:url - l'URL canonique du contenu
Des balises supplémentaires utiles comprennent :
- og:description - une courte description du contenu
- og:site_name - le nom du site
- og:locale - la langue du contenu (par ex. pl_PL)
Twitter Cards
Twitter Cards est une technologie similaire, spécifique à la plateforme Twitter (X), qui contrôle la façon dont le contenu partagé est affiché. Les balises les plus importantes sont :
- twitter:card - le type de carte (summary, summary_large_image, app, player)
- twitter:title - le titre du contenu
- twitter:description - la description du contenu
- twitter:image - l'image à afficher
- twitter:site - le nom du compte du site (@nomdusite)
- twitter:creator - le nom du compte de l'auteur du contenu (@author)
Implémenter Open Graph dans Next.js
Next.js 13+ simplifie considérablement l'implémentation du protocole Open Graph grâce à la nouvelle Metadata API. Voici comment faire :
Implémentation de base
1// app/page.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5 openGraph: {
6 title: 'Mon application Next.js',
7 description: 'Discover the capabilities of our application',
8 url: 'https://twojadomena.pl',
9 siteName: 'Nom de votre site',
10 images: [
11 {
12 url: 'https://twojadomena.pl/og-image.jpg',
13 width: 1200,
14 height: 630,
15 alt: 'Mon application Next.js',
16 },
17 ],
18 locale: 'pl_PL',
19 type: 'website',
20 },
21 twitter: {
22 card: 'summary_large_image',
23 title: 'Mon application Next.js',
24 description: 'Discover the capabilities of our application',
25 images: ['https://twojadomena.pl/twitter-image.jpg'],
26 creator: '@twojanazwa',
27 site: '@twojapage',
28 },
29};
30
31export default function Home() {
32 return (
33 <main>
34 {/* Contenu de la page */}
35 </main>
36 );
37}Métadonnées Open Graph dynamiques
Pour les pages dynamiques, comme les articles de blog ou les produits, nous pouvons générer les métadonnées Open Graph à partir de données provenant de l'API ou de la base de données :
1// app/blog/[slug]/page.tsx
2import { Metadata } from 'next';
3import { getBlogPost } from '@/lib/api';
4
5export async function generateMetadata({ params }): Promise<Metadata> {
6 const post = await getBlogPost((await params).slug);
7
8 if (!post) {
9 return {
10 openGraph: {
11 title: 'Article non trouvé',
12 description: "Désolé, nous n'avons pas trouvé l'article que tu cherchais",
13 type: 'article',
14 },
15 };
16 }
17
18 // Informations de base
19 const title = `${post.title} | Blog Mon Application`;
20 const description = post.excerpt;
21 const url = `https://twojadomena.pl/blog/${post.slug}`;
22
23 // Métadonnées Open Graph pour l'article
24 return {
25 openGraph: {
26 title: title,
27 description: description,
28 url: url,
29 siteName: 'Nom de votre site',
30 images: [
31 {
32 url: post.coverImage || 'https://twojadomena.pl/default-og.jpg',
33 width: 1200,
34 height: 630,
35 alt: post.title,
36 },
37 ],
38 locale: 'pl_PL',
39 type: 'article',
40 publishedTime: post.publishedAt,
41 authors: post.author.name,
42 tags: post.tags,
43 },
44 twitter: {
45 card: 'summary_large_image',
46 title: title,
47 description: description,
48 images: [post.coverImage || 'https://twojadomena.pl/default-twitter.jpg'],
49 creator: post.author.twitter || '@twojanazwa',
50 site: '@twojapage',
51 },
52 };
53}
54
55export default async function BlogPost({ params }) {
56 const post = await getBlogPost((await params).slug);
57
58 if (!post) {
59 return <div>Article non trouvé</div>;
60 }
61
62 return (
63 <article>
64 <h1>{post.title}</h1>
65 <div dangerouslySetInnerHTML={{ __html: post.content }} />
66 </article>
67 );
68}Créer et optimiser des images pour les réseaux sociaux
Chaque réseau social a ses dimensions d'image recommandées. Préparer les images selon ces recommandations garantit la meilleure expérience lors du partage :
Dimensions d'image recommandées
Facebook/Open Graph :
- Recommandé : 1200 × 630 pixels
- Minimum : 600 × 315 pixels
- Proportions : 1.91:1
Twitter :
- Grande carte (summary_large_image) : 1200 × 628 pixels
- Carte standard (summary) : 800 × 418 pixels
LinkedIn :
- Recommandé : 1200 × 627 pixels
- Proportions : 1.91:1
Pinterest :
- Recommandé : 1000 × 1500 pixels
- Proportions : 2:3
Génération dynamique d'images Open Graph
Au lieu de créer manuellement des images pour chaque page, nous pouvons les générer dynamiquement à l'aide des API Routes dans Next.js. C'est une excellente solution pour les applications avec beaucoup de pages dynamiques, comme l'e-commerce ou les blogs :
1// app/api/og/route.tsx
2import { ImageResponse } from 'next/og';
3import { NextRequest } from 'next/server';
4
5export const runtime = 'edge';
6
7export async function GET(req: NextRequest) {
8 const { searchParams } = new URL(req.url);
9
10 // Récupère les paramètres depuis l'URL
11 const title = searchParams.get('title') || 'Mon Application';
12 const description = searchParams.get('description') || 'Default description';
13 const imageUrl = searchParams.get('image');
14
15 // Définit les couleurs, les polices et le style
16 const bgColor = '#111827';
17 const textColor = '#ffffff';
18
19 // Les polices peuvent aussi être chargées dynamiquement
20 // const font = await fetch('...').then(res => res.arrayBuffer());
21
22 return new ImageResponse(
23 (
24 <div
25 style={{
26 display: 'flex',
27 flexDirection: 'column',
28 alignItems: 'center',
29 justifyContent: 'center',
30 width: '100%',
31 height: '100%',
32 backgroundColor: bgColor,
33 color: textColor,
34 padding: '40px',
35 position: 'relative',
36 }}
37 >
38 {imageUrl && (
39 <img
40 src={imageUrl}
41 style={{
42 position: 'absolute',
43 top: 0,
44 left: 0,
45 width: '100%',
46 height: '100%',
47 objectFit: 'cover',
48 opacity: 0.6,
49 }}
50 />
51 )}
52
53 <div
54 style={{
55 display: 'flex',
56 flexDirection: 'column',
57 zIndex: 10,
58 maxWidth: '80%',
59 textAlign: 'center',
60 }}
61 >
62 <h1 style={{ fontSize: '64px', fontWeight: 'bold', marginBottom: '20px' }}>
63 {title}
64 </h1>
65
66 <p style={{ fontSize: '32px', margin: 0 }}>
67 {description}
68 </p>
69
70 <div style={{
71 marginTop: '40px',
72 display: 'flex',
73 alignItems: 'center',
74 justifyContent: 'center'
75 }}>
76 <img
77 src="https://twojadomena.pl/logo.png"
78 width="60"
79 height="60"
80 style={{ marginRight: '20px' }}
81 />
82 <p style={{ fontSize: '36px', fontWeight: 'bold' }}>mojadomena.pl</p>
83 </div>
84 </div>
85 </div>
86 ),
87 {
88 width: 1200,
89 height: 630,
90 // Optionnellement, tu peux appliquer des polices
91 // fonts: [{ name: 'Inter', data: font, weight: 400 }],
92 },
93 );
94}Utilisation d'images Open Graph générées dynamiquement :
1// app/blog/[slug]/page.tsx
2export async function generateMetadata({ params }) {
3 const post = await getBlogPost((await params).slug);
4
5 const ogImageUrl = new URL('/api/og', 'https://twojadomena.pl');
6 ogImageUrl.searchParams.append('title', post.title);
7 ogImageUrl.searchParams.append('description', post.excerpt);
8 if (post.coverImage) {
9 ogImageUrl.searchParams.append('image', post.coverImage);
10 }
11
12 return {
13 openGraph: {
14 // ...autres métadonnées
15 images: [
16 {
17 url: ogImageUrl.toString(),
18 width: 1200,
19 height: 630,
20 alt: post.title,
21 },
22 ],
23 },
24 //
25 };
26}Boutons de partage sur les réseaux sociaux
Ajouter des boutons de partage à ton application est un moyen efficace d'augmenter la portée. Voici comment les implémenter dans Next.js :
Le composant ShareButtons
1// components/ShareButtons.tsx
2'use client';
3
4import { usePathname } from 'next/navigation';
5import { FaFacebook, FaTwitter, FaLinkedin, FaPinterest, FaEnvelope } from 'react-icons/fa';
6
7interface ShareButtonsProps {
8 title: string;
9 description?: string;
10 imageUrl?: string;
11 hashtags?: string[];
12}
13
14export default function ShareButtons({ title, description, imageUrl, hashtags = [] }: ShareButtonsProps) {
15 const pathname = usePathname();
16
17 // Crée l'URL complète
18 const baseUrl = 'https://twojadomena.pl';
19 const url = baseUrl + pathname;
20
21 // Encode les paramètres pour le partage
22 const encodedUrl = encodeURIComponent(url);
23 const encodedTitle = encodeURIComponent(title);
24 const encodedDescription = description ? encodeURIComponent(description) : '';
25 const encodedHashtags = hashtags.length > 0 ? encodeURIComponent(hashtags.join(',')) : '';
26
27 // Crée les liens de partage
28 const facebookShareUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`;
29 const twitterShareUrl = `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}&hashtags=${encodedHashtags}`;
30 const linkedinShareUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`;
31 const pinterestShareUrl = imageUrl
32 ? `https://pinterest.com/pin/create/button/?url=${encodedUrl}&media=${encodeURIComponent(imageUrl)}&description=${encodedTitle}`
33 : `https://pinterest.com/pin/create/button/?url=${encodedUrl}&description=${encodedTitle}`;
34 const emailShareUrl = `mailto:?subject=${encodedTitle}&body=${encodedDescription}%0A%0A${encodedUrl}`;
35
36 // Fonction qui gère le clic sur un bouton de partage
37 const handleShare = (url: string) => {
38 window.open(url, '_blank', 'width=600,height=400');
39 };
40
41 return (
42 <div className="flex space-x-4 items-center">
43 <span className="text-sm font-medium text-gray-700">Partager : </span>
44
45 <button
46 className="text-blue-600 hover:text-blue-800 transition-colors"
47 onClick={() => handleShare(facebookShareUrl)}
48 aria-label="Share on Facebook"
49 >
50 <FaFacebook size={24} />
51 </button>
52
53 <button
54 className="text-sky-500 hover:text-sky-700 transition-colors"
55 onClick={() => handleShare(twitterShareUrl)}
56 aria-label="Share on Twitter"
57 >
58 <FaTwitter size={24} />
59 </button>
60
61 <button
62 className="text-blue-700 hover:text-blue-900 transition-colors"
63 onClick={() => handleShare(linkedinShareUrl)}
64 aria-label="Share on LinkedIn"
65 >
66 <FaLinkedin size={24} />
67 </button>
68
69 <button
70 className="text-red-600 hover:text-red-800 transition-colors"
71 onClick={() => handleShare(pinterestShareUrl)}
72 aria-label="Share on Pinterest"
73 >
74 <FaPinterest size={24} />
75 </button>
76
77 <a
78 href={emailShareUrl}
79 className="text-gray-600 hover:text-gray-800 transition-colors"
80 aria-label="Share via email"
81 >
82 <FaEnvelope size={24} />
83 </a>
84 </div>
85 );
86}Utiliser le composant ShareButtons
1// app/blog/[slug]/page.tsx
2import ShareButtons from '@/components/ShareButtons';
3import { getBlogPost } from '@/lib/api';
4
5export default async function BlogPost({ params }) {
6 const post = await getBlogPost((await params).slug);
7
8 if (!post) {
9 return <div>Article non trouvé</div>;
10 }
11
12 return (
13 <article className="max-w-2xl mx-auto py-8">
14 <h1 className="text-3xl font-bold mb-4">{post.title}</h1>
15
16 <div className="my-6">
17 <ShareButtons
18 title={post.title}
19 description={post.excerpt}
20 imageUrl={post.coverImage}
21 hashtags={post.tags}
22 />
23 </div>
24
25 <div className="prose max-w-none" dangerouslySetInnerHTML={{ __html: post.content }} />
26
27 <div className="mt-8 pt-4 border-t">
28 <ShareButtons
29 title={post.title}
30 description={post.excerpt}
31 imageUrl={post.coverImage}
32 hashtags={post.tags}
33 />
34 </div>
35 </article>
36 );
37}Intégration avec la Web Share API
La Web Share API est une alternative moderne aux boutons de partage classiques, qui permet aux utilisateurs de partager du contenu via n'importe quelle application installée sur leur appareil :
1// components/NativeShareButton.tsx
2'use client';
3
4import { useState, useEffect } from 'react';
5import { FaShare } from 'react-icons/fa';
6import { usePathname } from 'next/navigation';
7
8interface NativeShareProps {
9 title: string;
10 text?: string;
11 url?: string;
12 files?: File[];
13}
14
15export default function NativeShareButton({ title, text, url, files }: NativeShareProps) {
16 const [isShareSupported, setIsShareSupported] = useState(false);
17 const pathname = usePathname();
18
19 useEffect(() => {
20 // Vérifie si le navigateur supporte la Web Share API
21 setIsShareSupported(
22 typeof navigator !== 'undefined' &&
23 !!navigator.share
24 );
25 }, []);
26
27 // Si le navigateur ne supporte pas la Web Share API, ne rends pas le bouton
28 if (!isShareSupported) {
29 return null;
30 }
31
32 const handleShare = async () => {
33 try {
34 const shareData: ShareData = {
35 title,
36 text: text || title,
37 url: url || window.location.href,
38 };
39
40 // Ajoute les fichiers s'ils sont disponibles et que le navigateur les supporte
41 if (files && navigator.canShare && navigator.canShare({ files })) {
42 shareData.files = files;
43 }
44
45 await navigator.share(shareData);
46 console.log('Le contenu a été partagé');
47 } catch (error) {
48 console.error('Erreur lors du partage :', error);
49 }
50 };
51
52 return (
53 <button
54 onClick={handleShare}
55 className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors"
56 aria-label="Share"
57 >
58 <FaShare />
59 <span>Partager</span>
60 </button>
61 );
62}Tester Open Graph et le partage
Une configuration correcte d'Open Graph et de Twitter Cards demande des tests. Voici les outils qui peuvent aider :
Outils de débogage
- Facebook Sharing Debugger : https://developers.facebook.com/tools/debug/
- Twitter Card Validator : https://cards-dev.twitter.com/validator
- LinkedIn Post Inspector : https://www.linkedin.com/post-inspector/
- Pinterest Rich Pins Validator : https://developers.pinterest.com/tools/url-debugger/
Automatiser les tests Open Graph
Tu peux automatiser les tests Open Graph dans le cadre du processus CI/CD :
1// scripts/test-og-tags.mjs
2import puppeteer from 'puppeteer';
3import { promises as fs } from 'fs';
4
5const pages = [
6 { url: '/', name: 'home' },
7 { url: '/blog/example-post', name: 'blog-post' },
8 { url: '/products/example-product', name: 'product' },
9 // Ajoute les autres pages importantes
10];
11
12async function testOgTags() {
13 const browser = await puppeteer.launch();
14 const results = [];
15
16 try {
17 for (const page of pages) {
18 const pageUrl = `https://twojadomena.pl${page.url}`;
19 const puppeteerPage = await browser.newPage();
20 await puppeteerPage.goto(pageUrl, { waitUntil: 'networkidle0' });
21
22 // Récupère toutes les balises Open Graph
23 const ogTags = await puppeteerPage.evaluate(() => {
24 const tags = {};
25 const metaTags = document.querySelectorAll('meta[property^="og:"], meta[name^="twitter:"]');
26
27 metaTags.forEach(tag => {
28 const property = tag.getAttribute('property') || tag.getAttribute('name');
29 const content = tag.getAttribute('content');
30 tags[property] = content;
31 });
32
33 return tags;
34 });
35
36 // Vérifie les balises requises
37 const requiredTags = ['og:title', 'og:description', 'og:image', 'og:url', 'twitter:card'];
38 const missingTags = requiredTags.filter(tag => !ogTags[tag]);
39
40 const result = {
41 page: page.name,
42 url: pageUrl,
43 ogTags,
44 missingTags,
45 hasErrors: missingTags.length > 0,
46 };
47
48 results.push(result);
49
50 await puppeteerPage.close();
51 }
52
53 // Enregistre le rapport
54 await fs.writeFile('./og-tags-report.json', JSON.stringify(results, null, 2));
55
56 // Toutes les pages ont-elles les balises requises
57 const hasErrors = results.some(result => result.hasErrors);
58
59 if (hasErrors) {
60 console.error('Des balises Open Graph manquantes ont été trouvées :');
61
62 for (const result of results) {
63 if (result.hasErrors) {
64 console.error(`Page : ${result.page}`);
65 console.error(`Missing tags: ${result.missingTags.join(', ')}`);
66 console.error('---');
67 }
68 }
69
70 process.exit(1);
71 } else {
72 console.log('All required Open Graph tags are present.');
73 }
74 } finally {
75 await browser.close();
76 }
77}
78
79testOgTags().catch(console.error);Bonnes pratiques et conseils
- Optimise les images - Utilise des images avec des dimensions et des proportions adaptées à chaque plateforme
- Utilise des images uniques - Crée des images uniques et accrocheuses pour les pages importantes
- Ajoute du branding - Place le logo ou des éléments d'identité visuelle sur les images OG
- Teste sur différentes plateformes - Vérifie l'apparence du contenu partagé sur toutes les grandes plateformes
- Expérimente avec les titres - Teste différents titres et descriptions pour augmenter le CTR des partages
- Mets à jour les métadonnées - Rafraîchis régulièrement les métadonnées des pages au contenu récurrent
Résumé
Une bonne implémentation des protocoles Open Graph et Twitter Cards, ainsi que des boutons de partage bien conçus, peut augmenter significativement la visibilité et la portée de ton application Next.js. L'utilisation d'images OG générées dynamiquement permet un affichage personnalisé et attractif du contenu partagé, et la Web Share API offre une expérience de partage native sur les appareils mobiles.
N'oublie pas que les réseaux sociaux mettent souvent à jour leurs exigences et leurs recommandations, alors vérifie régulièrement la documentation de chaque plateforme et teste le partage de contenu sur différents appareils et navigateurs.
Tu as repéré une erreur dans cette leçon ?