ai-wpa/lib/seo.ts

322 lines
6.8 KiB
TypeScript

/**
* SEO utilities for Next.js App Router
* Uses the new Metadata API for better SEO
*/
import { Metadata } from 'next'
export interface SEOConfig {
title?: string
description?: string
keywords?: string[]
authors?: Array<{ name: string; url?: string }>
creator?: string
publisher?: string
robots?: {
index?: boolean
follow?: boolean
nocache?: boolean
googleBot?: {
index?: boolean
follow?: boolean
noimageindex?: boolean
'max-video-preview'?: number
'max-image-preview'?: 'none' | 'standard' | 'large'
'max-snippet'?: number
}
}
openGraph?: {
title?: string
description?: string
url?: string
siteName?: string
images?: Array<{
url: string
width?: number
height?: number
alt?: string
}>
type?: 'website' | 'article'
publishedTime?: string
modifiedTime?: string
authors?: string[]
section?: string
tags?: string[]
}
twitter?: {
card?: 'summary' | 'summary_large_image' | 'app' | 'player'
title?: string
description?: string
siteId?: string
creator?: string
creatorId?: string
images?: string | Array<{ url: string; alt?: string }>
}
alternates?: {
canonical?: string
languages?: Record<string, string>
}
icons?: {
icon?: string | Array<{ url: string; sizes?: string; type?: string }>
shortcut?: string
apple?: string | Array<{ url: string; sizes?: string; type?: string }>
}
}
const defaultSEOConfig: SEOConfig = {
title: 'NextJS Boilerplate',
description:
'A production-ready NextJS boilerplate with TypeScript, Tailwind CSS, and authentication',
keywords: ['Next.js', 'React', 'TypeScript', 'Tailwind CSS', 'Authentication', 'Boilerplate'],
authors: [{ name: 'NextJS Boilerplate Team' }],
creator: 'NextJS Boilerplate',
publisher: 'NextJS Boilerplate',
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
openGraph: {
type: 'website',
siteName: 'NextJS Boilerplate',
images: [
{
url: '/og-image.png',
width: 1200,
height: 630,
alt: 'NextJS Boilerplate',
},
],
},
twitter: {
card: 'summary_large_image',
creator: '@nextjs_boilerplate',
},
icons: {
icon: '/favicon.ico',
shortcut: '/favicon.ico',
apple: '/apple-touch-icon.png',
},
}
/**
* Generate metadata for pages
*/
export function generateMetadata(config: SEOConfig = {}): Metadata {
const mergedConfig = {
...defaultSEOConfig,
...config,
openGraph: {
...defaultSEOConfig.openGraph,
...config.openGraph,
},
twitter: {
...defaultSEOConfig.twitter,
...config.twitter,
},
robots: {
...defaultSEOConfig.robots,
...config.robots,
},
}
return {
title: mergedConfig.title,
description: mergedConfig.description,
keywords: mergedConfig.keywords,
authors: mergedConfig.authors,
creator: mergedConfig.creator,
publisher: mergedConfig.publisher,
robots: mergedConfig.robots,
openGraph: mergedConfig.openGraph,
twitter: mergedConfig.twitter,
alternates: config.alternates,
icons: mergedConfig.icons,
}
}
/**
* Generate metadata for article pages
*/
export function generateArticleMetadata(
title: string,
description: string,
config: {
publishedTime?: string
modifiedTime?: string
authors?: string[]
section?: string
tags?: string[]
image?: string
url?: string
} = {}
): Metadata {
return generateMetadata({
title,
description,
openGraph: {
type: 'article',
title,
description,
publishedTime: config.publishedTime,
modifiedTime: config.modifiedTime,
authors: config.authors,
section: config.section,
tags: config.tags,
url: config.url,
images: config.image
? [
{
url: config.image,
width: 1200,
height: 630,
alt: title,
},
]
: undefined,
},
twitter: {
card: 'summary_large_image',
title,
description,
images: config.image ? [{ url: config.image, alt: title }] : undefined,
},
})
}
/**
* Generate structured data (JSON-LD)
*/
export function generateStructuredData(type: string, data: Record<string, any>) {
const structuredData = {
'@context': 'https://schema.org',
'@type': type,
...data,
}
return {
__html: JSON.stringify(structuredData),
}
}
/**
* Website structured data
*/
export function generateWebsiteStructuredData(
name: string,
url: string,
description?: string,
logo?: string
) {
return generateStructuredData('WebSite', {
name,
url,
description,
logo,
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: `${url}/search?q={search_term_string}`,
},
'query-input': 'required name=search_term_string',
},
})
}
/**
* Organization structured data
*/
export function generateOrganizationStructuredData(
name: string,
url: string,
logo?: string,
contactPoint?: {
telephone?: string
contactType?: string
email?: string
}
) {
return generateStructuredData('Organization', {
name,
url,
logo,
contactPoint: contactPoint
? {
'@type': 'ContactPoint',
...contactPoint,
}
: undefined,
})
}
/**
* Article structured data
*/
export function generateArticleStructuredData(
headline: string,
description: string,
author: string,
publishedDate: string,
modifiedDate?: string,
image?: string,
url?: string
) {
return generateStructuredData('Article', {
headline,
description,
author: {
'@type': 'Person',
name: author,
},
datePublished: publishedDate,
dateModified: modifiedDate || publishedDate,
image,
url,
publisher: {
'@type': 'Organization',
name: 'NextJS Boilerplate',
logo: {
'@type': 'ImageObject',
url: '/logo.png',
},
},
})
}
/**
* Breadcrumb structured data
*/
export function generateBreadcrumbStructuredData(items: Array<{ name: string; url: string }>) {
return generateStructuredData('BreadcrumbList', {
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: item.url,
})),
})
}
/**
* FAQ structured data
*/
export function generateFAQStructuredData(faqs: Array<{ question: string; answer: string }>) {
return generateStructuredData('FAQPage', {
mainEntity: faqs.map((faq) => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: {
'@type': 'Answer',
text: faq.answer,
},
})),
})
}