420 lines
17 KiB
TypeScript
420 lines
17 KiB
TypeScript
import { notFound } from "next/navigation";
|
|
import type { Metadata } from "next";
|
|
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 { RequestMachineForm } from "@/components/forms/request-machine-form";
|
|
import { PublicInset, PublicPageHeader, PublicSurface } from "@/components/public-surface";
|
|
|
|
interface LocationPageProps {
|
|
params: Promise<{ location: string }>;
|
|
}
|
|
|
|
// Known non-location routes that should be handled by the catch-all route
|
|
const NON_LOCATION_ROUTES = ['machines-we-use', 'machines-for-sale'];
|
|
|
|
// Required for static export
|
|
export const dynamic = 'force-static';
|
|
export const dynamicParams = false;
|
|
|
|
// Generate static params for all locations
|
|
export async function generateStaticParams() {
|
|
try {
|
|
const slugs = getAllLocationSlugs();
|
|
|
|
// Ensure we have location slugs
|
|
if (!slugs || slugs.length === 0) {
|
|
console.error('No location slugs found! This will cause build failures with static export.');
|
|
// Return at least one valid param to prevent build failure
|
|
return [{ location: 'ogden-utah' }];
|
|
}
|
|
|
|
const params = slugs.map((slug) => ({
|
|
location: slug,
|
|
}));
|
|
|
|
// Ensure we return at least one param
|
|
if (params.length === 0) {
|
|
console.error('generateStaticParams returned empty array! This will cause build failures.');
|
|
return [{ location: 'ogden-utah' }];
|
|
}
|
|
|
|
return params;
|
|
} catch (error) {
|
|
console.error('Error generating static params:', error);
|
|
// Return at least one valid param to prevent build failure
|
|
return [{ location: 'ogden-utah' }];
|
|
}
|
|
}
|
|
|
|
// Generate metadata for each location page
|
|
export async function generateMetadata({ params }: LocationPageProps): Promise<Metadata> {
|
|
const { location } = await params;
|
|
|
|
// If this is a known non-location route, return notFound to let catch-all handle it
|
|
if (NON_LOCATION_ROUTES.includes(location)) {
|
|
return {
|
|
title: "Page Not Found | Rocky Mountain Vending",
|
|
};
|
|
}
|
|
|
|
const locationData = getLocationBySlug(location);
|
|
|
|
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-${location}`,
|
|
type: "website",
|
|
locale: "en_US",
|
|
siteName: businessConfig.name,
|
|
},
|
|
twitter: {
|
|
card: "summary_large_image",
|
|
title,
|
|
description,
|
|
},
|
|
alternates: {
|
|
canonical: `${businessConfig.website}/vending-machines-${location}`,
|
|
},
|
|
};
|
|
}
|
|
|
|
export default async function LocationPage({ params }: LocationPageProps) {
|
|
const { location } = await params;
|
|
|
|
// If this is a known non-location route, return notFound to let catch-all handle it
|
|
if (NON_LOCATION_ROUTES.includes(location)) {
|
|
notFound();
|
|
}
|
|
|
|
const locationData = getLocationBySlug(location);
|
|
|
|
if (!locationData) {
|
|
notFound();
|
|
}
|
|
|
|
// Generate structured data for the location
|
|
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-${location}`,
|
|
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) => 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 */}
|
|
<PublicPageHeader
|
|
align="center"
|
|
className="mb-12 md:mb-16"
|
|
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.`}
|
|
/>
|
|
|
|
{/* Local Anecdote */}
|
|
<div className="mb-12 max-w-4xl mx-auto">
|
|
<PublicSurface 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>
|
|
</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>
|
|
<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 down—it 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 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, i) => (
|
|
<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, i) => (
|
|
<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) => (
|
|
<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>
|
|
<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>
|
|
<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>
|
|
<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>
|
|
|
|
{/* CRM Form */}
|
|
<PublicSurface className="p-6 md: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">
|
|
Tell us about your location and we'll follow up within one business day to talk through the best machine mix.
|
|
</p>
|
|
</div>
|
|
<PublicInset className="mb-5 p-5 text-sm leading-relaxed text-muted-foreground">
|
|
Free placement is for qualifying business locations. Share your foot traffic, preferred machine types,
|
|
and any site constraints so our team can recommend the right setup.
|
|
</PublicInset>
|
|
<RequestMachineForm />
|
|
</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>
|
|
<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 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">
|
|
<Button asChild size="lg" className="bg-primary hover:bg-primary/90">
|
|
<Link href="#contact">Get Your Free Machine Today</Link>
|
|
</Button>
|
|
</div>
|
|
</section>
|
|
</article>
|
|
|
|
{/* Google Reviews Section */}
|
|
<ReviewsSection />
|
|
</>
|
|
);
|
|
}
|