/** * 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 { VendingMachineCard } from "@/components/vending-machine-card" import { ImageCarousel } from "@/components/image-carousel" interface CleanContentOptions { imageMapping?: ImageMapping pageTitle?: string // Page title to check for duplicates prioritizeFirstImage?: boolean } /** * 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, prioritizeFirstImage = false } = options const components: ReactNode[] = [] let hasPrioritizedImage = false 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}
)}
) if (prioritizeFirstImage && !hasPrioritizedImage) { hasPrioritizedImage = true } } 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}
)}
) })}
) if (prioritizeFirstImage && !hasPrioritizedImage) { hasPrioritizedImage = true } } 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}
)}
) if (prioritizeFirstImage && !hasPrioritizedImage) { hasPrioritizedImage = true } } 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 && (
    )}
    , ] } }