/** * Clean WordPress/Elementor content and convert to styled React components */ import Image from 'next/image'; import React, { ReactNode } from 'react'; import { cleanHtmlEntities, getImagePath, getImageAlt, ImageMapping } from '@/lib/wordpress-content'; import { getAllLocations } from './location-data'; import { Card, CardContent } from '@/components/ui/card'; import { VendingMachineCard } from '@/components/vending-machine-card'; import { ImageCarousel } from '@/components/image-carousel'; interface CleanContentOptions { imageMapping?: ImageMapping; pageTitle?: string; // Page title to check for duplicates } /** * Clean and convert WordPress Elementor HTML to styled React components */ export function cleanWordPressContent( html: string, options: CleanContentOptions = {} ): ReactNode[] { if (!html || typeof html !== 'string') { return []; } const { imageMapping = {}, pageTitle } = options; const components: ReactNode[] = []; try { // Update location links to new format (currently just returns HTML unchanged) // TODO: If location links need updating in the future, implement URL pattern matching here // For example, convert old WordPress URLs to new Next.js route format // Example: /vending-machines/salt-lake-city/ -> /vending-machines-salt-lake-city-utah const htmlWithUpdatedLinks = html; // Remove Elementor wrapper and data attributes let cleaned = htmlWithUpdatedLinks // Remove Elementor wrapper div .replace(/]*data-elementor-type[^>]*>/gi, '') .replace(/<\/div>\s*$/, '') // Remove all Elementor data attributes .replace(/\s+data-[^=]*="[^"]*"/gi, '') // Remove Elementor-specific classes but keep useful ones .replace(/\s+class="elementor[^"]*"/gi, '') .replace(/\s+class="ekit[^"]*"/gi, '') // Remove SVG decorative shapes .replace(/]*>[\s\S]*?<\/svg>/gi, '') // Remove empty divs .replace(/]*>\s*<\/div>/gi, ''); // Extract sections (Elementor sections become our sections) const sectionRegex = /]*>([\s\S]*?)<\/section>/gi; let sectionMatch; const sections: string[] = []; while ((sectionMatch = sectionRegex.exec(cleaned)) !== null) { sections.push(sectionMatch[1]); } // If no sections found, treat entire content as one section if (sections.length === 0) { sections.push(cleaned); } sections.forEach((sectionHtml, sectionIndex) => { const sectionComponents: ReactNode[] = []; // Extract funfact widgets BEFORE cleaning (they contain SVG icons and special structure) const funfactRegex = /]*elementor-widget-elementskit-funfact[^>]*>([\s\S]*?)<\/div>\s*<\/div>\s*<\/div>/gi; const funfacts: Array<{ number: string; title: string; index: number }> = []; let funfactMatch; funfactRegex.lastIndex = 0; while ((funfactMatch = funfactRegex.exec(sectionHtml)) !== null) { const funfactHtml = funfactMatch[0]; // Extract text content (remove HTML tags and SVG) let text = funfactHtml.replace(/]*>[\s\S]*?<\/svg>/gi, ''); text = text.replace(/<[^>]+>/g, ' '); text = text.replace(/\s+/g, ' ').trim(); // Try to find number and title patterns // Funfacts typically have format: "NUMBER TITLE" or just text const parts = text.split(/\s+/); let number = ''; let title = ''; // Check if first part is a number if (parts.length > 0 && /^\d+[+\-]?$/.test(parts[0])) { number = parts[0]; title = parts.slice(1).join(' '); } else { // Try to extract from HTML structure const numberMatch = funfactHtml.match(/funfact-number[^>]*>([^<]+)]*>([^<]+)]*>(.*?)<\/h[1-6]>/gi; let headingMatch; const headings: Array<{ level: number; content: string; index: number }> = []; let isFirstHeading = true; // Helper function to check if headings are similar (for duplicate detection) const areHeadingsSimilar = (heading1: string, heading2: string): boolean => { const h1 = heading1.trim().toLowerCase(); const h2 = heading2.trim().toLowerCase(); // Exact match if (h1 === h2) return true; // Check if one contains the other if (h1.includes(h2) || h2.includes(h1)) { const shorter = h1.length < h2.length ? h1 : h2; const longer = h1.length >= h2.length ? h1 : h2; // If shorter is at least 60% of longer, consider similar if (shorter.length / longer.length >= 0.6) { return true; } // If shorter is a single word and appears as a word (not substring) in longer, consider similar // This catches cases like "Reviews" in "Rocky Mountain Vending: Reviews & Testimonials" if (shorter.split(/\s+/).length === 1) { // Check if it appears as a whole word (with word boundaries) const wordRegex = new RegExp(`\\b${shorter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i'); if (wordRegex.test(longer)) { return true; } } // If shorter is multiple words and all words appear in longer, consider similar const shorterWords = shorter.split(/\s+/).filter(w => w.length > 2); // Filter out short words like "a", "an", "the" if (shorterWords.length >= 2) { const allWordsMatch = shorterWords.every(word => { const wordRegex = new RegExp(`\\b${word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i'); return wordRegex.test(longer); }); if (allWordsMatch) { return true; } } } // Check if they start with the same words (for cases where one is a longer version) const h1Words = h1.split(/\s+/).slice(0, 3); // First 3 words const h2Words = h2.split(/\s+/).slice(0, 3); if (h1Words.length >= 2 && h2Words.length >= 2) { // Check if first 2 words match if (h1Words[0] === h2Words[0] && h1Words[1] === h2Words[1]) { return true; } } return false; }; headingRegex.lastIndex = 0; while ((headingMatch = headingRegex.exec(sectionHtml)) !== null) { const headingContent = cleanHtmlEntities(headingMatch[2]); const headingLevel = parseInt(headingMatch[1]); // Skip heading if: // 1. It's the first heading and matches the page title, OR // 2. It's an h1 that matches the page title (h1 should only be used for page title) if (pageTitle) { if (areHeadingsSimilar(headingContent, pageTitle)) { // Skip if it's the first heading, or if it's an h1 (h1 should only be page title) if (isFirstHeading || headingLevel === 1) { isFirstHeading = false; continue; } } } headings.push({ level: headingLevel, content: headingContent, index: headingMatch.index }); isFirstHeading = false; } // Check for slider/carousel widgets first (like elementskit-client-logo) // Look for the widget container that has elementskit-client-logo - match the entire widget including nested divs const sliderWidgetRegex = /]*elementor-widget-elementskit-client-logo[^>]*>([\s\S]*?)(?:<\/div>\s*){3,}/gi; let sliderMatch = sliderWidgetRegex.exec(sectionHtml); // Also try to find the slider div directly with the comment if (!sliderMatch) { const sliderDivRegex = /]*elementskit-clients-slider[^>]*>([\s\S]*?)/gi; sliderMatch = sliderDivRegex.exec(sectionHtml); } // Try a simpler approach - find any div with swiper-wrapper if (!sliderMatch) { const swiperWrapperRegex = /]*swiper-wrapper[^>]*>([\s\S]*?)<\/div>\s*<\/div>/gi; sliderMatch = swiperWrapperRegex.exec(sectionHtml); } let sliderImages: Array<{ src: string; alt: string; width?: number; height?: number; title?: string }> = []; let sliderIndex: number | null = null; if (sliderMatch) { sliderIndex = sliderMatch.index; const sliderContent = sliderMatch[1]; // Extract all images from the slider - look in swiper-slide divs const slideRegex = /]*swiper-slide[^>]*>([\s\S]*?)<\/div>\s*<\/div>/gi; let slideMatch; slideRegex.lastIndex = 0; while ((slideMatch = slideRegex.exec(sliderContent)) !== null) { const slideContent = slideMatch[1]; const imgMatch = slideContent.match(/]*)>/i); if (imgMatch) { const imgTag = imgMatch[0]; const srcMatch = imgTag.match(/src=["']([^"']+)["']/i); const altMatch = imgTag.match(/alt=["']([^"']*)["']/i); const titleMatch = slideContent.match(/title=["']([^"']*)["']/i) || imgTag.match(/title=["']([^"']*)["']/i); const widthMatch = imgTag.match(/width=["']?(\d+)["']?/i); const heightMatch = imgTag.match(/height=["']?(\d+)["']?/i); if (srcMatch) { // Fix malformed URLs (e.g., https:///wp-content -> https://rockymountainvending.com/wp-content) let imageSrc = srcMatch[1]; if (imageSrc.startsWith('https:///') || imageSrc.startsWith('http:///')) { imageSrc = imageSrc.replace(/^https?:\/\//, 'https://rockymountainvending.com/'); } else if (imageSrc.startsWith('https://wp-content') || imageSrc.startsWith('http://wp-content')) { imageSrc = imageSrc.replace(/^https?:\/\/wp-content/, 'https://rockymountainvending.com/wp-content'); } sliderImages.push({ src: imageSrc, alt: altMatch ? cleanHtmlEntities(altMatch[1]) : (titleMatch ? cleanHtmlEntities(titleMatch[1]) : ''), width: widthMatch ? parseInt(widthMatch[1]) : undefined, height: heightMatch ? parseInt(heightMatch[1]) : undefined, title: titleMatch ? cleanHtmlEntities(titleMatch[1]) : undefined }); } } } // If no slides found, try direct image extraction from slider content if (sliderImages.length === 0) { const sliderImageRegex = /]*)>/gi; let sliderImgMatch; sliderImageRegex.lastIndex = 0; while ((sliderImgMatch = sliderImageRegex.exec(sliderContent)) !== null) { const imgTag = sliderImgMatch[0]; const srcMatch = imgTag.match(/src=["']([^"']+)["']/i); const altMatch = imgTag.match(/alt=["']([^"']*)["']/i); const titleMatch = imgTag.match(/title=["']([^"']*)["']/i); const widthMatch = imgTag.match(/width=["']?(\d+)["']?/i); const heightMatch = imgTag.match(/height=["']?(\d+)["']?/i); if (srcMatch) { // Fix malformed URLs (e.g., https:///wp-content -> https://rockymountainvending.com/wp-content) let imageSrc = srcMatch[1]; if (imageSrc.startsWith('https:///') || imageSrc.startsWith('http:///')) { imageSrc = imageSrc.replace(/^https?:\/\//, 'https://rockymountainvending.com/'); } else if (imageSrc.startsWith('https://wp-content') || imageSrc.startsWith('http://wp-content')) { imageSrc = imageSrc.replace(/^https?:\/\/wp-content/, 'https://rockymountainvending.com/wp-content'); } sliderImages.push({ src: imageSrc, alt: altMatch ? cleanHtmlEntities(altMatch[1]) : (titleMatch ? cleanHtmlEntities(titleMatch[1]) : ''), width: widthMatch ? parseInt(widthMatch[1]) : undefined, height: heightMatch ? parseInt(heightMatch[1]) : undefined, title: titleMatch ? cleanHtmlEntities(titleMatch[1]) : undefined }); } } } } // Extract Elementor gallery widget items (e.g., repairs page image grid) const galleryImages: Array<{ src: string; alt: string; width?: number; height?: number; index: number }> = []; const galleryImageRegex = /]*class="[^"]*e-gallery-image[^"]*"[^>]*data-thumbnail="([^"]+)"[^>]*data-width="(\d+)"[^>]*data-height="(\d+)"[^>]*aria-label="([^"]*)"[^>]*>/gi; let galleryMatch; galleryImageRegex.lastIndex = 0; while ((galleryMatch = galleryImageRegex.exec(sectionHtml)) !== null) { let imageSrc = galleryMatch[1]; // Fix malformed URLs (e.g., https:///wp-content -> https://rockymountainvending.com/wp-content) if (imageSrc.startsWith('https:///') || imageSrc.startsWith('http:///')) { imageSrc = imageSrc.replace(/^https?:\/\//, 'https://rockymountainvending.com/'); } else if (imageSrc.startsWith('https://wp-content') || imageSrc.startsWith('http://wp-content')) { imageSrc = imageSrc.replace( /^https?:\/\/wp-content/, 'https://rockymountainvending.com/wp-content' ); } const width = parseInt(galleryMatch[2], 10); const height = parseInt(galleryMatch[3], 10); const alt = cleanHtmlEntities(galleryMatch[4] || ''); galleryImages.push({ src: imageSrc, alt, width: Number.isNaN(width) ? undefined : width, height: Number.isNaN(height) ? undefined : height, index: galleryMatch.index }); } // Extract and process regular images (excluding slider images) const imageRegex = /]*)>/gi; const images: Array<{ src: string; alt: string; width?: number; height?: number; index: number }> = []; let imageMatch; imageRegex.lastIndex = 0; while ((imageMatch = imageRegex.exec(sectionHtml)) !== null) { // Skip if this image is part of the slider if (sliderIndex !== null && imageMatch.index >= sliderIndex && imageMatch.index < sliderIndex + sliderMatch![0].length) { continue; } const imgTag = imageMatch[0]; const srcMatch = imgTag.match(/src=["']([^"']+)["']/i); const altMatch = imgTag.match(/alt=["']([^"']*)["']/i); const widthMatch = imgTag.match(/width=["']?(\d+)["']?/i); const heightMatch = imgTag.match(/height=["']?(\d+)["']?/i); if (srcMatch) { // Fix malformed URLs (e.g., https:///wp-content -> https://rockymountainvending.com/wp-content) let imageSrc = srcMatch[1]; if (imageSrc.startsWith('https:///') || imageSrc.startsWith('http:///')) { imageSrc = imageSrc.replace(/^https?:\/\//, 'https://rockymountainvending.com/'); } else if (imageSrc.startsWith('https://wp-content') || imageSrc.startsWith('http://wp-content')) { imageSrc = imageSrc.replace(/^https?:\/\/wp-content/, 'https://rockymountainvending.com/wp-content'); } // Fix specific auto-repair image URL if (imageSrc.includes('Vending-Machine-at-an-Auto-Repair-Facility') && !imageSrc.includes('rockymountainvending.com')) { imageSrc = 'https://rockymountainvending.com/wp-content/uploads/2024/01/Vending-Machine-at-an-Auto-Repair-Facility-768x1024.webp'; } images.push({ src: imageSrc, alt: altMatch ? cleanHtmlEntities(altMatch[1]) : '', width: widthMatch ? parseInt(widthMatch[1]) : undefined, height: heightMatch ? parseInt(heightMatch[1]) : undefined, index: imageMatch.index }); } } // Extract paragraphs const paragraphRegex = /]*>(.*?)<\/p>/gi; const paragraphs: Array<{ content: string; index: number; hasList: boolean }> = []; let paraMatch; paragraphRegex.lastIndex = 0; while ((paraMatch = paragraphRegex.exec(sectionHtml)) !== null) { const content = paraMatch[1]; const hasList = /<(ul|ol)[^>]*>/i.test(content); const cleanContent = cleanHtmlEntities(content).trim(); if (cleanContent.length > 0) { paragraphs.push({ content: cleanContent, index: paraMatch.index, hasList }); } } // Extract lists - handle nested structures better // First try to find lists at the top level const listRegex = /<(ul|ol)[^>]*>([\s\S]*?)<\/\1>/gi; const lists: Array<{ listType: 'ul' | 'ol'; items: Array<{ text: string; html: string }>; index: number }> = []; let listMatch; // Reset regex lastIndex listRegex.lastIndex = 0; while ((listMatch = listRegex.exec(sectionHtml)) !== null) { const listType = listMatch[1] as 'ul' | 'ol'; const listContent = listMatch[2]; const itemRegex = /]*>(.*?)<\/li>/gi; const items: Array<{ text: string; html: string }> = []; let itemMatch; // Reset item regex itemRegex.lastIndex = 0; while ((itemMatch = itemRegex.exec(listContent)) !== null) { const itemHtml = itemMatch[1]; // Clean HTML entities but preserve basic formatting const cleanText = cleanHtmlEntities(itemHtml); items.push({ text: cleanText, html: itemHtml // Keep original HTML for processing }); } if (items.length > 0) { lists.push({ listType: listType, items, index: listMatch.index }); } } // If no lists found at top level, try to find them in paragraphs (they might be nested) if (lists.length === 0) { // Look for lists inside paragraph content - need to use original HTML, not cleaned const paragraphRegexOriginal = /]*>(.*?)<\/p>/gi; let paraMatchOriginal; paragraphRegexOriginal.lastIndex = 0; while ((paraMatchOriginal = paragraphRegexOriginal.exec(sectionHtml)) !== null) { const paraContent = paraMatchOriginal[1]; const paraListRegex = /<(ul|ol)[^>]*>([\s\S]*?)<\/\1>/gi; let paraListMatch; paraListRegex.lastIndex = 0; while ((paraListMatch = paraListRegex.exec(paraContent)) !== null) { const listType = paraListMatch[1] as 'ul' | 'ol'; const listContent = paraListMatch[2]; const itemRegex = /]*>(.*?)<\/li>/gi; const items: Array<{ text: string; html: string }> = []; let itemMatch; itemRegex.lastIndex = 0; while ((itemMatch = itemRegex.exec(listContent)) !== null) { const itemHtml = itemMatch[1]; items.push({ text: cleanHtmlEntities(itemHtml), html: itemHtml }); } if (items.length > 0) { lists.push({ listType: listType, items, index: paraMatchOriginal.index + (paraListMatch.index || 0) }); } } } } // Group images that should be side-by-side (images close together in same section) const imageGroups: Array> = []; const processedImageIndices = new Set(); images.forEach((img, idx) => { if (processedImageIndices.has(idx)) return; // Check if this image is in a column layout const imgContext = sectionHtml.substring( Math.max(0, img.index - 2000), Math.min(sectionHtml.length, img.index + 2000) ); const isInColumn = /elementor-col-(33|50|66|25|75)/i.test(imgContext); const group = [img]; processedImageIndices.add(idx); // Find other images close to this one (within 2000 chars) images.forEach((otherImg, otherIdx) => { if (otherIdx !== idx && !processedImageIndices.has(otherIdx)) { const distance = Math.abs(otherImg.index - img.index); // Check if they're in the same section (no section break between them) const betweenContent = sectionHtml.substring( Math.min(img.index, otherImg.index), Math.max(img.index, otherImg.index) ); const hasSectionBreak = /<\/section>/i.test(betweenContent); if (distance < 2000 && !hasSectionBreak) { // If first image is in a column, check if second is too if (isInColumn) { const otherContext = sectionHtml.substring( Math.max(0, otherImg.index - 2000), Math.min(sectionHtml.length, otherImg.index + 2000) ); const otherIsInColumn = /elementor-col-(33|50|66|25|75)/i.test(otherContext); if (otherIsInColumn) { group.push(otherImg); processedImageIndices.add(otherIdx); } } else { // If first image is not in column, group any nearby images (for general side-by-side) group.push(otherImg); processedImageIndices.add(otherIdx); } } } }); // Only create group if there are 2+ images if (group.length > 1) { imageGroups.push(group.map(img => ({ ...img, type: 'image' as const }))); } else { // Single image, don't group processedImageIndices.delete(idx); } }); // Check if this section should have machine cards (detect "Vending Machines for Gyms" pattern) let shouldShowMachineCards = false; let machineCardsHeadingIndex: number | null = null; let machineCardsParagraphIndex: number | null = null; // Look for heading "Vending Machines for Gyms" or similar patterns // Also match "Vending Machines We Use for..." variations const gymMachinesHeading = headings.find(h => { const cleanHeading = cleanHtmlEntities(h.content.replace(/<[^>]+>/g, '')).toLowerCase(); return (cleanHeading.includes('vending machines for') || cleanHeading.includes('vending machines we use')) && (cleanHeading.includes('gym') || cleanHeading.includes('studio') || cleanHeading.includes('community') || cleanHeading.includes('dance') || cleanHeading.includes('car wash')); }); if (gymMachinesHeading) { machineCardsHeadingIndex = gymMachinesHeading.index; // Look for paragraph about machines - more flexible pattern matching const gymMachinesParagraph = paragraphs.find(p => { const cleanPara = cleanHtmlEntities(p.content.replace(/<[^>]+>/g, '')).toLowerCase(); // Match various patterns: // - "Many of our [gyms/studios/community centers/dance studios/car washes]" // - "Primarily we will install" // - "Many of our dance studios have just one" // - "Many of our community centers have many machines" // - "We offer vending machines that are perfectly suited" // - Any paragraph that mentions QFV, AMS, Crane, USI, or Seaga machines return (cleanPara.includes('many of our') && (cleanPara.includes('gym') || cleanPara.includes('studio') || cleanPara.includes('community') || cleanPara.includes('dance') || cleanPara.includes('car wash'))) || cleanPara.includes('primarily we will install') || (cleanPara.includes('many of our') && cleanPara.includes('vending machine')) || (cleanPara.includes('we offer vending machines') && (cleanPara.includes('car wash') || cleanPara.includes('location'))) || (cleanPara.includes('qfv') || cleanPara.includes('ams') || cleanPara.includes('crane') || cleanPara.includes('usi') || cleanPara.includes('seaga')); }); if (gymMachinesParagraph) { machineCardsParagraphIndex = gymMachinesParagraph.index; shouldShowMachineCards = true; } } // Machine data from machines-we-use page - QFV 50 should always be first const gymMachines = [ { name: "QFV 50 Combo", description: "The Ultimate Combo Machine: The QFV 50 stands out as our top combo machine, offering a remarkable blend of style and functionality. With 30 customizable drink selections, it caters to a variety of tastes and preferences.", image: "/images/wordpress/EH0A1551-HDR.webp", alt: "Quick Fresh Vending QFV 50 combo vending machine showcasing dual compartments for snacks and drinks, with dimensions and payment system details.", selections: 50, items: 300, }, { name: "Crane Bev Max 4", description: "A Beverage Powerhouse: Bev Max 4 is a testament to efficiency in the vending world, boasting an impressive 48 drink selections. Its standout feature is the incredibly low fault rate of just 0.003%.", image: "/images/wordpress/Crane-Bev-Max-4-Drink.webp", alt: "Crane Bev Max 4 Classic", selections: 48, items: 400, }, { name: "USI 3130", description: "Compact Snack Solution: The USI 3130 is designed for locations where space is at a premium. Despite its compact size, it doesn't compromise on functionality.", image: "/images/wordpress/USI-3130-pps9s29iuucicxizctvma2nj0qezz66y25gy5nrsf4.webp", alt: "Rocky Mountain Vending USI 3130", selections: 24, items: 420, }, { name: "AMS 39 VCB", description: "Reliability Redefined: The AMS 39 VCB is a dependable combo machine that consistently meets our high standards. Its robust design ensures long-term reliability.", image: "/images/wordpress/AMS-39-VCB-Combo.webp", alt: "AMS 39 VCB", selections: 36, items: 375, }, { name: "Crane Merchant Media Snack", description: "The Snack Giant: When it comes to snack machines, the Crane Merchant Media Snack is unparalleled in capacity. It's the largest snack machine available, capable of holding a diverse range of snack options.", image: "/images/wordpress/Crane-Merchant-6-Media-Snack-pps9rxkbwo62qvpt49uhflu81t25wooadi7ir9yra8.webp", alt: "Crane Merchant Media 6", selections: 44, items: 375, }, ]; // Filter out images that are part of the machine cards section (they'll be replaced) const imagesToExclude = new Set(); const imageGroupsToExclude = new Set(); if (shouldShowMachineCards && machineCardsParagraphIndex !== null) { // Find all images after the paragraph that should be replaced // Look for images in the next section after the paragraph (within 10000 chars to catch all) const sectionEndIndex = machineCardsParagraphIndex + 10000; images.forEach((img, idx) => { if (img.index > machineCardsParagraphIndex! && img.index < sectionEndIndex) { // Check if it's one of the machine images (by checking alt text or src) const imgAlt = img.alt.toLowerCase(); const imgSrc = img.src.toLowerCase(); // Exclude ANY image in elementor/thumbs folder (these are the old machine images) // Also exclude any image that matches machine patterns if (imgSrc.includes('elementor/thumbs') || imgAlt.includes('crane bev max') || imgAlt.includes('bev max') || imgAlt.includes('usi 3130') || imgAlt.includes('usi') || imgAlt.includes('ams 39') || imgAlt.includes('ams') || imgAlt.includes('crane merchant') || imgAlt.includes('merchant media') || imgAlt.includes('qfv') || imgAlt.includes('quick fresh') || imgAlt.includes('seaga') || imgAlt.includes('seage') || imgSrc.includes('bev-max') || imgSrc.includes('usi-3130') || imgSrc.includes('ams-39') || imgSrc.includes('merchant') || imgSrc.includes('qfv') || imgSrc.includes('qfv-50') || imgSrc.includes('seaga') || imgSrc.includes('seage')) { imagesToExclude.add(idx); } } }); // Also exclude image groups if they're in the machine cards section imageGroups.forEach((group, groupIdx) => { if (group[0].index > machineCardsParagraphIndex! && group[0].index < sectionEndIndex) { // Check if any image in the group matches machine patterns // Prioritize checking for elementor/thumbs first (most reliable indicator) const hasMachineImage = group.some((img) => { const imgAlt = img.alt.toLowerCase(); const imgSrc = img.src.toLowerCase(); return imgSrc.includes('elementor/thumbs') || imgAlt.includes('crane bev max') || imgAlt.includes('bev max') || imgAlt.includes('usi 3130') || imgAlt.includes('usi') || imgAlt.includes('ams 39') || imgAlt.includes('ams') || imgAlt.includes('crane merchant') || imgAlt.includes('merchant media') || imgAlt.includes('qfv') || imgAlt.includes('quick fresh') || imgAlt.includes('seaga') || imgAlt.includes('seage') || imgSrc.includes('bev-max') || imgSrc.includes('usi-3130') || imgSrc.includes('ams-39') || imgSrc.includes('merchant') || imgSrc.includes('qfv') || imgSrc.includes('qfv-50') || imgSrc.includes('seaga') || imgSrc.includes('seage'); }); if (hasMachineImage) { imageGroupsToExclude.add(groupIdx); // Mark all images in this group as processed so they're excluded group.forEach((img) => { const imgIndex = images.findIndex(i => i.index === img.index); if (imgIndex !== -1) { processedImageIndices.add(imgIndex); } }); } } }); } // Process content in order const allElements = [ ...headings.map(h => ({ type: 'heading' as const, ...h })), ...funfacts.map(f => ({ type: 'funfact' as const, ...f })), ...(sliderImages.length > 0 ? [{ type: 'slider' as const, images: sliderImages, index: sliderIndex! }] : []), ...(galleryImages.length > 0 ? galleryImages.map(img => ({ type: 'gallery' as const, ...img })) : []), ...imageGroups.filter((group, groupIdx) => !imageGroupsToExclude.has(groupIdx)).map((group, groupIdx) => ({ type: 'image-group' as const, images: group, index: group[0].index })), ...images.filter((img, idx) => !processedImageIndices.has(idx) && !imagesToExclude.has(idx)).map(img => ({ type: 'image' as const, ...img })), ...paragraphs.map(p => ({ type: 'paragraph' as const, ...p })), ...lists.map(l => ({ type: 'list' as const, listType: l.listType, items: l.items, index: l.index })), // Add machine cards after the paragraph if detected ...(shouldShowMachineCards && machineCardsParagraphIndex !== null ? [{ type: 'machine-cards' as const, index: machineCardsParagraphIndex + 1000 }] : []) ].sort((a, b) => a.index - b.index); // Render elements allElements.forEach((element) => { if (element.type === 'heading') { const headingElement = element as { type: 'heading'; level: number; content: string; index: number }; const HeadingTag = `h${headingElement.level}` as keyof React.JSX.IntrinsicElements; // Clean HTML tags from heading content let cleanHeadingContent = headingElement.content; // Remove HTML tags but preserve text content cleanHeadingContent = cleanHeadingContent.replace(/<[^>]+>/g, ''); // Clean HTML entities cleanHeadingContent = cleanHtmlEntities(cleanHeadingContent); sectionComponents.push( {cleanHeadingContent} ); } else if (element.type === 'slider') { // Render slider images as a horizontal scrolling carousel with auto-rotation const carouselImages = element.images.map((img) => { const imagePath = getImagePath(img.src, imageMapping); const alt = getImageAlt(img.src, imageMapping, img.alt || img.title || ''); // Use the mapped local path if available, otherwise fix malformed URLs let finalSrc = imagePath; // If imagePath is a local path (starts with /), use it directly if (imagePath.startsWith('/') && !imagePath.startsWith('//')) { finalSrc = imagePath; } else { // Fix malformed URLs in imagePath first if (imagePath.startsWith('https:///') || imagePath.startsWith('http:///')) { finalSrc = imagePath.replace(/^https?:\/\//, 'https://rockymountainvending.com/'); } else if (imagePath.startsWith('https://wp-content') || imagePath.startsWith('http://wp-content')) { finalSrc = imagePath.replace(/^https?:\/\/wp-content/, 'https://rockymountainvending.com/wp-content'); } else if (imagePath.startsWith('http')) { finalSrc = imagePath; // Keep as-is if valid URL } else { // Fallback to original src and fix it finalSrc = img.src; if (finalSrc.startsWith('https:///') || finalSrc.startsWith('http:///')) { finalSrc = finalSrc.replace(/^https?:\/\//, 'https://rockymountainvending.com/'); } else if (finalSrc.startsWith('https://wp-content') || finalSrc.startsWith('http://wp-content')) { finalSrc = finalSrc.replace(/^https?:\/\/wp-content/, 'https://rockymountainvending.com/wp-content'); } else if (finalSrc.startsWith('http')) { finalSrc = finalSrc; // Keep as-is if valid URL } else if (finalSrc.startsWith('/')) { finalSrc = `https://rockymountainvending.com${finalSrc}`; } else { finalSrc = `https://rockymountainvending.com/${finalSrc}`; } } } return { src: finalSrc, alt: alt, title: img.title }; }); sectionComponents.push(
); } else if (element.type === 'gallery') { const imagePath = getImagePath(element.src, imageMapping); const alt = getImageAlt(element.src, imageMapping, element.alt); // Use the mapped local path if available, otherwise fix malformed URLs let finalSrc = imagePath; if (imagePath.startsWith('/') && !imagePath.startsWith('//')) { finalSrc = imagePath; } else { if (imagePath.startsWith('https:///') || imagePath.startsWith('http:///')) { finalSrc = imagePath.replace(/^https?:\/\//, 'https://rockymountainvending.com/'); } else if (imagePath.startsWith('https://wp-content') || imagePath.startsWith('http://wp-content')) { finalSrc = imagePath.replace( /^https?:\/\/wp-content/, 'https://rockymountainvending.com/wp-content' ); } else if (imagePath.startsWith('http')) { finalSrc = imagePath; } else { finalSrc = element.src; if (finalSrc.startsWith('https:///') || finalSrc.startsWith('http:///')) { finalSrc = finalSrc.replace(/^https?:\/\//, 'https://rockymountainvending.com/'); } else if (finalSrc.startsWith('https://wp-content') || finalSrc.startsWith('http://wp-content')) { finalSrc = finalSrc.replace( /^https?:\/\/wp-content/, 'https://rockymountainvending.com/wp-content' ); } else if (finalSrc.startsWith('http')) { finalSrc = finalSrc; } else if (finalSrc.startsWith('/')) { finalSrc = `https://rockymountainvending.com${finalSrc}`; } else { finalSrc = `https://rockymountainvending.com/${finalSrc}`; } } } // Constrain image dimensions per style guide (max 600px for standalone images) const constrainedWidth = element.width ? Math.min(element.width, 600) : 600; const constrainedHeight = element.height ? Math.min(element.height, 600) : 600; sectionComponents.push(
{element.width && element.height ? ( {alt} ) : (
{alt}
)}
); } else if (element.type === 'image-group') { // Render grouped images side-by-side const groupImages = element.images; const imageCount = groupImages.length; sectionComponents.push(
{groupImages.map((img, imgIdx) => { const imagePath = getImagePath(img.src, imageMapping); const alt = getImageAlt(img.src, imageMapping, img.alt); // Use the mapped local path if available, otherwise fix malformed URLs let finalSrc = imagePath; // If imagePath is a local path (starts with /), use it directly if (imagePath.startsWith('/') && !imagePath.startsWith('//')) { finalSrc = imagePath; } else { // Fix malformed URLs in imagePath first if (imagePath.startsWith('https:///') || imagePath.startsWith('http:///')) { finalSrc = imagePath.replace(/^https?:\/\//, 'https://rockymountainvending.com/'); } else if (imagePath.startsWith('https://wp-content') || imagePath.startsWith('http://wp-content')) { finalSrc = imagePath.replace(/^https?:\/\/wp-content/, 'https://rockymountainvending.com/wp-content'); } else if (imagePath.startsWith('http')) { finalSrc = imagePath; // Keep as-is if valid URL } else { // Fallback to original src and fix it finalSrc = img.src; if (finalSrc.startsWith('https:///') || finalSrc.startsWith('http:///')) { finalSrc = finalSrc.replace(/^https?:\/\//, 'https://rockymountainvending.com/'); } else if (finalSrc.startsWith('https://wp-content') || finalSrc.startsWith('http://wp-content')) { finalSrc = finalSrc.replace(/^https?:\/\/wp-content/, 'https://rockymountainvending.com/wp-content'); } else if (finalSrc.startsWith('http')) { finalSrc = finalSrc; // Keep as-is if valid URL } else if (finalSrc.startsWith('/')) { finalSrc = `https://rockymountainvending.com${finalSrc}`; } else { finalSrc = `https://rockymountainvending.com/${finalSrc}`; } } } // Constrain image dimensions per style guide (max 300px for grid images) const constrainedWidth = img.width ? Math.min(img.width, 300) : 300; const constrainedHeight = img.height ? Math.min(img.height, 300) : 300; return (
{img.width && img.height ? ( {alt} ) : (
{alt}
)}
); })}
); } else if (element.type === 'image') { const imagePath = getImagePath(element.src, imageMapping); const alt = getImageAlt(element.src, imageMapping, element.alt); // Use the mapped local path if available, otherwise fix malformed URLs let finalSrc = imagePath; // If imagePath is a local path (starts with /), use it directly if (imagePath.startsWith('/') && !imagePath.startsWith('//')) { finalSrc = imagePath; } else { // Fix malformed URLs in imagePath first if (imagePath.startsWith('https:///') || imagePath.startsWith('http:///')) { finalSrc = imagePath.replace(/^https?:\/\//, 'https://rockymountainvending.com/'); } else if (imagePath.startsWith('https://wp-content') || imagePath.startsWith('http://wp-content')) { finalSrc = imagePath.replace(/^https?:\/\/wp-content/, 'https://rockymountainvending.com/wp-content'); } else if (imagePath.startsWith('http')) { finalSrc = imagePath; // Keep as-is if valid URL } else { // Fallback to original src and fix it finalSrc = element.src; if (finalSrc.startsWith('https:///') || finalSrc.startsWith('http:///')) { finalSrc = finalSrc.replace(/^https?:\/\//, 'https://rockymountainvending.com/'); } else if (finalSrc.startsWith('https://wp-content') || finalSrc.startsWith('http://wp-content')) { finalSrc = finalSrc.replace(/^https?:\/\/wp-content/, 'https://rockymountainvending.com/wp-content'); } else if (finalSrc.startsWith('http')) { finalSrc = finalSrc; // Keep as-is if valid URL } else if (finalSrc.startsWith('/')) { finalSrc = `https://rockymountainvending.com${finalSrc}`; } else { finalSrc = `https://rockymountainvending.com/${finalSrc}`; } } } // Check if this image was in a column layout by looking at surrounding HTML // Look for elementor-col classes near this image's position const imageContext = sectionHtml.substring( Math.max(0, element.index - 1000), Math.min(sectionHtml.length, element.index + 1000) ); const hasColumn33 = /elementor-col-33|elementor-col-25/i.test(imageContext); const hasColumn50 = /elementor-col-50/i.test(imageContext); const hasColumn66 = /elementor-col-66|elementor-col-75/i.test(imageContext); // Check if there are multiple images in the same section (side-by-side layout) const imagesInSection = images.filter(img => img.index >= element.index - 2000 && img.index <= element.index + 2000 && img.index !== element.index ); // Determine container width based on original layout - follow style guide let containerClass = 'w-full'; let maxWidthClass = 'max-w-md'; let imageMaxWidth = 600; // Max 600px for standalone images per style guide let imageMaxHeight = 600; if (hasColumn33 || (imagesInSection.length >= 2 && imagesInSection.length < 4)) { containerClass = 'w-full md:w-1/3'; maxWidthClass = 'max-w-xs'; imageMaxWidth = 300; // Max 300px for grid images per style guide imageMaxHeight = 300; } else if (hasColumn50 || (imagesInSection.length === 1)) { containerClass = 'w-full md:w-1/2'; maxWidthClass = 'max-w-sm'; imageMaxWidth = 300; // Max 300px for grid images per style guide imageMaxHeight = 300; } else if (hasColumn66) { containerClass = 'w-full md:w-2/3'; maxWidthClass = 'max-w-md'; imageMaxWidth = 500; // Slightly larger for 2/3 width imageMaxHeight = 500; } // Constrain image dimensions per style guide const constrainedWidth = element.width ? Math.min(element.width, imageMaxWidth) : imageMaxWidth; const constrainedHeight = element.height ? Math.min(element.height, imageMaxHeight) : imageMaxHeight; sectionComponents.push(
{element.width && element.height ? ( {alt} ) : (
{alt}
)}
); } else if (element.type === 'paragraph') { // Process paragraph HTML to handle nested tags (em, strong, links, etc.) let processedHtml = element.content; // Convert strong/bold processedHtml = processedHtml.replace(/]*>(.*?)<\/strong>/gi, '$1'); processedHtml = processedHtml.replace(/]*>(.*?)<\/b>/gi, '$1'); // Convert emphasis/italic processedHtml = processedHtml.replace(/]*>(.*?)<\/em>/gi, '$1'); processedHtml = processedHtml.replace(/]*>(.*?)<\/i>/gi, '$1'); // Convert links processedHtml = processedHtml.replace(/]+href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi, (_match: string, href: string, text: string) => { return `${text}`; }); // Clean HTML entities processedHtml = cleanHtmlEntities(processedHtml); sectionComponents.push(

); } else if (element.type === 'list') { const listElement = element as { type: 'list'; listType: 'ul' | 'ol'; items: Array<{ text: string; html: string }>; index: number }; const ListTag = listElement.listType === 'ul' ? 'ul' : 'ol'; // Process list items to handle nested HTML const processedItems = listElement.items.map((item: { text: string; html: string }) => { // Extract and process nested HTML (strong, em, links, etc.) let processed = item.html; // Convert strong/bold processed = processed.replace(/]*>(.*?)<\/strong>/gi, '$1'); processed = processed.replace(/]*>(.*?)<\/b>/gi, '$1'); // Convert emphasis/italic processed = processed.replace(/]*>(.*?)<\/em>/gi, '$1'); processed = processed.replace(/]*>(.*?)<\/i>/gi, '$1'); // Convert links processed = processed.replace(/]+href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi, (match, href, text) => { return `${text}`; }); // Clean remaining HTML entities processed = cleanHtmlEntities(processed); return processed; }); sectionComponents.push( {processedItems.map((item: string, itemIndex: number) => (

  • ))} ); } else if (element.type === 'machine-cards') { // Render machine cards using the same component from machines-we-use page sectionComponents.push(
    {gymMachines.map((machine) => ( ))}
    ); } }); // If we have components, wrap them in a section if (sectionComponents.length > 0) { components.push(
    {sectionComponents}
    ); } }); // If no components were created, create a fallback if (components.length === 0) { const textContent = cleanHtmlEntities( html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim() ); if (textContent.length > 50) { components.push(

    {textContent}

    ); } } return components; } catch (error) { // Enhanced error logging for development if (process.env.NODE_ENV === 'development') { console.error('Error cleaning WordPress content:', error); if (error instanceof Error) { console.error('Error message:', error.message); console.error('Error stack:', error.stack); } console.error('HTML length:', html?.length || 0); console.error('HTML preview:', html?.substring(0, 500) || 'No HTML provided'); } else { // Minimal logging in production console.error('Error processing WordPress content'); } // Return error fallback with raw content preview return [

    Error processing content.

    {process.env.NODE_ENV === 'development' && error instanceof Error && (

    {error.message}

    )} {html && html.length > 0 && (
    )}
    ]; } }