typescriptintermediate

SEO Metadata Patterns

Generate dynamic SEO metadata with Open Graph, Twitter cards, JSON-LD, and canonical URLs.

typescript
import type { Metadata, ResolvingMetadata } from 'next';

const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://example.com';

// Metadata factory for dynamic pages
interface PageSeoProps {
  title: string;
  description: string;
  path: string;
  image?: string;
  type?: 'website' | 'article';
  publishedTime?: string;
  tags?: string[];
}

function generatePageMetadata(props: PageSeoProps): Metadata {
  const { title, description, path, image, type = 'website', publishedTime, tags } = props;
  const url = `${BASE_URL}${path}`;
  const ogImage = image ?? `${BASE_URL}/og-default.png`;

  return {
    title,
    description,
    alternates: { canonical: url },
    openGraph: {
      title,
      description,
      url,
      siteName: 'SnippetsLab',
      type,
      images: [{ url: ogImage, width: 1200, height: 630, alt: title }],
      ...(publishedTime && { publishedTime }),
    },
    twitter: {
      card: 'summary_large_image',
      title,
      description,
      images: [ogImage],
    },
    other: tags ? { keywords: tags.join(', ') } : undefined,
  };
}

// JSON-LD structured data component
function JsonLd({ data }: { data: Record<string, unknown> }) {
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
    />
  );
}

// Article JSON-LD factory
function articleJsonLd(props: {
  title: string;
  description: string;
  url: string;
  datePublished: string;
  tags?: string[];
}) {
  return {
    '@context': 'https://schema.org',
    '@type': 'TechArticle',
    headline: props.title,
    description: props.description,
    url: props.url,
    datePublished: props.datePublished,
    publisher: {
      '@type': 'Organization',
      name: 'SnippetsLab',
      url: BASE_URL,
    },
    ...(props.tags && { keywords: props.tags.join(', ') }),
  };
}

// Usage in a page
export async function generateMetadata(
  { params }: { params: { slug: string } },
  parent: ResolvingMetadata,
): Promise<Metadata> {
  const snippet = await getSnippet(params.slug); // your data fetcher

  return generatePageMetadata({
    title: `${snippet.title} — SnippetsLab`,
    description: snippet.description,
    path: `/snippets/${params.slug}`,
    type: 'article',
    tags: snippet.tags,
  });
}

async function getSnippet(slug: string) {
  return { title: 'Example', description: 'A snippet', tags: ['react'] };
}

export { generatePageMetadata, JsonLd, articleJsonLd };

Use Cases

  • Dynamic page SEO
  • Open Graph optimization
  • Google rich results with JSON-LD

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.