411 lines
16 KiB
TypeScript
411 lines
16 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";
|
|
|
|
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'];
|
|
|
|
// Generate static params for all locations
|
|
export async function generateStaticParams() {
|
|
const slugs = getAllLocationSlugs();
|
|
return slugs.map((slug) => ({
|
|
location: slug,
|
|
}));
|
|
}
|
|
|
|
// 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 */}
|
|
<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 door—no 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 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">{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="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) => (
|
|
<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 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-secondary hover:bg-secondary/90">
|
|
<Link href="#contact">Get Your Free Machine Today</Link>
|
|
</Button>
|
|
</div>
|
|
</section>
|
|
</article>
|
|
|
|
{/* Google Reviews Section */}
|
|
<ReviewsSection />
|
|
</>
|
|
);
|
|
}
|