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>
225 lines
5.9 KiB
TypeScript
225 lines
5.9 KiB
TypeScript
/**
|
||
* Utilities for converting WordPress HTML content to React components
|
||
*/
|
||
|
||
import Image from 'next/image';
|
||
import { ReactNode } from 'react';
|
||
|
||
export interface WordPressImage {
|
||
originalUrl: string;
|
||
localPath: string;
|
||
alt: string;
|
||
needsAltText: boolean;
|
||
}
|
||
|
||
export interface ImageMapping {
|
||
[originalUrl: string]: WordPressImage;
|
||
}
|
||
|
||
/**
|
||
* Load image mapping
|
||
*/
|
||
export function loadImageMapping(): ImageMapping {
|
||
try {
|
||
// Use dynamic import for fs in Next.js server components
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// In Next.js, process.cwd() returns the code directory (where Next.js runs from)
|
||
const mappingPath = path.join(process.cwd(), 'lib', 'wordpress-data', 'image-mapping.json');
|
||
|
||
if (!fs.existsSync(mappingPath)) {
|
||
console.warn('Image mapping file not found at:', mappingPath);
|
||
return {};
|
||
}
|
||
|
||
const mappingData = JSON.parse(fs.readFileSync(mappingPath, 'utf8'));
|
||
const mapping: ImageMapping = {};
|
||
|
||
// Handle both array and object formats
|
||
if (Array.isArray(mappingData)) {
|
||
mappingData.forEach((img: any) => {
|
||
if (img && img.originalUrl) {
|
||
// Map the structure to match WordPressImage interface
|
||
mapping[img.originalUrl] = {
|
||
originalUrl: img.originalUrl,
|
||
localPath: img.localPath || img.path || '',
|
||
alt: img.alt || img.altText || '',
|
||
needsAltText: img.needsAltText || false,
|
||
};
|
||
}
|
||
});
|
||
} else if (typeof mappingData === 'object' && mappingData !== null) {
|
||
Object.assign(mapping, mappingData);
|
||
}
|
||
|
||
return mapping;
|
||
} catch (e) {
|
||
console.warn('Could not load image mapping:', e);
|
||
// Return empty object instead of throwing
|
||
return {};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Clean HTML entities
|
||
*/
|
||
export function cleanHtmlEntities(text: string): string {
|
||
if (!text || typeof text !== 'string') {
|
||
return '';
|
||
}
|
||
|
||
return text
|
||
// First, handle HTML entities
|
||
.replace(/’/g, "'")
|
||
.replace(/‘/g, "'")
|
||
.replace(/“/g, '"')
|
||
.replace(/”/g, '"')
|
||
.replace(/–/g, '–')
|
||
.replace(/—/g, '—')
|
||
.replace(/&/g, '&')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, "'")
|
||
.replace(/ /g, ' ')
|
||
.replace(/…/g, '...')
|
||
.replace(/\[…\]/g, '...')
|
||
// Remove any remaining HTML tags (except those we want to preserve in dangerouslySetInnerHTML contexts)
|
||
// This strips out any stray tags that shouldn't be in plain text content
|
||
.replace(/<(?!\/?(strong|b|em|i|a)\b)[^>]+>/gi, '')
|
||
.trim();
|
||
}
|
||
|
||
/**
|
||
* Clean HTML from headings - strips ALL HTML tags and entities
|
||
* Use this for headings where we never want any HTML markup
|
||
*/
|
||
export function cleanHeadingText(text: string): string {
|
||
if (!text || typeof text !== 'string') {
|
||
return '';
|
||
}
|
||
|
||
return text
|
||
// First, handle HTML entities
|
||
.replace(/’/g, "'")
|
||
.replace(/‘/g, "'")
|
||
.replace(/“/g, '"')
|
||
.replace(/”/g, '"')
|
||
.replace(/–/g, '–')
|
||
.replace(/—/g, '—')
|
||
.replace(/&/g, '&')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, "'")
|
||
.replace(/ /g, ' ')
|
||
.replace(/…/g, '...')
|
||
.replace(/\[…\]/g, '...')
|
||
// Remove ALL HTML tags - no exceptions for headings
|
||
.replace(/<[^>]+>/g, '')
|
||
// Clean up any leftover whitespace
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
/**
|
||
* Strip HTML tags and get clean text
|
||
*/
|
||
export function stripHtml(html: string): string {
|
||
if (!html || typeof html !== 'string') {
|
||
return '';
|
||
}
|
||
return html
|
||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||
.replace(/<[^>]+>/g, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
/**
|
||
* Extract text content from HTML
|
||
*/
|
||
export function extractTextContent(html: string): string {
|
||
return cleanHtmlEntities(stripHtml(html));
|
||
}
|
||
|
||
/**
|
||
* Normalize WordPress image URL (fix malformed URLs)
|
||
*/
|
||
function normalizeImageUrl(url: string): string {
|
||
if (!url) return url;
|
||
// Fix malformed URLs like https:///wp-content -> https://rockymountainvending.com/wp-content
|
||
if (url.startsWith('https:///') || url.startsWith('http:///')) {
|
||
return url.replace(/^https?:\/\//, 'https://rockymountainvending.com/');
|
||
}
|
||
return url;
|
||
}
|
||
|
||
/**
|
||
* Convert WordPress image URL to local path
|
||
*/
|
||
export function getImagePath(originalUrl: string, imageMapping: ImageMapping): string {
|
||
// Normalize the URL first
|
||
const normalizedUrl = normalizeImageUrl(originalUrl);
|
||
|
||
// Try lookup with normalized URL first
|
||
let mapping = imageMapping[normalizedUrl];
|
||
|
||
// If not found, try with original URL
|
||
if (!mapping) {
|
||
mapping = imageMapping[originalUrl];
|
||
}
|
||
|
||
if (mapping) {
|
||
// Handle both 'localPath' and 'altText' properties
|
||
const path = (mapping as any).localPath || (mapping as any).path;
|
||
if (path && !path.startsWith('http')) {
|
||
return path;
|
||
}
|
||
}
|
||
|
||
// If we have a normalized URL that's different from original, use it
|
||
if (normalizedUrl !== originalUrl && normalizedUrl.startsWith('http')) {
|
||
return normalizedUrl;
|
||
}
|
||
|
||
// Fallback to original URL if not found in mapping
|
||
return originalUrl;
|
||
}
|
||
|
||
/**
|
||
* Get alt text for image
|
||
*/
|
||
export function getImageAlt(originalUrl: string, imageMapping: ImageMapping, fallback: string = ''): string {
|
||
// Normalize the URL first
|
||
const normalizedUrl = normalizeImageUrl(originalUrl);
|
||
|
||
// Try lookup with normalized URL first
|
||
let mapping = imageMapping[normalizedUrl];
|
||
|
||
// If not found, try with original URL
|
||
if (!mapping) {
|
||
mapping = imageMapping[originalUrl];
|
||
}
|
||
|
||
if (mapping) {
|
||
// Handle both 'alt' and 'altText' properties
|
||
const alt = (mapping as any).alt || (mapping as any).altText;
|
||
if (alt) {
|
||
return alt;
|
||
}
|
||
}
|
||
return fallback || 'Vending machine image';
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|