/** * 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 } 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) { 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, }, })), }) }