Rocky_Mountain_Vending/app/[...slug]/page.tsx

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&apos;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>
)
}
}