201 lines
5.1 KiB
TypeScript
201 lines
5.1 KiB
TypeScript
/**
|
|
* SEO Component System
|
|
* Provides comprehensive SEO meta tags and Open Graph support
|
|
*/
|
|
|
|
import Head from 'next/head'
|
|
|
|
export interface SEOProps {
|
|
title?: string
|
|
description?: string
|
|
canonical?: string
|
|
openGraph?: {
|
|
type?: 'website' | 'article' | 'profile'
|
|
title?: string
|
|
description?: string
|
|
image?: string
|
|
url?: string
|
|
siteName?: string
|
|
}
|
|
twitter?: {
|
|
card?: 'summary' | 'summary_large_image' | 'app' | 'player'
|
|
site?: string
|
|
creator?: string
|
|
title?: string
|
|
description?: string
|
|
image?: string
|
|
}
|
|
additionalMetaTags?: Array<{
|
|
name?: string
|
|
property?: string
|
|
content: string
|
|
}>
|
|
additionalLinkTags?: Array<{
|
|
rel: string
|
|
href: string
|
|
type?: string
|
|
sizes?: string
|
|
}>
|
|
noindex?: boolean
|
|
nofollow?: boolean
|
|
}
|
|
|
|
const defaultSEOConfig = {
|
|
title: 'NextJS Boilerplate',
|
|
description:
|
|
'A production-ready NextJS boilerplate with TypeScript, Tailwind CSS, and authentication',
|
|
openGraph: {
|
|
type: 'website' as const,
|
|
siteName: 'NextJS Boilerplate',
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image' as const,
|
|
},
|
|
}
|
|
|
|
export function SEO({
|
|
title,
|
|
description,
|
|
canonical,
|
|
openGraph,
|
|
twitter,
|
|
additionalMetaTags = [],
|
|
additionalLinkTags = [],
|
|
noindex = false,
|
|
nofollow = false,
|
|
}: SEOProps) {
|
|
const seo = {
|
|
title: title || defaultSEOConfig.title,
|
|
description: description || defaultSEOConfig.description,
|
|
openGraph: {
|
|
...defaultSEOConfig.openGraph,
|
|
...openGraph,
|
|
},
|
|
twitter: {
|
|
...defaultSEOConfig.twitter,
|
|
...twitter,
|
|
},
|
|
}
|
|
|
|
// Create robots meta content
|
|
const robotsContent = []
|
|
if (noindex) robotsContent.push('noindex')
|
|
if (nofollow) robotsContent.push('nofollow')
|
|
if (robotsContent.length === 0) robotsContent.push('index', 'follow')
|
|
|
|
return (
|
|
<Head>
|
|
{/* Basic Meta Tags */}
|
|
<title>{seo.title}</title>
|
|
<meta name="description" content={seo.description} />
|
|
<meta name="robots" content={robotsContent.join(', ')} />
|
|
|
|
{/* Canonical URL */}
|
|
{canonical && <link rel="canonical" href={canonical} />}
|
|
|
|
{/* Open Graph */}
|
|
<meta property="og:type" content={seo.openGraph.type} />
|
|
<meta property="og:title" content={seo.openGraph.title || seo.title} />
|
|
<meta property="og:description" content={seo.openGraph.description || seo.description} />
|
|
{seo.openGraph.image && <meta property="og:image" content={seo.openGraph.image} />}
|
|
{seo.openGraph.url && <meta property="og:url" content={seo.openGraph.url} />}
|
|
{seo.openGraph.siteName && <meta property="og:site_name" content={seo.openGraph.siteName} />}
|
|
|
|
{/* Twitter Card */}
|
|
<meta name="twitter:card" content={seo.twitter.card} />
|
|
{seo.twitter.site && <meta name="twitter:site" content={seo.twitter.site} />}
|
|
{seo.twitter.creator && <meta name="twitter:creator" content={seo.twitter.creator} />}
|
|
<meta name="twitter:title" content={seo.twitter.title || seo.title} />
|
|
<meta name="twitter:description" content={seo.twitter.description || seo.description} />
|
|
{seo.twitter.image && <meta name="twitter:image" content={seo.twitter.image} />}
|
|
|
|
{/* Additional Meta Tags */}
|
|
{additionalMetaTags.map((tag, index) => (
|
|
<meta
|
|
key={index}
|
|
{...(tag.name ? { name: tag.name } : { property: tag.property })}
|
|
content={tag.content}
|
|
/>
|
|
))}
|
|
|
|
{/* Additional Link Tags */}
|
|
{additionalLinkTags.map((tag, index) => (
|
|
<link key={index} {...tag} />
|
|
))}
|
|
</Head>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Article SEO component for topic posts
|
|
*/
|
|
interface ArticleSEOProps extends Omit<SEOProps, 'openGraph'> {
|
|
article: {
|
|
publishedTime?: string
|
|
modifiedTime?: string
|
|
author?: string
|
|
section?: string
|
|
tags?: string[]
|
|
}
|
|
openGraph?: SEOProps['openGraph']
|
|
}
|
|
|
|
export function ArticleSEO({ article, openGraph, ...props }: ArticleSEOProps) {
|
|
const articleOpenGraph = {
|
|
...openGraph,
|
|
type: 'article' as const,
|
|
}
|
|
|
|
const additionalMetaTags = [
|
|
...(props.additionalMetaTags || []),
|
|
...(article.publishedTime
|
|
? [
|
|
{
|
|
property: 'article:published_time',
|
|
content: article.publishedTime,
|
|
},
|
|
]
|
|
: []),
|
|
...(article.modifiedTime
|
|
? [
|
|
{
|
|
property: 'article:modified_time',
|
|
content: article.modifiedTime,
|
|
},
|
|
]
|
|
: []),
|
|
...(article.author
|
|
? [
|
|
{
|
|
property: 'article:author',
|
|
content: article.author,
|
|
},
|
|
]
|
|
: []),
|
|
...(article.section
|
|
? [
|
|
{
|
|
property: 'article:section',
|
|
content: article.section,
|
|
},
|
|
]
|
|
: []),
|
|
...(article.tags
|
|
? article.tags.map((tag) => ({
|
|
property: 'article:tag',
|
|
content: tag,
|
|
}))
|
|
: []),
|
|
]
|
|
|
|
return <SEO {...props} openGraph={articleOpenGraph} additionalMetaTags={additionalMetaTags} />
|
|
}
|
|
|
|
/**
|
|
* Hook to generate SEO-friendly URLs
|
|
*/
|
|
export function useSEOUrl(baseUrl: string, path?: string) {
|
|
const cleanPath = path?.startsWith('/') ? path : `/${path || ''}`
|
|
return `${baseUrl}${cleanPath}`
|
|
}
|