726 lines
28 KiB
TypeScript
726 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';
|
|
import { PublicPageHeader, PublicSurface } from '@/components/public-surface';
|
|
import { GetFreeMachineCta } from '@/components/get-free-machine-cta';
|
|
|
|
// Required for static export - ensures this route is statically generated
|
|
export const dynamic = 'force-static';
|
|
export const dynamicParams = false;
|
|
|
|
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',
|
|
'contact-us': 'contact-us',
|
|
};
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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-10 md:py-14">
|
|
{/* Hero Section */}
|
|
<PublicPageHeader
|
|
align="center"
|
|
eyebrow="Local Service Area"
|
|
title={`Vending Machine Supplier in ${locationData.city}, ${locationData.state}`}
|
|
description={`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 door—no hassle, no fuss.`}
|
|
className="mb-12 md:mb-16"
|
|
/>
|
|
|
|
{/* Local Anecdote */}
|
|
<div className="mb-12 max-w-4xl mx-auto">
|
|
<PublicSurface>
|
|
<CardContent className="p-0 md:p-1">
|
|
<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>
|
|
</PublicSurface>
|
|
</div>
|
|
|
|
{/* Services Section */}
|
|
<section className="mb-16 max-w-4xl mx-auto">
|
|
<h2 className="text-3xl font-bold mb-8 tracking-tight text-balance">{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 className="rounded-[1.75rem] border border-border/70 bg-background shadow-[0_18px_45px_rgba(15,23,42,0.08)] transition-all hover:-translate-y-0.5 hover:border-primary/35">
|
|
<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 className="rounded-[1.75rem] border border-border/70 bg-background shadow-[0_18px_45px_rgba(15,23,42,0.08)] transition-all hover:-translate-y-0.5 hover:border-primary/35">
|
|
<CardContent className="p-6">
|
|
<h3 className="text-xl font-semibold mb-3">Vending Machine Repair</h3>
|
|
<p className="text-muted-foreground">
|
|
Machines break down—it happens. When yours does, we fix it fast so you're not stuck with an empty
|
|
snack spot.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="rounded-[1.75rem] border border-border/70 bg-background shadow-[0_18px_45px_rgba(15,23,42,0.08)] transition-all hover:-translate-y-0.5 hover:border-primary/35">
|
|
<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 className="rounded-[1.75rem] border border-border/70 bg-background shadow-[0_18px_45px_rgba(15,23,42,0.08)] transition-all hover:-translate-y-0.5 hover:border-primary/35">
|
|
<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 tracking-tight text-balance">{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="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="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 tracking-tight text-balance">{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 className="rounded-[1.75rem] border border-border/70 bg-background shadow-[0_18px_45px_rgba(15,23,42,0.08)] transition-all hover:-translate-y-0.5 hover:border-primary/35">
|
|
<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="hover:underline">
|
|
{businessConfig.phone}
|
|
</a>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="rounded-[1.75rem] border border-border/70 bg-background shadow-[0_18px_45px_rgba(15,23,42,0.08)] transition-all hover:-translate-y-0.5 hover:border-primary/35">
|
|
<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="hover:underline">
|
|
{businessConfig.phone}
|
|
</a>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="rounded-[1.75rem] border border-border/70 bg-background shadow-[0_18px_45px_rgba(15,23,42,0.08)] transition-all hover:-translate-y-0.5 hover:border-primary/35">
|
|
<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="hover:underline text-sm"
|
|
>
|
|
rockymountainvending.com
|
|
</a>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Placement CTA */}
|
|
<PublicSurface>
|
|
<div className="p-1 text-center">
|
|
<h3 className="text-2xl font-bold mb-2">Get Your Free Vending Machine</h3>
|
|
<p className="text-muted-foreground">
|
|
Tell us about your location and we'll follow up within one business day with recommendations for the right setup.
|
|
</p>
|
|
<div className="mt-6 flex flex-col items-center gap-3">
|
|
<GetFreeMachineCta buttonLabel="Get Free Placement" />
|
|
<a
|
|
href={businessConfig.publicCallUrl}
|
|
className="inline-flex min-h-11 items-center justify-center rounded-full border border-border bg-white px-4 text-sm font-medium text-foreground transition hover:border-primary/40 hover:text-primary"
|
|
>
|
|
Call Instead
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</PublicSurface>
|
|
</section>
|
|
|
|
{/* Payment Options */}
|
|
<section className="mb-16 max-w-4xl mx-auto">
|
|
<h2 className="text-3xl font-bold mb-6 tracking-tight text-balance">{locationData.h2Variants.payments}</h2>
|
|
<PublicSurface>
|
|
<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>
|
|
</PublicSurface>
|
|
</section>
|
|
|
|
{/* Why Choose Us */}
|
|
<section className="mb-16 max-w-4xl mx-auto">
|
|
<h2 className="text-3xl font-bold mb-6 tracking-tight text-balance">{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 weather—cold winters, hot summers, all of it. You won't have to
|
|
worry about breakdowns when the temperature drops.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="text-center">
|
|
<GetFreeMachineCta buttonLabel="Get Your Free Machine Today" />
|
|
</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) => {
|
|
if (locationSlug) {
|
|
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 at least one valid param to prevent build failure
|
|
return [{ slug: ['vending-machines-ogden-utah'] }];
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
|
|
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>
|
|
);
|
|
}
|
|
}
|