initial commit
This commit is contained in:
321
lib/seo.ts
Normal file
321
lib/seo.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
})),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user