Rocky_Mountain_Vending/artifacts/backups/formatting/app/[...slug]/page.tsx

741 lines
28 KiB
TypeScript

import { notFound } from 'next/navigation';
import { loadImageMapping } from '@/lib/wordpress-content';
import { generateSEOMetadata, generateStructuredData } from '@/lib/seo';
import { getPageBySlug, getAllPageSlugs } from '@/lib/wordpress-data-loader';
import { cleanWordPressContent } from '@/lib/clean-wordpress-content';
import { getLocationBySlug, getAllLocationSlugs } from '@/lib/location-data';
import { businessConfig, socialProfiles } from '@/lib/seo-config';
import { Phone, Mail, Globe, Clock, CreditCard, MapPin } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { ReviewsSection } from '@/components/reviews-section';
import { Button } from '@/components/ui/button';
import Link from 'next/link';
import type { Metadata } from 'next';
import React from 'react';
import { FAQSchema } from '@/components/faq-schema';
import { FAQSection } from '@/components/faq-section';
import { ContactPage } from '@/components/contact-page';
import { AboutPage } from '@/components/about-page';
import { WhoWeServePage } from '@/components/who-we-serve-page';
interface PageProps {
params: Promise<{ slug: string[] }>;
}
// Route mapping: navigation URLs -> WordPress page slugs
const routeMapping: Record<string, string> = {
// Services
'services/repairs': 'vending-machine-repairs',
'services/moving': 'vending-machine-repairs', // Placeholder - no moving page exists
'services/parts': 'parts-and-support',
'services': 'vending-machine-repairs', // Default to repairs page
// Vending Machines
'vending-machines': 'vending-machines', // Main vending machines page
'vending-machines/machines-we-use': 'vending-machines', // Use main page
'vending-machines/machines-for-sale': 'vending-machines-for-sale-in-utah',
// Who We Serve
'warehouses': 'streamlining-snack-and-beverage-access-in-warehouse-environments',
'auto-repair': 'enhancing-auto-repair-facilities-with-convenient-vending-solutions',
'gyms': 'vending-machine-for-your-gym',
'community-centers': 'vending-for-your-community-centers',
'dance-studios': 'vending-machine-for-your-dance-studio',
'car-washes': 'vending-machines-for-your-car-wash',
// Food & Beverage
'food-and-beverage/healthy-options': 'healthy-vending',
'food-and-beverage/traditional-options': 'traditional-vending',
'food-and-beverage/suppliers': 'diverse-vending-options-with-rocky-mountain-vendings-exclusive-wholesale-accounts',
// About
'about-us': 'about-us',
'about/faqs': 'faqs',
};
// Helper function to check if a route is a location page
function isLocationRoute(slugArray: string[]): boolean {
// Location pages follow pattern: vending-machines-{location}
// e.g., ["vending-machines-salt-lake-city-utah"] or ["vending-machines", "salt-lake-city-utah"]
if (slugArray.length === 1) {
const slug = slugArray[0];
// Check if it starts with "vending-machines-" and the rest is a valid location slug
if (slug.startsWith('vending-machines-')) {
const locationSlug = slug.replace('vending-machines-', '');
return !!getLocationBySlug(locationSlug);
}
} else if (slugArray.length === 2 && slugArray[0] === 'vending-machines') {
return !!getLocationBySlug(slugArray[1]);
}
return false;
}
// Helper function to resolve route to WordPress slug
function resolveRouteToSlug(slugArray: string[]): string | null {
const route = slugArray.join('/');
// Check if this is a location page - if so, return null to let Next.js handle it
// (location pages are handled by vending-machines-[location] route)
if (isLocationRoute(slugArray)) {
return null; // Let the location route handle it
}
// Check direct mapping first
if (routeMapping[route]) {
return routeMapping[route];
}
// Check if it's a direct WordPress slug
const directSlug = slugArray.join('-');
if (getPageBySlug(directSlug)) {
return directSlug;
}
// Check last segment as fallback (for nested routes)
if (slugArray.length > 1) {
const lastSegment = slugArray[slugArray.length - 1];
if (getPageBySlug(lastSegment)) {
return lastSegment;
}
}
// Try the full route as-is
if (getPageBySlug(route)) {
return route;
}
return null;
}
// Render location page component
function renderLocationPage(locationData: any, locationSlug: string) {
const structuredData = {
"@context": "https://schema.org",
"@type": "LocalBusiness",
name: businessConfig.name,
description: `Rocky Mountain Vending provides high-quality vending machines, vending machine sales, and vending machine repair services to businesses and schools across ${locationData.city}, ${locationData.state}.`,
url: `${businessConfig.website}/vending-machines-${locationSlug}`,
telephone: businessConfig.phoneFormatted,
priceRange: "$$",
foundingDate: businessConfig.openingDate,
areaServed: {
"@type": "City",
name: locationData.city,
address: {
"@type": "PostalAddress",
addressLocality: locationData.city,
addressRegion: locationData.stateAbbr,
postalCode: locationData.zipCode,
addressCountry: "US",
},
},
geo: {
"@type": "GeoCoordinates",
latitude: locationData.latitude,
longitude: locationData.longitude,
},
openingHoursSpecification: [
{
"@type": "OpeningHoursSpecification",
dayOfWeek: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
opens: "08:00",
closes: "17:00",
},
],
paymentAccepted: "Credit Card, Debit Card, American Express, Discover, MasterCard, Visa",
availableLanguage: ["English"],
sameAs: [
socialProfiles.linkedin,
socialProfiles.facebook,
socialProfiles.youtube,
socialProfiles.twitter,
locationData.chamberUrl,
locationData.cityWebsite,
...locationData.localLinks.map((link: any) => link.url),
],
};
return (
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
<article className="container mx-auto px-4 py-8 md:py-12">
{/* Hero Section */}
<header className="mb-12 text-center">
<h1 className="text-4xl md:text-5xl font-bold mb-6">
Vending Machine Supplier in {locationData.city}, {locationData.state}
</h1>
<p className="text-lg md:text-xl text-muted-foreground max-w-3xl mx-auto leading-relaxed">
Need a vending machine supplier in {locationData.city}, {locationData.state}? Rocky Mountain Vending has
been helping local businesses and schools since 2019 with quality vending solutions. We bring healthy
snacks, cold drinks, and dependable service right to your doorno hassle, no fuss.
</p>
</header>
{/* Local Anecdote */}
<div className="mb-12 max-w-4xl mx-auto">
<Card className="border-secondary/20 bg-secondary/5">
<CardContent className="p-6 md:p-8">
<p className="text-base md:text-lg leading-relaxed">
A while back, we worked with a {locationData.anecdote.customer} near {locationData.anecdote.location}.
We set them up with a {locationData.anecdote.solution}. Now {locationData.anecdote.outcome}. That's
what we do best—make vending simple.
</p>
</CardContent>
</Card>
</div>
{/* Services Section */}
<section className="mb-16 max-w-4xl mx-auto">
<h2 className="text-3xl font-bold mb-8">{locationData.h2Variants.services}</h2>
<p className="text-muted-foreground mb-8">
We handle everything from picking the right machine to keeping it running smoothly. Here's what we offer:
</p>
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardContent className="p-6">
<h3 className="text-xl font-semibold mb-3">Vending Machine Sales</h3>
<p className="text-muted-foreground">
Need a new machine? We've got options that fit your space and budget. Whether you run a school,
office, or warehouse, we'll help you choose one that works.
</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<h3 className="text-xl font-semibold mb-3">Vending Machine Repair</h3>
<p className="text-muted-foreground">
Machines break downit happens. When yours does, we fix it fast so you're not stuck with an empty
snack spot.
</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<h3 className="text-xl font-semibold mb-3">Healthy Snack and Beverage Options</h3>
<p className="text-muted-foreground">
We stock machines with healthy choices like granola bars, fruit snacks, and sparkling water. Great
for schools and gyms that want better options.
</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<h3 className="text-xl font-semibold mb-3">Maintenance Services</h3>
<p className="text-muted-foreground">
Regular checkups keep machines working right. We handle restocking, cleaning, and small fixes so you
don't have to think about it.
</p>
</CardContent>
</Card>
</div>
</section>
{/* Coverage Section */}
<section className="mb-16 max-w-4xl mx-auto">
<h2 className="text-3xl font-bold mb-6">{locationData.h2Variants.coverage}</h2>
<div className="prose prose-lg max-w-none">
<p className="text-muted-foreground mb-4">
We service all of {locationData.city}, including{" "}
{locationData.neighborhoods.map((n: string, i: number) => (
<span key={n}>
{i > 0 && i === locationData.neighborhoods.length - 1 && ", and "}
{i > 0 && i < locationData.neighborhoods.length - 1 && ", "}
{n}
</span>
))}
. We also deliver to nearby cities like{" "}
{locationData.nearbyCities.map((c: string, i: number) => (
<span key={c}>
{i > 0 && i === locationData.nearbyCities.length - 1 && ", and "}
{i > 0 && i < locationData.nearbyCities.length - 1 && ", "}
{c}
</span>
))}
free delivery within 50 miles of {locationData.city}.
</p>
<p className="text-muted-foreground mb-6">
The{" "}
<a
href={locationData.chamberUrl}
target="_blank"
rel="noopener noreferrer"
className="text-secondary hover:underline font-medium"
>
{locationData.chamberName}
</a>{" "}
connects us with local businesses, and we're proud to serve this community. Here are some helpful local
resources:
</p>
<ul className="space-y-2 mb-6">
{locationData.localLinks.map((link: any) => (
<li key={link.url}>
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="text-secondary hover:underline flex items-center gap-2"
>
<MapPin className="h-4 w-4" />
{link.name}
</a>
</li>
))}
</ul>
</div>
</section>
{/* Contact Section */}
<section className="mb-16 max-w-4xl mx-auto">
<h2 className="text-3xl font-bold mb-6">{locationData.h2Variants.contact}</h2>
<p className="text-muted-foreground mb-8">
We're open Monday through Friday, 8:00 AM to 5:00 PM. Closed on weekends, but you can always reach out by
phone or text.
</p>
<div className="grid gap-4 md:grid-cols-3 mb-8">
<Card>
<CardContent className="p-6 flex items-start gap-4">
<Phone className="h-6 w-6 text-secondary flex-shrink-0 mt-1" />
<div>
<div className="font-semibold mb-1">Phone</div>
<a href={businessConfig.phoneUrl} className="text-secondary hover:underline">
{businessConfig.phone}
</a>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6 flex items-start gap-4">
<Mail className="h-6 w-6 text-secondary flex-shrink-0 mt-1" />
<div>
<div className="font-semibold mb-1">Text</div>
<a href={businessConfig.smsUrl} className="text-secondary hover:underline">
{businessConfig.phone}
</a>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6 flex items-start gap-4">
<Globe className="h-6 w-6 text-secondary flex-shrink-0 mt-1" />
<div>
<div className="font-semibold mb-1">Website</div>
<a
href={businessConfig.website}
target="_blank"
rel="noopener noreferrer"
className="text-secondary hover:underline text-sm"
>
rockymountainvending.com
</a>
</div>
</CardContent>
</Card>
</div>
{/* CRM Form */}
<Card className="border-secondary/20">
<CardContent className="p-8">
<div className="text-center mb-6">
<h3 className="text-2xl font-bold mb-2">Get Your Free Vending Machine</h3>
<p className="text-muted-foreground">
Fill out the form below and we'll contact you within 24 hours to discuss your needs.
</p>
</div>
<iframe
src="https://link.sluice-box.io/widget/form/T76mIdPvC5iBwAI2wFPg"
style={{ width: "100%", height: "650px", border: "none", borderRadius: "4px" }}
id="inline-T76mIdPvC5iBwAI2wFPg"
data-layout="{'id':'INLINE'}"
data-trigger-type="alwaysShow"
data-trigger-value=""
data-activation-type="alwaysActivated"
data-activation-value=""
data-deactivation-type="neverDeactivate"
data-deactivation-value=""
data-form-name="Request Machine Short"
data-height="638"
data-layout-iframe-id="inline-T76mIdPvC5iBwAI2wFPg"
data-form-id="T76mIdPvC5iBwAI2wFPg"
title="Request Free Vending Machine Form"
/>
</CardContent>
</Card>
</section>
{/* Payment Options */}
<section className="mb-16 max-w-4xl mx-auto">
<h2 className="text-3xl font-bold mb-6">{locationData.h2Variants.payments}</h2>
<Card>
<CardContent className="p-6">
<div className="flex items-start gap-4 mb-4">
<CreditCard className="h-6 w-6 text-secondary flex-shrink-0 mt-1" />
<div>
<div className="font-semibold mb-2">Payment Methods</div>
<p className="text-muted-foreground">
We accept credit cards, debit cards, American Express, Discover, MasterCard, and Visa.
</p>
</div>
</div>
<div className="flex items-start gap-4">
<Clock className="h-6 w-6 text-secondary flex-shrink-0 mt-1" />
<div>
<div className="font-semibold mb-2">Language</div>
<p className="text-muted-foreground">
Our team speaks English, and we're happy to answer any questions you have.
</p>
</div>
</div>
</CardContent>
</Card>
</section>
{/* Why Choose Us */}
<section className="mb-16 max-w-4xl mx-auto">
<h2 className="text-3xl font-bold mb-6">{locationData.h2Variants.whyChoose}</h2>
<div className="prose prose-lg max-w-none">
<p className="text-muted-foreground mb-4">
Since 2019, we've been serving businesses and schools across Utah County, Salt Lake County, and Davis
County. We're local, we're fast, and we care about getting it right. About 95% of our {locationData.city}{" "}
clients stick with us because we show up when we say we will and fix problems quickly. That's based on our
own records, so we're confident in it.
</p>
<p className="text-muted-foreground mb-6">
We built our machines to handle Utah's weathercold winters, hot summers, all of it. You won't have to
worry about breakdowns when the temperature drops.
</p>
</div>
<div className="text-center">
<Button asChild size="lg" className="bg-secondary hover:bg-secondary/90">
<Link href="#contact">Get Your Free Machine Today</Link>
</Button>
</div>
</section>
</article>
{/* Google Reviews Section */}
<ReviewsSection />
</>
);
}
// Generate static params for all pages
export async function generateStaticParams() {
try {
const slugs = getAllPageSlugs();
const params: Array<{ slug: string[] }> = [];
// Add all WordPress page slugs
slugs.forEach((slug: string) => {
params.push({
slug: [slug], // Catch-all routes need arrays
});
});
// Add mapped routes (like /services, /services/repairs, etc.)
Object.keys(routeMapping).forEach((route) => {
const routeArray = route.split('/');
// Only add if it's not already added as a WordPress slug
if (!slugs.includes(route)) {
params.push({
slug: routeArray,
});
}
});
// Add location routes (e.g., /vending-machines-salt-lake-city-utah)
const locationSlugs = getAllLocationSlugs();
locationSlugs.forEach((locationSlug: string) => {
params.push({
slug: [`vending-machines-${locationSlug}`],
});
});
return params;
} catch (error) {
// Silently return empty array in production
if (process.env.NODE_ENV === 'development') {
console.error('Error generating static params:', error);
}
return [];
}
}
// Generate metadata for a page
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
try {
const { slug } = await params;
const slugArray = Array.isArray(slug) ? slug : [slug];
// Handle location routes
if (isLocationRoute(slugArray)) {
let locationSlug: string;
if (slugArray.length === 1) {
locationSlug = slugArray[0].replace('vending-machines-', '');
} else {
locationSlug = slugArray[1];
}
const locationData = getLocationBySlug(locationSlug);
if (!locationData) {
return {
title: 'Location Not Found | Rocky Mountain Vending',
};
}
const title = `Vending Machine Supplier in ${locationData.city}, ${locationData.stateAbbr} | Rocky Mountain Vending`;
const description = `Get FREE vending machines for your ${locationData.city} business! Rocky Mountain Vending provides quality vending machine sales, repairs, and service in ${locationData.city}, ${locationData.state}. Call (435) 233-9668.`;
return {
title,
description,
keywords: [
`vending machines ${locationData.city}`,
`vending machine supplier ${locationData.city}`,
`free vending machines ${locationData.city}`,
`vending machine repair ${locationData.city}`,
`${locationData.city} vending`,
...locationData.neighborhoods.map((n) => `vending machines ${n}`),
],
openGraph: {
title,
description,
url: `${businessConfig.website}/vending-machines-${locationSlug}`,
type: "website",
locale: "en_US",
siteName: businessConfig.name,
},
twitter: {
card: "summary_large_image",
title,
description,
},
};
}
const pageSlug = resolveRouteToSlug(slugArray);
if (!pageSlug) {
return {
title: 'Page Not Found | Rocky Mountain Vending',
};
}
const page = getPageBySlug(pageSlug);
if (!page) {
return {
title: 'Page Not Found | Rocky Mountain Vending',
};
}
return generateSEOMetadata({
title: page.title || 'Page',
description: page.seoDescription || page.excerpt || '',
excerpt: page.excerpt,
date: page.date,
modified: page.modified,
image: page.images?.[0]?.localPath,
});
} catch (error) {
// Silently return fallback metadata in production
if (process.env.NODE_ENV === 'development') {
console.error('Error generating metadata:', error);
}
return {
title: 'Rocky Mountain Vending',
description: 'Rocky Mountain Vending provides quality vending machine services in Utah.',
};
}
}
export default async function WordPressPage({ params }: PageProps) {
try {
const { slug } = await params;
const slugArray = Array.isArray(slug) ? slug : [slug];
// If this is a location route, render the location page
if (isLocationRoute(slugArray)) {
let locationSlug: string;
if (slugArray.length === 1) {
locationSlug = slugArray[0].replace('vending-machines-', '');
} else {
locationSlug = slugArray[1];
}
const locationData = getLocationBySlug(locationSlug);
if (!locationData) {
notFound();
}
// Render location page
return renderLocationPage(locationData, locationSlug);
}
const pageSlug = resolveRouteToSlug(slugArray);
if (!pageSlug) {
notFound();
}
const page = getPageBySlug(pageSlug);
if (!page) {
notFound();
}
// Load image mapping (optional, won't break if it fails)
let imageMapping: any = {};
try {
imageMapping = loadImageMapping();
} catch (e) {
// Silently fail - image mapping is optional
}
// Clean and render WordPress content as styled React components
const content = page.content ? (
<div className="max-w-none">
{cleanWordPressContent(String(page.content), {
imageMapping,
pageTitle: page.title // Pass page title to avoid duplicate headings
})}
</div>
) : (
<p className="text-muted-foreground">No content available.</p>
);
// Generate structured data
let structuredData;
try {
structuredData = generateStructuredData({
title: page.title || 'Page',
description: page.seoDescription || page.excerpt || '',
url: page.link || page.urlPath || `https://rockymountainvending.com/${pageSlug}/`,
datePublished: page.date,
dateModified: page.modified || page.date,
type: 'WebPage',
});
} catch (e) {
// Silently use fallback structured data in production
if (process.env.NODE_ENV === 'development') {
console.error('Error generating structured data:', e);
}
structuredData = {
'@context': 'https://schema.org',
'@type': 'WebPage',
headline: page.title || 'Page',
description: page.seoDescription || '',
url: `https://rockymountainvending.com/${pageSlug}/`,
};
}
// Extract FAQs from content if this is the FAQ page
const faqs: Array<{ question: string; answer: string }> = [];
if (pageSlug === 'faqs' && page.content) {
const contentStr = String(page.content);
// Extract FAQ items from accordion structure
const questionMatches = contentStr.matchAll(/<span class="ekit-accordion-title">([^<]+)<\/span>/g);
// Extract full answer content - match everything inside the card-body div until the closing div
const answerMatches = contentStr.matchAll(/<div class="elementskit-card-body ekit-accordion--content">([\s\S]*?)<\/div>\s*<\/div>\s*<!-- \.elementskit-card END -->/g);
const questions = Array.from(questionMatches).map(m => m[1].trim());
const answers = Array.from(answerMatches).map(m => {
// Keep HTML but clean up whitespace
let answer = m[1].trim();
// Remove the opening <p> and closing </p> if they wrap everything, but keep other HTML
// Clean up excessive whitespace but preserve HTML structure
answer = answer.replace(/\n\s*\n/g, '\n').replace(/>\s+</g, '><').trim();
return answer;
});
// Match questions with answers
questions.forEach((question, index) => {
if (answers[index]) {
faqs.push({ question, answer: answers[index] });
}
});
}
// Check if this is a "Who We Serve" page
const whoWeServeSlugs = [
'streamlining-snack-and-beverage-access-in-warehouse-environments',
'enhancing-auto-repair-facilities-with-convenient-vending-solutions',
'vending-machine-for-your-gym',
'vending-for-your-community-centers',
'vending-machine-for-your-dance-studio',
'vending-machines-for-your-car-wash',
];
const isWhoWeServePage = whoWeServeSlugs.includes(pageSlug);
// Debug logging for about-us page
if (process.env.NODE_ENV === 'development' && pageSlug === 'about-us') {
console.log('About Us Page Debug:', {
pageSlug,
faqsLength: faqs.length,
shouldRenderAboutPage: faqs.length === 0 && pageSlug === 'about-us'
});
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
/>
{faqs.length > 0 && (
<>
<FAQSchema
faqs={faqs}
pageUrl={page.link || page.urlPath || `https://rockymountainvending.com/${pageSlug}/`}
/>
<FAQSection faqs={faqs} />
</>
)}
{faqs.length === 0 && pageSlug === 'contact-us' && (
<ContactPage />
)}
{faqs.length === 0 && pageSlug === 'about-us' && (
<AboutPage />
)}
{faqs.length === 0 && isWhoWeServePage && (
<WhoWeServePage title={page.title || 'Page'} content={content} />
)}
{faqs.length === 0 && pageSlug !== 'contact-us' && pageSlug !== 'about-us' && !isWhoWeServePage && (
<article className="container mx-auto px-4 py-8 md:py-12 max-w-4xl">
<header className="mb-8">
<h1 className="text-4xl md:text-5xl font-bold mb-6">{page.title || 'Page'}</h1>
</header>
{content}
</article>
)}
</>
);
} catch (error) {
// Silently return error fallback in production
if (process.env.NODE_ENV === 'development') {
console.error('Error rendering page:', error);
}
return (
<div className="container mx-auto px-4 py-8 md:py-12">
<h1 className="text-4xl md:text-5xl font-bold mb-4">Error Loading Page</h1>
<p className="text-destructive">There was an error loading this page. Please try again later.</p>
{process.env.NODE_ENV === 'development' && (
<pre className="mt-4 p-4 bg-muted rounded">
{error instanceof Error ? error.message : String(error)}
</pre>
)}
</div>
);
}
}