Rocky_Mountain_Vending/lib/seo.ts

222 lines
5.1 KiB
TypeScript

import type { Metadata } from "next"
import { businessConfig } from "@/lib/seo-config"
import {
buildAbsoluteUrl,
getSeoPageDefinition,
ogImagePath,
type StaticSeoPageKey,
} from "@/lib/seo-registry"
/**
* Clean HTML entities from text (e.g., & -> &, " -> ")
*/
function cleanHtmlEntities(text: string): string {
if (!text) return ""
return text
.replace(/&/g, "&")
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&nbsp;/g, " ")
.trim()
}
export interface SEOData {
title: string
description: string
excerpt?: string
date?: string
modified?: string
image?: string
robots?: Metadata["robots"]
path?: string
keywords?: string[]
}
function createRobotsValue(noindex?: boolean): Metadata["robots"] | undefined {
if (!noindex) {
return undefined
}
return {
index: false,
follow: false,
googleBot: {
index: false,
follow: false,
},
}
}
/**
* Generate SEO metadata for a WordPress page/post
*/
export function generateSEOMetadata(data: SEOData): Metadata {
const {
title,
description,
excerpt,
date,
modified,
image,
robots,
path,
keywords,
} = data
const cleanTitle = cleanHtmlEntities(title)
const fullTitle = cleanTitle.includes(businessConfig.name)
? cleanTitle
: `${cleanTitle} | ${businessConfig.name}`
let seoDescription = cleanHtmlEntities(description || excerpt || "")
.replace(/\s+/g, " ")
.trim()
if (!seoDescription) {
seoDescription =
"Rocky Mountain Vending provides vending machine placement, service, repairs, and support across Utah."
}
const ogImage = image || ogImagePath
const canonicalUrl = path ? buildAbsoluteUrl(path) : undefined
const openGraph: NonNullable<Metadata["openGraph"]> = {
title: fullTitle,
description: seoDescription,
type: date || modified ? "article" : "website",
...(canonicalUrl ? { url: canonicalUrl } : {}),
images: [
{
url: ogImage,
width: 1200,
height: 630,
alt: cleanTitle,
},
],
siteName: businessConfig.name,
...(date && { publishedTime: date }),
...(modified && { modifiedTime: modified }),
}
const metadata: Metadata = {
title: fullTitle,
description: seoDescription,
keywords,
openGraph,
twitter: {
card: "summary_large_image",
title: fullTitle,
description: seoDescription,
images: [ogImage],
},
alternates: canonicalUrl ? { canonical: canonicalUrl } : undefined,
robots,
}
return metadata
}
export function generateRegistryMetadata(
key: StaticSeoPageKey,
overrides?: Partial<SEOData>
): Metadata {
const page = getSeoPageDefinition(key)
return generateSEOMetadata({
title: overrides?.title ?? page.title,
description: overrides?.description ?? page.description,
excerpt: overrides?.excerpt,
date: overrides?.date,
modified: overrides?.modified,
image: overrides?.image,
robots: overrides?.robots ?? createRobotsValue(page.noindex),
path: overrides?.path ?? page.canonicalPath ?? page.path,
keywords: overrides?.keywords ?? [...page.keywords],
})
}
export function generateRegistryStructuredData(
key: StaticSeoPageKey,
overrides?: {
title?: string
description?: string
url?: string
datePublished?: string
dateModified?: string
type?: "Article" | "WebPage"
}
) {
const page = getSeoPageDefinition(key)
return generateStructuredData({
title: overrides?.title ?? page.title,
description: overrides?.description ?? page.description,
url: overrides?.url ?? buildAbsoluteUrl(page.canonicalPath ?? page.path),
datePublished: overrides?.datePublished,
dateModified: overrides?.dateModified,
type: overrides?.type,
})
}
/**
* Generate structured data (JSON-LD) for a page
*/
export function generateStructuredData(data: {
title: string
description: string
url: string
datePublished?: string
dateModified?: string
type?: "Article" | "WebPage"
}) {
const {
title,
description,
url,
datePublished,
dateModified,
type = "WebPage",
} = data
const structuredData: Record<string, unknown> = {
"@context": "https://schema.org",
"@type": type,
headline: cleanHtmlEntities(title),
description: cleanHtmlEntities(description),
url: url,
}
if (datePublished) {
structuredData.datePublished = datePublished
}
if (dateModified) {
structuredData.dateModified = dateModified
}
if (type === "Article") {
structuredData.author = {
"@type": "Organization",
name: businessConfig.name,
url: businessConfig.website,
}
structuredData.publisher = {
"@type": "Organization",
name: businessConfig.name,
legalName: businessConfig.legalName,
url: businessConfig.website,
logo: {
"@type": "ImageObject",
url: `${businessConfig.website}/rmv-logo.png`,
width: 180,
height: 45,
},
}
structuredData.mainEntityOfPage = {
"@type": "WebPage",
"@id": url,
}
}
return structuredData
}