413 lines
16 KiB
TypeScript
413 lines
16 KiB
TypeScript
import type { Metadata } from "next"
|
|
import Link from "next/link"
|
|
import { ArrowRight, Clock, Globe, Mail, MapPin, Phone, Wrench } from "lucide-react"
|
|
import { Breadcrumbs } from "@/components/breadcrumbs"
|
|
import { ReviewsSection } from "@/components/reviews-section"
|
|
import { GetFreeMachineCta } from "@/components/get-free-machine-cta"
|
|
import { Card, CardContent } from "@/components/ui/card"
|
|
import {
|
|
PublicInset,
|
|
PublicPageHeader,
|
|
PublicSurface,
|
|
} from "@/components/public-surface"
|
|
import type { LocationData } from "@/lib/location-data"
|
|
import { generateSEOMetadata } from "@/lib/seo"
|
|
import { buildAbsoluteUrl, buildLocationRoute } from "@/lib/seo-registry"
|
|
import { businessConfig } from "@/lib/seo-config"
|
|
|
|
const SALT_LAKE_COUNTY = new Set([
|
|
"salt-lake-city-utah",
|
|
"sandy-utah",
|
|
"draper-utah",
|
|
"murray-utah",
|
|
"midvale-utah",
|
|
"south-salt-lake-utah",
|
|
"west-valley-city-utah",
|
|
"west-jordan-utah",
|
|
"south-jordan-utah",
|
|
"riverton-utah",
|
|
"herriman-utah",
|
|
"holladay-utah",
|
|
"millcreek-utah",
|
|
"cottonwood-heights-utah",
|
|
])
|
|
|
|
const DAVIS_COUNTY = new Set([
|
|
"ogden-utah",
|
|
"layton-utah",
|
|
"clearfield-utah",
|
|
"syracuse-utah",
|
|
"clinton-utah",
|
|
])
|
|
|
|
function getCountyName(locationSlug: string) {
|
|
if (SALT_LAKE_COUNTY.has(locationSlug)) {
|
|
return "Salt Lake County"
|
|
}
|
|
|
|
if (DAVIS_COUNTY.has(locationSlug)) {
|
|
return "Davis County"
|
|
}
|
|
|
|
return "Utah County"
|
|
}
|
|
|
|
function getIndustryFocus(locationData: LocationData) {
|
|
const county = getCountyName(locationData.slug)
|
|
const countyIndustries: Record<string, string[]> = {
|
|
"Salt Lake County": [
|
|
"offices",
|
|
"warehouses",
|
|
"gyms",
|
|
"schools",
|
|
"service businesses",
|
|
],
|
|
"Davis County": [
|
|
"warehouses",
|
|
"auto repair shops",
|
|
"community centers",
|
|
"schools",
|
|
"offices",
|
|
],
|
|
"Utah County": [
|
|
"offices",
|
|
"training spaces",
|
|
"schools",
|
|
"fitness locations",
|
|
"community facilities",
|
|
],
|
|
}
|
|
|
|
return Array.from(
|
|
new Set([locationData.anecdote.customer, ...countyIndustries[county]])
|
|
).slice(0, 5)
|
|
}
|
|
|
|
export function generateLocationPageMetadata(
|
|
locationData: LocationData
|
|
): Metadata {
|
|
return generateSEOMetadata({
|
|
title: `Vending Machines in ${locationData.city}, ${locationData.stateAbbr}`,
|
|
description: `Rocky Mountain Vending provides free placement for qualifying locations, machine sales, repairs, and vending service for businesses in ${locationData.city}, ${locationData.stateAbbr}. Explore local coverage and contact options.`,
|
|
path: buildLocationRoute(locationData.slug),
|
|
keywords: [
|
|
`vending machines ${locationData.city.toLowerCase()}`,
|
|
`vending machine repair ${locationData.city.toLowerCase()}`,
|
|
`vending service ${locationData.city.toLowerCase()}`,
|
|
`${locationData.city.toLowerCase()} vending company`,
|
|
...locationData.neighborhoods.map(
|
|
(neighborhood) => `vending machines ${neighborhood.toLowerCase()}`
|
|
),
|
|
],
|
|
})
|
|
}
|
|
|
|
export function LocationLandingPage({
|
|
locationData,
|
|
}: {
|
|
locationData: LocationData
|
|
}) {
|
|
const countyName = getCountyName(locationData.slug)
|
|
const industries = getIndustryFocus(locationData)
|
|
const canonicalUrl = buildAbsoluteUrl(buildLocationRoute(locationData.slug))
|
|
const title = `Vending Machines in ${locationData.city}, ${locationData.stateAbbr}`
|
|
const description = `Rocky Mountain Vending provides free placement for qualifying locations, machine sales, repairs, and vending service for businesses in ${locationData.city}, ${locationData.stateAbbr}.`
|
|
|
|
const structuredData = {
|
|
"@context": "https://schema.org",
|
|
"@graph": [
|
|
{
|
|
"@type": "WebPage",
|
|
name: title,
|
|
description,
|
|
url: canonicalUrl,
|
|
isPartOf: {
|
|
"@type": "WebSite",
|
|
name: businessConfig.name,
|
|
url: businessConfig.website,
|
|
},
|
|
},
|
|
{
|
|
"@type": "Service",
|
|
name: `${businessConfig.name} vending services in ${locationData.city}, ${locationData.stateAbbr}`,
|
|
description,
|
|
url: canonicalUrl,
|
|
serviceType:
|
|
"Free vending machine placement, vending machine sales, repairs, parts, moving, and ongoing service",
|
|
provider: {
|
|
"@type": "Organization",
|
|
name: businessConfig.name,
|
|
url: businessConfig.website,
|
|
telephone: businessConfig.phoneFormatted,
|
|
email: businessConfig.email,
|
|
},
|
|
areaServed: {
|
|
"@type": "City",
|
|
name: locationData.city,
|
|
containedInPlace: {
|
|
"@type": "AdministrativeArea",
|
|
name: countyName,
|
|
},
|
|
address: {
|
|
"@type": "PostalAddress",
|
|
addressLocality: locationData.city,
|
|
addressRegion: locationData.stateAbbr,
|
|
postalCode: locationData.zipCode,
|
|
addressCountry: "US",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<script
|
|
type="application/ld+json"
|
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
|
|
/>
|
|
<article className="container mx-auto px-4 py-10 md:py-14">
|
|
<Breadcrumbs
|
|
className="mb-6"
|
|
items={[
|
|
{ label: "Service Areas", href: "/service-areas" },
|
|
{
|
|
label: `${locationData.city}, ${locationData.stateAbbr}`,
|
|
href: buildLocationRoute(locationData.slug),
|
|
},
|
|
]}
|
|
/>
|
|
|
|
<PublicPageHeader
|
|
align="center"
|
|
eyebrow="Local Service Area"
|
|
title={`${locationData.city}, ${locationData.stateAbbr} vending machine service`}
|
|
description={`Rocky Mountain Vending serves businesses in ${locationData.city}, ${locationData.stateAbbr} with free placement for qualifying locations, machine sales, repairs, parts, and ongoing restocking and service across ${countyName} and nearby communities.`}
|
|
className="mb-12 md:mb-16"
|
|
/>
|
|
|
|
<div className="mx-auto mb-12 grid max-w-5xl gap-6 lg:grid-cols-[1.1fr_0.9fr]">
|
|
<PublicSurface className="p-6 md:p-8">
|
|
<h2 className="text-2xl font-semibold tracking-tight text-balance">
|
|
Vending service for businesses across {locationData.city}
|
|
</h2>
|
|
<p className="mt-4 text-base leading-relaxed text-muted-foreground">
|
|
If your business is in {locationData.neighborhoods.join(", ")},
|
|
or elsewhere in {locationData.city}, we can review the location,
|
|
recommend the right machine mix, and explain what service would
|
|
look like once the machines are in place.
|
|
</p>
|
|
<p className="mt-4 text-base leading-relaxed text-muted-foreground">
|
|
We regularly work with businesses in {countyName} that want snack,
|
|
beverage, combo, or healthier vending options without asking
|
|
their own staff to handle stocking, service calls, or day-to-day
|
|
machine issues.
|
|
</p>
|
|
</PublicSurface>
|
|
|
|
<PublicSurface className="p-6 md:p-8">
|
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
|
Common Location Types
|
|
</p>
|
|
<h2 className="mt-3 text-2xl font-semibold tracking-tight text-balance">
|
|
Common business types we serve in {locationData.city}
|
|
</h2>
|
|
<div className="mt-5 flex flex-wrap gap-2">
|
|
{industries.map((industry) => (
|
|
<span
|
|
key={industry}
|
|
className="rounded-full border border-border/60 bg-background px-3 py-1 text-sm text-muted-foreground"
|
|
>
|
|
{industry}
|
|
</span>
|
|
))}
|
|
</div>
|
|
<PublicInset className="mt-6">
|
|
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
Looking for coverage beyond {locationData.city}? We also work in{" "}
|
|
{locationData.nearbyCities.join(", ")} and across our broader
|
|
Utah service area.
|
|
</p>
|
|
</PublicInset>
|
|
</PublicSurface>
|
|
</div>
|
|
|
|
<section className="mx-auto mb-16 max-w-5xl">
|
|
<h2 className="text-3xl font-bold tracking-tight text-balance">
|
|
Vending services available in {locationData.city}
|
|
</h2>
|
|
<div className="mt-8 grid gap-6 md:grid-cols-2">
|
|
{[
|
|
{
|
|
title: "Free vending placement",
|
|
body: "For qualifying locations, we review traffic, layout, and product demand before recommending placement and ongoing service.",
|
|
href: "/",
|
|
cta: "Start a placement request",
|
|
},
|
|
{
|
|
title: "Machine sales and upgrades",
|
|
body: "If you want to buy equipment, we can help you compare machine types, payment options, and layouts that work for the way your location operates.",
|
|
href: "/vending-machines",
|
|
cta: "See machine options",
|
|
},
|
|
{
|
|
title: "Repairs and troubleshooting",
|
|
body: "We help with machine issues, maintenance needs, and day-to-day operating problems throughout our Utah service area.",
|
|
href: "/services/repairs",
|
|
cta: "Explore repair service",
|
|
},
|
|
{
|
|
title: "Parts, manuals, and moving help",
|
|
body: "We also help with parts sourcing, manuals, and machine moving when your location needs more than routine vending service.",
|
|
href: "/services/parts",
|
|
cta: "View manuals and parts",
|
|
},
|
|
].map((service) => (
|
|
<Card key={service.title} className="h-full">
|
|
<CardContent className="p-6">
|
|
<h3 className="text-xl font-semibold">{service.title}</h3>
|
|
<p className="mt-3 text-muted-foreground">{service.body}</p>
|
|
<Link
|
|
href={service.href}
|
|
className="mt-5 inline-flex items-center gap-2 text-sm font-medium text-primary hover:underline"
|
|
>
|
|
{service.cta}
|
|
<ArrowRight className="h-4 w-4" />
|
|
</Link>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="mx-auto mb-16 grid max-w-5xl gap-6 lg:grid-cols-[1.05fr_0.95fr]">
|
|
<PublicSurface className="p-6 md:p-8">
|
|
<h2 className="text-3xl font-bold tracking-tight text-balance">
|
|
Coverage around {locationData.city}
|
|
</h2>
|
|
<p className="mt-4 text-base leading-relaxed text-muted-foreground">
|
|
We serve businesses throughout {locationData.city} and nearby
|
|
areas such as {locationData.nearbyCities.join(", ")}. If your
|
|
location is nearby but not listed, our team can confirm whether
|
|
the site fits our current route coverage.
|
|
</p>
|
|
<ul className="mt-6 space-y-3 text-sm text-muted-foreground">
|
|
<li className="flex items-start gap-3">
|
|
<MapPin className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
|
Neighborhood coverage includes {locationData.neighborhoods.join(", ")}.
|
|
</li>
|
|
<li className="flex items-start gap-3">
|
|
<Globe className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
|
City reference:{" "}
|
|
<a
|
|
href={locationData.cityWebsite}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="underline decoration-primary/35 underline-offset-4 hover:decoration-primary"
|
|
>
|
|
{locationData.cityWebsite.replace(/^https?:\/\//, "")}
|
|
</a>
|
|
</li>
|
|
<li className="flex items-start gap-3">
|
|
<Globe className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
|
Business network:{" "}
|
|
<a
|
|
href={locationData.chamberUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="underline decoration-primary/35 underline-offset-4 hover:decoration-primary"
|
|
>
|
|
{locationData.chamberName}
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
<Link
|
|
href="/service-areas"
|
|
className="mt-6 inline-flex items-center gap-2 text-sm font-medium text-primary hover:underline"
|
|
>
|
|
View all Utah service areas
|
|
<ArrowRight className="h-4 w-4" />
|
|
</Link>
|
|
</PublicSurface>
|
|
|
|
<PublicSurface className="p-6 md:p-8">
|
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
|
Contact Rocky Mountain Vending
|
|
</p>
|
|
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance">
|
|
Reach out about a {locationData.city} location
|
|
</h2>
|
|
<div className="mt-6 grid gap-4">
|
|
<PublicInset className="flex items-start gap-4">
|
|
<Phone className="mt-1 h-5 w-5 flex-shrink-0 text-primary" />
|
|
<div>
|
|
<div className="font-semibold text-foreground">Call</div>
|
|
<a href={businessConfig.phoneUrl} className="text-muted-foreground hover:underline">
|
|
{businessConfig.phone}
|
|
</a>
|
|
</div>
|
|
</PublicInset>
|
|
<PublicInset className="flex items-start gap-4">
|
|
<Mail className="mt-1 h-5 w-5 flex-shrink-0 text-primary" />
|
|
<div>
|
|
<div className="font-semibold text-foreground">Email</div>
|
|
<a
|
|
href={`mailto:${businessConfig.email}`}
|
|
className="text-muted-foreground hover:underline"
|
|
>
|
|
{businessConfig.email}
|
|
</a>
|
|
</div>
|
|
</PublicInset>
|
|
<PublicInset className="flex items-start gap-4">
|
|
<Clock className="mt-1 h-5 w-5 flex-shrink-0 text-primary" />
|
|
<div>
|
|
<div className="font-semibold text-foreground">Hours</div>
|
|
<p className="text-muted-foreground">
|
|
Monday through Friday, 8:00 AM to 5:00 PM
|
|
</p>
|
|
</div>
|
|
</PublicInset>
|
|
<PublicInset className="flex items-start gap-4">
|
|
<Wrench className="mt-1 h-5 w-5 flex-shrink-0 text-primary" />
|
|
<div>
|
|
<div className="font-semibold text-foreground">
|
|
Service questions
|
|
</div>
|
|
<p className="text-muted-foreground">
|
|
Ask about placement, repairs, moving, parts, or machine
|
|
sales for your {locationData.city} business.
|
|
</p>
|
|
</div>
|
|
</PublicInset>
|
|
</div>
|
|
</PublicSurface>
|
|
</section>
|
|
|
|
<PublicSurface className="mx-auto max-w-4xl p-6 md:p-8">
|
|
<div className="text-center">
|
|
<h2 className="text-3xl font-bold tracking-tight text-balance">
|
|
Request vending service for your {locationData.city} location
|
|
</h2>
|
|
<p className="mx-auto mt-4 max-w-2xl text-base leading-relaxed text-muted-foreground">
|
|
Tell us about your space, expected traffic, and the type of
|
|
vending help you need. We'll follow up with the next best
|
|
option for your location.
|
|
</p>
|
|
<div className="mt-6 flex flex-col items-center gap-3">
|
|
<GetFreeMachineCta buttonLabel="See If Your Location Qualifies" />
|
|
<Link
|
|
href="/contact-us#contact-form"
|
|
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"
|
|
>
|
|
Talk to Our Team
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</PublicSurface>
|
|
</article>
|
|
|
|
<ReviewsSection />
|
|
</>
|
|
)
|
|
}
|