import { notFound } from "next/navigation" import Link from "next/link" import { loadImageMapping, cleanHtmlEntities } from "@/lib/wordpress-content" import { generateSEOMetadata, generateStructuredData } from "@/lib/seo" import { getPageBySlug } from "@/lib/wordpress-data-loader" import { cleanWordPressContent } from "@/lib/clean-wordPress-content" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Separator } from "@/components/ui/separator" import { Button } from "@/components/ui/button" import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion" import { CheckCircle2, Package, Wrench, Phone, Clock, Shield, ShoppingCart, ArrowRight, MapPin, Truck, Award, Users, DollarSign, Search, CreditCard, } from "lucide-react" import { FAQSection } from "@/components/faq-section" import { Breadcrumbs } from "@/components/breadcrumbs" import type { Metadata } from "next" const WORDPRESS_SLUG = "parts-and-support" export async function generateMetadata(): Promise { const page = getPageBySlug(WORDPRESS_SLUG) if (!page) { return { title: "Vending Machine Parts & Support | Rocky Mountain Vending", description: "Vending machine parts and support services in Utah. Replacement parts for all major vending machine brands.", } } return generateSEOMetadata({ title: page.title || "Vending Machine Parts & Support", description: page.seoDescription || page.excerpt || "", excerpt: page.excerpt, date: page.date, modified: page.modified, image: page.images?.[0]?.localPath, path: "/services/parts", }) } export default async function PartsPage() { try { const page = getPageBySlug(WORDPRESS_SLUG) if (!page) { notFound() } let imageMapping: any = {} try { imageMapping = loadImageMapping() } catch (e) { imageMapping = {} } // Extract FAQs from content (similar to repairs page) const faqs: Array<{ question: string; answer: string }> = [] let contentWithoutFAQs = page.content || "" let contentWithoutBrands = "" if (page.content) { const contentStr = String(page.content) // Simple FAQ extraction - adjust regex based on content structure const questionMatches = contentStr.matchAll( /]*>([^<]+?)<\/h[2-4]>/gi ) const potentialAnswers = contentStr .split(/]*>/) .map((section, index) => { if (index > 0 && section.trim()) { return section.split(/]*>/)[0].trim() } return null }) .filter(Boolean) // Basic matching - this may need refinement based on actual content const questions = Array.from(questionMatches) .map((m) => m[1].trim()) .filter( (q) => q.toLowerCase().includes("?") || q.includes("What") || q.includes("How") ) questions .slice(0, Math.min(questions.length, potentialAnswers.length)) .forEach((question, index) => { if (potentialAnswers[index]) { const cleanQuestion = question .replace(/'/g, "'") .replace(/"/g, '"') .trim() faqs.push({ question: cleanQuestion, answer: potentialAnswers[index], }) } }) // Remove FAQ-like sections if found if (faqs.length > 0) { contentWithoutFAQs = contentStr .replace( /]*>.*?Questions.*?<\/h[2-4]>([\s\S]*?)(?=]*>.*?Compatible.*?Brands.*?<\/h[2-4]>[\s\S]*?(?=]*>.*?Vending.*?Machine.*?Brands.*?<\/h[2-4]>[\s\S]*?(?=]*>.*?Card.*?Reader.*?Brands.*?<\/h[2-4]>[\s\S]*?(?=]*>.*?Bill.*?Validator.*?<\/h[2-4]>[\s\S]*?(?=]*>.*?Coin.*?Mechanism.*?<\/h[2-4]>[\s\S]*?(?= { contentWithoutBrands = contentWithoutBrands.replace(pattern, "").trim() }) // Remove "Available Parts Include" section (we'll show it in a card) // Match various patterns - heading, paragraph, lists, etc. - be very aggressive const availablePartsPatterns = [ // Match heading followed by content until next heading or end /]*>.*?Available.*?Parts.*?Include.*?<\/h[1-6]>[\s\S]*?(?=]*>.*?Available.*?Parts.*?Include.*?<\/p>[\s\S]*?(?=]*>[\s\S]*?Bill validators and coin mechanisms[\s\S]*?Card readers for cashless payments[\s\S]*?<\/div>/gi, /]*>[\s\S]*?Bill validators and coin mechanisms[\s\S]*?Card readers for cashless payments[\s\S]*?<\/section>/gi, ] availablePartsPatterns.forEach((pattern) => { contentWithoutBrands = contentWithoutBrands.replace(pattern, "").trim() }) // Remove individual parts list items if they appear as separate elements const partsItems = [ "Bill validators and coin mechanisms", "Card readers for cashless payments", "Refrigeration components", "Keypads and control boards", "Motors and actuators", "Vending machine locks and security components", "Shelves, trays, and spirals for product dispensing", "LED lighting upgrades", "Replacement doors and panels", "Electrical components", ] // Remove any list items or paragraphs containing these specific parts partsItems.forEach((item) => { const itemPattern = new RegExp( `]*>.*?${item.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}.*?<\/li>`, "gi" ) contentWithoutBrands = contentWithoutBrands .replace(itemPattern, "") .trim() const paraPattern = new RegExp( `]*>.*?${item.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}.*?<\/p>`, "gi" ) contentWithoutBrands = contentWithoutBrands .replace(paraPattern, "") .trim() }) // Remove "Don't see the part you need?" paragraph if it's standalone contentWithoutBrands = contentWithoutBrands .replace( /]*>.*?Don.*?see.*?part.*?need.*?contact.*?us.*?<\/p>/gi, "" ) .trim() // Remove "Why Choose" section (we have it as a card below) const whyChoosePatterns = [ /]*>.*?Why.*?Choose.*?Rocky.*?Mountain.*?Vending.*?<\/h[1-6]>[\s\S]*?(?=]*>.*?Why.*?Choose.*?<\/h[1-6]>[\s\S]*?(?= { contentWithoutBrands = contentWithoutBrands.replace(pattern, "").trim() }) // Remove the benefits list items if they appear const benefitsItems = [ "Local Expertise", "Fast Delivery", "Quality Assurance", "Expert Support", "Competitive Pricing", "Custom Solutions", ] benefitsItems.forEach((item) => { const benefitPattern = new RegExp( `]*>.*?${item.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}.*?<\/li>`, "gi" ) contentWithoutBrands = contentWithoutBrands .replace(benefitPattern, "") .trim() const paraPattern = new RegExp( `]*>.*?${item.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}.*?<\/p>`, "gi" ) contentWithoutBrands = contentWithoutBrands .replace(paraPattern, "") .trim() }) // Remove duplicate opening paragraph that matches excerpt/description // This removes the intro paragraph that duplicates what's in the hero section // Match the exact paragraph with ellipsis entity and variations const duplicateIntroPatterns = [ // Match paragraph with ellipsis entity […] /]*>.*?Vending Machine Parts.*?Rocky Mountain Vending.*?trusted source.*?high-quality.*?vending machine parts.*?Salt Lake City.*?surrounding areas.*?Whether you need replacement components.*?\[…\].*?<\/p>/gi, /]*>.*?Vending Machine Parts.*?Rocky Mountain Vending.*?trusted source.*?high-quality.*?vending machine parts.*?Salt Lake City.*?\[…\].*?<\/p>/gi, // Match without "Vending Machine Parts" prefix but with same content /]*>.*?Rocky Mountain Vending.*?trusted source.*?high-quality.*?vending machine parts.*?Salt Lake City.*?surrounding areas.*?Whether you need replacement components.*?\[…\].*?<\/p>/gi, /]*>.*?Rocky Mountain Vending.*?trusted source.*?high-quality.*?vending machine parts.*?Salt Lake City.*?Whether you need replacement components.*?\[…\].*?<\/p>/gi, // Match without ellipsis but with full text /]*>.*?Vending Machine Parts.*?Rocky Mountain Vending.*?trusted source.*?high-quality.*?vending machine parts.*?Salt Lake City.*?surrounding areas.*?Whether you need replacement components.*?repairs or upgrades.*?we.*?got you covered.*?<\/p>/gi, /]*>.*?Rocky Mountain Vending.*?trusted source.*?high-quality.*?vending machine parts.*?Salt Lake City.*?surrounding areas.*?Whether you need replacement components.*?repairs or upgrades.*?we.*?got you covered.*?<\/p>/gi, // Match with "about us've got you covered" typo /]*>.*?Rocky Mountain Vending.*?trusted source.*?high-quality.*?vending machine parts.*?Salt Lake City.*?surrounding areas.*?Whether you need replacement components.*?about us.*?got you covered.*?<\/p>/gi, // Match with minimal overlap /]*>.*?Vending Machine Parts.*?Rocky Mountain Vending.*?trusted source.*?Whether you need replacement components.*?<\/p>/gi, /]*>.*?Rocky Mountain Vending.*?trusted source.*?Whether you need replacement components.*?<\/p>/gi, ] duplicateIntroPatterns.forEach((pattern) => { contentWithoutBrands = contentWithoutBrands.replace(pattern, "").trim() }) // Remove redundant "Vending Machine Parts" heading if it's just a duplicate of the page title contentWithoutBrands = contentWithoutBrands .replace( /]*>.*?Vending Machine Parts.*?<\/h[1-6]>\s*(?=]*>.*?Rocky Mountain Vending.*?trusted source)/gi, "" ) .trim() contentWithoutBrands = contentWithoutBrands .replace(/]*>.*?Vending Machine Parts.*?<\/h[1-6]>\s*$/gi, "") .trim() // Remove image references that are just placeholders or duplicates contentWithoutBrands = contentWithoutBrands .replace(/]*alt=["']Vending machine image["'][^>]*>/gi, "") .trim() contentWithoutBrands = contentWithoutBrands .replace( /]*>[\s\S]*?Vending machine image[\s\S]*?<\/figure>/gi, "" ) .trim() contentWithoutBrands = contentWithoutBrands .replace(/]*>[\s\S]*?Vending machine image[\s\S]*?<\/div>/gi, "") .trim() // Remove any remaining empty paragraphs or divs contentWithoutBrands = contentWithoutBrands .replace(/]*>\s*<\/p>/gi, "") .trim() contentWithoutBrands = contentWithoutBrands .replace(/]*>\s*<\/div>/gi, "") .trim() // Clean up multiple consecutive line breaks contentWithoutBrands = contentWithoutBrands .replace(/\n\s*\n\s*\n/g, "\n\n") .trim() // Remove ellipsis entities and other HTML entities that shouldn't be displayed contentWithoutBrands = contentWithoutBrands .replace(/\[…\]/gi, "") .trim() contentWithoutBrands = contentWithoutBrands .replace(/…/gi, "") .trim() // Remove any remaining HTML tags that are just showing as text (malformed) // This handles cases where HTML tags are being displayed as text instead of being rendered contentWithoutBrands = contentWithoutBrands .replace(/<p>/gi, "") .trim() contentWithoutBrands = contentWithoutBrands .replace(/<\/p>/gi, "") .trim() contentWithoutBrands = contentWithoutBrands .replace(/<\/?[^&]+>/gi, "") .trim() } else { contentWithoutBrands = contentWithoutFAQs } // Only show content if there's meaningful content (more than just whitespace and minimal text) const hasMeaningfulContent = contentWithoutBrands && contentWithoutBrands.trim().length > 100 && !contentWithoutBrands.match(/^[\s\n\r]*$/) const content = hasMeaningfulContent ? (
{cleanWordPressContent(String(contentWithoutBrands), { imageMapping, pageTitle: page.title, })}
) : null let structuredData try { structuredData = generateStructuredData({ title: page.title || "Vending Machine Parts & Support", description: page.seoDescription || page.excerpt || "", url: page.link || page.urlPath || `https://rockymountainvending.com/services/parts/`, datePublished: page.date, dateModified: page.modified || page.date, type: "WebPage", }) } catch (e) { structuredData = { "@context": "https://schema.org", "@type": "WebPage", headline: page.title || "Vending Machine Parts & Support", description: page.seoDescription || "", url: `https://rockymountainvending.com/services/parts/`, } } const cleanExcerpt = page.excerpt ? cleanHtmlEntities(page.excerpt) .replace(/…/g, "...") .replace(/\[…\]/g, "...") .replace(/\[\.\.\.\]/g, "...") .replace(/\[\.\.\./g, "...") .replace(/\.\.\.\]/g, "...") .replace(/\[\.\.\./g, "...") .replace(/\[\.\./g, "...") .replace(/\.\.\]/g, "...") .trim() : "" return ( <>