initial migration
This commit is contained in:
144
components/SEO.tsx
Normal file
144
components/SEO.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { getSiteInfo } from '@/lib/i18n';
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
interface SEOProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
locale?: 'en' | 'de';
|
||||
path?: string;
|
||||
type?: 'website' | 'article' | 'product';
|
||||
publishedTime?: string;
|
||||
modifiedTime?: string;
|
||||
authors?: string[];
|
||||
images?: string[];
|
||||
}
|
||||
|
||||
export function SEO({
|
||||
title,
|
||||
description,
|
||||
locale = 'en',
|
||||
path = '/',
|
||||
type = 'website',
|
||||
publishedTime,
|
||||
modifiedTime,
|
||||
authors,
|
||||
images
|
||||
}: SEOProps): ReactElement {
|
||||
const site = getSiteInfo();
|
||||
const fullTitle = title === 'Home' ? site.title : `${title} | ${site.title}`;
|
||||
const fullDescription = description || site.description;
|
||||
const canonicalUrl = `${site.baseUrl}${path}`;
|
||||
|
||||
// Generate alternate URLs
|
||||
const alternateLocale = locale === 'en' ? 'de' : 'en';
|
||||
const alternatePath = path === '/' ? '' : path;
|
||||
const alternateUrl = `${site.baseUrl}/${alternateLocale}${alternatePath}`;
|
||||
|
||||
// Open Graph images
|
||||
const ogImages = images && images.length > 0
|
||||
? images
|
||||
: [`${site.baseUrl}/og-image.jpg`];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Basic Meta Tags */}
|
||||
<title>{fullTitle}</title>
|
||||
<meta name="description" content={fullDescription} />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
|
||||
{/* Canonical URL */}
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
|
||||
{/* Alternate Languages */}
|
||||
<link rel="alternate" hrefLang={locale} href={canonicalUrl} />
|
||||
<link rel="alternate" hrefLang={alternateLocale} href={alternateUrl} />
|
||||
<link rel="alternate" hrefLang="x-default" href={`${site.baseUrl}${alternatePath}`} />
|
||||
|
||||
{/* Open Graph */}
|
||||
<meta property="og:type" content={type} />
|
||||
<meta property="og:title" content={fullTitle} />
|
||||
<meta property="og:description" content={fullDescription} />
|
||||
<meta property="og:url" content={canonicalUrl} />
|
||||
<meta property="og:locale" content={locale === 'en' ? 'en_US' : 'de_DE'} />
|
||||
<meta property="og:site_name" content={site.title} />
|
||||
|
||||
{ogImages.map((image, index) => (
|
||||
<meta key={index} property="og:image" content={image} />
|
||||
))}
|
||||
|
||||
{publishedTime && (
|
||||
<meta property="article:published_time" content={publishedTime} />
|
||||
)}
|
||||
|
||||
{modifiedTime && (
|
||||
<meta property="article:modified_time" content={modifiedTime} />
|
||||
)}
|
||||
|
||||
{authors && authors.length > 0 && (
|
||||
<meta property="article:author" content={authors.join(', ')} />
|
||||
)}
|
||||
|
||||
{/* Twitter Card */}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={fullTitle} />
|
||||
<meta name="twitter:description" content={fullDescription} />
|
||||
{ogImages[0] && (
|
||||
<meta name="twitter:image" content={ogImages[0]} />
|
||||
)}
|
||||
|
||||
{/* Site Info */}
|
||||
<meta name="author" content="KLZ Kabelwerke" />
|
||||
<meta name="copyright" content="KLZ Kabelwerke" />
|
||||
|
||||
{/* Favicon (placeholder) */}
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function generateSEOMetadata(props: SEOProps) {
|
||||
const site = getSiteInfo();
|
||||
const fullTitle = props.title === 'Home' ? site.title : `${props.title} | ${site.title}`;
|
||||
const description = props.description || site.description;
|
||||
const canonicalUrl = `${site.baseUrl}${props.path || '/'}`;
|
||||
|
||||
const alternateLocale = props.locale === 'en' ? 'de' : 'en';
|
||||
const alternatePath = props.path && props.path !== '/' ? props.path : '';
|
||||
const alternateUrl = `${site.baseUrl}/${alternateLocale}${alternatePath}`;
|
||||
|
||||
return {
|
||||
title: fullTitle,
|
||||
description,
|
||||
metadataBase: new URL(site.baseUrl),
|
||||
alternates: {
|
||||
canonical: canonicalUrl,
|
||||
languages: {
|
||||
[props.locale || 'en']: canonicalUrl,
|
||||
[alternateLocale]: alternateUrl,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: fullTitle,
|
||||
description,
|
||||
type: props.type || 'website',
|
||||
locale: props.locale || 'en',
|
||||
siteName: site.title,
|
||||
url: canonicalUrl,
|
||||
...(props.images && props.images.length > 0 && {
|
||||
images: props.images.map(img => ({ url: img, alt: fullTitle })),
|
||||
}),
|
||||
...(props.publishedTime && { publishedTime: props.publishedTime }),
|
||||
...(props.modifiedTime && { modifiedTime: props.modifiedTime }),
|
||||
...(props.authors && { authors: props.authors }),
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: fullTitle,
|
||||
description,
|
||||
...(props.images && props.images[0] && { images: [props.images[0]] }),
|
||||
},
|
||||
authors: props.authors ? props.authors.map(name => ({ name })) : undefined,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user