467 lines
16 KiB
TypeScript
467 lines
16 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 type { Metadata } from "next"
|
|
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 { Breadcrumbs } from "@/components/breadcrumbs"
|
|
import {
|
|
PublicInset,
|
|
PublicPageHeader,
|
|
PublicProse,
|
|
PublicSurface,
|
|
} from "@/components/public-surface"
|
|
import {
|
|
generateLocationPageMetadata,
|
|
LocationLandingPage,
|
|
} from "@/components/location-landing-page"
|
|
import Link from "next/link"
|
|
|
|
// 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/snack-and-drink-delivery": "snack-and-drink-delivery",
|
|
"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
|
|
}
|
|
|
|
// 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",
|
|
}
|
|
}
|
|
|
|
return generateLocationPageMetadata(locationData)
|
|
}
|
|
|
|
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,
|
|
path: `/${slugArray.join("/")}`,
|
|
})
|
|
} 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()
|
|
}
|
|
|
|
return <LocationLandingPage locationData={locationData} />
|
|
}
|
|
|
|
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)
|
|
const routePath = `/${slugArray.join("/")}`
|
|
|
|
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 max-w-5xl px-4 py-10 md:py-14">
|
|
<Breadcrumbs
|
|
className="mb-6"
|
|
items={[
|
|
{ label: "Home", href: "/" },
|
|
{ label: page.title || "Page", href: routePath },
|
|
]}
|
|
/>
|
|
<PublicPageHeader
|
|
eyebrow={pageSlug.startsWith("blog") ? "Article" : "Information"}
|
|
title={page.title || "Page"}
|
|
description={
|
|
page.seoDescription ||
|
|
page.excerpt ||
|
|
"Explore the details, service guidance, and next steps from Rocky Mountain Vending."
|
|
}
|
|
className="mx-auto mb-10 max-w-3xl text-center"
|
|
align="center"
|
|
/>
|
|
<PublicSurface className="p-5 md:p-7 lg:p-9">
|
|
<PublicProse className="mx-auto max-w-3xl">{content}</PublicProse>
|
|
</PublicSurface>
|
|
<PublicInset className="mx-auto mt-8 max-w-4xl border-primary/12 bg-[linear-gradient(180deg,rgba(41,160,71,0.06),rgba(255,255,255,0.84))] p-5 md:p-6">
|
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
<div>
|
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
|
Need Help Choosing The Right Next Step?
|
|
</p>
|
|
<h2 className="mt-2 text-2xl font-semibold tracking-tight text-foreground">
|
|
Talk with Rocky Mountain Vending
|
|
</h2>
|
|
<p className="mt-2 max-w-2xl text-sm leading-6 text-muted-foreground">
|
|
Reach out about placement, machine sales, repairs, moving help,
|
|
manuals, or parts and we'll point you in the right direction.
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-col gap-3 sm:flex-row">
|
|
<Link
|
|
href="/contact-us#contact-form"
|
|
className="inline-flex min-h-11 items-center justify-center rounded-full bg-primary px-5 text-sm font-medium text-primary-foreground transition hover:bg-primary/90"
|
|
>
|
|
Talk to Our Team
|
|
</Link>
|
|
<Link
|
|
href="/#request-machine"
|
|
className="inline-flex min-h-11 items-center justify-center rounded-full border border-border bg-white px-5 text-sm font-medium text-foreground transition hover:border-primary/35 hover:text-primary"
|
|
>
|
|
See If You Qualify
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</PublicInset>
|
|
</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>
|
|
)
|
|
}
|
|
}
|