Skip to main content

Social Sharing

The template provides social sharing capabilities through a dedicated share button component, Open Graph image generation, structured data markup, and SEO metadata utilities. Together, these features ensure that shared links render rich previews across social platforms.

Architecture Overview

components/item-detail/
share-button.tsx -- Share dropdown component

app/
opengraph-image.tsx -- Dynamic OG image generation

lib/seo/
schema.ts -- JSON-LD structured data
listing-metadata.ts -- Next.js Metadata generation
hreflang.ts -- Hreflang alternate links

Share Button Component

The ShareButton at components/item-detail/share-button.tsx provides a dropdown menu with sharing options for X (Twitter), Facebook, LinkedIn, and clipboard copy:

// components/item-detail/share-button.tsx
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { toast } from "sonner";

export const ShareButton = ({ url, title }: { url: string; title: string }) => {
const [isCopying, setIsCopying] = useState(false);
const t = useTranslations("common");

const handleShare = async (type: string) => {
try {
switch (type) {
case "copy":
setIsCopying(true);
await navigator.clipboard.writeText(url);
await new Promise(resolve => setTimeout(resolve, 500));
toast.success(t("LINK_COPIED"));
setIsCopying(false);
break;
case "twitter":
window.open(
`https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`,
"_blank"
);
break;
case "facebook":
window.open(
`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
"_blank"
);
break;
case "linkedin":
window.open(
`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`,
"_blank"
);
break;
}
} catch (error) {
toast.error(t("SHARE_ERROR"));
setIsCopying(false);
}
};

return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button className="...">
{/* Share icon */}
<span>{t("SHARE")}</span>
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item onSelect={() => handleShare("copy")}>
Copy Link
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={() => handleShare("twitter")}>
X
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={() => handleShare("facebook")}>
Facebook
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={() => handleShare("linkedin")}>
LinkedIn
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
};

Share URL Formats

PlatformURL Pattern
X (Twitter)https://twitter.com/intent/tweet?url=...&text=...
Facebookhttps://www.facebook.com/sharer/sharer.php?u=...
LinkedInhttps://www.linkedin.com/sharing/share-offsite/?url=...
Copy LinkUses navigator.clipboard.writeText()

UI Features

  • Built on Radix UI DropdownMenu for accessible keyboard navigation
  • Copy loading state: shows a spinner during the clipboard write delay
  • Toast notifications: success/error feedback via sonner
  • Dark mode support: all styles include dark variants
  • i18n: all labels use next-intl translations

Open Graph Image Generation

The app/opengraph-image.tsx file generates dynamic OG images using Next.js ImageResponse:

// app/opengraph-image.tsx
import { ImageResponse } from 'next/og';
import { siteConfig } from '@/lib/config';

export const runtime = 'nodejs';
export const alt = `${siteConfig.name} - ${siteConfig.tagline}`;
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

export default async function Image() {
const gradient = `linear-gradient(135deg, ${siteConfig.ogImage.gradientStart} 0%, ${siteConfig.ogImage.gradientEnd} 100%)`;

return new ImageResponse(
(
<div style={{ background: gradient, width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', padding: '80px' }}>
<div style={{ fontSize: 96, fontWeight: 'bold', color: 'white' }}>
{siteConfig.name}
</div>
<div style={{ fontSize: 36, color: '#e0e0e0', textAlign: 'center' }}>
{siteConfig.tagline}
</div>
</div>
),
{ ...size }
);
}

The OG image is automatically served at /opengraph-image.png and referenced by Next.js metadata.

SEO Metadata Generation

The lib/seo/listing-metadata.ts utility generates complete Next.js Metadata objects including Open Graph and Twitter card tags:

// lib/seo/listing-metadata.ts
export function generateListingMetadata({
title,
description,
path,
locale,
itemCount,
keywords,
imageUrl,
}: ListingMetadataOptions): Metadata {
const fullTitle = `${title} | ${siteConfig.name}`;
const canonicalUrl = `${appUrl}${localePath}${path}`;

return {
title: fullTitle,
description: metaDescription,
keywords: keywords?.join(', '),
openGraph: {
title: fullTitle,
description: metaDescription,
type: 'website',
siteName: siteConfig.name,
url: canonicalUrl,
...(imageUrl && { images: [{ url: imageUrl }] }),
},
twitter: {
card: 'summary_large_image',
title: fullTitle,
description: metaDescription,
},
alternates: {
canonical: canonicalUrl,
languages: generateHreflangAlternates(path),
},
};
}

Structured Data (JSON-LD)

The lib/seo/schema.ts module generates Schema.org structured data for rich search results:

Organization Schema

export function generateOrganizationSchema() {
const sameAs = [
siteConfig.social.github,
siteConfig.social.x,
siteConfig.social.linkedin,
siteConfig.social.facebook,
].filter(Boolean);

return {
'@context': 'https://schema.org',
'@type': 'Organization',
name: siteConfig.brandName,
url: siteConfig.url,
logo: `${siteConfig.url}${siteConfig.logo}`,
sameAs,
};
}

Product Schema

export function generateProductSchema(input: ProductSchemaInput) {
return {
'@context': 'https://schema.org',
'@type': 'Product',
name: input.name,
description: input.description,
url: input.url,
image: input.image,
category: input.category,
brand: input.brandName
? { '@type': 'Brand', name: input.brandName }
: undefined,
};
}

WebSite Schema with Search Action

export function generateWebSiteSchema(locale: string) {
return {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: siteConfig.name,
url: `${siteConfig.url}${localePrefix}`,
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: `${siteConfig.url}${localePrefix}?q={search_term_string}`,
},
'query-input': 'required name=search_term_string',
},
};
}
export function generateBreadcrumbSchema(items: BreadcrumbItem[]) {
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: item.url,
})),
};
}

Hreflang Alternates

The lib/seo/hreflang.ts module generates alternate language links for international SEO:

import { generateHreflangAlternates } from './hreflang';

// Returns: { 'en': '/path', 'fr': '/fr/path', 'es': '/es/path', ... }
const languages = generateHreflangAlternates('/categories/design');

Usage in Item Detail Pages

The share button is used on item detail pages alongside the generated metadata:

// In an item detail page component
<ShareButton
url={`${siteConfig.url}/items/${item.slug}`}
title={item.name}
/>

File Reference

FilePurpose
components/item-detail/share-button.tsxSocial share dropdown component
app/opengraph-image.tsxDynamic OG image generation
lib/seo/schema.tsJSON-LD structured data generators
lib/seo/listing-metadata.tsNext.js Metadata generation
lib/seo/hreflang.tsHreflang alternate links
lib/config/client.tsSite configuration (social URLs, branding)