322 lines
6.8 KiB
TypeScript
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,
|
|
},
|
|
})),
|
|
})
|
|
}
|