Odpowiednie udostępnianie treści w mediach społecznościowych to kluczowy element strategii marketingowej i SEO każdej nowoczesnej aplikacji. Protokoły takie jak Open Graph i Twitter Cards pozwalają na kontrolę wyglądu udostępnianych linków, co znacznie zwiększa ich atrakcyjność i skuteczność. W tym rozdziale dowiesz się, jak zoptymalizować swoją aplikację Next.js pod kątem udostępniania w mediach społecznościowych.
Open Graph Protocol (OGP) to technologia opracowana przez Facebook, która umożliwia każdej stronie internetowej stanie się bogatym obiektem w mediach społecznościowych. Gdy użytkownik udostępnia link do twojej strony na Facebooku, LinkedInie czy innych platformach społecznościowych, protokół OG określa, jakie informacje i elementy wizualne powinny być wyświetlane.
Podstawowy zestaw tagów Open Graph obejmuje:
Dodatkowe przydatne tagi obejmują:
Twitter Cards to podobna technologia, specyficzna dla platformy Twitter (X), która kontroluje sposób wyświetlania udostępnionych treści. Najważniejsze tagi to:
Next.js 13+ znacznie upraszcza implementację protokołu Open Graph poprzez nowe API Metadanych. Oto jak to zrobić:
1// app/page.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5 openGraph: {
6 title: 'Moja aplikacja Next.js',
7 description: 'Poznaj możliwości naszej aplikacji',
8 url: 'https://twojadomena.pl',
9 siteName: 'Nazwa Twojej Strony',
10 images: [
11 {
12 url: 'https://twojadomena.pl/og-image.jpg',
13 width: 1200,
14 height: 630,
15 alt: 'Moja aplikacja Next.js',
16 },
17 ],
18 locale: 'pl_PL',
19 type: 'website',
20 },
21 twitter: {
22 card: 'summary_large_image',
23 title: 'Moja aplikacja Next.js',
24 description: 'Poznaj możliwości naszej aplikacji',
25 images: ['https://twojadomena.pl/twitter-image.jpg'],
26 creator: '@twojanazwa',
27 site: '@twojastrona',
28 },
29};
30
31export default function Home() {
32 return (
33 <main>
34 {/* Treść strony */}
35 </main>
36 );
37}Dla stron dynamicznych, takich jak artykuły bloga czy produkty, możemy generować metadane Open Graph na podstawie danych z API lub bazy danych:
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: 'Post nie znaleziony',
12 description: 'Przepraszamy, nie znaleźliśmy szukanego artykułu',
13 type: 'article',
14 },
15 };
16 }
17
18 // Podstawowe informacje
19 const title = `${post.title} | Blog Moja Aplikacja`;
20 const description = post.excerpt;
21 const url = `https://twojadomena.pl/blog/${post.slug}`;
22
23 // Metadane Open Graph dla artykułu
24 return {
25 openGraph: {
26 title: title,
27 description: description,
28 url: url,
29 siteName: 'Nazwa Twojej Strony',
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: '@twojastrona',
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>Post nie znaleziony</div>;
60 }
61
62 return (
63 <article>
64 <h1>{post.title}</h1>
65 <div dangerouslySetInnerHTML={{ __html: post.content }} />
66 </article>
67 );
68}Każda platforma społecznościowa ma swoje zalecane wymiary obrazów. Przygotowanie obrazów zgodnie z tymi wytycznymi zapewnia najlepsze doświadczenie podczas udostępniania:
Facebook/Open Graph:
Twitter:
LinkedIn:
Pinterest:
Zamiast ręcznie tworzyć obrazy dla każdej strony, możemy generować je dynamicznie przy użyciu API Routes w Next.js. To świetne rozwiązanie dla aplikacji z dużą liczbą dynamicznych stron, takich jak e-commerce czy blogi:
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 // Pobierz parametry z URL
11 const title = searchParams.get('title') || 'Moja Aplikacja';
12 const description = searchParams.get('description') || 'Domyślny opis';
13 const imageUrl = searchParams.get('image');
14
15 // Określ kolory, czcionki i styl
16 const bgColor = '#111827';
17 const textColor = '#ffffff';
18
19 // Można również pobierać czcionki dynamicznie
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 // Opcjonalnie możesz zastosować czcionki
91 // fonts: [{ name: 'Inter', data: font, weight: 400 }],
92 },
93 );
94}Używanie dynamicznie generowanych obrazów Open Graph:
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 // ...inne metadane
15 images: [
16 {
17 url: ogImageUrl.toString(),
18 width: 1200,
19 height: 630,
20 alt: post.title,
21 },
22 ],
23 },
24 //
25 };
26}Dodanie przycisków udostępniania do twojej aplikacji to skuteczny sposób na zwiększenie zasięgu. Oto jak zaimplementować je w Next.js:
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 // Tworzymy pełny adres URL
18 const baseUrl = 'https://twojadomena.pl';
19 const url = baseUrl + pathname;
20
21 // Kodujemy parametry do udostępniania
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 // Tworzymy linki do udostępniania
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 // Funkcja obsługująca kliknięcie w przycisk udostępniania
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">Udostępnij: </span>
44
45 <button
46 className="text-blue-600 hover:text-blue-800 transition-colors"
47 onClick={() => handleShare(facebookShareUrl)}
48 aria-label="Udostępnij na Facebooku"
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="Udostępnij na Twitterze"
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="Udostępnij na 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="Udostępnij na 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="Udostępnij przez email"
81 >
82 <FaEnvelope size={24} />
83 </a>
84 </div>
85 );
86}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>Post nie znaleziony</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}Web Share API to nowoczesna alternatywa dla tradycyjnych przycisków udostępniania, która pozwala użytkownikom udostępniać treści za pomocą dowolnej aplikacji zainstalowanej na ich urządzeniu:
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 // Sprawdź czy przeglądarka wspiera Web Share API
21 setIsShareSupported(
22 typeof navigator !== 'undefined' &&
23 !!navigator.share
24 );
25 }, []);
26
27 // Jeśli przeglądarka nie wspiera Web Share API, nie renderuj przycisku
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 // Dodaj pliki, jeśli są dostępne i przeglądarka je obsługuje
41 if (files && navigator.canShare && navigator.canShare({ files })) {
42 shareData.files = files;
43 }
44
45 await navigator.share(shareData);
46 console.log('Treść została udostępniona');
47 } catch (error) {
48 console.error('Błąd podczas udostępniania:', 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="Udostępnij"
57 >
58 <FaShare />
59 <span>Udostępnij</span>
60 </button>
61 );
62}Poprawne skonfigurowanie Open Graph i Twitter Cards wymaga testowania. Oto narzędzia, które mogą pomóc:
Możesz zautomatyzować testy Open Graph w ramach procesu 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 // Dodaj inne ważne strony
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 // Pobierz wszystkie tagi 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 // Sprawdź wymagane tagi
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 // Zapisz raport
54 await fs.writeFile('./og-tags-report.json', JSON.stringify(results, null, 2));
55
56 // Czy wszystkie strony mają wymagane tagi
57 const hasErrors = results.some(result => result.hasErrors);
58
59 if (hasErrors) {
60 console.error('❌ Znaleziono braki w tagach Open Graph:');
61
62 for (const result of results) {
63 if (result.hasErrors) {
64 console.error(`Strona: ${result.page}`);
65 console.error(`Brakujące tagi: ${result.missingTags.join(', ')}`);
66 console.error('---');
67 }
68 }
69
70 process.exit(1);
71 } else {
72 console.log('✅ Wszystkie wymagane tagi Open Graph są obecne.');
73 }
74 } finally {
75 await browser.close();
76 }
77}
78
79testOgTags().catch(console.error);Odpowiednia implementacja protokołów Open Graph i Twitter Cards oraz dobrze zaprojektowane przyciski udostępniania mogą znacząco zwiększyć widoczność i zasięg twojej aplikacji Next.js. Wykorzystanie dynamicznie generowanych obrazów OG pozwala na spersonalizowane i atrakcyjne wyświetlanie udostępnianych treści, a Web Share API oferuje natywne doświadczenie udostępniania na urządzeniach mobilnych.
Pamiętaj, że platformy społecznościowe często aktualizują swoje wymagania i zalecenia, dlatego warto regularnie sprawdzać dokumentację każdej platformy i testować udostępnianie treści na różnych urządzeniach i przeglądarkach.