563 lines
23 KiB
TypeScript
563 lines
23 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 {
|
|
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 isSaltLakeCity = locationData.slug === "salt-lake-city-utah"
|
|
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 comparisonRows = [
|
|
["Credit card readers", "Yes", "Maybe", "Yes"],
|
|
["Locally owned", "Yes", "Yes", "Maybe"],
|
|
["Fast service", "Yes", "Maybe", "Maybe"],
|
|
["Large selection of products", "Yes", "Maybe", "Yes"],
|
|
["Quality of equipment used", "Excellent", "Varies", "Excellent"],
|
|
["Locked into Coke or Pepsi equipment", "No", "Maybe", "Probably"],
|
|
]
|
|
const saltLakeServiceLinks = [
|
|
{
|
|
title: "Traditional snacks and drinks",
|
|
body: "Stock the machine with the classic snacks, sodas, and convenience items most locations still want every day.",
|
|
href: "/food-and-beverage/traditional-options",
|
|
},
|
|
{
|
|
title: "Healthy snacks and drinks",
|
|
body: "Offer protein bars, better-for-you snacks, and drink choices that fit health-conscious teams and customers.",
|
|
href: "/food-and-beverage/healthy-options",
|
|
},
|
|
{
|
|
title: "Snack and drink delivery",
|
|
body: "Need product delivery or a broader refreshment setup beyond standard machine placement? We can help there too.",
|
|
href: "/food-and-beverage/snack-and-drink-delivery",
|
|
},
|
|
{
|
|
title: "Vending machine sales",
|
|
body: "Compare purchase options if you want equipment ownership instead of a free-placement arrangement.",
|
|
href: "/vending-machines/machines-for-sale",
|
|
},
|
|
{
|
|
title: "Parts, repairs, and moving",
|
|
body: "Get support for repair work, machine moving, replacement parts, and operational issues that need direct service help.",
|
|
href: "/services/parts",
|
|
},
|
|
{
|
|
title: "Training and support",
|
|
body: "Browse the support pages and machine guides if you need help with specific models, manuals, or machine operation questions.",
|
|
href: "/blog",
|
|
},
|
|
]
|
|
|
|
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={`Vending machine service for businesses in ${locationData.city}, ${locationData.stateAbbr}`}
|
|
description={`Rocky Mountain Vending helps businesses in ${locationData.city} with free placement for qualifying locations, machine sales, repairs, parts, and ongoing service across ${countyName} and nearby communities.`}
|
|
className="mb-12 md:mb-16"
|
|
/>
|
|
|
|
<div className="mx-auto mb-14 grid max-w-5xl gap-6 lg:grid-cols-[1.08fr_0.92fr]">
|
|
<PublicSurface className="p-6 md:p-8">
|
|
<h2 className="text-2xl font-semibold tracking-tight text-balance">
|
|
A local vending partner 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.5">
|
|
{industries.map((industry) => (
|
|
<span
|
|
key={industry}
|
|
className="rounded-full border border-border/60 bg-background px-3 py-1.5 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>
|
|
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
|
|
<GetFreeMachineCta buttonLabel="Check Placement Fit" />
|
|
<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>
|
|
</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-5 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) => (
|
|
<PublicSurface key={service.title} className="h-full p-6 md:p-7">
|
|
<h3 className="text-xl font-semibold">{service.title}</h3>
|
|
<p className="mt-3 leading-7 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>
|
|
</PublicSurface>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{isSaltLakeCity ? (
|
|
<section className="mx-auto mb-16 max-w-5xl space-y-6">
|
|
<PublicSurface className="p-6 md:p-8">
|
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
|
Why Rocky Mountain Vending
|
|
</p>
|
|
<h2 className="mt-3 text-3xl font-bold tracking-tight text-balance">
|
|
What Salt Lake City businesses usually want to verify before they choose a vendor.
|
|
</h2>
|
|
<p className="mt-4 text-base leading-relaxed text-muted-foreground">
|
|
Most businesses care about the same things: service speed,
|
|
product flexibility, local ownership, and whether the machines
|
|
feel modern and dependable after install.
|
|
</p>
|
|
<div className="mt-6 overflow-hidden rounded-[1.5rem] border border-border/60">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full min-w-[680px] border-collapse text-sm">
|
|
<thead className="bg-muted/55">
|
|
<tr className="border-b border-border/60">
|
|
<th className="px-4 py-3 text-left font-semibold text-foreground">
|
|
Comparison point
|
|
</th>
|
|
<th className="px-4 py-3 text-left font-semibold text-foreground">
|
|
Rocky Mountain Vending
|
|
</th>
|
|
<th className="px-4 py-3 text-left font-semibold text-foreground">
|
|
Small Vendor
|
|
</th>
|
|
<th className="px-4 py-3 text-left font-semibold text-foreground">
|
|
Large Vendor
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{comparisonRows.map((row, index) => (
|
|
<tr
|
|
key={row[0]}
|
|
className={`border-b border-border/50 ${index % 2 === 0 ? "bg-background" : "bg-muted/20"}`}
|
|
>
|
|
<td className="px-4 py-3 font-medium text-foreground">
|
|
{row[0]}
|
|
</td>
|
|
<td className="px-4 py-3 text-muted-foreground">
|
|
{row[1]}
|
|
</td>
|
|
<td className="px-4 py-3 text-muted-foreground">
|
|
{row[2]}
|
|
</td>
|
|
<td className="px-4 py-3 text-muted-foreground">
|
|
{row[3]}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</PublicSurface>
|
|
|
|
<PublicSurface className="p-6 md:p-8">
|
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
|
Salt Lake City Services
|
|
</p>
|
|
<h2 className="mt-3 text-3xl font-bold tracking-tight text-balance">
|
|
The service paths Salt Lake City businesses usually ask about first.
|
|
</h2>
|
|
<div className="mt-6 grid gap-4 md:grid-cols-2">
|
|
{saltLakeServiceLinks.map((item) => (
|
|
<PublicInset key={item.title} className="flex h-full flex-col">
|
|
<h3 className="text-lg font-semibold text-foreground">
|
|
{item.title}
|
|
</h3>
|
|
<p className="mt-2 flex-1 text-sm leading-relaxed text-muted-foreground">
|
|
{item.body}
|
|
</p>
|
|
<Link
|
|
href={item.href}
|
|
className="mt-4 inline-flex items-center gap-2 text-sm font-medium text-primary hover:underline"
|
|
>
|
|
Learn more
|
|
<ArrowRight className="h-4 w-4" />
|
|
</Link>
|
|
</PublicInset>
|
|
))}
|
|
</div>
|
|
</PublicSurface>
|
|
</section>
|
|
) : null}
|
|
|
|
<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 sm:flex-row sm:justify-center">
|
|
<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 />
|
|
</>
|
|
)
|
|
}
|