initial commit
This commit is contained in:
200
components/seo/SEO.tsx
Normal file
200
components/seo/SEO.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* 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}`
|
||||
}
|
||||
Reference in New Issue
Block a user