Proper content sharing on social media is a key element of the marketing and SEO strategy of any modern application. Protocols such as Open Graph and Twitter Cards allow control over the appearance of shared links, which significantly increases their attractiveness and effectiveness. In this chapter, you will learn how to optimize your Next.js application for social media sharing.
Open Graph Protocol (OGP) is a technology developed by Facebook that enables any web page to become a rich object on social media. When a user shares a link to your page on Facebook, LinkedIn, or other social media platforms, the OG protocol determines what information and visual elements should be displayed.
The basic set of Open Graph tags includes:
Additional useful tags include:
Twitter Cards is a similar technology, specific to the Twitter (X) platform, which controls how shared content is displayed. The most important tags are:
Next.js 13+ significantly simplifies the implementation of the Open Graph protocol through the new Metadata API. Here's how to do it:
1// app/page.tsx
2import { Metadata } from 'next';
3
4export const metadata: Metadata = {
5 openGraph: {
6 title: 'My Next.js application',
7 description: 'Discover the capabilities of our application',
8 url: 'https://twojadomena.pl',
9 siteName: 'Your Site Name',
10 images: [
11 {
12 url: 'https://twojadomena.pl/og-image.jpg',
13 width: 1200,
14 height: 630,
15 alt: 'My Next.js application',
16 },
17 ],
18 locale: 'pl_PL',
19 type: 'website',
20 },
21 twitter: {
22 card: 'summary_large_image',
23 title: 'My Next.js application',
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 {/* Page content */}
35 </main>
36 );
37}For dynamic pages, such as blog articles or products, we can generate Open Graph metadata based on data from the API or database:
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 not found',
12 description: 'Sorry, we didn't find the article you were looking for',
13 type: 'article',
14 },
15 };
16 }
17
18 // Basic information
19 const title = `${post.title} | Blog My Application`;
20 const description = post.excerpt;
21 const url = `https://twojadomena.pl/blog/${post.slug}`;
22
23 // Open Graph metadata for the article
24 return {
25 openGraph: {
26 title: title,
27 description: description,
28 url: url,
29 siteName: 'Your Site Name',
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>Post not found</div>;
60 }
61
62 return (
63 <article>
64 <h1>{post.title}</h1>
65 <div dangerouslySetInnerHTML={{ __html: post.content }} />
66 </article>
67 );
68}Each social media platform has its recommended image dimensions. Preparing images in accordance with these guidelines ensures the best experience when sharing:
Facebook/Open Graph:
Twitter:
LinkedIn:
Pinterest:
Instead of manually creating images for each page, we can generate them dynamically using API Routes in Next.js. This is a great solution for applications with many dynamic pages, such as e-commerce or 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 // Get parameters from the URL
11 const title = searchParams.get('title') || 'My Application';
12 const description = searchParams.get('description') || 'Default description';
13 const imageUrl = searchParams.get('image');
14
15 // Define colors, fonts, and style
16 const bgColor = '#111827';
17 const textColor = '#ffffff';
18
19 // Fonts can also be loaded dynamically
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 // Optionally you can apply fonts
91 // fonts: [{ name: 'Inter', data: font, weight: 400 }],
92 },
93 );
94}Using dynamically generated Open Graph images:
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 // ...other metadata
15 images: [
16 {
17 url: ogImageUrl.toString(),
18 width: 1200,
19 height: 630,
20 alt: post.title,
21 },
22 ],
23 },
24 //
25 };
26}Adding share buttons to your application is an effective way to increase reach. Here's how to implement them in 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 // Create the full URL
18 const baseUrl = 'https://twojadomena.pl';
19 const url = baseUrl + pathname;
20
21 // Encode parameters for sharing
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 // Create share links
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 // Function handling share button click
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">Share: </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}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 not found</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 is a modern alternative to traditional share buttons, which allows users to share content using any application installed on their device:
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 // Check if the browser supports the Web Share API
21 setIsShareSupported(
22 typeof navigator !== 'undefined' &&
23 !!navigator.share
24 );
25 }, []);
26
27 // If the browser does not support the Web Share API, do not render the button
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 // Add files if they are available and the browser supports them
41 if (files && navigator.canShare && navigator.canShare({ files })) {
42 shareData.files = files;
43 }
44
45 await navigator.share(shareData);
46 console.log('Content has been shared');
47 } catch (error) {
48 console.error('Error during sharing:', 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>Share</span>
60 </button>
61 );
62}Proper configuration of Open Graph and Twitter Cards requires testing. Here are the tools that can help:
You can automate Open Graph tests as part of the CI/CD process:
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 // Add other important pages
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 // Get all Open Graph tags
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 // Check required tags
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 // Save the report
54 await fs.writeFile('./og-tags-report.json', JSON.stringify(results, null, 2));
55
56 // Whether all pages have required tags
57 const hasErrors = results.some(result => result.hasErrors);
58
59 if (hasErrors) {
60 console.error('❌ Missing Open Graph tags found:');
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);Proper implementation of Open Graph and Twitter Cards protocols and well-designed share buttons can significantly increase the visibility and reach of your Next.js application. Using dynamically generated OG images allows for personalized and attractive display of shared content, and the Web Share API offers a native sharing experience on mobile devices.
Remember that social media platforms often update their requirements and recommendations, so it's worth regularly checking the documentation of each platform and testing content sharing on different devices and browsers.