Next.js website for Rocky Mountain Vending company featuring: - Product catalog with Stripe integration - Service areas and parts pages - Admin dashboard with Clerk authentication - SEO optimized pages with JSON-LD structured data Co-authored-by: Cursor <cursoragent@cursor.com>
143 lines
3.5 KiB
TypeScript
143 lines
3.5 KiB
TypeScript
import type { Metadata } from 'next';
|
|
|
|
/**
|
|
* Clean HTML entities from text (e.g., & -> &, " -> ")
|
|
*/
|
|
function cleanHtmlEntities(text: string): string {
|
|
if (!text) return '';
|
|
return text
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, "'")
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/ /g, ' ')
|
|
.trim();
|
|
}
|
|
|
|
export interface SEOData {
|
|
title: string;
|
|
description: string;
|
|
excerpt?: string;
|
|
date?: string;
|
|
modified?: string;
|
|
image?: string;
|
|
}
|
|
|
|
/**
|
|
* Generate SEO metadata for a WordPress page/post
|
|
*/
|
|
export function generateSEOMetadata(data: SEOData): Metadata {
|
|
const { title, description, excerpt, date, modified, image } = data;
|
|
|
|
// Clean title (max 60 chars for SEO)
|
|
const cleanTitle = cleanHtmlEntities(title);
|
|
const seoTitle = cleanTitle.length > 60
|
|
? cleanTitle.substring(0, 57) + '...'
|
|
: cleanTitle;
|
|
|
|
const fullTitle = `${seoTitle} | Rocky Mountain Vending`;
|
|
|
|
// Clean description (150-160 chars)
|
|
let seoDescription = description || excerpt || '';
|
|
if (seoDescription.length > 160) {
|
|
seoDescription = seoDescription.substring(0, 157) + '...';
|
|
} else if (seoDescription.length < 120) {
|
|
seoDescription = seoDescription || 'Rocky Mountain Vending provides quality vending machine services in Utah.';
|
|
}
|
|
|
|
// Default image
|
|
const ogImage = image || '/images/rocky-mountain-vending-service-area-926x1024.webp';
|
|
|
|
const metadata: Metadata = {
|
|
title: fullTitle,
|
|
description: seoDescription,
|
|
openGraph: {
|
|
title: fullTitle,
|
|
description: seoDescription,
|
|
type: 'website',
|
|
images: [
|
|
{
|
|
url: ogImage,
|
|
width: 1200,
|
|
height: 630,
|
|
alt: cleanTitle,
|
|
},
|
|
],
|
|
siteName: 'Rocky Mountain Vending',
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image',
|
|
title: fullTitle,
|
|
description: seoDescription,
|
|
images: [ogImage],
|
|
},
|
|
};
|
|
|
|
// Add dates if available (via openGraph)
|
|
if (date || modified) {
|
|
metadata.openGraph = {
|
|
...metadata.openGraph,
|
|
...(date && { publishedTime: date }),
|
|
...(modified && { modifiedTime: modified }),
|
|
};
|
|
}
|
|
|
|
return metadata;
|
|
}
|
|
|
|
/**
|
|
* 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: any = {
|
|
'@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: 'Rocky Mountain Vending',
|
|
url: 'https://rockymountainvending.com',
|
|
};
|
|
structuredData.publisher = {
|
|
'@type': 'Organization',
|
|
name: 'Rocky Mountain Vending',
|
|
legalName: 'Rocky Mountain Vending LLC',
|
|
url: 'https://rockymountainvending.com',
|
|
logo: {
|
|
'@type': 'ImageObject',
|
|
url: 'https://rockymountainvending.com/rmv-logo.png',
|
|
width: 180,
|
|
height: 45,
|
|
},
|
|
};
|
|
structuredData.mainEntityOfPage = {
|
|
'@type': 'WebPage',
|
|
'@id': url,
|
|
};
|
|
}
|
|
|
|
return structuredData;
|
|
}
|
|
|