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
| Platform | URL Pattern |
|---|---|
| X (Twitter) | https://twitter.com/intent/tweet?url=...&text=... |
https://www.facebook.com/sharer/sharer.php?u=... | |
https://www.linkedin.com/sharing/share-offsite/?url=... | |
| Copy Link | Uses 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-intltranslations
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',
},
};
}
Breadcrumb Schema
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
| File | Purpose |
|---|---|
components/item-detail/share-button.tsx | Social share dropdown component |
app/opengraph-image.tsx | Dynamic OG image generation |
lib/seo/schema.ts | JSON-LD structured data generators |
lib/seo/listing-metadata.ts | Next.js Metadata generation |
lib/seo/hreflang.ts | Hreflang alternate links |
lib/config/client.ts | Site configuration (social URLs, branding) |