We use cookies to enhance your experience on the site
CodeWorlds

Open Graph and sharing on social media platforms

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.

What is the Open Graph Protocol?

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.

Basic Open Graph tags

The basic set of Open Graph tags includes:

  1. og:title - content title
  2. og:type - content type (e.g., website, article, product)
  3. og:image - URL of the image that should represent the content
  4. og:url - canonical URL of the content

Additional useful tags include:

  1. og:description - short content description
  2. og:site_name - site name
  3. og:locale - content language (e.g., pl_PL)

Twitter Cards

Twitter Cards is a similar technology, specific to the Twitter (X) platform, which controls how shared content is displayed. The most important tags are:

  1. twitter:card - card type (summary, summary_large_image, app, player)
  2. twitter:title - content title
  3. twitter:description - content description
  4. twitter:image - image to display
  5. twitter:site - site account name (@sitename)
  6. twitter:creator - content author's account name (@author)

Implementing Open Graph in Next.js

Next.js 13+ significantly simplifies the implementation of the Open Graph protocol through the new Metadata API. Here's how to do it:

Basic implementation

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}

Dynamic Open Graph metadata

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}

Creating and optimizing images for social media

Each social media platform has its recommended image dimensions. Preparing images in accordance with these guidelines ensures the best experience when sharing:

Recommended image dimensions

  1. Facebook/Open Graph:

    • Recommended: 1200 × 630 pixels
    • Minimum: 600 × 315 pixels
    • Aspect ratio: 1.91:1
  2. Twitter:

    • Large card (summary_large_image): 1200 × 628 pixels
    • Regular card (summary): 800 × 418 pixels
  3. LinkedIn:

    • Recommended: 1200 × 627 pixels
    • Aspect ratio: 1.91:1
  4. Pinterest:

    • Recommended: 1000 × 1500 pixels
    • Aspect ratio: 2:3

Dynamic Open Graph image generation

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}

Social media share buttons

Adding share buttons to your application is an effective way to increase reach. Here's how to implement them in Next.js:

ShareButtons component

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}

Using the ShareButtons component

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}

Integration with the Web Share API

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}

Testing Open Graph and sharing

Proper configuration of Open Graph and Twitter Cards requires testing. Here are the tools that can help:

Debugging tools

  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. Pinterest Rich Pins Validator: https://developers.pinterest.com/tools/url-debugger/

Open Graph test automation

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);

Best practices and tips

  1. Optimize images - Use images with appropriate dimensions and proportions for each platform
  2. Use unique images - Create unique and eye-catching images for important pages
  3. Add branding - Place logo or visual identity elements on OG images
  4. Test on different platforms - Check how shared content looks on all major platforms
  5. Experiment with titles - Test different titles and descriptions to increase CTR from shares
  6. Update metadata - Regularly refresh metadata for pages with recurring content

Summary

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.

Go to CodeWorlds