deploy: ship 2026 local SEO overhaul
This commit is contained in:
parent
1948fd564e
commit
1c1c01069c
41 changed files with 3530 additions and 2404 deletions
|
|
@ -1,100 +1,98 @@
|
|||
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 { 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 type { Metadata } from 'next';
|
||||
import React from 'react';
|
||||
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 { PublicPageHeader, PublicSurface } from '@/components/public-surface';
|
||||
import { GetFreeMachineCta } from '@/components/get-free-machine-cta';
|
||||
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 {
|
||||
generateLocationPageMetadata,
|
||||
LocationLandingPage,
|
||||
} from "@/components/location-landing-page"
|
||||
|
||||
// Required for static export - ensures this route is statically generated
|
||||
export const dynamic = 'force-static';
|
||||
export const dynamicParams = false;
|
||||
export const dynamic = "force-static"
|
||||
export const dynamicParams = false
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ slug: string[] }>;
|
||||
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
|
||||
|
||||
"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',
|
||||
|
||||
"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',
|
||||
|
||||
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/traditional-options': 'traditional-vending',
|
||||
'food-and-beverage/suppliers': 'diverse-vending-options-with-rocky-mountain-vendings-exclusive-wholesale-accounts',
|
||||
|
||||
"food-and-beverage/healthy-options": "healthy-vending",
|
||||
"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',
|
||||
};
|
||||
"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('/');
|
||||
|
||||
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
|
||||
return null // Let the location route handle it
|
||||
}
|
||||
|
||||
|
||||
// Check direct mapping first
|
||||
if (routeMapping[route]) {
|
||||
return routeMapping[route];
|
||||
return routeMapping[route]
|
||||
}
|
||||
|
||||
|
||||
// Check if it's a direct WordPress slug
|
||||
const directSlug = slugArray.join('-');
|
||||
const directSlug = slugArray.join("-")
|
||||
if (getPageBySlug(directSlug)) {
|
||||
return directSlug;
|
||||
return directSlug
|
||||
}
|
||||
|
||||
|
||||
// Check last segment as fallback (for nested routes)
|
||||
if (slugArray.length > 1) {
|
||||
const lastSegment = slugArray[slugArray.length - 1];
|
||||
const lastSegment = slugArray[slugArray.length - 1]
|
||||
if (getPageBySlug(lastSegment)) {
|
||||
return lastSegment;
|
||||
return lastSegment
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Try the full route as-is
|
||||
if (getPageBySlug(route)) {
|
||||
return route;
|
||||
return route
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Helper function to check if a route is a location page
|
||||
|
|
@ -102,497 +100,166 @@ 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];
|
||||
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);
|
||||
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]);
|
||||
} else if (slugArray.length === 2 && slugArray[0] === "vending-machines") {
|
||||
return !!getLocationBySlug(slugArray[1])
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Render location page component
|
||||
function renderLocationPage(locationData: any, locationSlug: string) {
|
||||
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-${locationSlug}`,
|
||||
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: any) => link.url),
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
|
||||
|
||||
<article className="container mx-auto px-4 py-10 md:py-14">
|
||||
{/* Hero Section */}
|
||||
<PublicPageHeader
|
||||
align="center"
|
||||
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.`}
|
||||
className="mb-12 md:mb-16"
|
||||
/>
|
||||
|
||||
{/* Local Anecdote */}
|
||||
<div className="mb-12 max-w-4xl mx-auto">
|
||||
<PublicSurface>
|
||||
<CardContent className="p-0 md:p-1">
|
||||
<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>
|
||||
</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 className="rounded-[1.75rem] border border-border/70 bg-background shadow-[0_18px_45px_rgba(15,23,42,0.08)] transition-all hover:-translate-y-0.5 hover:border-primary/35">
|
||||
<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 className="rounded-[1.75rem] border border-border/70 bg-background shadow-[0_18px_45px_rgba(15,23,42,0.08)] transition-all hover:-translate-y-0.5 hover:border-primary/35">
|
||||
<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 className="rounded-[1.75rem] border border-border/70 bg-background shadow-[0_18px_45px_rgba(15,23,42,0.08)] transition-all hover:-translate-y-0.5 hover:border-primary/35">
|
||||
<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 className="rounded-[1.75rem] border border-border/70 bg-background shadow-[0_18px_45px_rgba(15,23,42,0.08)] transition-all hover:-translate-y-0.5 hover:border-primary/35">
|
||||
<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: string, i: number) => (
|
||||
<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: string, i: number) => (
|
||||
<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: any) => (
|
||||
<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 className="rounded-[1.75rem] border border-border/70 bg-background shadow-[0_18px_45px_rgba(15,23,42,0.08)] transition-all hover:-translate-y-0.5 hover:border-primary/35">
|
||||
<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 className="rounded-[1.75rem] border border-border/70 bg-background shadow-[0_18px_45px_rgba(15,23,42,0.08)] transition-all hover:-translate-y-0.5 hover:border-primary/35">
|
||||
<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 className="rounded-[1.75rem] border border-border/70 bg-background shadow-[0_18px_45px_rgba(15,23,42,0.08)] transition-all hover:-translate-y-0.5 hover:border-primary/35">
|
||||
<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>
|
||||
|
||||
{/* Placement CTA */}
|
||||
<PublicSurface>
|
||||
<div className="p-1 text-center">
|
||||
<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 with recommendations for the right setup.
|
||||
</p>
|
||||
<div className="mt-6 flex flex-col items-center gap-3">
|
||||
<GetFreeMachineCta buttonLabel="Get Free Placement" />
|
||||
<a
|
||||
href={businessConfig.publicCallUrl}
|
||||
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"
|
||||
>
|
||||
Call Instead
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<PublicSurface>
|
||||
<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>
|
||||
</PublicSurface>
|
||||
</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">
|
||||
<GetFreeMachineCta buttonLabel="Get Your Free Machine Today" />
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* Google Reviews Section */}
|
||||
<ReviewsSection />
|
||||
</>
|
||||
);
|
||||
return false
|
||||
}
|
||||
|
||||
// Generate static params for all pages
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
const slugs = getAllPageSlugs();
|
||||
const params: Array<{ slug: string[] }> = [];
|
||||
|
||||
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('/');
|
||||
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();
|
||||
const locationSlugs = getAllLocationSlugs()
|
||||
locationSlugs.forEach((locationSlug: string) => {
|
||||
if (locationSlug) {
|
||||
params.push({
|
||||
slug: [`vending-machines-${locationSlug}`],
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
return params;
|
||||
})
|
||||
|
||||
return params
|
||||
} catch (error) {
|
||||
// Silently return empty array in production
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Error generating static params:', error);
|
||||
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'] }];
|
||||
return [{ slug: ["vending-machines-ogden-utah"] }]
|
||||
}
|
||||
}
|
||||
|
||||
// Generate metadata for a page
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: PageProps): Promise<Metadata> {
|
||||
try {
|
||||
const { slug } = await params;
|
||||
const slugArray = Array.isArray(slug) ? slug : [slug];
|
||||
|
||||
const { slug } = await params
|
||||
const slugArray = Array.isArray(slug) ? slug : [slug]
|
||||
|
||||
// Handle location routes
|
||||
if (isLocationRoute(slugArray)) {
|
||||
let locationSlug: string;
|
||||
let locationSlug: string
|
||||
if (slugArray.length === 1) {
|
||||
locationSlug = slugArray[0].replace('vending-machines-', '');
|
||||
locationSlug = slugArray[0].replace("vending-machines-", "")
|
||||
} else {
|
||||
locationSlug = slugArray[1];
|
||||
locationSlug = slugArray[1]
|
||||
}
|
||||
|
||||
const locationData = getLocationBySlug(locationSlug);
|
||||
|
||||
const locationData = getLocationBySlug(locationSlug)
|
||||
if (!locationData) {
|
||||
return {
|
||||
title: 'Location Not Found | Rocky Mountain Vending',
|
||||
};
|
||||
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-${locationSlug}`,
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
siteName: businessConfig.name,
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title,
|
||||
description,
|
||||
},
|
||||
};
|
||||
return generateLocationPageMetadata(locationData)
|
||||
}
|
||||
|
||||
const pageSlug = resolveRouteToSlug(slugArray);
|
||||
|
||||
|
||||
const pageSlug = resolveRouteToSlug(slugArray)
|
||||
|
||||
if (!pageSlug) {
|
||||
return {
|
||||
title: 'Page Not Found | Rocky Mountain Vending',
|
||||
};
|
||||
title: "Page Not Found | Rocky Mountain Vending",
|
||||
}
|
||||
}
|
||||
|
||||
const page = getPageBySlug(pageSlug);
|
||||
|
||||
const page = getPageBySlug(pageSlug)
|
||||
|
||||
if (!page) {
|
||||
return {
|
||||
title: 'Page Not Found | Rocky Mountain Vending',
|
||||
};
|
||||
title: "Page Not Found | Rocky Mountain Vending",
|
||||
}
|
||||
}
|
||||
|
||||
return generateSEOMetadata({
|
||||
title: page.title || 'Page',
|
||||
description: page.seoDescription || page.excerpt || '',
|
||||
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);
|
||||
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.',
|
||||
};
|
||||
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];
|
||||
|
||||
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;
|
||||
let locationSlug: string
|
||||
if (slugArray.length === 1) {
|
||||
locationSlug = slugArray[0].replace('vending-machines-', '');
|
||||
locationSlug = slugArray[0].replace("vending-machines-", "")
|
||||
} else {
|
||||
locationSlug = slugArray[1];
|
||||
locationSlug = slugArray[1]
|
||||
}
|
||||
|
||||
const locationData = getLocationBySlug(locationSlug);
|
||||
|
||||
const locationData = getLocationBySlug(locationSlug)
|
||||
if (!locationData) {
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
|
||||
// Render location page
|
||||
return renderLocationPage(locationData, locationSlug);
|
||||
|
||||
return <LocationLandingPage locationData={locationData} />
|
||||
}
|
||||
|
||||
const pageSlug = resolveRouteToSlug(slugArray);
|
||||
|
||||
const pageSlug = resolveRouteToSlug(slugArray)
|
||||
|
||||
if (!pageSlug) {
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
|
||||
const page = getPageBySlug(pageSlug);
|
||||
|
||||
const page = getPageBySlug(pageSlug)
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
|
||||
// Load image mapping (optional, won't break if it fails)
|
||||
let imageMapping: any = {};
|
||||
let imageMapping: any = {}
|
||||
try {
|
||||
imageMapping = loadImageMapping();
|
||||
imageMapping = loadImageMapping()
|
||||
} catch (e) {
|
||||
// Silently fail - image mapping is optional
|
||||
}
|
||||
|
|
@ -600,127 +267,146 @@ export default async function WordPressPage({ params }: PageProps) {
|
|||
// Clean and render WordPress content as styled React components
|
||||
const content = page.content ? (
|
||||
<div className="max-w-none">
|
||||
{cleanWordPressContent(String(page.content), {
|
||||
{cleanWordPressContent(String(page.content), {
|
||||
imageMapping,
|
||||
pageTitle: page.title // Pass page title to avoid duplicate headings
|
||||
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] });
|
||||
// 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}/`,
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
// 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
|
||||
)
|
||||
|
||||
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 px-4 py-8 md:py-12 max-w-4xl">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-6">{page.title || 'Page'}</h1>
|
||||
</header>
|
||||
{content}
|
||||
</article>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
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)
|
||||
|
||||
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 px-4 py-8 md:py-12 max-w-4xl">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-6">
|
||||
{page.title || "Page"}
|
||||
</h1>
|
||||
</header>
|
||||
{content}
|
||||
</article>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
} catch (error) {
|
||||
// Silently return error fallback in production
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Error rendering page:', error);
|
||||
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' && (
|
||||
<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>
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,57 +1,61 @@
|
|||
import { notFound } from 'next/navigation';
|
||||
import { loadImageMapping } from '@/lib/wordpress-content';
|
||||
import { generateSEOMetadata, generateStructuredData } from '@/lib/seo';
|
||||
import { getPageBySlug } from '@/lib/wordpress-data-loader';
|
||||
import { AboutPage } from '@/components/about-page';
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from "next/navigation"
|
||||
import { loadImageMapping } from "@/lib/wordpress-content"
|
||||
import { generateSEOMetadata, generateStructuredData } from "@/lib/seo"
|
||||
import { getPageBySlug } from "@/lib/wordpress-data-loader"
|
||||
import { AboutPage } from "@/components/about-page"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
const WORDPRESS_SLUG = 'about-us';
|
||||
const WORDPRESS_SLUG = "about-us"
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const page = getPageBySlug(WORDPRESS_SLUG);
|
||||
const page = getPageBySlug(WORDPRESS_SLUG)
|
||||
|
||||
if (!page) {
|
||||
return {
|
||||
title: 'Page Not Found | Rocky Mountain Vending',
|
||||
};
|
||||
title: "Page Not Found | Rocky Mountain Vending",
|
||||
}
|
||||
}
|
||||
|
||||
return generateSEOMetadata({
|
||||
title: page.title || 'About Us',
|
||||
description: page.seoDescription || page.excerpt || '',
|
||||
title: page.title || "About Us",
|
||||
description: page.seoDescription || page.excerpt || "",
|
||||
excerpt: page.excerpt,
|
||||
date: page.date,
|
||||
modified: page.modified,
|
||||
image: page.images?.[0]?.localPath,
|
||||
});
|
||||
path: "/about-us",
|
||||
})
|
||||
}
|
||||
|
||||
export default async function AboutUsPage() {
|
||||
try {
|
||||
const page = getPageBySlug(WORDPRESS_SLUG);
|
||||
const page = getPageBySlug(WORDPRESS_SLUG)
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
|
||||
let structuredData;
|
||||
let structuredData
|
||||
try {
|
||||
structuredData = generateStructuredData({
|
||||
title: page.title || 'About Us',
|
||||
description: page.seoDescription || page.excerpt || '',
|
||||
url: page.link || page.urlPath || `https://rockymountainvending.com/about-us/`,
|
||||
title: page.title || "About Us",
|
||||
description: page.seoDescription || page.excerpt || "",
|
||||
url:
|
||||
page.link ||
|
||||
page.urlPath ||
|
||||
`https://rockymountainvending.com/about-us/`,
|
||||
datePublished: page.date,
|
||||
dateModified: page.modified || page.date,
|
||||
type: 'WebPage',
|
||||
});
|
||||
type: "WebPage",
|
||||
})
|
||||
} catch (e) {
|
||||
structuredData = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
headline: page.title || 'About Us',
|
||||
description: page.seoDescription || '',
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
headline: page.title || "About Us",
|
||||
description: page.seoDescription || "",
|
||||
url: `https://rockymountainvending.com/about-us/`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -62,19 +66,11 @@ export default async function AboutUsPage() {
|
|||
/>
|
||||
<AboutPage />
|
||||
</>
|
||||
);
|
||||
)
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Error rendering About Us page:', error);
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("Error rendering About Us page:", error)
|
||||
}
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,34 @@
|
|||
import { generateSEOMetadata, generateStructuredData } from '@/lib/seo';
|
||||
import { AboutPage } from '@/components/about-page';
|
||||
import type { Metadata } from 'next';
|
||||
import { generateSEOMetadata, generateStructuredData } from "@/lib/seo"
|
||||
import { AboutPage } from "@/components/about-page"
|
||||
import type { Metadata } from "next"
|
||||
import { businessConfig } from "@/lib/seo-config"
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return generateSEOMetadata({
|
||||
title: 'About Us | Rocky Mountain Vending',
|
||||
description: 'Learn more about Rocky Mountain Vending, a family-owned business dedicated to providing exceptional vending services across Utah',
|
||||
});
|
||||
return {
|
||||
...generateSEOMetadata({
|
||||
title: "About Rocky Mountain Vending | Utah Vending Company",
|
||||
description:
|
||||
"Learn about Rocky Mountain Vending, the Utah service-area business behind our vending placement, repair, sales, and support services.",
|
||||
path: "/about",
|
||||
}),
|
||||
alternates: {
|
||||
canonical: `${businessConfig.website}/about-us`,
|
||||
},
|
||||
robots: {
|
||||
index: false,
|
||||
follow: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function About() {
|
||||
const structuredData = generateStructuredData({
|
||||
title: 'About Us',
|
||||
description: 'Learn more about Rocky Mountain Vending, a family-owned business dedicated to providing exceptional vending services across Utah',
|
||||
url: 'https://rockymountainvending.com/about/',
|
||||
type: 'WebPage',
|
||||
});
|
||||
title: "About Rocky Mountain Vending",
|
||||
description:
|
||||
"Learn about Rocky Mountain Vending, the Utah service-area business behind our vending placement, repair, sales, and support services.",
|
||||
url: "https://rockymountainvending.com/about-us/",
|
||||
type: "WebPage",
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -25,5 +38,5 @@ export default function About() {
|
|||
/>
|
||||
<AboutPage />
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,76 +1,80 @@
|
|||
import { notFound } from 'next/navigation';
|
||||
import { loadImageMapping } from '@/lib/wordpress-content';
|
||||
import { generateSEOMetadata, generateStructuredData } from '@/lib/seo';
|
||||
import { getPageBySlug } from '@/lib/wordpress-data-loader';
|
||||
import { cleanWordPressContent } from '@/lib/clean-wordPress-content';
|
||||
import { Breadcrumbs } from '@/components/breadcrumbs';
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from "next/navigation"
|
||||
import { loadImageMapping } from "@/lib/wordpress-content"
|
||||
import { generateSEOMetadata, generateStructuredData } from "@/lib/seo"
|
||||
import { getPageBySlug } from "@/lib/wordpress-data-loader"
|
||||
import { cleanWordPressContent } from "@/lib/clean-wordPress-content"
|
||||
import { Breadcrumbs } from "@/components/breadcrumbs"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
const WORDPRESS_SLUG = 'abandoned-vending-machines';
|
||||
const WORDPRESS_SLUG = "abandoned-vending-machines"
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const page = getPageBySlug(WORDPRESS_SLUG);
|
||||
const page = getPageBySlug(WORDPRESS_SLUG)
|
||||
|
||||
if (!page) {
|
||||
return {
|
||||
title: 'Page Not Found | Rocky Mountain Vending',
|
||||
};
|
||||
title: "Page Not Found | Rocky Mountain Vending",
|
||||
}
|
||||
}
|
||||
|
||||
return generateSEOMetadata({
|
||||
title: page.title || 'Abandoned Vending Machines',
|
||||
description: page.seoDescription || page.excerpt || '',
|
||||
title: page.title || "Abandoned Vending Machines",
|
||||
description: page.seoDescription || page.excerpt || "",
|
||||
excerpt: page.excerpt,
|
||||
date: page.date,
|
||||
modified: page.modified,
|
||||
image: page.images?.[0]?.localPath,
|
||||
});
|
||||
path: "/blog/abandoned-vending-machines",
|
||||
})
|
||||
}
|
||||
|
||||
export default async function AbandonedVendingMachinesPage() {
|
||||
try {
|
||||
const page = getPageBySlug(WORDPRESS_SLUG);
|
||||
const page = getPageBySlug(WORDPRESS_SLUG)
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
|
||||
let imageMapping: any = {};
|
||||
let imageMapping: any = {}
|
||||
try {
|
||||
imageMapping = loadImageMapping();
|
||||
imageMapping = loadImageMapping()
|
||||
} catch (e) {
|
||||
imageMapping = {};
|
||||
imageMapping = {}
|
||||
}
|
||||
|
||||
const content = page.content ? (
|
||||
<div className="max-w-none">
|
||||
{cleanWordPressContent(String(page.content), {
|
||||
{cleanWordPressContent(String(page.content), {
|
||||
imageMapping,
|
||||
pageTitle: page.title
|
||||
pageTitle: page.title,
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">No content available.</p>
|
||||
);
|
||||
)
|
||||
|
||||
let structuredData;
|
||||
let structuredData
|
||||
try {
|
||||
structuredData = generateStructuredData({
|
||||
title: page.title || 'Abandoned Vending Machines',
|
||||
description: page.seoDescription || page.excerpt || '',
|
||||
url: page.link || page.urlPath || `https://rockymountainvending.com/abandoned-vending-machines/`,
|
||||
title: page.title || "Abandoned Vending Machines",
|
||||
description: page.seoDescription || page.excerpt || "",
|
||||
url:
|
||||
page.link ||
|
||||
page.urlPath ||
|
||||
`https://rockymountainvending.com/abandoned-vending-machines/`,
|
||||
datePublished: page.date,
|
||||
dateModified: page.modified || page.date,
|
||||
type: 'WebPage',
|
||||
});
|
||||
type: "WebPage",
|
||||
})
|
||||
} catch (e) {
|
||||
structuredData = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
headline: page.title || 'Abandoned Vending Machines',
|
||||
description: page.seoDescription || '',
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
headline: page.title || "Abandoned Vending Machines",
|
||||
description: page.seoDescription || "",
|
||||
url: `https://rockymountainvending.com/abandoned-vending-machines/`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -81,19 +85,22 @@ export default async function AbandonedVendingMachinesPage() {
|
|||
/>
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ label: 'Blog', href: '/blog' },
|
||||
{ label: page.title || 'Abandoned Vending Machines', href: '/blog/abandoned-vending-machines' },
|
||||
{ label: "Blog", href: "/blog" },
|
||||
{
|
||||
label: page.title || "Abandoned Vending Machines",
|
||||
href: "/blog/abandoned-vending-machines",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<article className="container mx-auto px-4 py-8 md:py-12 max-w-4xl">
|
||||
{content}
|
||||
</article>
|
||||
</>
|
||||
);
|
||||
)
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Error rendering Abandoned Vending Machines page:', error);
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("Error rendering Abandoned Vending Machines page:", error)
|
||||
}
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,53 +1,55 @@
|
|||
import { notFound } from 'next/navigation';
|
||||
import { loadImageMapping } from '@/lib/wordpress-content';
|
||||
import { generateSEOMetadata, generateStructuredData } from '@/lib/seo';
|
||||
import { getPageBySlug } from '@/lib/wordpress-data-loader';
|
||||
import { cleanWordPressContent } from '@/lib/clean-wordPress-content';
|
||||
import { Breadcrumbs } from '@/components/breadcrumbs';
|
||||
import { PublicPageHeader, PublicSurface } from '@/components/public-surface';
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from "next/navigation"
|
||||
import { loadImageMapping } from "@/lib/wordpress-content"
|
||||
import { generateSEOMetadata, generateStructuredData } from "@/lib/seo"
|
||||
import { getPageBySlug } from "@/lib/wordpress-data-loader"
|
||||
import { cleanWordPressContent } from "@/lib/clean-wordPress-content"
|
||||
import { Breadcrumbs } from "@/components/breadcrumbs"
|
||||
import { PublicPageHeader, PublicSurface } from "@/components/public-surface"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
const WORDPRESS_SLUG = 'best-vending-machine-supplier-in-salt-lake-city-utah';
|
||||
const DISPLAY_TITLE = 'The Best Vending Machine Supplier in Salt Lake City, Utah';
|
||||
const WORDPRESS_SLUG = "best-vending-machine-supplier-in-salt-lake-city-utah"
|
||||
const DISPLAY_TITLE =
|
||||
"The Best Vending Machine Supplier in Salt Lake City, Utah"
|
||||
const DISPLAY_DESCRIPTION =
|
||||
'A closer look at how Rocky Mountain Vending supports Utah businesses with free placement, machine sales, repairs, manuals, and responsive local service.';
|
||||
"A closer look at how Rocky Mountain Vending supports Utah businesses with free placement, machine sales, repairs, manuals, and responsive local service."
|
||||
|
||||
function stripLeadingH1(html: string) {
|
||||
return html.replace(/<h1[^>]*>[\s\S]*?<\/h1>/i, '');
|
||||
return html.replace(/<h1[^>]*>[\s\S]*?<\/h1>/i, "")
|
||||
}
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const page = getPageBySlug(WORDPRESS_SLUG);
|
||||
const page = getPageBySlug(WORDPRESS_SLUG)
|
||||
|
||||
if (!page) {
|
||||
return {
|
||||
title: 'Page Not Found | Rocky Mountain Vending',
|
||||
};
|
||||
title: "Page Not Found | Rocky Mountain Vending",
|
||||
}
|
||||
}
|
||||
|
||||
return generateSEOMetadata({
|
||||
title: DISPLAY_TITLE,
|
||||
description: page.seoDescription || page.excerpt || '',
|
||||
description: page.seoDescription || page.excerpt || "",
|
||||
excerpt: page.excerpt,
|
||||
date: page.date,
|
||||
modified: page.modified,
|
||||
image: page.images?.[0]?.localPath,
|
||||
});
|
||||
path: "/blog/best-vending-machine-supplier-in-salt-lake-city-utah",
|
||||
})
|
||||
}
|
||||
|
||||
export default async function BestVendingMachineSupplierPage() {
|
||||
try {
|
||||
const page = getPageBySlug(WORDPRESS_SLUG);
|
||||
const page = getPageBySlug(WORDPRESS_SLUG)
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
|
||||
let imageMapping: any = {};
|
||||
let imageMapping: any = {}
|
||||
try {
|
||||
imageMapping = loadImageMapping();
|
||||
imageMapping = loadImageMapping()
|
||||
} catch (e) {
|
||||
imageMapping = {};
|
||||
imageMapping = {}
|
||||
}
|
||||
|
||||
const content = page.content ? (
|
||||
|
|
@ -60,26 +62,29 @@ export default async function BestVendingMachineSupplierPage() {
|
|||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">No content available.</p>
|
||||
);
|
||||
)
|
||||
|
||||
let structuredData;
|
||||
let structuredData
|
||||
try {
|
||||
structuredData = generateStructuredData({
|
||||
title: DISPLAY_TITLE,
|
||||
description: page.seoDescription || page.excerpt || '',
|
||||
url: page.link || page.urlPath || `https://rockymountainvending.com/best-vending-machine-supplier-in-salt-lake-city-utah/`,
|
||||
description: page.seoDescription || page.excerpt || "",
|
||||
url:
|
||||
page.link ||
|
||||
page.urlPath ||
|
||||
`https://rockymountainvending.com/best-vending-machine-supplier-in-salt-lake-city-utah/`,
|
||||
datePublished: page.date,
|
||||
dateModified: page.modified || page.date,
|
||||
type: 'WebPage',
|
||||
});
|
||||
type: "WebPage",
|
||||
})
|
||||
} catch (e) {
|
||||
structuredData = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
headline: DISPLAY_TITLE,
|
||||
description: page.seoDescription || '',
|
||||
description: page.seoDescription || "",
|
||||
url: `https://rockymountainvending.com/best-vending-machine-supplier-in-salt-lake-city-utah/`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -91,8 +96,11 @@ export default async function BestVendingMachineSupplierPage() {
|
|||
<Breadcrumbs
|
||||
className="container mx-auto max-w-6xl px-4 pt-6"
|
||||
items={[
|
||||
{ label: 'Blog', href: '/blog' },
|
||||
{ label: DISPLAY_TITLE, href: '/blog/best-vending-machine-supplier-in-salt-lake-city-utah' },
|
||||
{ label: "Blog", href: "/blog" },
|
||||
{
|
||||
label: DISPLAY_TITLE,
|
||||
href: "/blog/best-vending-machine-supplier-in-salt-lake-city-utah",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<article className="container mx-auto max-w-6xl px-4 py-8 md:py-12">
|
||||
|
|
@ -108,11 +116,14 @@ export default async function BestVendingMachineSupplierPage() {
|
|||
</PublicSurface>
|
||||
</article>
|
||||
</>
|
||||
);
|
||||
)
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Error rendering Best Vending Machine Supplier page:', error);
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error(
|
||||
"Error rendering Best Vending Machine Supplier page:",
|
||||
error
|
||||
)
|
||||
}
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +1,60 @@
|
|||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { Breadcrumbs } from '@/components/breadcrumbs'
|
||||
import { PublicInset, PublicPageHeader, PublicSurface } from '@/components/public-surface'
|
||||
import type { Metadata } from 'next'
|
||||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
import { Breadcrumbs } from "@/components/breadcrumbs"
|
||||
import {
|
||||
PublicInset,
|
||||
PublicPageHeader,
|
||||
PublicSurface,
|
||||
} from "@/components/public-surface"
|
||||
import type { Metadata } from "next"
|
||||
import { generateSEOMetadata } from "@/lib/seo"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Blog | Rocky Mountain Vending',
|
||||
description: 'Read our latest blog posts about vending machines, services, and industry insights from Rocky Mountain Vending.',
|
||||
alternates: {
|
||||
canonical: 'https://rockymountainvending.com/blog/',
|
||||
},
|
||||
}
|
||||
export const metadata: Metadata = generateSEOMetadata({
|
||||
title: "Utah Vending Blog | Rocky Mountain Vending",
|
||||
description:
|
||||
"Read Rocky Mountain Vending guides, reviews, and Utah-focused vending insights for businesses and property managers.",
|
||||
path: "/blog",
|
||||
keywords: [
|
||||
"Utah vending blog",
|
||||
"vending machine guides Utah",
|
||||
"Rocky Mountain Vending blog",
|
||||
],
|
||||
})
|
||||
|
||||
const blogPosts = [
|
||||
{
|
||||
title: 'How to Remove an Abandoned Vending Machine in Utah',
|
||||
description: 'A comprehensive guide for Utah businesses dealing with unwanted vending machines on their property.',
|
||||
slug: 'abandoned-vending-machines',
|
||||
date: 'March 20, 2025',
|
||||
image: '/images/abandoned-vending-machine-guide.jpg',
|
||||
imageAlt: 'Abandoned vending machine guide',
|
||||
title: "How to Remove an Abandoned Vending Machine in Utah",
|
||||
description:
|
||||
"A comprehensive guide for Utah businesses dealing with unwanted vending machines on their property.",
|
||||
slug: "abandoned-vending-machines",
|
||||
date: "March 20, 2025",
|
||||
image: "/images/abandoned-vending-machine-guide.jpg",
|
||||
imageAlt: "Abandoned vending machine guide",
|
||||
},
|
||||
{
|
||||
title: 'Rocky Mountain Vending Reviews & Testimonials',
|
||||
description: 'Read customer reviews and testimonials about our vending machine services in Utah.',
|
||||
slug: 'reviews',
|
||||
date: 'March 20, 2025',
|
||||
image: '/images/customer-reviews.jpg',
|
||||
imageAlt: 'Customer reviews and testimonials',
|
||||
title: "Rocky Mountain Vending Reviews & Testimonials",
|
||||
description:
|
||||
"Read customer reviews and testimonials about our vending machine services in Utah.",
|
||||
slug: "reviews",
|
||||
date: "March 20, 2025",
|
||||
image: "/images/customer-reviews.jpg",
|
||||
imageAlt: "Customer reviews and testimonials",
|
||||
},
|
||||
{
|
||||
title: 'The Best Vending Machine Supplier in Salt Lake City, Utah',
|
||||
description: 'Why Rocky Mountain Vending is the top choice for vending machine services in Salt Lake City.',
|
||||
slug: 'best-vending-machine-supplier-in-salt-lake-city-utah',
|
||||
date: 'March 20, 2025',
|
||||
image: '/images/salt-lake-city-vending.jpg',
|
||||
imageAlt: 'Vending machine supplier in Salt Lake City',
|
||||
title: "The Best Vending Machine Supplier in Salt Lake City, Utah",
|
||||
description:
|
||||
"Why Rocky Mountain Vending is the top choice for vending machine services in Salt Lake City.",
|
||||
slug: "best-vending-machine-supplier-in-salt-lake-city-utah",
|
||||
date: "March 20, 2025",
|
||||
image: "/images/salt-lake-city-vending.jpg",
|
||||
imageAlt: "Vending machine supplier in Salt Lake City",
|
||||
},
|
||||
]
|
||||
|
||||
export default function BlogPage() {
|
||||
return (
|
||||
<>
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ label: 'Blog', href: '/blog' },
|
||||
]}
|
||||
/>
|
||||
<Breadcrumbs items={[{ label: "Blog", href: "/blog" }]} />
|
||||
<article className="container mx-auto max-w-5xl px-4 py-10 md:py-14">
|
||||
<PublicPageHeader
|
||||
align="center"
|
||||
|
|
@ -74,7 +82,10 @@ export default function BlogPage() {
|
|||
</div>
|
||||
<div className="md:w-2/3">
|
||||
<h2 className="text-2xl md:text-3xl font-semibold mb-2 tracking-tight text-balance">
|
||||
<Link href={`/blog/${post.slug}`} className="transition-colors hover:text-primary">
|
||||
<Link
|
||||
href={`/blog/${post.slug}`}
|
||||
className="transition-colors hover:text-primary"
|
||||
>
|
||||
{post.title}
|
||||
</Link>
|
||||
</h2>
|
||||
|
|
|
|||
|
|
@ -1,56 +1,60 @@
|
|||
import { notFound } from 'next/navigation';
|
||||
import { generateSEOMetadata, generateStructuredData } from '@/lib/seo';
|
||||
import { getPageBySlug } from '@/lib/wordpress-data-loader';
|
||||
import { ContactPage } from '@/components/contact-page';
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from "next/navigation"
|
||||
import { generateSEOMetadata, generateStructuredData } from "@/lib/seo"
|
||||
import { getPageBySlug } from "@/lib/wordpress-data-loader"
|
||||
import { ContactPage } from "@/components/contact-page"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
const WORDPRESS_SLUG = 'contact-us';
|
||||
const WORDPRESS_SLUG = "contact-us"
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const page = getPageBySlug(WORDPRESS_SLUG);
|
||||
const page = getPageBySlug(WORDPRESS_SLUG)
|
||||
|
||||
if (!page) {
|
||||
return {
|
||||
title: 'Page Not Found | Rocky Mountain Vending',
|
||||
};
|
||||
title: "Page Not Found | Rocky Mountain Vending",
|
||||
}
|
||||
}
|
||||
|
||||
return generateSEOMetadata({
|
||||
title: page.title || 'Contact Us',
|
||||
description: page.seoDescription || page.excerpt || '',
|
||||
title: page.title || "Contact Us",
|
||||
description: page.seoDescription || page.excerpt || "",
|
||||
excerpt: page.excerpt,
|
||||
date: page.date,
|
||||
modified: page.modified,
|
||||
image: page.images?.[0]?.localPath,
|
||||
});
|
||||
path: "/contact-us",
|
||||
})
|
||||
}
|
||||
|
||||
export default async function ContactUsPage() {
|
||||
try {
|
||||
const page = getPageBySlug(WORDPRESS_SLUG);
|
||||
const page = getPageBySlug(WORDPRESS_SLUG)
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
|
||||
let structuredData;
|
||||
let structuredData
|
||||
try {
|
||||
structuredData = generateStructuredData({
|
||||
title: page.title || 'Contact Us',
|
||||
description: page.seoDescription || page.excerpt || '',
|
||||
url: page.link || page.urlPath || `https://rockymountainvending.com/contact-us/`,
|
||||
title: page.title || "Contact Us",
|
||||
description: page.seoDescription || page.excerpt || "",
|
||||
url:
|
||||
page.link ||
|
||||
page.urlPath ||
|
||||
`https://rockymountainvending.com/contact-us/`,
|
||||
datePublished: page.date,
|
||||
dateModified: page.modified || page.date,
|
||||
type: 'WebPage',
|
||||
});
|
||||
type: "WebPage",
|
||||
})
|
||||
} catch (e) {
|
||||
structuredData = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
headline: page.title || 'Contact Us',
|
||||
description: page.seoDescription || '',
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
headline: page.title || "Contact Us",
|
||||
description: page.seoDescription || "",
|
||||
url: `https://rockymountainvending.com/contact-us/`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -61,19 +65,11 @@ export default async function ContactUsPage() {
|
|||
/>
|
||||
<ContactPage />
|
||||
</>
|
||||
);
|
||||
)
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Error rendering Contact Us page:', error);
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("Error rendering Contact Us page:", error)
|
||||
}
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,19 +3,17 @@ import type { Metadata } from "next"
|
|||
import { Inter, Geist_Mono } from "next/font/google"
|
||||
import { Header } from "@/components/header"
|
||||
import { Footer } from "@/components/footer"
|
||||
import { StructuredData } from "@/components/structured-data"
|
||||
import { OrganizationSchema } from "@/components/organization-schema"
|
||||
import { SiteChatWidget } from "@/components/site-chat-widget"
|
||||
import { CartProvider } from "@/lib/cart/context"
|
||||
import { businessConfig } from "@/lib/seo-config"
|
||||
import "./globals.css"
|
||||
|
||||
const inter = Inter({
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
display: "swap",
|
||||
})
|
||||
const geistMono = Geist_Mono({
|
||||
const geistMono = Geist_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-geist-mono",
|
||||
display: "swap",
|
||||
|
|
@ -23,21 +21,12 @@ const geistMono = Geist_Mono({
|
|||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(businessConfig.website),
|
||||
title: "Free Vending Machines Utah | Rocky Mountain Vending | Salt Lake City, Ogden, Provo",
|
||||
description:
|
||||
"Get a FREE vending machine for your Utah business! No cost installation. Serving Salt Lake City, Ogden, Provo, and surrounding areas since 2019. 100+ machines installed. Call (435) 233-9668.",
|
||||
title: {
|
||||
default: businessConfig.name,
|
||||
template: "%s",
|
||||
},
|
||||
description: businessConfig.description,
|
||||
generator: "Next.js",
|
||||
keywords: [
|
||||
"vending machines",
|
||||
"vending machine supplier",
|
||||
"free vending machines",
|
||||
"Utah vending",
|
||||
"Salt Lake City vending",
|
||||
"Ogden vending",
|
||||
"Provo vending",
|
||||
"vending machine repair",
|
||||
"vending machine service",
|
||||
],
|
||||
authors: [{ name: businessConfig.name }],
|
||||
creator: businessConfig.name,
|
||||
publisher: businessConfig.name,
|
||||
|
|
@ -71,11 +60,7 @@ export const metadata: Metadata = {
|
|||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
url: businessConfig.website,
|
||||
siteName: businessConfig.name,
|
||||
title: "Free Vending Machines Utah | Rocky Mountain Vending",
|
||||
description:
|
||||
"Get a FREE vending machine for your Utah business! No cost installation. Serving Salt Lake City, Ogden, Provo, and surrounding areas since 2019. 100+ machines installed.",
|
||||
images: [
|
||||
{
|
||||
url: `${businessConfig.website}/images/rocky-mountain-vending-service-area-926x1024.webp`,
|
||||
|
|
@ -87,15 +72,11 @@ export const metadata: Metadata = {
|
|||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Free Vending Machines Utah | Rocky Mountain Vending",
|
||||
description:
|
||||
"Get a FREE vending machine for your Utah business! No cost installation. Serving Salt Lake City, Ogden, Provo, and surrounding areas since 2019.",
|
||||
images: [`${businessConfig.website}/images/rocky-mountain-vending-service-area-926x1024.webp`],
|
||||
images: [
|
||||
`${businessConfig.website}/images/rocky-mountain-vending-service-area-926x1024.webp`,
|
||||
],
|
||||
creator: "@RMVVending",
|
||||
},
|
||||
alternates: {
|
||||
canonical: businessConfig.website,
|
||||
},
|
||||
verification: {
|
||||
// Google Search Console verification
|
||||
// To enable: Add your verification code from Google Search Console
|
||||
|
|
@ -112,28 +93,24 @@ export default function RootLayout({
|
|||
}>) {
|
||||
return (
|
||||
<html lang="en" className={`${inter.variable} ${geistMono.variable}`}>
|
||||
<head>
|
||||
<StructuredData />
|
||||
<OrganizationSchema />
|
||||
</head>
|
||||
<body className="font-sans antialiased">
|
||||
<CartProvider>
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* Skip to main content link for keyboard users */}
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-background focus:border focus:border-primary focus:rounded-md focus:text-primary focus:font-medium"
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
<Header />
|
||||
<main id="main-content" className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
<SiteChatWidget />
|
||||
</CartProvider>
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* Skip to main content link for keyboard users */}
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-background focus:border focus:border-primary focus:rounded-md focus:text-primary focus:font-medium"
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
<Header />
|
||||
<main id="main-content" className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
<SiteChatWidget />
|
||||
</CartProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
|
|
|||
16
app/page.tsx
16
app/page.tsx
|
|
@ -1,3 +1,4 @@
|
|||
import type { Metadata } from "next"
|
||||
import { HeroSection } from "@/components/hero-section"
|
||||
import { StatsSection } from "@/components/stats-section"
|
||||
import { FeaturesSection } from "@/components/features-section"
|
||||
|
|
@ -8,10 +9,25 @@ import { ServiceAreasSection } from "@/components/service-areas-section"
|
|||
import { ReviewsSection } from "@/components/reviews-section"
|
||||
import { RequestMachineSection } from "@/components/request-machine-section"
|
||||
import { ContactSection } from "@/components/contact-section"
|
||||
import { StructuredData } from "@/components/structured-data"
|
||||
import { OrganizationSchema } from "@/components/organization-schema"
|
||||
import { generateSEOMetadata } from "@/lib/seo"
|
||||
import { getSeoPageDefinition } from "@/lib/seo-registry"
|
||||
|
||||
const homeSeo = getSeoPageDefinition("home")
|
||||
|
||||
export const metadata: Metadata = generateSEOMetadata({
|
||||
title: homeSeo.title,
|
||||
description: homeSeo.description,
|
||||
path: homeSeo.path,
|
||||
keywords: [...homeSeo.keywords],
|
||||
})
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<StructuredData />
|
||||
<OrganizationSchema />
|
||||
<HeroSection />
|
||||
<StatsSection />
|
||||
<FeaturesSection />
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
import { generateSEOMetadata, generateStructuredData } from '@/lib/seo';
|
||||
import { ReviewsPage } from '@/components/reviews-page';
|
||||
import type { Metadata } from 'next';
|
||||
import { generateSEOMetadata, generateStructuredData } from "@/lib/seo"
|
||||
import { ReviewsPage } from "@/components/reviews-page"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return generateSEOMetadata({
|
||||
title: 'Customer Reviews | Rocky Mountain Vending',
|
||||
description: 'Read authentic customer reviews and testimonials about Rocky Mountain Vending\'s exceptional vending services in Utah. See why businesses trust us for free vending machines.',
|
||||
});
|
||||
title: "Customer Reviews | Rocky Mountain Vending",
|
||||
description:
|
||||
"Browse Rocky Mountain Vending reviews and the live Google review feed to see what Utah businesses say about placement, restocking, repairs, and service.",
|
||||
path: "/reviews",
|
||||
})
|
||||
}
|
||||
|
||||
export default function Reviews() {
|
||||
const structuredData = generateStructuredData({
|
||||
title: 'Customer Reviews',
|
||||
description: 'See what our customers are saying about Rocky Mountain Vending\'s exceptional service',
|
||||
url: 'https://rockymountainvending.com/reviews/',
|
||||
type: 'WebPage',
|
||||
});
|
||||
title: "Customer Reviews",
|
||||
description:
|
||||
"Browse Rocky Mountain Vending reviews and the live Google review feed",
|
||||
url: "https://rockymountainvending.com/reviews/",
|
||||
type: "WebPage",
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -25,5 +28,5 @@ export default function Reviews() {
|
|||
/>
|
||||
<ReviewsPage />
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { MetadataRoute } from "next"
|
|||
import { businessConfig } from "@/lib/seo-config"
|
||||
|
||||
// Required for static export
|
||||
export const dynamic = 'force-static'
|
||||
export const dynamic = "force-static"
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
|
|
@ -34,6 +34,3 @@ export default function robots(): MetadataRoute.Robots {
|
|||
host: businessConfig.website,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
16
app/seaga-hy900-support/layout.tsx
Normal file
16
app/seaga-hy900-support/layout.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { Metadata } from "next"
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Seaga HY900 Support | Rocky Mountain Vending",
|
||||
description:
|
||||
"Watch Seaga HY900 support videos and access the owner manual from Rocky Mountain Vending.",
|
||||
}
|
||||
|
||||
export default function SeagaSupportLayout({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) {
|
||||
return children
|
||||
}
|
||||
|
|
@ -2,35 +2,27 @@ import type { Metadata } from "next"
|
|||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
import { getAllLocations } from "@/lib/location-data"
|
||||
import { generateSEOMetadata } from "@/lib/seo"
|
||||
import { businessConfig } from "@/lib/seo-config"
|
||||
import { MapPin, Phone, ArrowRight, Wrench, Clock } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { PublicInset, PublicPageHeader, PublicSurface } from "@/components/public-surface"
|
||||
import {
|
||||
PublicInset,
|
||||
PublicPageHeader,
|
||||
PublicSurface,
|
||||
} from "@/components/public-surface"
|
||||
import { GetFreeMachineCta } from "@/components/get-free-machine-cta"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Service Areas | Vending Machines Across Utah | Rocky Mountain Vending",
|
||||
export const metadata: Metadata = generateSEOMetadata({
|
||||
title: "Utah Service Areas | Rocky Mountain Vending",
|
||||
description:
|
||||
"Rocky Mountain Vending serves 20+ cities across Utah including Salt Lake City, Ogden, Provo, Sandy, and more. Free vending machine delivery and installation. View all service areas.",
|
||||
"See the Utah cities and counties Rocky Mountain Vending serves for free placement, machine sales, repairs, moving, parts, and ongoing vending service.",
|
||||
path: "/service-areas",
|
||||
keywords: [
|
||||
"vending machines Utah",
|
||||
"Utah vending service areas",
|
||||
"vending machine supplier Utah",
|
||||
"Salt Lake County vending",
|
||||
"Davis County vending",
|
||||
"Utah County vending",
|
||||
"vending machine service areas Utah",
|
||||
"Salt Lake City vending company",
|
||||
],
|
||||
openGraph: {
|
||||
title: "Service Areas | Vending Machines Across Utah",
|
||||
description:
|
||||
"Rocky Mountain Vending serves 20+ cities across Utah. Free vending machine delivery and installation. View all service areas.",
|
||||
url: `${businessConfig.website}/service-areas`,
|
||||
type: "website",
|
||||
},
|
||||
alternates: {
|
||||
canonical: `${businessConfig.website}/service-areas`,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
function LocationCard({
|
||||
city,
|
||||
|
|
@ -48,7 +40,9 @@ function LocationCard({
|
|||
<PublicSurface className="h-full p-5 transition-all hover:-translate-y-0.5 hover:shadow-[0_26px_65px_rgba(0,0,0,0.12)] md:p-6">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-xl font-semibold text-foreground transition-colors group-hover:text-primary">{city}</h3>
|
||||
<h3 className="text-xl font-semibold text-foreground transition-colors group-hover:text-primary">
|
||||
{city}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">{zipCode}</p>
|
||||
</div>
|
||||
<div className="rounded-full border border-border/60 bg-background/90 p-2 text-muted-foreground transition-all group-hover:border-primary/30 group-hover:text-primary group-hover:translate-x-0.5">
|
||||
|
|
@ -56,8 +50,12 @@ function LocationCard({
|
|||
</div>
|
||||
</div>
|
||||
<PublicInset className="mt-5">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-primary/75">Popular Areas</p>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{neighborhoods.slice(0, 2).join(", ")}</p>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-primary/75">
|
||||
Popular Areas
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
{neighborhoods.slice(0, 2).join(", ")}
|
||||
</p>
|
||||
</PublicInset>
|
||||
</PublicSurface>
|
||||
</Link>
|
||||
|
|
@ -84,22 +82,30 @@ export default function ServiceAreasPage() {
|
|||
"holladay-utah",
|
||||
"millcreek-utah",
|
||||
"cottonwood-heights-utah",
|
||||
].includes(loc.slug),
|
||||
].includes(loc.slug)
|
||||
)
|
||||
|
||||
const davisCounty = locations.filter((loc) =>
|
||||
["ogden-utah", "layton-utah", "clearfield-utah", "syracuse-utah", "clinton-utah"].includes(loc.slug),
|
||||
[
|
||||
"ogden-utah",
|
||||
"layton-utah",
|
||||
"clearfield-utah",
|
||||
"syracuse-utah",
|
||||
"clinton-utah",
|
||||
].includes(loc.slug)
|
||||
)
|
||||
|
||||
const utahCounty = locations.filter((loc) => ["provo-utah"].includes(loc.slug))
|
||||
const utahCounty = locations.filter((loc) =>
|
||||
["provo-utah"].includes(loc.slug)
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-10 md:py-14">
|
||||
<PublicPageHeader
|
||||
align="center"
|
||||
eyebrow="Service Coverage"
|
||||
title="Utah locations we serve with vending, repairs, parts, and machine support."
|
||||
description="Rocky Mountain Vending supports businesses and schools across Utah with free placement, dependable service, and fast local follow-up. Browse the cities we cover and reach out if your location is nearby but not listed yet."
|
||||
title="Utah locations we serve with vending, repairs, parts, and machine service."
|
||||
description="Rocky Mountain Vending currently runs service routes across Salt Lake, Davis, and Utah counties, with free placement for qualifying locations, machine sales, repairs, parts, and ongoing restocking and service."
|
||||
/>
|
||||
|
||||
<div className="mt-10 grid gap-6 lg:grid-cols-[1.1fr_0.9fr]">
|
||||
|
|
@ -116,12 +122,16 @@ export default function ServiceAreasPage() {
|
|||
|
||||
<PublicSurface className="flex flex-col justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">Need Coverage Confirmation?</p>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
||||
Need Coverage Confirmation?
|
||||
</p>
|
||||
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance text-foreground">
|
||||
Don't see your city yet?
|
||||
</h2>
|
||||
<p className="mt-3 text-base leading-relaxed text-muted-foreground">
|
||||
We may still be able to help. If you're near one of these service zones, reach out and we'll let you know if we can cover your location.
|
||||
If you're close to one of our current routes, we may still
|
||||
be able to help. Reach out and we'll confirm whether your
|
||||
location fits our current coverage and service schedule.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -134,9 +144,16 @@ export default function ServiceAreasPage() {
|
|||
<Phone className="h-5 w-5 text-primary" />
|
||||
{businessConfig.phone}
|
||||
</a>
|
||||
<p className="mt-2 text-sm text-muted-foreground">Tell us where you're located and we'll let you know what service looks like in your area.</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Tell us where you're located and what kind of vending help
|
||||
you need, and we'll let you know what coverage looks like
|
||||
for your area.
|
||||
</p>
|
||||
</PublicInset>
|
||||
<GetFreeMachineCta buttonLabel="Request a Free Machine" className="h-11 px-5" />
|
||||
<GetFreeMachineCta
|
||||
buttonLabel="See If My Location Qualifies"
|
||||
className="h-11 px-5"
|
||||
/>
|
||||
</div>
|
||||
</PublicSurface>
|
||||
</div>
|
||||
|
|
@ -144,10 +161,16 @@ export default function ServiceAreasPage() {
|
|||
<section className="mt-12">
|
||||
<PublicSurface>
|
||||
<div className="text-center">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">Core Services</p>
|
||||
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance md:text-4xl">Our vending machine services across {state}</h2>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
||||
Core Services
|
||||
</p>
|
||||
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance md:text-4xl">
|
||||
Our vending machine services across {state}
|
||||
</h2>
|
||||
<p className="mx-auto mt-3 max-w-2xl text-base leading-relaxed text-muted-foreground">
|
||||
From free placement and machine sales to repairs, moving, and parts help, we support businesses across Utah with dependable local service.
|
||||
From free placement and machine sales to repairs, moving, and
|
||||
parts help, we help Utah businesses keep vending available
|
||||
without having to manage the machines themselves.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -158,12 +181,12 @@ export default function ServiceAreasPage() {
|
|||
title: "Repairs",
|
||||
body: "Expert repair and maintenance for snack, beverage, food, and combo machines.",
|
||||
href: "/services/repairs",
|
||||
cta: "Learn more",
|
||||
cta: "See repair service",
|
||||
},
|
||||
{
|
||||
icon: MapPin,
|
||||
title: "Parts",
|
||||
body: "Replacement parts, manuals, and support for major vending machine brands.",
|
||||
body: "Replacement parts, manuals, and machine resources for major vending brands.",
|
||||
href: "/services/parts",
|
||||
cta: "Shop parts",
|
||||
},
|
||||
|
|
@ -175,13 +198,21 @@ export default function ServiceAreasPage() {
|
|||
cta: "Moving services",
|
||||
},
|
||||
].map((service) => (
|
||||
<PublicInset key={service.title} className="h-full p-5 text-center">
|
||||
<PublicInset
|
||||
key={service.title}
|
||||
className="h-full p-5 text-center"
|
||||
>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||
<service.icon className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="mt-4 text-xl font-semibold">{service.title}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed 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">
|
||||
<p className="mt-2 text-sm leading-relaxed 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>
|
||||
|
|
@ -195,25 +226,34 @@ export default function ServiceAreasPage() {
|
|||
{[
|
||||
{
|
||||
title: "Salt Lake County",
|
||||
description: "Serving the heart of Utah's business district with comprehensive vending solutions.",
|
||||
description:
|
||||
"Serving a wide range of offices, schools, gyms, and workplaces across the Salt Lake County route.",
|
||||
items: saltLakeCounty,
|
||||
},
|
||||
{
|
||||
title: "Davis County",
|
||||
description: "Supporting businesses from Ogden to Layton with reliable vending service and repairs.",
|
||||
description:
|
||||
"Serving businesses from Ogden to Layton with reliable vending service and repairs.",
|
||||
items: davisCounty,
|
||||
},
|
||||
{
|
||||
title: "Utah County",
|
||||
description: "Delivering quality vending solutions to Provo and surrounding areas.",
|
||||
description:
|
||||
"Serving Provo and nearby Utah County locations with vending placement, service, and repairs.",
|
||||
items: utahCounty,
|
||||
},
|
||||
].map((section) => (
|
||||
<div key={section.title} className="space-y-5">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">Coverage Area</p>
|
||||
<h2 className="mt-2 text-3xl font-semibold tracking-tight text-balance">{section.title}</h2>
|
||||
<p className="mt-2 max-w-3xl text-base leading-relaxed text-muted-foreground">{section.description}</p>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
||||
Coverage Area
|
||||
</p>
|
||||
<h2 className="mt-2 text-3xl font-semibold tracking-tight text-balance">
|
||||
{section.title}
|
||||
</h2>
|
||||
<p className="mt-2 max-w-3xl text-base leading-relaxed text-muted-foreground">
|
||||
{section.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{section.items.map((location) => (
|
||||
|
|
@ -232,36 +272,70 @@ export default function ServiceAreasPage() {
|
|||
|
||||
<section className="mt-12 grid gap-6 lg:grid-cols-[1.15fr_0.85fr]">
|
||||
<PublicSurface>
|
||||
<h2 className="text-3xl font-semibold tracking-tight text-balance">Why businesses choose Rocky Mountain Vending</h2>
|
||||
<h2 className="text-3xl font-semibold tracking-tight text-balance">
|
||||
Why businesses choose Rocky Mountain Vending
|
||||
</h2>
|
||||
<div className="mt-6 grid gap-5 md:grid-cols-2">
|
||||
{[
|
||||
["FREE vending machines", "No upfront costs, hidden fees, or machine charges for qualifying businesses."],
|
||||
["FREE delivery and installation", "Within range of our service areas, we handle setup and launch for you."],
|
||||
["FREE maintenance and repairs", "We keep machines running smoothly so your team has fewer interruptions."],
|
||||
["Healthy and traditional options", "We tailor product mix to the people using the machines every day."],
|
||||
[
|
||||
"Free placement for qualifying locations",
|
||||
"If the location qualifies, we can place and service machines without charging upfront machine or installation fees.",
|
||||
],
|
||||
[
|
||||
"Delivery and installation",
|
||||
"Within our current routes, we handle setup, stocking, and launch so the machines are ready to use.",
|
||||
],
|
||||
[
|
||||
"Repairs and routine service",
|
||||
"We keep machines running, stocked, and clean so your team deals with fewer interruptions.",
|
||||
],
|
||||
[
|
||||
"Healthy and traditional options",
|
||||
"We match the product mix to the people who will actually use the machines every day.",
|
||||
],
|
||||
].map(([title, body]) => (
|
||||
<PublicInset key={title}>
|
||||
<h3 className="text-lg font-semibold text-foreground">{title}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{body}</p>
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
{body}
|
||||
</p>
|
||||
</PublicInset>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<GetFreeMachineCta buttonLabel="Get Your Free Machine" className="h-11 px-5" />
|
||||
<GetFreeMachineCta
|
||||
buttonLabel="See If Your Location Qualifies"
|
||||
className="h-11 px-5"
|
||||
/>
|
||||
</div>
|
||||
</PublicSurface>
|
||||
|
||||
<PublicSurface>
|
||||
<h2 className="text-3xl font-semibold tracking-tight text-balance">What we support in every service area</h2>
|
||||
<h2 className="text-3xl font-semibold tracking-tight text-balance">
|
||||
The types of locations we commonly serve
|
||||
</h2>
|
||||
<div className="mt-6 space-y-4">
|
||||
{[
|
||||
["For businesses", "Offices, warehouses, auto shops, and other workplaces that want reliable on-site vending."],
|
||||
["For schools", "Healthy options for students and staff with a service plan that stays easy to manage."],
|
||||
["For gyms and fitness spaces", "Protein bars, sports drinks, and post-workout options that match the location."],
|
||||
[
|
||||
"For businesses",
|
||||
"Offices, warehouses, auto shops, and other workplaces that want reliable on-site vending.",
|
||||
],
|
||||
[
|
||||
"For schools",
|
||||
"Snacks and drinks for students and staff with service that does not add more work to school staff.",
|
||||
],
|
||||
[
|
||||
"For gyms and fitness spaces",
|
||||
"Protein bars, sports drinks, and post-workout options that match the traffic and audience at the location.",
|
||||
],
|
||||
].map(([title, body]) => (
|
||||
<PublicInset key={title}>
|
||||
<h3 className="text-lg font-semibold">{title}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{body}</p>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
{body}
|
||||
</p>
|
||||
</PublicInset>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,38 +1,36 @@
|
|||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { businessConfig } from "@/lib/seo-config";
|
||||
import { Phone, CheckCircle2, Shield, Clock, MapPin } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { PublicPageHeader, PublicSurface } from "@/components/public-surface";
|
||||
import type { Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { businessConfig } from "@/lib/seo-config"
|
||||
import { Phone, CheckCircle2, Shield, Clock, MapPin } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { PublicPageHeader, PublicSurface } from "@/components/public-surface"
|
||||
import { Breadcrumbs } from "@/components/breadcrumbs"
|
||||
import { generateSEOMetadata } from "@/lib/seo"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Vending Machine Moving & Relocation Services | Rocky Mountain Vending",
|
||||
export const metadata: Metadata = generateSEOMetadata({
|
||||
title: "Vending Machine Moving in Utah | Rocky Mountain Vending",
|
||||
description:
|
||||
"Professional vending machine moving services in Utah. Safe relocation of snack, beverage, and combo machines. Stair climbing, tight spaces, secure transport. Fully insured. Serving Ogden, Salt Lake City, Provo, and surrounding areas.",
|
||||
"Professional vending machine moving and relocation across Utah for snack, beverage, and combo machines.",
|
||||
path: "/services/moving",
|
||||
keywords: [
|
||||
"vending machine moving Utah",
|
||||
"vending machine relocation",
|
||||
"vending machine moving services",
|
||||
"professional vending machine movers",
|
||||
"Utah vending machine transport",
|
||||
"vending machine relocation Utah",
|
||||
"vending machine movers",
|
||||
],
|
||||
openGraph: {
|
||||
title: "Vending Machine Moving & Relocation Services | Rocky Mountain Vending",
|
||||
description:
|
||||
"Professional vending machine moving services in Utah. Safe relocation of snack, beverage, and combo machines. Fully insured.",
|
||||
url: `${businessConfig.website}/services/moving`,
|
||||
type: "website",
|
||||
},
|
||||
alternates: {
|
||||
canonical: `${businessConfig.website}/services/moving`,
|
||||
},
|
||||
};
|
||||
})
|
||||
|
||||
export default function MovingServicesPage() {
|
||||
return (
|
||||
<div className="container mx-auto max-w-6xl px-4 py-10 md:py-14">
|
||||
<Breadcrumbs
|
||||
className="mb-6"
|
||||
items={[
|
||||
{ label: "Services", href: "/services" },
|
||||
{ label: "Moving", href: "/services/moving" },
|
||||
]}
|
||||
/>
|
||||
<PublicPageHeader
|
||||
align="center"
|
||||
eyebrow="Moving Services"
|
||||
|
|
@ -46,16 +44,22 @@ export default function MovingServicesPage() {
|
|||
<CardContent className="p-6 md:p-8">
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<p className="text-muted-foreground leading-relaxed mb-4">
|
||||
At Rocky Mountain Vending LLC, we specialize in the safe and efficient relocation of vending machines of
|
||||
all types and sizes — from compact snack machines to full-size refrigerated beverage and combo units.
|
||||
Whether you're rearranging equipment within a building, moving to a new location, or removing an old
|
||||
machine, our experienced team handles every detail to minimize downtime and protect your investment.
|
||||
At Rocky Mountain Vending LLC, we specialize in the safe and
|
||||
efficient relocation of vending machines of all types and sizes
|
||||
— from compact snack machines to full-size refrigerated beverage
|
||||
and combo units. Whether you're rearranging equipment within a
|
||||
building, moving to a new location, or removing an old machine,
|
||||
our experienced team handles every detail to minimize downtime
|
||||
and protect your investment.
|
||||
</p>
|
||||
<p className="text-muted-foreground leading-relaxed mb-4">
|
||||
Vending machines are heavy (often 400–900+ lbs), delicate, and require specialized handling to avoid
|
||||
damage to internal components like compressors, electronics, glass fronts, or refrigeration systems.
|
||||
Attempting a DIY move can lead to costly repairs, injuries, or property damage. We use proven techniques
|
||||
and professional-grade equipment to ensure a smooth, damage-free process every time.
|
||||
Vending machines are heavy (often 400–900+ lbs), delicate, and
|
||||
require specialized handling to avoid damage to internal
|
||||
components like compressors, electronics, glass fronts, or
|
||||
refrigeration systems. Attempting a DIY move can lead to costly
|
||||
repairs, injuries, or property damage. We use proven techniques
|
||||
and professional-grade equipment to ensure a smooth, damage-free
|
||||
process every time.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -71,7 +75,10 @@ export default function MovingServicesPage() {
|
|||
{/* Image 1 */}
|
||||
<Card className="border-border/50 shadow-md overflow-hidden hover:border-secondary/50 transition-colors">
|
||||
<CardContent className="p-0">
|
||||
<div className="relative w-full bg-muted" style={{ height: '600px' }}>
|
||||
<div
|
||||
className="relative w-full bg-muted"
|
||||
style={{ height: "600px" }}
|
||||
>
|
||||
<Image
|
||||
src="/images/vending-machine-moving-service-1.png"
|
||||
alt="Vending machine securely packaged for transport with dark green protective blankets, clear shrink wrap, bright yellow straps, and yellow corner protectors on wooden pallet - professional moving service in Utah"
|
||||
|
|
@ -84,7 +91,9 @@ export default function MovingServicesPage() {
|
|||
</div>
|
||||
<div className="p-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Our experienced team uses professional-grade equipment and proven techniques to safely relocate vending machines of all sizes, ensuring your equipment arrives damage-free.
|
||||
Our experienced team uses professional-grade equipment and
|
||||
proven techniques to safely relocate vending machines of all
|
||||
sizes, ensuring your equipment arrives damage-free.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -93,7 +102,10 @@ export default function MovingServicesPage() {
|
|||
{/* Image 2 */}
|
||||
<Card className="border-border/50 shadow-md overflow-hidden hover:border-secondary/50 transition-colors">
|
||||
<CardContent className="p-0">
|
||||
<div className="relative w-full bg-muted" style={{ height: '600px' }}>
|
||||
<div
|
||||
className="relative w-full bg-muted"
|
||||
style={{ height: "600px" }}
|
||||
>
|
||||
<Image
|
||||
src="/images/vending-machine-moving-service-2.png"
|
||||
alt="Vending machine wrapped in dark blue protective blankets and clear shrink wrap with yellow straps, foam padding, and safety markings secured on wooden pallets for safe transport"
|
||||
|
|
@ -106,7 +118,9 @@ export default function MovingServicesPage() {
|
|||
</div>
|
||||
<div className="p-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Every move is carefully planned and executed with attention to detail, protecting both your vending machine and your property throughout the relocation process.
|
||||
Every move is carefully planned and executed with attention to
|
||||
detail, protecting both your vending machine and your property
|
||||
throughout the relocation process.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -115,7 +129,10 @@ export default function MovingServicesPage() {
|
|||
{/* Image 3 */}
|
||||
<Card className="border-border/50 shadow-md overflow-hidden hover:border-secondary/50 transition-colors">
|
||||
<CardContent className="p-0">
|
||||
<div className="relative w-full bg-muted" style={{ height: '600px' }}>
|
||||
<div
|
||||
className="relative w-full bg-muted"
|
||||
style={{ height: "600px" }}
|
||||
>
|
||||
<Image
|
||||
src="/images/vending-machine-moving-service-3.webp"
|
||||
alt="Utah vending machine moving professionals transporting commercial vending equipment with secure transport methods and fully insured relocation services"
|
||||
|
|
@ -128,7 +145,9 @@ export default function MovingServicesPage() {
|
|||
</div>
|
||||
<div className="p-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
From stair climbing to tight spaces, we handle challenging access situations with specialized equipment designed for heavy vending machine transport.
|
||||
From stair climbing to tight spaces, we handle challenging
|
||||
access situations with specialized equipment designed for
|
||||
heavy vending machine transport.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -138,7 +157,9 @@ export default function MovingServicesPage() {
|
|||
|
||||
{/* Specialized Moving Capabilities Section */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-3xl font-bold mb-8 tracking-tight text-balance">Our Specialized Moving Capabilities</h2>
|
||||
<h2 className="text-3xl font-bold mb-8 tracking-tight text-balance">
|
||||
Our Specialized Moving Capabilities
|
||||
</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Card className="border-border/50 shadow-md hover:border-secondary/50 transition-colors">
|
||||
<CardContent className="p-6">
|
||||
|
|
@ -147,11 +168,14 @@ export default function MovingServicesPage() {
|
|||
<CheckCircle2 className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-2">Stair Climbing Expertise</h3>
|
||||
<h3 className="text-xl font-semibold mb-2">
|
||||
Stair Climbing Expertise
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Equipped with powered stair-climbing dollies and hand trucks designed specifically for heavy vending
|
||||
equipment, we safely navigate stairs — up or down, straight or curved — without risking strain or
|
||||
tipping.
|
||||
Equipped with powered stair-climbing dollies and hand trucks
|
||||
designed specifically for heavy vending equipment, we safely
|
||||
navigate stairs — up or down, straight or curved — without
|
||||
risking strain or tipping.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -165,11 +189,14 @@ export default function MovingServicesPage() {
|
|||
<CheckCircle2 className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-2">Tight Spaces & Challenging Access</h3>
|
||||
<h3 className="text-xl font-semibold mb-2">
|
||||
Tight Spaces & Challenging Access
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Narrow hallways, elevators, doorways, ramps, or multi-level buildings are no problem. We measure
|
||||
pathways in advance and use low-profile dollies, straps, and protective padding to maneuver through
|
||||
confined areas.
|
||||
Narrow hallways, elevators, doorways, ramps, or multi-level
|
||||
buildings are no problem. We measure pathways in advance and
|
||||
use low-profile dollies, straps, and protective padding to
|
||||
maneuver through confined areas.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -183,10 +210,14 @@ export default function MovingServicesPage() {
|
|||
<Shield className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-2">Secure Transport</h3>
|
||||
<h3 className="text-xl font-semibold mb-2">
|
||||
Secure Transport
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Machines are fully secured upright (critical for refrigerated units) using heavy-duty straps,
|
||||
ramps, and lift gates on our enclosed trailers to prevent shifting during transit.
|
||||
Machines are fully secured upright (critical for
|
||||
refrigerated units) using heavy-duty straps, ramps, and lift
|
||||
gates on our enclosed trailers to prevent shifting during
|
||||
transit.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -200,10 +231,13 @@ export default function MovingServicesPage() {
|
|||
<Shield className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-2">Floor & Property Protection</h3>
|
||||
<h3 className="text-xl font-semibold mb-2">
|
||||
Floor & Property Protection
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
We use mats, corner guards, and non-marking equipment to safeguard floors, walls, doors, and
|
||||
elevators from scratches or dents.
|
||||
We use mats, corner guards, and non-marking equipment to
|
||||
safeguard floors, walls, doors, and elevators from scratches
|
||||
or dents.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -217,11 +251,14 @@ export default function MovingServicesPage() {
|
|||
<CheckCircle2 className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-2">Full Preparation & Setup</h3>
|
||||
<h3 className="text-xl font-semibold mb-2">
|
||||
Full Preparation & Setup
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
We empty product and coin/cash if needed, disconnect utilities safely, protect fragile parts,
|
||||
transport securely, and reposition/level the machine at the new site. Reconnection and testing
|
||||
available upon request.
|
||||
We empty product and coin/cash if needed, disconnect
|
||||
utilities safely, protect fragile parts, transport securely,
|
||||
and reposition/level the machine at the new site.
|
||||
Reconnection and testing available upon request.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -235,10 +272,13 @@ export default function MovingServicesPage() {
|
|||
<MapPin className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-2">Indoor-to-Indoor or Building-to-Building</h3>
|
||||
<h3 className="text-xl font-semibold mb-2">
|
||||
Indoor-to-Indoor or Building-to-Building
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
From simple on-site repositioning to full relocations across Northern Utah (Ogden, Salt Lake City,
|
||||
Layton, Provo, Park City, and surrounding areas).
|
||||
From simple on-site repositioning to full relocations across
|
||||
Northern Utah (Ogden, Salt Lake City, Layton, Provo, Park
|
||||
City, and surrounding areas).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -249,41 +289,55 @@ export default function MovingServicesPage() {
|
|||
|
||||
{/* Why Choose Us Section */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-3xl font-bold mb-8 tracking-tight text-balance">Why Choose Us for Your Vending Move?</h2>
|
||||
<h2 className="text-3xl font-bold mb-8 tracking-tight text-balance">
|
||||
Why Choose Us for Your Vending Move?
|
||||
</h2>
|
||||
<Card className="border-border/70">
|
||||
<CardContent className="p-6 md:p-8">
|
||||
<ul className="space-y-4">
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-muted-foreground">
|
||||
<strong className="text-foreground">Years of hands-on vending industry experience</strong> — we
|
||||
understand the unique vulnerabilities of snack, drink, combo, and refrigerated machines.
|
||||
<strong className="text-foreground">
|
||||
Years of hands-on vending industry experience
|
||||
</strong>{" "}
|
||||
— we understand the unique vulnerabilities of snack, drink,
|
||||
combo, and refrigerated machines.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<Shield className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-muted-foreground">
|
||||
<strong className="text-foreground">Fully insured</strong> for complete peace of mind.
|
||||
<strong className="text-foreground">Fully insured</strong> for
|
||||
complete peace of mind.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<Clock className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-muted-foreground">
|
||||
<strong className="text-foreground">Minimal disruption:</strong> Fast, coordinated service scheduled
|
||||
around your business hours.
|
||||
<strong className="text-foreground">
|
||||
Minimal disruption:
|
||||
</strong>{" "}
|
||||
Fast, coordinated service scheduled around your business
|
||||
hours.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-muted-foreground">
|
||||
<strong className="text-foreground">One-machine or multi-machine jobs</strong> handled efficiently.
|
||||
<strong className="text-foreground">
|
||||
One-machine or multi-machine jobs
|
||||
</strong>{" "}
|
||||
handled efficiently.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<Shield className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-muted-foreground">
|
||||
<strong className="text-foreground">Commitment to safety</strong> for our team, your staff, and your
|
||||
equipment.
|
||||
<strong className="text-foreground">
|
||||
Commitment to safety
|
||||
</strong>{" "}
|
||||
for our team, your staff, and your equipment.
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -294,26 +348,40 @@ export default function MovingServicesPage() {
|
|||
{/* CTA Section */}
|
||||
<section className="mb-12">
|
||||
<PublicSurface className="p-10 text-center md:p-12">
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-foreground mb-4 tracking-tight text-balance">
|
||||
Ready to Schedule a Hassle-Free Vending Machine Move?
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-8 max-w-2xl mx-auto text-lg leading-relaxed">
|
||||
Contact us today for a custom quote based on machine type, pickup/drop-off locations, access challenges,
|
||||
and any stairs or special requirements involved. We're here to make your relocation simple and stress-free!
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button asChild size="lg" className="text-lg h-12 px-8 font-semibold rounded-full">
|
||||
<Link href="/contact-us#contact-form">Request a Quote</Link>
|
||||
</Button>
|
||||
<Button asChild size="lg" variant="outline" className="text-lg h-12 px-8 font-semibold rounded-full">
|
||||
<a href={businessConfig.phoneUrl} className="flex items-center gap-2">
|
||||
<Phone className="w-5 h-5" />
|
||||
{businessConfig.phone}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-foreground mb-4 tracking-tight text-balance">
|
||||
Ready to Schedule a Hassle-Free Vending Machine Move?
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-8 max-w-2xl mx-auto text-lg leading-relaxed">
|
||||
Contact us today for a custom quote based on machine type,
|
||||
pickup/drop-off locations, access challenges, and any stairs or
|
||||
special requirements involved. We're here to make your relocation
|
||||
simple and stress-free!
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="text-lg h-12 px-8 font-semibold rounded-full"
|
||||
>
|
||||
<Link href="/contact-us#contact-form">Request a Quote</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="text-lg h-12 px-8 font-semibold rounded-full"
|
||||
>
|
||||
<a
|
||||
href={businessConfig.phoneUrl}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Phone className="w-5 h-5" />
|
||||
{businessConfig.phone}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</PublicSurface>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,75 +1,79 @@
|
|||
import { notFound } from 'next/navigation';
|
||||
import { loadImageMapping } from '@/lib/wordpress-content';
|
||||
import { generateSEOMetadata, generateStructuredData } from '@/lib/seo';
|
||||
import { getPageBySlug } from '@/lib/wordpress-data-loader';
|
||||
import { cleanWordPressContent } from '@/lib/clean-wordPress-content';
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from "next/navigation"
|
||||
import { loadImageMapping } from "@/lib/wordpress-content"
|
||||
import { generateSEOMetadata, generateStructuredData } from "@/lib/seo"
|
||||
import { getPageBySlug } from "@/lib/wordpress-data-loader"
|
||||
import { cleanWordPressContent } from "@/lib/clean-wordPress-content"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
const WORDPRESS_SLUG = 'vending-machine-repairs';
|
||||
const WORDPRESS_SLUG = "vending-machine-repairs"
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const page = getPageBySlug(WORDPRESS_SLUG);
|
||||
const page = getPageBySlug(WORDPRESS_SLUG)
|
||||
|
||||
if (!page) {
|
||||
return {
|
||||
title: 'Page Not Found | Rocky Mountain Vending',
|
||||
};
|
||||
title: "Page Not Found | Rocky Mountain Vending",
|
||||
}
|
||||
}
|
||||
|
||||
return generateSEOMetadata({
|
||||
title: page.title || 'Vending Machine Services',
|
||||
description: page.seoDescription || page.excerpt || '',
|
||||
title: page.title || "Vending Machine Services",
|
||||
description: page.seoDescription || page.excerpt || "",
|
||||
excerpt: page.excerpt,
|
||||
date: page.date,
|
||||
modified: page.modified,
|
||||
image: page.images?.[0]?.localPath,
|
||||
});
|
||||
path: "/services",
|
||||
})
|
||||
}
|
||||
|
||||
export default async function ServicesPage() {
|
||||
try {
|
||||
const page = getPageBySlug(WORDPRESS_SLUG);
|
||||
const page = getPageBySlug(WORDPRESS_SLUG)
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
|
||||
let imageMapping: any = {};
|
||||
let imageMapping: any = {}
|
||||
try {
|
||||
imageMapping = loadImageMapping();
|
||||
imageMapping = loadImageMapping()
|
||||
} catch (e) {
|
||||
imageMapping = {};
|
||||
imageMapping = {}
|
||||
}
|
||||
|
||||
const content = page.content ? (
|
||||
<div className="max-w-none">
|
||||
{cleanWordPressContent(String(page.content), {
|
||||
{cleanWordPressContent(String(page.content), {
|
||||
imageMapping,
|
||||
pageTitle: page.title
|
||||
pageTitle: page.title,
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">No content available.</p>
|
||||
);
|
||||
)
|
||||
|
||||
let structuredData;
|
||||
let structuredData
|
||||
try {
|
||||
structuredData = generateStructuredData({
|
||||
title: page.title || 'Vending Machine Services',
|
||||
description: page.seoDescription || page.excerpt || '',
|
||||
url: page.link || page.urlPath || `https://rockymountainvending.com/services/`,
|
||||
title: page.title || "Vending Machine Services",
|
||||
description: page.seoDescription || page.excerpt || "",
|
||||
url:
|
||||
page.link ||
|
||||
page.urlPath ||
|
||||
`https://rockymountainvending.com/services/`,
|
||||
datePublished: page.date,
|
||||
dateModified: page.modified || page.date,
|
||||
type: 'WebPage',
|
||||
});
|
||||
type: "WebPage",
|
||||
})
|
||||
} catch (e) {
|
||||
structuredData = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
headline: page.title || 'Vending Machine Services',
|
||||
description: page.seoDescription || '',
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
headline: page.title || "Vending Machine Services",
|
||||
description: page.seoDescription || "",
|
||||
url: `https://rockymountainvending.com/services/`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -80,24 +84,18 @@ export default async function ServicesPage() {
|
|||
/>
|
||||
<article className="container mx-auto px-4 py-8 md:py-12 max-w-4xl">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-balance mb-6">{page.title || 'Vending Machine Services'}</h1>
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-balance mb-6">
|
||||
{page.title || "Vending Machine Services"}
|
||||
</h1>
|
||||
</header>
|
||||
{content}
|
||||
</article>
|
||||
</>
|
||||
);
|
||||
)
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Error rendering Services page:', error);
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("Error rendering Services page:", error)
|
||||
}
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,79 +1,96 @@
|
|||
import { notFound } from 'next/navigation';
|
||||
import { loadImageMapping } from '@/lib/wordpress-content';
|
||||
import { generateSEOMetadata, generateStructuredData } from '@/lib/seo';
|
||||
import { getPageBySlug } from '@/lib/wordpress-data-loader';
|
||||
import { cleanWordPressContent } from '@/lib/clean-wordPress-content';
|
||||
import { ServicesSection } from '@/components/services-section';
|
||||
import { FAQSection } from '@/components/faq-section';
|
||||
import { ServiceAreasSection } from '@/components/service-areas-section';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { CheckCircle2, Wrench, Clock, Phone, Shield, MapPin } from 'lucide-react';
|
||||
import { RepairsImageCarousel } from '@/components/repairs-image-carousel';
|
||||
import { ContactForm } from '@/components/forms/contact-form';
|
||||
import { PublicInset, PublicSurface } from '@/components/public-surface';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { businessConfig } from '@/lib/seo-config';
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from "next/navigation"
|
||||
import { loadImageMapping } from "@/lib/wordpress-content"
|
||||
import { generateSEOMetadata, generateStructuredData } from "@/lib/seo"
|
||||
import { getPageBySlug } from "@/lib/wordpress-data-loader"
|
||||
import { cleanWordPressContent } from "@/lib/clean-wordPress-content"
|
||||
import { ServicesSection } from "@/components/services-section"
|
||||
import { FAQSection } from "@/components/faq-section"
|
||||
import { ServiceAreasSection } from "@/components/service-areas-section"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import {
|
||||
CheckCircle2,
|
||||
Wrench,
|
||||
Clock,
|
||||
Phone,
|
||||
Shield,
|
||||
MapPin,
|
||||
} from "lucide-react"
|
||||
import { RepairsImageCarousel } from "@/components/repairs-image-carousel"
|
||||
import { ContactForm } from "@/components/forms/contact-form"
|
||||
import { Breadcrumbs } from "@/components/breadcrumbs"
|
||||
import { PublicInset, PublicSurface } from "@/components/public-surface"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { ArrowRight } from "lucide-react"
|
||||
import { businessConfig } from "@/lib/seo-config"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
const WORDPRESS_SLUG = 'vending-machine-repairs';
|
||||
const WORDPRESS_SLUG = "vending-machine-repairs"
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const page = getPageBySlug(WORDPRESS_SLUG);
|
||||
const page = getPageBySlug(WORDPRESS_SLUG)
|
||||
|
||||
if (!page) {
|
||||
return {
|
||||
title: 'Vending Machine Repairs | Rocky Mountain Vending',
|
||||
description: 'Professional vending machine repair services in Utah. Expert technicians for all vending machine types.',
|
||||
};
|
||||
title: "Vending Machine Repairs | Rocky Mountain Vending",
|
||||
description:
|
||||
"Professional vending machine repair services in Utah. Expert technicians for all vending machine types.",
|
||||
}
|
||||
}
|
||||
|
||||
return generateSEOMetadata({
|
||||
title: page.title || 'Vending Machine Repairs',
|
||||
description: page.seoDescription || page.excerpt || '',
|
||||
title: page.title || "Vending Machine Repairs",
|
||||
description: page.seoDescription || page.excerpt || "",
|
||||
excerpt: page.excerpt,
|
||||
date: page.date,
|
||||
modified: page.modified,
|
||||
image: page.images?.[0]?.localPath,
|
||||
});
|
||||
path: "/services/repairs",
|
||||
})
|
||||
}
|
||||
|
||||
export default async function RepairsPage() {
|
||||
try {
|
||||
const page = getPageBySlug(WORDPRESS_SLUG);
|
||||
const page = getPageBySlug(WORDPRESS_SLUG)
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
|
||||
let imageMapping: any = {};
|
||||
let imageMapping: any = {}
|
||||
try {
|
||||
imageMapping = loadImageMapping();
|
||||
imageMapping = loadImageMapping()
|
||||
} catch (e) {
|
||||
imageMapping = {};
|
||||
imageMapping = {}
|
||||
}
|
||||
|
||||
// Extract FAQs from content
|
||||
const faqs: Array<{ question: string; answer: string }> = [];
|
||||
let contentWithoutFAQs = page.content || '';
|
||||
let contentWithoutVirtualServices = '';
|
||||
let virtualServicesContent = '';
|
||||
|
||||
const faqs: Array<{ question: string; answer: string }> = []
|
||||
let contentWithoutFAQs = page.content || ""
|
||||
let contentWithoutVirtualServices = ""
|
||||
let virtualServicesContent = ""
|
||||
|
||||
if (page.content) {
|
||||
const contentStr = String(page.content);
|
||||
|
||||
const contentStr = String(page.content)
|
||||
|
||||
// Extract FAQ items from accordion structure
|
||||
const questionMatches = contentStr.matchAll(/<span class="ekit-accordion-title">([^<]+)<\/span>/g);
|
||||
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 => {
|
||||
let answer = m[1].trim();
|
||||
answer = answer.replace(/\n\s*\n/g, '\n').replace(/>\s+</g, '><').trim();
|
||||
return answer;
|
||||
});
|
||||
|
||||
const questionMatches = contentStr.matchAll(
|
||||
/<span class="ekit-accordion-title">([^<]+)<\/span>/g
|
||||
)
|
||||
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) => {
|
||||
let answer = m[1].trim()
|
||||
answer = answer
|
||||
.replace(/\n\s*\n/g, "\n")
|
||||
.replace(/>\s+</g, "><")
|
||||
.trim()
|
||||
return answer
|
||||
})
|
||||
|
||||
// Match questions with answers and clean HTML entities
|
||||
questions.forEach((question, index) => {
|
||||
if (answers[index]) {
|
||||
|
|
@ -81,62 +98,69 @@ export default async function RepairsPage() {
|
|||
const cleanQuestion = question
|
||||
.replace(/'/g, "'")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/ /g, ' ')
|
||||
.trim();
|
||||
faqs.push({ question: cleanQuestion, answer: answers[index] });
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/ /g, " ")
|
||||
.trim()
|
||||
faqs.push({ question: cleanQuestion, answer: answers[index] })
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Remove FAQ section from content if FAQs were found
|
||||
if (faqs.length > 0) {
|
||||
const faqSectionRegex = /<h2[^>]*>.*?Answers\s+To\s+Common\s+Questions.*?<\/h2>[\s\S]*?(?=<h2[^>]*>.*?Virtual\s+Services|<h2[^>]*>.*?Service\s+Area|$)/i;
|
||||
contentWithoutFAQs = contentStr.replace(faqSectionRegex, '').trim();
|
||||
const faqSectionRegex =
|
||||
/<h2[^>]*>.*?Answers\s+To\s+Common\s+Questions.*?<\/h2>[\s\S]*?(?=<h2[^>]*>.*?Virtual\s+Services|<h2[^>]*>.*?Service\s+Area|$)/i
|
||||
contentWithoutFAQs = contentStr.replace(faqSectionRegex, "").trim()
|
||||
}
|
||||
|
||||
// Extract Virtual Services section
|
||||
const virtualServicesRegex = /<h2[^>]*>.*?Virtual\s+Services.*?<\/h2>([\s\S]*?)(?=<h2[^>]*>.*?Service\s+Area|$)/i;
|
||||
const virtualMatch = contentStr.match(virtualServicesRegex);
|
||||
const virtualServicesRegex =
|
||||
/<h2[^>]*>.*?Virtual\s+Services.*?<\/h2>([\s\S]*?)(?=<h2[^>]*>.*?Service\s+Area|$)/i
|
||||
const virtualMatch = contentStr.match(virtualServicesRegex)
|
||||
if (virtualMatch) {
|
||||
virtualServicesContent = virtualMatch[1];
|
||||
virtualServicesContent = virtualMatch[1]
|
||||
// Remove Virtual Services from main content
|
||||
contentWithoutVirtualServices = contentWithoutFAQs.replace(virtualServicesRegex, '').trim();
|
||||
contentWithoutVirtualServices = contentWithoutFAQs
|
||||
.replace(virtualServicesRegex, "")
|
||||
.trim()
|
||||
} else {
|
||||
contentWithoutVirtualServices = contentWithoutFAQs;
|
||||
contentWithoutVirtualServices = contentWithoutFAQs
|
||||
}
|
||||
}
|
||||
|
||||
const content = contentWithoutVirtualServices ? (
|
||||
<div className="max-w-none">
|
||||
{cleanWordPressContent(String(contentWithoutVirtualServices), {
|
||||
{cleanWordPressContent(String(contentWithoutVirtualServices), {
|
||||
imageMapping,
|
||||
pageTitle: page.title
|
||||
pageTitle: page.title,
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">No content available.</p>
|
||||
);
|
||||
)
|
||||
|
||||
let structuredData;
|
||||
let structuredData
|
||||
try {
|
||||
structuredData = generateStructuredData({
|
||||
title: page.title || 'Vending Machine Repairs',
|
||||
description: page.seoDescription || page.excerpt || '',
|
||||
url: page.link || page.urlPath || `https://rockymountainvending.com/services/repairs/`,
|
||||
title: page.title || "Vending Machine Repairs",
|
||||
description: page.seoDescription || page.excerpt || "",
|
||||
url:
|
||||
page.link ||
|
||||
page.urlPath ||
|
||||
`https://rockymountainvending.com/services/repairs/`,
|
||||
datePublished: page.date,
|
||||
dateModified: page.modified || page.date,
|
||||
type: 'WebPage',
|
||||
});
|
||||
type: "WebPage",
|
||||
})
|
||||
} catch (e) {
|
||||
structuredData = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
headline: page.title || 'Vending Machine Repairs',
|
||||
description: page.seoDescription || '',
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
headline: page.title || "Vending Machine Repairs",
|
||||
description: page.seoDescription || "",
|
||||
url: `https://rockymountainvending.com/services/repairs/`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -148,12 +172,43 @@ export default async function RepairsPage() {
|
|||
{/* Hero Header */}
|
||||
<section className="py-20 md:py-28 bg-background">
|
||||
<div className="container mx-auto px-4 max-w-4xl">
|
||||
<Breadcrumbs
|
||||
className="mb-6"
|
||||
items={[
|
||||
{ label: "Services", href: "/services" },
|
||||
{ label: "Repairs", href: "/services/repairs" },
|
||||
]}
|
||||
/>
|
||||
<header className="mb-8 text-center">
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-balance mb-4">
|
||||
{page.title || 'Vending Machine Repairs and Service'}
|
||||
{page.title || "Vending Machine Repairs and Service"}
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground max-w-3xl mx-auto leading-relaxed mb-8">
|
||||
Rocky Mountain Vending delivers expert <Link href="/services/repairs" className="text-primary hover:underline font-semibold">vending machine repair</Link> and maintenance services to keep your business thriving. From resolving jammed coin slots and refrigeration issues to fixing non-dispensing machines, our skilled technicians ensure reliable performance. For all your <Link href="/services/parts" className="text-primary hover:underline">vending machine parts</Link> needs and professional <Link href="/services/moving" className="text-primary hover:underline">vending machine moving</Link> services, contact us today for fast, professional solutions!
|
||||
Rocky Mountain Vending delivers expert{" "}
|
||||
<Link
|
||||
href="/services/repairs"
|
||||
className="text-primary hover:underline font-semibold"
|
||||
>
|
||||
vending machine repair
|
||||
</Link>{" "}
|
||||
and maintenance services to keep your business thriving. From
|
||||
resolving jammed coin slots and refrigeration issues to fixing
|
||||
non-dispensing machines, our skilled technicians ensure reliable
|
||||
performance. For all your{" "}
|
||||
<Link
|
||||
href="/services/parts"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
vending machine parts
|
||||
</Link>{" "}
|
||||
needs and professional{" "}
|
||||
<Link
|
||||
href="/services/moving"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
vending machine moving
|
||||
</Link>{" "}
|
||||
services, contact us today for fast, professional solutions!
|
||||
</p>
|
||||
</header>
|
||||
{/* Images Carousel */}
|
||||
|
|
@ -167,7 +222,9 @@ export default async function RepairsPage() {
|
|||
<section className="py-20 md:py-28 bg-muted/30">
|
||||
<div className="container mx-auto px-4 max-w-6xl">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl mb-4 text-balance">Services</h2>
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl mb-4 text-balance">
|
||||
Services
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto text-pretty leading-relaxed mb-8">
|
||||
<strong>Our Repair and Maintenance Services Include:</strong>
|
||||
</p>
|
||||
|
|
@ -178,35 +235,51 @@ export default async function RepairsPage() {
|
|||
<ul className="space-y-4">
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">Diagnosing and fixing vending machine errors</span>
|
||||
<span className="text-foreground">
|
||||
Diagnosing and fixing vending machine errors
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">Bill and coin mechanism repairs</span>
|
||||
<span className="text-foreground">
|
||||
Bill and coin mechanism repairs
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">Refrigeration system maintenance and repair</span>
|
||||
<span className="text-foreground">
|
||||
Refrigeration system maintenance and repair
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">Card reader troubleshooting and setup</span>
|
||||
<span className="text-foreground">
|
||||
Card reader troubleshooting and setup
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">Software updates and programming</span>
|
||||
<span className="text-foreground">
|
||||
Software updates and programming
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">Machine calibration and inventory setup</span>
|
||||
<span className="text-foreground">
|
||||
Machine calibration and inventory setup
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">Preventative maintenance services</span>
|
||||
<span className="text-foreground">
|
||||
Preventative maintenance services
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">Credit card reader upgrade and installation</span>
|
||||
<span className="text-foreground">
|
||||
Credit card reader upgrade and installation
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
|
|
@ -228,7 +301,9 @@ export default async function RepairsPage() {
|
|||
<section className="py-20 md:py-28">
|
||||
<div className="container mx-auto px-4 max-w-6xl">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl mb-4 text-balance">Why Choose Rocky Mountain Vending?</h2>
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl mb-4 text-balance">
|
||||
Why Choose Rocky Mountain Vending?
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid gap-8 md:grid-cols-2 items-start">
|
||||
<div className="relative aspect-video rounded-lg overflow-hidden shadow-lg">
|
||||
|
|
@ -245,27 +320,42 @@ export default async function RepairsPage() {
|
|||
<ul className="space-y-4">
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">Local experts serving Salt Lake City for over 10 years experience</span>
|
||||
<span className="text-foreground">
|
||||
Local experts serving Salt Lake City for over 10 years
|
||||
experience
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">Fast response times to minimize downtime</span>
|
||||
<span className="text-foreground">
|
||||
Fast response times to minimize downtime
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">Experienced technicians familiar with all major vending machine brands</span>
|
||||
<span className="text-foreground">
|
||||
Experienced technicians familiar with all major vending
|
||||
machine brands
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">Affordable service plans tailored to your needs</span>
|
||||
<span className="text-foreground">
|
||||
Affordable service plans tailored to your needs
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">Preventative maintenance services</span>
|
||||
<span className="text-foreground">
|
||||
Preventative maintenance services
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">We've often come in and fixed problems others couldn't fix</span>
|
||||
<span className="text-foreground">
|
||||
We've often come in and fixed problems others couldn't
|
||||
fix
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
|
|
@ -275,25 +365,34 @@ export default async function RepairsPage() {
|
|||
</section>
|
||||
|
||||
{/* How It Works Section */}
|
||||
|
||||
|
||||
{/* Related Services Section */}
|
||||
<section className="py-20 md:py-28 bg-background">
|
||||
<div className="container mx-auto px-4 max-w-6xl">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl mb-4 text-balance">Our Complete Service Network</h2>
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl mb-4 text-balance">
|
||||
Our Complete Service Network
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto text-pretty leading-relaxed mb-8">
|
||||
Rocky Mountain Vending provides comprehensive vending solutions across Utah
|
||||
Rocky Mountain Vending provides comprehensive vending solutions
|
||||
across Utah
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
<Card className="border-border/50 hover:border-secondary/50 transition-colors">
|
||||
<CardContent className="pt-8 text-center">
|
||||
<Wrench className="h-12 w-12 text-primary mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold mb-3">Vending Machine Parts</h3>
|
||||
<h3 className="text-xl font-semibold mb-3">
|
||||
Vending Machine Parts
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Quality replacement parts and components for all major vending machine brands
|
||||
Quality replacement parts and components for all major
|
||||
vending machine brands
|
||||
</p>
|
||||
<Link href="/services/parts" className="inline-flex items-center gap-2 text-primary hover:underline font-medium">
|
||||
<Link
|
||||
href="/services/parts"
|
||||
className="inline-flex items-center gap-2 text-primary hover:underline font-medium"
|
||||
>
|
||||
View Parts Services <ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</CardContent>
|
||||
|
|
@ -303,9 +402,13 @@ export default async function RepairsPage() {
|
|||
<MapPin className="h-12 w-12 text-primary mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold mb-3">Service Areas</h3>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Serving 20+ cities across Utah with free delivery and installation
|
||||
Serving 20+ cities across Utah with free delivery and
|
||||
installation
|
||||
</p>
|
||||
<Link href="/service-areas" className="inline-flex items-center gap-2 text-primary hover:underline font-medium">
|
||||
<Link
|
||||
href="/service-areas"
|
||||
className="inline-flex items-center gap-2 text-primary hover:underline font-medium"
|
||||
>
|
||||
View Service Areas <ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</CardContent>
|
||||
|
|
@ -313,11 +416,17 @@ export default async function RepairsPage() {
|
|||
<Card className="border-border/50 hover:border-secondary/50 transition-colors">
|
||||
<CardContent className="pt-8 text-center">
|
||||
<Clock className="h-12 w-12 text-primary mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold mb-3">Moving Services</h3>
|
||||
<h3 className="text-xl font-semibold mb-3">
|
||||
Moving Services
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Professional vending machine relocation and installation services
|
||||
Professional vending machine relocation and installation
|
||||
services
|
||||
</p>
|
||||
<Link href="/services/moving" className="inline-flex items-center gap-2 text-primary hover:underline font-medium">
|
||||
<Link
|
||||
href="/services/moving"
|
||||
className="inline-flex items-center gap-2 text-primary hover:underline font-medium"
|
||||
>
|
||||
View Moving Services <ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</CardContent>
|
||||
|
|
@ -326,11 +435,15 @@ export default async function RepairsPage() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section id="how-it-works" className="py-20 md:py-28 bg-muted/30 scroll-mt-24">
|
||||
<section
|
||||
id="how-it-works"
|
||||
className="py-20 md:py-28 bg-muted/30 scroll-mt-24"
|
||||
>
|
||||
<div className="container mx-auto px-4 max-w-6xl">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl mb-4 text-balance">How It Works</h2>
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl mb-4 text-balance">
|
||||
How It Works
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-4 mb-12">
|
||||
<Card className="border-border/50 hover:border-secondary/50 transition-colors">
|
||||
|
|
@ -340,9 +453,15 @@ export default async function RepairsPage() {
|
|||
01
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-3">Fill Out the Form</h3>
|
||||
<h3 className="text-xl font-semibold mb-3">
|
||||
Fill Out the Form
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
Start by filling out the service form with as much detail as possible about your vending machine and the issue you're experiencing. By providing detailed information upfront, you help us save you time and money by allowing our technicians to potentially diagnose the issue before arriving.
|
||||
Start by filling out the service form with as much detail as
|
||||
possible about your vending machine and the issue you're
|
||||
experiencing. By providing detailed information upfront, you
|
||||
help us save you time and money by allowing our technicians
|
||||
to potentially diagnose the issue before arriving.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -353,9 +472,13 @@ export default async function RepairsPage() {
|
|||
02
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-3">We'll Review Your Request</h3>
|
||||
<h3 className="text-xl font-semibold mb-3">
|
||||
We'll Review Your Request
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
Once we receive your request, our team will review it and contact you to confirm whether the issue can be resolved virtually or needs in-person service.
|
||||
Once we receive your request, our team will review it and
|
||||
contact you to confirm whether the issue can be resolved
|
||||
virtually or needs in-person service.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -366,9 +489,15 @@ export default async function RepairsPage() {
|
|||
03
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-3">Schedule Your Service</h3>
|
||||
<h3 className="text-xl font-semibold mb-3">
|
||||
Schedule Your Service
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
After reviewing your request, we'll help you schedule an in-person service visit or a virtual session via Google Meet. Virtual sessions will include payment details before you can schedule a time. You won't be charged until you select a time that works for you.
|
||||
After reviewing your request, we'll help you schedule an
|
||||
in-person service visit or a virtual session via Google
|
||||
Meet. Virtual sessions will include payment details before
|
||||
you can schedule a time. You won't be charged until you
|
||||
select a time that works for you.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -379,37 +508,53 @@ export default async function RepairsPage() {
|
|||
04
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-3">Get Your Machine Back Up and Running</h3>
|
||||
<h3 className="text-xl font-semibold mb-3">
|
||||
Get Your Machine Back Up and Running
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
Our expert technicians will repair your vending machine or guide you step-by-step during the virtual session.
|
||||
Our expert technicians will repair your vending machine or
|
||||
guide you step-by-step during the virtual session.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Request Service Form */}
|
||||
<div id="request-service" className="max-w-3xl mx-auto scroll-mt-24">
|
||||
<div
|
||||
id="request-service"
|
||||
className="max-w-3xl mx-auto scroll-mt-24"
|
||||
>
|
||||
<div className="text-center mb-8">
|
||||
<h3 className="text-2xl font-bold tracking-tight mb-2 text-balance">
|
||||
Request Service
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Tell us what your machine is doing and our team will help you figure out the fastest next step.
|
||||
Tell us what your machine is doing and our team will help you
|
||||
figure out the fastest next step.
|
||||
</p>
|
||||
</div>
|
||||
<PublicSurface className="space-y-6 p-5 md:p-7">
|
||||
<PublicInset className="flex flex-col gap-3 p-5 text-sm text-muted-foreground md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p className="font-semibold text-foreground">Before you submit</p>
|
||||
<p className="font-semibold text-foreground">
|
||||
Before you submit
|
||||
</p>
|
||||
<p className="mt-1 leading-relaxed">
|
||||
Include the machine model, what it is doing, and whether you need on-site help or virtual support.
|
||||
Include the machine model, what it is doing, and whether
|
||||
you need on-site help or virtual support.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 text-left md:text-right">
|
||||
<a href={businessConfig.publicCallUrl} className="font-medium text-foreground hover:text-primary">
|
||||
<a
|
||||
href={businessConfig.publicCallUrl}
|
||||
className="font-medium text-foreground hover:text-primary"
|
||||
>
|
||||
Call {businessConfig.publicCallNumber}
|
||||
</a>
|
||||
<a href={businessConfig.publicSmsUrl} className="font-medium text-foreground hover:text-primary">
|
||||
<a
|
||||
href={businessConfig.publicSmsUrl}
|
||||
className="font-medium text-foreground hover:text-primary"
|
||||
>
|
||||
Text photos or videos
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -424,16 +569,29 @@ export default async function RepairsPage() {
|
|||
<section className="py-20 md:py-28 bg-background">
|
||||
<div className="container mx-auto px-4 max-w-4xl">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl mb-4 text-balance">Brands We Commonly Service</h2>
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl mb-4 text-balance">
|
||||
Brands We Commonly Service
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto text-pretty leading-relaxed mb-8">
|
||||
If you don't see your brand listed, feel free to reach out! We may still be able to service it, but there are some brands we may not support. <Link href="/contact" className="text-primary hover:underline">Contact us</Link>, and we'll let you know how we can help.
|
||||
If you don't see your brand listed, feel free to reach out! We
|
||||
may still be able to service it, but there are some brands we
|
||||
may not support.{" "}
|
||||
<Link
|
||||
href="/contact-us"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Contact us
|
||||
</Link>
|
||||
, and we'll let you know how we can help.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
{/* Vending Machine Brands */}
|
||||
<Card className="border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Vending Machine Brands We Commonly Service</CardTitle>
|
||||
<CardTitle className="text-2xl">
|
||||
Vending Machine Brands We Commonly Service
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<ul className="space-y-2">
|
||||
|
|
@ -458,7 +616,9 @@ export default async function RepairsPage() {
|
|||
{/* Card Reader Brands */}
|
||||
<Card className="border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Card Reader Brands We Service</CardTitle>
|
||||
<CardTitle className="text-2xl">
|
||||
Card Reader Brands We Service
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<ul className="space-y-2">
|
||||
|
|
@ -472,7 +632,9 @@ export default async function RepairsPage() {
|
|||
{/* Bill Validator and Coin Mechanism Brands */}
|
||||
<Card className="border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Bill Validator and Coin Mechanism Brands We Service</CardTitle>
|
||||
<CardTitle className="text-2xl">
|
||||
Bill Validator and Coin Mechanism Brands We Service
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<ul className="space-y-2">
|
||||
|
|
@ -493,25 +655,46 @@ export default async function RepairsPage() {
|
|||
<section className="py-20 md:py-28 bg-muted/30">
|
||||
<div className="container mx-auto px-4 max-w-4xl">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl mb-4 text-balance">Technologies and Protocols We Service</h2>
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl mb-4 text-balance">
|
||||
Technologies and Protocols We Service
|
||||
</h2>
|
||||
</div>
|
||||
<Card className="border-border/50">
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-muted-foreground mb-6 leading-relaxed">
|
||||
At Rocky Mountain Vending, we support three key vending machine technologies to boost your business profitability. The MDB (Multi-Drop Bus) protocol connects payment systems seamlessly, ensuring smooth transactions. DEX (Data Exchange) provides remote auditing of sales and inventory, helping you optimize stock and reduce waste. CCI (Crane Connectivity Interface) enhances cashless payments and telemetry, increasing convenience and revenue potential. Together, these technologies streamline operations and maximize your earnings.
|
||||
At Rocky Mountain Vending, we support three key vending
|
||||
machine technologies to boost your business profitability. The
|
||||
MDB (Multi-Drop Bus) protocol connects payment systems
|
||||
seamlessly, ensuring smooth transactions. DEX (Data Exchange)
|
||||
provides remote auditing of sales and inventory, helping you
|
||||
optimize stock and reduce waste. CCI (Crane Connectivity
|
||||
Interface) enhances cashless payments and telemetry,
|
||||
increasing convenience and revenue potential. Together, these
|
||||
technologies streamline operations and maximize your earnings.
|
||||
</p>
|
||||
<ul className="space-y-4">
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground"><strong>CCI (Crane Connectivity Interface)</strong> – A modern protocol for enhanced cashless payments and telemetry integration; we can provide limited support.</span>
|
||||
<span className="text-foreground">
|
||||
<strong>CCI (Crane Connectivity Interface)</strong> – A
|
||||
modern protocol for enhanced cashless payments and
|
||||
telemetry integration; we can provide limited support.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground"><strong>DEX (Data Exchange)</strong> – A standard protocol for auditing sales and inventory data remotely.</span>
|
||||
<span className="text-foreground">
|
||||
<strong>DEX (Data Exchange)</strong> – A standard protocol
|
||||
for auditing sales and inventory data remotely.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground"><strong>MDB (Multi-Drop Bus)</strong> – The industry-standard protocol for connecting payment systems to vending controllers.</span>
|
||||
<span className="text-foreground">
|
||||
<strong>MDB (Multi-Drop Bus)</strong> – The
|
||||
industry-standard protocol for connecting payment systems
|
||||
to vending controllers.
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
|
|
@ -527,14 +710,21 @@ export default async function RepairsPage() {
|
|||
<section className="py-20 md:py-28 bg-muted/30">
|
||||
<div className="container mx-auto px-4 max-w-4xl">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl mb-4 text-balance">Virtual Services</h2>
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl mb-4 text-balance">
|
||||
Virtual Services
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
{/* How It Works */}
|
||||
<div className="prose prose-lg max-w-none prose-headings:text-foreground prose-p:text-muted-foreground prose-a:text-foreground prose-a:hover:text-secondary prose-a:transition-colors">
|
||||
<h3 className="text-2xl font-bold mb-4">How It Works</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
Virtual support is a convenient way to resolve vending machine issues remotely. Through a <strong>Google Meet session</strong>, our expert technicians will guide you step-by-step to troubleshoot and repair your machine. This saves time and allows you to get back to business quickly without waiting for an onsite visit.
|
||||
Virtual support is a convenient way to resolve vending
|
||||
machine issues remotely. Through a{" "}
|
||||
<strong>Google Meet session</strong>, our expert technicians
|
||||
will guide you step-by-step to troubleshoot and repair your
|
||||
machine. This saves time and allows you to get back to
|
||||
business quickly without waiting for an onsite visit.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -545,23 +735,35 @@ export default async function RepairsPage() {
|
|||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-muted-foreground mb-4 leading-relaxed">
|
||||
To make the most of your virtual session, it's important to be prepared with the necessary tools. Having the right equipment on hand ensures we can troubleshoot effectively and minimize delays.
|
||||
To make the most of your virtual session, it's important
|
||||
to be prepared with the necessary tools. Having the right
|
||||
equipment on hand ensures we can troubleshoot effectively
|
||||
and minimize delays.
|
||||
</p>
|
||||
<p className="text-muted-foreground mb-4 leading-relaxed">
|
||||
<strong className="text-foreground">Required Tools:</strong>
|
||||
<strong className="text-foreground">
|
||||
Required Tools:
|
||||
</strong>
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">Multimeter (to test electrical components)</span>
|
||||
<span className="text-foreground">
|
||||
Multimeter (to test electrical components)
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">11/32" Deep Socket Nut Driver (in case we need to remove the bill or coin mechs)</span>
|
||||
<span className="text-foreground">
|
||||
11/32" Deep Socket Nut Driver (in case we need to
|
||||
remove the bill or coin mechs)
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">#2 Phillips Screwdriver</span>
|
||||
<span className="text-foreground">
|
||||
#2 Phillips Screwdriver
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-primary flex-shrink-0 mt-0.5" />
|
||||
|
|
@ -573,11 +775,19 @@ export default async function RepairsPage() {
|
|||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-primary flex-shrink-0 mt-0.5" />
|
||||
<span className="text-foreground">Any additional tools you would typically need to service a vending machine</span>
|
||||
<span className="text-foreground">
|
||||
Any additional tools you would typically need to
|
||||
service a vending machine
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p className="text-muted-foreground mt-4 text-sm leading-relaxed">
|
||||
Not having the right tools on hand will make it so we won't be able to help you. There might be more tools that you will need. In that case we if we know of these we will advise you in advance of what you will need to get. Please ensure you have these and are comfortable using these tools. We can't do HVAC refill repairs.
|
||||
Not having the right tools on hand will make it so we
|
||||
won't be able to help you. There might be more tools that
|
||||
you will need. In that case we if we know of these we will
|
||||
advise you in advance of what you will need to get. Please
|
||||
ensure you have these and are comfortable using these
|
||||
tools. We can't do HVAC refill repairs.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -592,19 +802,22 @@ export default async function RepairsPage() {
|
|||
<li className="text-foreground">
|
||||
<strong>Be On-Site with Your Machine:</strong>
|
||||
<span className="text-muted-foreground block mt-2">
|
||||
Ensure you're at the location of the vending machine during the session.
|
||||
Ensure you're at the location of the vending machine
|
||||
during the session.
|
||||
</span>
|
||||
</li>
|
||||
<li className="text-foreground">
|
||||
<strong>Stable Internet Connection:</strong>
|
||||
<span className="text-muted-foreground block mt-2">
|
||||
Make sure you have a stable internet connection for the Google Meet call.
|
||||
Make sure you have a stable internet connection for
|
||||
the Google Meet call.
|
||||
</span>
|
||||
</li>
|
||||
<li className="text-foreground">
|
||||
<strong>Camera Device:</strong>
|
||||
<span className="text-muted-foreground block mt-2">
|
||||
Use a device with a camera so you can show our technician the machine and any issues in real-time.
|
||||
Use a device with a camera so you can show our
|
||||
technician the machine and any issues in real-time.
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
|
|
@ -614,7 +827,9 @@ export default async function RepairsPage() {
|
|||
{/* Important Policies Card */}
|
||||
<Card className="border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Important Policies</CardTitle>
|
||||
<CardTitle className="text-2xl">
|
||||
Important Policies
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<ul className="space-y-4">
|
||||
|
|
@ -622,28 +837,49 @@ export default async function RepairsPage() {
|
|||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<strong className="text-foreground">Payment:</strong>
|
||||
<span className="text-muted-foreground"> All virtual sessions must be paid for upfront.</span>
|
||||
<span className="text-muted-foreground">
|
||||
{" "}
|
||||
All virtual sessions must be paid for upfront.
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<strong className="text-foreground">Session Duration:</strong>
|
||||
<span className="text-muted-foreground"> Sessions are scheduled in 30-minute increments. Additional time can be purchased if needed.</span>
|
||||
<strong className="text-foreground">
|
||||
Session Duration:
|
||||
</strong>
|
||||
<span className="text-muted-foreground">
|
||||
{" "}
|
||||
Sessions are scheduled in 30-minute increments.
|
||||
Additional time can be purchased if needed.
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<strong className="text-foreground">Cancellation Policy:</strong>
|
||||
<span className="text-muted-foreground"> Cancellations must be made at least 48 hours in advance to avoid being charged. We will send reminders and confirmations with links to reschedule.</span>
|
||||
<strong className="text-foreground">
|
||||
Cancellation Policy:
|
||||
</strong>
|
||||
<span className="text-muted-foreground">
|
||||
{" "}
|
||||
Cancellations must be made at least 48 hours in
|
||||
advance to avoid being charged. We will send
|
||||
reminders and confirmations with links to
|
||||
reschedule.
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-primary flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<strong className="text-foreground">No-Shows:</strong>
|
||||
<span className="text-muted-foreground"> If you miss your scheduled session, the full amount will still be billed.</span>
|
||||
<span className="text-muted-foreground">
|
||||
{" "}
|
||||
If you miss your scheduled session, the full amount
|
||||
will still be billed.
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -653,11 +889,22 @@ export default async function RepairsPage() {
|
|||
{/* Schedule Your Virtual Session Today */}
|
||||
<Card className="border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Schedule Your Virtual Session Today</CardTitle>
|
||||
<CardTitle className="text-2xl">
|
||||
Schedule Your Virtual Session Today
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-muted-foreground leading-relaxed mb-4">
|
||||
Ready to get started? Fill out our <Link href="#request-service" className="text-primary hover:underline font-semibold">Service Request Form</Link> and select "Virtual Support" to book your session. Our technicians are here to help you get your vending machine up and running as quickly as possible.
|
||||
Ready to get started? Fill out our{" "}
|
||||
<Link
|
||||
href="#request-service"
|
||||
className="text-primary hover:underline font-semibold"
|
||||
>
|
||||
Service Request Form
|
||||
</Link>{" "}
|
||||
and select "Virtual Support" to book your session. Our
|
||||
technicians are here to help you get your vending machine
|
||||
up and running as quickly as possible.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -669,11 +916,11 @@ export default async function RepairsPage() {
|
|||
{/* Service Areas Section */}
|
||||
<ServiceAreasSection />
|
||||
</>
|
||||
);
|
||||
)
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Error rendering Repairs page:', error);
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("Error rendering Repairs page:", error)
|
||||
}
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { getAllLocations } from "@/lib/location-data";
|
||||
import { businessConfig } from "@/lib/seo-config";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowRight, MapPin } from "lucide-react";
|
||||
import type { Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
import { getAllLocations } from "@/lib/location-data"
|
||||
import { businessConfig } from "@/lib/seo-config"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ArrowRight, MapPin } from "lucide-react"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Vending Machine Services by Location | All 20 Service Areas | Rocky Mountain Vending",
|
||||
title: "Utah Service Areas | Rocky Mountain Vending",
|
||||
description:
|
||||
"View vending machine services for all 20 service areas across Utah. Find services in Salt Lake City, Ogden, Provo, Sandy, and more. Free delivery and installation.",
|
||||
"Legacy service-area route for Rocky Mountain Vending. Use the main Utah service areas page for the canonical version.",
|
||||
keywords: [
|
||||
"vending machine services by location",
|
||||
"Utah service areas",
|
||||
|
|
@ -17,19 +17,23 @@ export const metadata: Metadata = {
|
|||
"vending machine locations Utah",
|
||||
],
|
||||
openGraph: {
|
||||
title: "Vending Machine Services by Location | All 20 Service Areas",
|
||||
title: "Utah Service Areas | Rocky Mountain Vending",
|
||||
description:
|
||||
"View vending machine services for all 20 service areas across Utah. Free delivery and installation.",
|
||||
url: `${businessConfig.website}/services/service-areas`,
|
||||
"Legacy service-area route for Rocky Mountain Vending. Use the main Utah service areas page for the canonical version.",
|
||||
url: `${businessConfig.website}/service-areas`,
|
||||
type: "website",
|
||||
},
|
||||
alternates: {
|
||||
canonical: `${businessConfig.website}/services/service-areas`,
|
||||
canonical: `${businessConfig.website}/service-areas`,
|
||||
},
|
||||
};
|
||||
robots: {
|
||||
index: false,
|
||||
follow: true,
|
||||
},
|
||||
}
|
||||
|
||||
export default function ServiceAreasServicesPage() {
|
||||
const locations = getAllLocations();
|
||||
const locations = getAllLocations()
|
||||
|
||||
// Group locations by county for better organization
|
||||
const saltLakeCounty = locations.filter((loc) =>
|
||||
|
|
@ -49,32 +53,44 @@ export default function ServiceAreasServicesPage() {
|
|||
"millcreek-utah",
|
||||
"cottonwood-heights-utah",
|
||||
].includes(loc.slug)
|
||||
);
|
||||
)
|
||||
|
||||
const davisCounty = locations.filter((loc) =>
|
||||
["ogden-utah", "layton-utah", "clearfield-utah", "syracuse-utah", "clinton-utah"].includes(loc.slug)
|
||||
);
|
||||
[
|
||||
"ogden-utah",
|
||||
"layton-utah",
|
||||
"clearfield-utah",
|
||||
"syracuse-utah",
|
||||
"clinton-utah",
|
||||
].includes(loc.slug)
|
||||
)
|
||||
|
||||
const utahCounty = locations.filter((loc) => ["provo-utah"].includes(loc.slug));
|
||||
const utahCounty = locations.filter((loc) =>
|
||||
["provo-utah"].includes(loc.slug)
|
||||
)
|
||||
|
||||
const services = [
|
||||
{
|
||||
title: "Vending Machine Sales",
|
||||
description: "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.",
|
||||
description:
|
||||
"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.",
|
||||
},
|
||||
{
|
||||
title: "Vending Machine Repair",
|
||||
description: "Machines break down—it happens. When yours does, we fix it fast so you're not stuck with an empty snack spot.",
|
||||
description:
|
||||
"Machines break down—it happens. When yours does, we fix it fast so you're not stuck with an empty snack spot.",
|
||||
},
|
||||
{
|
||||
title: "Healthy Snack and Beverage Options",
|
||||
description: "We stock machines with healthy choices like granola bars, fruit snacks, and sparkling water. Great for schools and gyms that want better options.",
|
||||
description:
|
||||
"We stock machines with healthy choices like granola bars, fruit snacks, and sparkling water. Great for schools and gyms that want better options.",
|
||||
},
|
||||
{
|
||||
title: "Maintenance Services",
|
||||
description: "Regular checkups keep machines working right. We handle restocking, cleaning, and small fixes so you don't have to think about it.",
|
||||
description:
|
||||
"Regular checkups keep machines working right. We handle restocking, cleaning, and small fixes so you don't have to think about it.",
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8 md:py-12">
|
||||
|
|
@ -83,14 +99,17 @@ export default function ServiceAreasServicesPage() {
|
|||
Vending Machine Services by Location
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground max-w-3xl mx-auto text-pretty leading-relaxed">
|
||||
Rocky Mountain Vending provides comprehensive vending machine services across 20 service areas in Utah.
|
||||
Find services available in your city below.
|
||||
Rocky Mountain Vending provides comprehensive vending machine services
|
||||
across 20 service areas in Utah. Find services available in your city
|
||||
below.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Services Overview */}
|
||||
<section className="mb-16 max-w-4xl mx-auto">
|
||||
<h2 className="text-3xl font-bold mb-8 text-center tracking-tight text-balance">Our Services</h2>
|
||||
<h2 className="text-3xl font-bold mb-8 text-center tracking-tight text-balance">
|
||||
Our Services
|
||||
</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{services.map((service, index) => (
|
||||
<Card key={index}>
|
||||
|
|
@ -106,18 +125,25 @@ export default function ServiceAreasServicesPage() {
|
|||
{/* Salt Lake County */}
|
||||
<section className="mb-16">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold mb-2 tracking-tight text-balance">Salt Lake County</h2>
|
||||
<h2 className="text-3xl font-bold mb-2 tracking-tight text-balance">
|
||||
Salt Lake County
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Serving 14 cities in Salt Lake County with reliable vending services
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{saltLakeCounty.map((location) => (
|
||||
<Card key={location.slug} className="h-full hover:border-secondary/50 transition-colors">
|
||||
<Card
|
||||
key={location.slug}
|
||||
className="h-full hover:border-secondary/50 transition-colors"
|
||||
>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-1">{location.city}</h3>
|
||||
<h3 className="text-xl font-semibold mb-1">
|
||||
{location.city}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||
<MapPin className="h-4 w-4" />
|
||||
{location.zipCode}
|
||||
|
|
@ -125,7 +151,9 @@ export default function ServiceAreasServicesPage() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-muted-foreground mb-2">Services Available:</p>
|
||||
<p className="text-sm font-medium text-muted-foreground mb-2">
|
||||
Services Available:
|
||||
</p>
|
||||
<ul className="text-sm text-muted-foreground space-y-1">
|
||||
<li>• Vending Machine Sales</li>
|
||||
<li>• Repair Services</li>
|
||||
|
|
@ -148,18 +176,26 @@ export default function ServiceAreasServicesPage() {
|
|||
{/* Davis County */}
|
||||
<section className="mb-16">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold mb-2 tracking-tight text-balance">Davis County</h2>
|
||||
<h2 className="text-3xl font-bold mb-2 tracking-tight text-balance">
|
||||
Davis County
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Supporting businesses from Ogden to Layton with reliable vending services
|
||||
Supporting businesses from Ogden to Layton with reliable vending
|
||||
services
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{davisCounty.map((location) => (
|
||||
<Card key={location.slug} className="h-full hover:border-secondary/50 transition-colors">
|
||||
<Card
|
||||
key={location.slug}
|
||||
className="h-full hover:border-secondary/50 transition-colors"
|
||||
>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-1">{location.city}</h3>
|
||||
<h3 className="text-xl font-semibold mb-1">
|
||||
{location.city}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||
<MapPin className="h-4 w-4" />
|
||||
{location.zipCode}
|
||||
|
|
@ -167,7 +203,9 @@ export default function ServiceAreasServicesPage() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-muted-foreground mb-2">Services Available:</p>
|
||||
<p className="text-sm font-medium text-muted-foreground mb-2">
|
||||
Services Available:
|
||||
</p>
|
||||
<ul className="text-sm text-muted-foreground space-y-1">
|
||||
<li>• Vending Machine Sales</li>
|
||||
<li>• Repair Services</li>
|
||||
|
|
@ -190,18 +228,25 @@ export default function ServiceAreasServicesPage() {
|
|||
{/* Utah County */}
|
||||
<section className="mb-16">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold mb-2 tracking-tight text-balance">Utah County</h2>
|
||||
<h2 className="text-3xl font-bold mb-2 tracking-tight text-balance">
|
||||
Utah County
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Serving Provo and surrounding areas with quality vending services
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{utahCounty.map((location) => (
|
||||
<Card key={location.slug} className="h-full hover:border-secondary/50 transition-colors">
|
||||
<Card
|
||||
key={location.slug}
|
||||
className="h-full hover:border-secondary/50 transition-colors"
|
||||
>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-1">{location.city}</h3>
|
||||
<h3 className="text-xl font-semibold mb-1">
|
||||
{location.city}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||
<MapPin className="h-4 w-4" />
|
||||
{location.zipCode}
|
||||
|
|
@ -209,7 +254,9 @@ export default function ServiceAreasServicesPage() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-muted-foreground mb-2">Services Available:</p>
|
||||
<p className="text-sm font-medium text-muted-foreground mb-2">
|
||||
Services Available:
|
||||
</p>
|
||||
<ul className="text-sm text-muted-foreground space-y-1">
|
||||
<li>• Vending Machine Sales</li>
|
||||
<li>• Repair Services</li>
|
||||
|
|
@ -231,23 +278,24 @@ export default function ServiceAreasServicesPage() {
|
|||
|
||||
{/* Call to Action */}
|
||||
<section className="max-w-4xl mx-auto text-center py-12 bg-muted/30 rounded-lg">
|
||||
<h2 className="text-3xl font-bold mb-4 tracking-tight text-balance">Ready to Get Started?</h2>
|
||||
<h2 className="text-3xl font-bold mb-4 tracking-tight text-balance">
|
||||
Ready to Get Started?
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground mb-6 text-pretty leading-relaxed">
|
||||
All services are available across all 20 service areas. Contact us today to learn more about
|
||||
vending machine solutions for your business.
|
||||
All services are available across all 20 service areas. Contact us
|
||||
today to learn more about vending machine solutions for your business.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link href="/contact-us">
|
||||
<Button size="lg">Contact Us</Button>
|
||||
</Link>
|
||||
<Link href="/service-areas">
|
||||
<Button size="lg" variant="outline">View All Service Areas</Button>
|
||||
<Button size="lg" variant="outline">
|
||||
View All Service Areas
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@ import { MetadataRoute } from "next"
|
|||
import { businessConfig } from "@/lib/seo-config"
|
||||
|
||||
// Required for static export
|
||||
export const dynamic = 'force-static'
|
||||
export const dynamic = "force-static"
|
||||
import { getAllLocationSlugs } from "@/lib/location-data"
|
||||
import { buildAbsoluteUrl } from "@/lib/seo-registry"
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const baseUrl = businessConfig.website
|
||||
const currentDate = new Date().toISOString()
|
||||
|
||||
// Get all location pages
|
||||
const locationSlugs = getAllLocationSlugs()
|
||||
const locationPages = locationSlugs.map((slug) => ({
|
||||
url: `${baseUrl}/vending-machines-${slug}`,
|
||||
url: buildAbsoluteUrl(`/vending-machines-${slug}`),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.9,
|
||||
|
|
@ -21,157 +21,174 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||
// Static pages with priorities and change frequencies
|
||||
const staticPages = [
|
||||
{
|
||||
url: baseUrl,
|
||||
url: businessConfig.website,
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "weekly" as const,
|
||||
priority: 1.0,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/contact-us`,
|
||||
url: buildAbsoluteUrl("/contact-us"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/about-us`,
|
||||
url: buildAbsoluteUrl("/about-us"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/about/faqs`,
|
||||
url: buildAbsoluteUrl("/about/faqs"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.7,
|
||||
},
|
||||
// Services
|
||||
{
|
||||
url: `${baseUrl}/services`,
|
||||
url: buildAbsoluteUrl("/services"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/services/service-areas`,
|
||||
url: buildAbsoluteUrl("/services/repairs"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/services/repairs`,
|
||||
url: buildAbsoluteUrl("/services/moving"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/services/moving`,
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/services/parts`,
|
||||
url: buildAbsoluteUrl("/services/parts"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.8,
|
||||
},
|
||||
// Vending Machines
|
||||
{
|
||||
url: `${baseUrl}/vending-machines`,
|
||||
url: buildAbsoluteUrl("/vending-machines"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/vending-machines/machines-we-use`,
|
||||
url: buildAbsoluteUrl("/vending-machines/machines-we-use"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/vending-machines/machines-for-sale`,
|
||||
url: buildAbsoluteUrl("/vending-machines/machines-for-sale"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.8,
|
||||
},
|
||||
// Who We Serve
|
||||
{
|
||||
url: `${baseUrl}/warehouses`,
|
||||
url: buildAbsoluteUrl("/warehouses"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/auto-repair`,
|
||||
url: buildAbsoluteUrl("/auto-repair"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/gyms`,
|
||||
url: buildAbsoluteUrl("/gyms"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/community-centers`,
|
||||
url: buildAbsoluteUrl("/community-centers"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/dance-studios`,
|
||||
url: buildAbsoluteUrl("/dance-studios"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/car-washes`,
|
||||
url: buildAbsoluteUrl("/car-washes"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.7,
|
||||
},
|
||||
// Food and Beverage
|
||||
{
|
||||
url: `${baseUrl}/food-and-beverage/healthy-options`,
|
||||
url: buildAbsoluteUrl("/food-and-beverage/healthy-options"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/food-and-beverage/traditional-options`,
|
||||
url: buildAbsoluteUrl("/food-and-beverage/traditional-options"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/food-and-beverage/suppliers`,
|
||||
url: buildAbsoluteUrl("/food-and-beverage/suppliers"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.7,
|
||||
},
|
||||
// Service Areas Page
|
||||
{
|
||||
url: `${baseUrl}/service-areas`,
|
||||
url: buildAbsoluteUrl("/service-areas"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.9,
|
||||
},
|
||||
// Manuals Page
|
||||
{
|
||||
url: `${baseUrl}/manuals`,
|
||||
url: buildAbsoluteUrl("/manuals"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "weekly" as const,
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: buildAbsoluteUrl("/reviews"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: buildAbsoluteUrl("/blog"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "weekly" as const,
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: buildAbsoluteUrl("/privacy-policy"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "yearly" as const,
|
||||
priority: 0.3,
|
||||
},
|
||||
{
|
||||
url: buildAbsoluteUrl("/terms-and-conditions"),
|
||||
lastModified: currentDate,
|
||||
changeFrequency: "yearly" as const,
|
||||
priority: 0.3,
|
||||
},
|
||||
]
|
||||
|
||||
// Combine all pages, removing duplicates by URL
|
||||
const allPages = [...staticPages, ...locationPages]
|
||||
const uniquePages = allPages.filter((page, index, self) =>
|
||||
index === self.findIndex((p) => p.url === page.url)
|
||||
const uniquePages = allPages.filter(
|
||||
(page, index, self) => index === self.findIndex((p) => p.url === page.url)
|
||||
)
|
||||
|
||||
return uniquePages
|
||||
}
|
||||
|
||||
|
|
|
|||
19
app/style-guide/layout.tsx
Normal file
19
app/style-guide/layout.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { Metadata } from "next"
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Style Guide | Rocky Mountain Vending",
|
||||
description: "Internal style guide and component reference.",
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
}
|
||||
|
||||
export default function StyleGuideLayout({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) {
|
||||
return children
|
||||
}
|
||||
|
|
@ -1,9 +1,23 @@
|
|||
import type { Metadata } from "next"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Test Page | Rocky Mountain Vending",
|
||||
description: "Internal routing test page.",
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
}
|
||||
|
||||
export default function TestPage() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8 md:py-12">
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-balance mb-4">Test Page</h1>
|
||||
<p className="text-muted-foreground">This is a test page to verify routing works.</p>
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-balance mb-4">
|
||||
Test Page
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
This is a test page to verify routing works.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,426 +1,60 @@
|
|||
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 { GetFreeMachineCta } from "@/components/get-free-machine-cta";
|
||||
import { PublicInset, PublicPageHeader, PublicSurface } from "@/components/public-surface";
|
||||
import { notFound } from "next/navigation"
|
||||
import type { Metadata } from "next"
|
||||
import { getAllLocationSlugs, getLocationBySlug } from "@/lib/location-data"
|
||||
import {
|
||||
generateLocationPageMetadata,
|
||||
LocationLandingPage,
|
||||
} from "@/components/location-landing-page"
|
||||
|
||||
interface LocationPageProps {
|
||||
params: Promise<{ location: string }>;
|
||||
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'];
|
||||
const NON_LOCATION_ROUTES = ["machines-we-use", "machines-for-sale"]
|
||||
|
||||
// Required for static export
|
||||
export const dynamic = 'force-static';
|
||||
export const dynamicParams = false;
|
||||
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' }];
|
||||
const slugs = getAllLocationSlugs()
|
||||
|
||||
if (!slugs.length) {
|
||||
return [{ location: "ogden-utah" }]
|
||||
}
|
||||
|
||||
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
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: LocationPageProps): Promise<Metadata> {
|
||||
const { location } = await params
|
||||
|
||||
if (NON_LOCATION_ROUTES.includes(location)) {
|
||||
return {
|
||||
title: "Page Not Found | Rocky Mountain Vending",
|
||||
};
|
||||
return { title: "Page Not Found | Rocky Mountain Vending" }
|
||||
}
|
||||
|
||||
const locationData = getLocationBySlug(location);
|
||||
|
||||
const locationData = getLocationBySlug(location)
|
||||
|
||||
if (!locationData) {
|
||||
return {
|
||||
title: "Location Not Found | Rocky Mountain Vending",
|
||||
};
|
||||
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}`,
|
||||
},
|
||||
};
|
||||
return generateLocationPageMetadata(locationData)
|
||||
}
|
||||
|
||||
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
|
||||
const { location } = await params
|
||||
|
||||
if (NON_LOCATION_ROUTES.includes(location)) {
|
||||
notFound();
|
||||
notFound()
|
||||
}
|
||||
|
||||
const locationData = getLocationBySlug(location);
|
||||
|
||||
const locationData = getLocationBySlug(location)
|
||||
|
||||
if (!locationData) {
|
||||
notFound();
|
||||
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>
|
||||
|
||||
{/* Placement CTA */}
|
||||
<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 for your business.
|
||||
</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>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<GetFreeMachineCta buttonLabel="Get Free Placement" />
|
||||
<a
|
||||
href={businessConfig.publicCallUrl}
|
||||
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"
|
||||
>
|
||||
Call Instead
|
||||
</a>
|
||||
</div>
|
||||
</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">
|
||||
<GetFreeMachineCta buttonLabel="Get Your Free Machine Today" />
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* Google Reviews Section */}
|
||||
<ReviewsSection />
|
||||
</>
|
||||
);
|
||||
return <LocationLandingPage locationData={locationData} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
import { generateSEOMetadata, generateStructuredData } from '@/lib/seo';
|
||||
import { VendingMachinesPage } from '@/components/vending-machines-page';
|
||||
import type { Metadata } from 'next';
|
||||
import { generateSEOMetadata, generateStructuredData } from "@/lib/seo"
|
||||
import { VendingMachinesPage } from "@/components/vending-machines-page"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return generateSEOMetadata({
|
||||
title: 'Vending Machines | Rocky Mountain Vending',
|
||||
description: 'Browse our selection of high-quality vending machines with advanced features and cashless payment options',
|
||||
});
|
||||
title: "Vending Machines | Rocky Mountain Vending",
|
||||
description:
|
||||
"Compare snack, beverage, and combo vending machines for Utah businesses, including payment options, layouts, and placement or purchase paths.",
|
||||
path: "/vending-machines",
|
||||
})
|
||||
}
|
||||
|
||||
export default function VendingMachines() {
|
||||
const structuredData = generateStructuredData({
|
||||
title: 'Vending Machines',
|
||||
description: 'Browse our selection of high-quality vending machines with advanced features and cashless payment options',
|
||||
url: 'https://rockymountainvending.com/vending-machines/',
|
||||
type: 'WebPage',
|
||||
});
|
||||
title: "Vending Machines",
|
||||
description:
|
||||
"Compare snack, beverage, and combo vending machines for Utah businesses",
|
||||
url: "https://rockymountainvending.com/vending-machines/",
|
||||
type: "WebPage",
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -25,5 +28,5 @@ export default function VendingMachines() {
|
|||
/>
|
||||
<VendingMachinesPage />
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,3 @@ export function Breadcrumbs({ items, className = "" }: BreadcrumbsProps) {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@
|
|||
|
||||
import Link from "next/link"
|
||||
import { MapPin, Phone, Wrench } from "lucide-react"
|
||||
import { PublicInset, PublicPageHeader, PublicSurface } from "@/components/public-surface"
|
||||
import {
|
||||
PublicInset,
|
||||
PublicPageHeader,
|
||||
PublicSurface,
|
||||
} from "@/components/public-surface"
|
||||
import { businessConfig } from "@/lib/seo-config"
|
||||
|
||||
export function ContactSection() {
|
||||
|
|
@ -13,17 +17,20 @@ export function ContactSection() {
|
|||
className="mb-10 max-w-3xl"
|
||||
eyebrow="Need Something Else?"
|
||||
title="Need help with repairs, moving, manuals, or machine sales?"
|
||||
description="Tell us what you need and our team will point you in the right direction."
|
||||
description="Tell us what you need and we'll route your request to the right person."
|
||||
/>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-[1.05fr_0.95fr] lg:items-start">
|
||||
<PublicSurface className="p-6 md:p-8">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-primary/80">Talk to Our Team</p>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-primary/80">
|
||||
Talk to Our Team
|
||||
</p>
|
||||
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance">
|
||||
Share a few details and we'll help you get moving.
|
||||
Share a few details and we'll help you take the next step.
|
||||
</h2>
|
||||
<p className="mt-3 text-base leading-relaxed text-muted-foreground">
|
||||
Whether you need service, moving help, manuals, or sales support, we'll make sure your message gets to the right person.
|
||||
Whether you need repairs, moving help, manuals, or machine sales,
|
||||
we'll make sure your message gets to the right person.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
|
||||
|
|
@ -48,11 +55,18 @@ export function ContactSection() {
|
|||
<Phone className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-foreground">Call or Text Us</div>
|
||||
<a href={businessConfig.publicCallUrl} className="mt-1 block text-muted-foreground transition hover:text-foreground">
|
||||
<div className="font-semibold text-foreground">
|
||||
Call or Text Us
|
||||
</div>
|
||||
<a
|
||||
href={businessConfig.publicCallUrl}
|
||||
className="mt-1 block text-muted-foreground transition hover:text-foreground"
|
||||
>
|
||||
{businessConfig.publicCallNumber}
|
||||
</a>
|
||||
<p className="mt-1 text-xs text-muted-foreground">Mon-Fri: 8:00 AM - 5:00 PM</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Mon-Fri: 8:00 AM - 5:00 PM
|
||||
</p>
|
||||
</div>
|
||||
</PublicInset>
|
||||
|
||||
|
|
@ -61,10 +75,16 @@ export function ContactSection() {
|
|||
<Wrench className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-foreground">For Repairs or Moving</div>
|
||||
<div className="font-semibold text-foreground">
|
||||
For Repairs or Moving
|
||||
</div>
|
||||
<p className="mt-1 text-sm leading-relaxed text-muted-foreground">
|
||||
Include the machine model and a clear text description. You can also text photos or videos to{" "}
|
||||
<a href={businessConfig.publicSmsUrl} className="font-medium text-foreground underline decoration-primary/35 underline-offset-4 hover:decoration-primary">
|
||||
Include the machine model and a clear text description. You
|
||||
can also text photos or videos to{" "}
|
||||
<a
|
||||
href={businessConfig.publicSmsUrl}
|
||||
className="font-medium text-foreground underline decoration-primary/35 underline-offset-4 hover:decoration-primary"
|
||||
>
|
||||
{businessConfig.publicSmsNumber}
|
||||
</a>
|
||||
.
|
||||
|
|
@ -77,8 +97,13 @@ export function ContactSection() {
|
|||
<MapPin className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-foreground">Service Areas</div>
|
||||
<p className="mt-1 text-sm leading-relaxed text-muted-foreground">Davis, Salt Lake, and Utah Counties, plus the surrounding Rocky Mountain Vending service footprint already listed on the site.</p>
|
||||
<div className="font-semibold text-foreground">
|
||||
Service Areas
|
||||
</div>
|
||||
<p className="mt-1 text-sm leading-relaxed text-muted-foreground">
|
||||
Davis, Salt Lake, and Utah Counties, plus nearby locations
|
||||
already listed on our service areas page.
|
||||
</p>
|
||||
</div>
|
||||
</PublicInset>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,32 +5,35 @@ import { PublicInset, PublicSurface } from "@/components/public-surface"
|
|||
const features = [
|
||||
{
|
||||
icon: Sparkles,
|
||||
title: "Zero Cost Installation",
|
||||
title: "Free Placement",
|
||||
description:
|
||||
"Get your vending machine installed completely free. No upfront costs, hidden fees, or awkward setup process.",
|
||||
link: "/about",
|
||||
linkText: "Learn more",
|
||||
"For qualifying locations, we can place the machines, stock them, and stay responsible for day-to-day service after install.",
|
||||
link: "/about-us",
|
||||
linkText: "How placement works",
|
||||
},
|
||||
{
|
||||
icon: Wrench,
|
||||
title: "Full Maintenance Support",
|
||||
description: "We handle repairs, restocking, and service so your location stays easy to manage.",
|
||||
title: "Repairs and Service",
|
||||
description:
|
||||
"We handle repairs, restocking, and routine service so your team does not have to manage the machines.",
|
||||
link: "/services/repairs",
|
||||
linkText: "Repair services",
|
||||
},
|
||||
{
|
||||
icon: MonitorSmartphone,
|
||||
title: "24/7 Remote Monitoring",
|
||||
description: "Advanced monitoring helps us keep machines stocked and working with fewer interruptions.",
|
||||
title: "Machine Visibility",
|
||||
description:
|
||||
"Machine monitoring helps us catch stock and service issues sooner and reduce unnecessary downtime.",
|
||||
link: "/services",
|
||||
linkText: "Our services",
|
||||
},
|
||||
{
|
||||
icon: CreditCard,
|
||||
title: "All Payment Options",
|
||||
description: "Cash, cards, and mobile payments are supported with modern payment hardware.",
|
||||
description:
|
||||
"Machines can be set up for cash, cards, and mobile payments with modern payment hardware.",
|
||||
link: "/contact-us#contact-form",
|
||||
linkText: "Contact us",
|
||||
linkText: "Ask about payment options",
|
||||
},
|
||||
]
|
||||
|
||||
|
|
@ -40,24 +43,34 @@ export function FeaturesSection() {
|
|||
<div className="container mx-auto px-4">
|
||||
<PublicSurface className="p-6 md:p-8">
|
||||
<div className="text-center mb-10 md:mb-12">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">Why Rocky</p>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
||||
Why Rocky
|
||||
</p>
|
||||
<h2 className="mt-3 text-3xl font-bold tracking-tight text-balance md:text-4xl lg:text-5xl">
|
||||
Why businesses choose Rocky Mountain Vending
|
||||
</h2>
|
||||
<p className="mx-auto mt-3 max-w-2xl text-lg text-muted-foreground text-pretty leading-relaxed">
|
||||
One visual system, one service standard, and a much more polished way to explain what makes our vending support easy to trust.
|
||||
Utah businesses choose Rocky Mountain Vending when they want a
|
||||
simpler vending setup, cleaner follow-through, and fewer machine
|
||||
headaches for their staff.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
|
||||
{features.map((feature) => (
|
||||
<Link key={feature.title} href={feature.link} className="group block h-full">
|
||||
<Link
|
||||
key={feature.title}
|
||||
href={feature.link}
|
||||
className="group block h-full"
|
||||
>
|
||||
<PublicInset className="flex h-full flex-col p-5 transition-all group-hover:-translate-y-0.5 group-hover:shadow-md">
|
||||
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||
<feature.icon className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold">{feature.title}</h3>
|
||||
<p className="mt-2 flex-1 text-sm leading-relaxed text-muted-foreground">{feature.description}</p>
|
||||
<p className="mt-2 flex-1 text-sm leading-relaxed text-muted-foreground">
|
||||
{feature.description}
|
||||
</p>
|
||||
<p className="mt-5 text-sm font-medium text-primary transition group-hover:translate-x-0.5">
|
||||
{feature.linkText} →
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import Link from "next/link"
|
|||
import Image from "next/image"
|
||||
import { Facebook, Twitter, Linkedin, Youtube } from "lucide-react"
|
||||
import { Separator } from "./ui/separator"
|
||||
import { Button } from "./ui/button"
|
||||
|
||||
export function Footer() {
|
||||
const currentYear = new Date().getFullYear()
|
||||
|
|
@ -23,8 +22,9 @@ export function Footer() {
|
|||
/>
|
||||
</Link>
|
||||
<p className="text-sm text-muted-foreground mb-6 max-w-md leading-relaxed">
|
||||
Providing FREE vending machine services to businesses across Utah since 2019. Zero cost, hassle-free
|
||||
service, and dedicated support.
|
||||
Serving Utah businesses with free placement for qualifying
|
||||
locations, machine sales, repairs, restocking, and ongoing
|
||||
service since 2019.
|
||||
</p>
|
||||
<div className="flex flex-col gap-3 text-sm text-muted-foreground mb-6">
|
||||
<a href="tel:+14352339668" className="transition-colors">
|
||||
|
|
@ -37,7 +37,7 @@ export function Footer() {
|
|||
✉️ info@rockymountainvending.com
|
||||
</a>
|
||||
<a
|
||||
href="http://rockymountainvending.com/"
|
||||
href="https://rockymountainvending.com/"
|
||||
className="transition-colors"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
|
@ -94,32 +94,50 @@ export function Footer() {
|
|||
<h3 className="font-semibold mb-5 text-base">Services</h3>
|
||||
<ul className="space-y-3 text-sm text-muted-foreground">
|
||||
<li>
|
||||
<Link href="/services/repairs" className="transition-colors inline-block py-0.5">
|
||||
<Link
|
||||
href="/services/repairs"
|
||||
className="transition-colors inline-block py-0.5"
|
||||
>
|
||||
Vending Machine Repairs
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/services/moving" className="transition-colors inline-block py-0.5">
|
||||
<Link
|
||||
href="/services/moving"
|
||||
className="transition-colors inline-block py-0.5"
|
||||
>
|
||||
Vending Machine Moving
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/services/parts" className="transition-colors inline-block py-0.5">
|
||||
<Link
|
||||
href="/services/parts"
|
||||
className="transition-colors inline-block py-0.5"
|
||||
>
|
||||
Vending Machine Parts
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/manuals" className="transition-colors inline-block py-0.5">
|
||||
<Link
|
||||
href="/manuals"
|
||||
className="transition-colors inline-block py-0.5"
|
||||
>
|
||||
Vending Machine Manuals
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/vending-machines" className="transition-colors inline-block py-0.5">
|
||||
<Link
|
||||
href="/vending-machines"
|
||||
className="transition-colors inline-block py-0.5"
|
||||
>
|
||||
Machine Sales
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/services" className="transition-colors inline-block py-0.5">
|
||||
<Link
|
||||
href="/services"
|
||||
className="transition-colors inline-block py-0.5"
|
||||
>
|
||||
All Services
|
||||
</Link>
|
||||
</li>
|
||||
|
|
@ -131,22 +149,34 @@ export function Footer() {
|
|||
<h3 className="font-semibold mb-5 text-base">Company</h3>
|
||||
<ul className="space-y-3 text-sm text-muted-foreground">
|
||||
<li>
|
||||
<Link href="/about-us" className="transition-colors inline-block py-0.5">
|
||||
<Link
|
||||
href="/about-us"
|
||||
className="transition-colors inline-block py-0.5"
|
||||
>
|
||||
About Us
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/reviews" className="transition-colors inline-block py-0.5">
|
||||
<Link
|
||||
href="/reviews"
|
||||
className="transition-colors inline-block py-0.5"
|
||||
>
|
||||
Customer Reviews
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/contact-us" className="transition-colors inline-block py-0.5">
|
||||
<Link
|
||||
href="/contact-us"
|
||||
className="transition-colors inline-block py-0.5"
|
||||
>
|
||||
Contact Us
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/about/faqs" className="transition-colors inline-block py-0.5">
|
||||
<Link
|
||||
href="/about/faqs"
|
||||
className="transition-colors inline-block py-0.5"
|
||||
>
|
||||
FAQs
|
||||
</Link>
|
||||
</li>
|
||||
|
|
@ -158,33 +188,59 @@ export function Footer() {
|
|||
<h3 className="font-semibold mb-5 text-base">Service Areas</h3>
|
||||
<ul className="space-y-3 text-sm text-muted-foreground">
|
||||
<li>
|
||||
<Link href="/vending-machines-salt-lake-city-utah" className="transition-colors inline-block py-0.5">
|
||||
<Link
|
||||
href="/service-areas"
|
||||
className="transition-colors inline-block py-0.5"
|
||||
>
|
||||
View All Service Areas
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/vending-machines-salt-lake-city-utah"
|
||||
className="transition-colors inline-block py-0.5"
|
||||
>
|
||||
Salt Lake City, UT
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/vending-machines-ogden-utah" className="transition-colors inline-block py-0.5">
|
||||
<Link
|
||||
href="/vending-machines-ogden-utah"
|
||||
className="transition-colors inline-block py-0.5"
|
||||
>
|
||||
Ogden, UT
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/vending-machines-provo-utah" className="transition-colors inline-block py-0.5">
|
||||
<Link
|
||||
href="/vending-machines-provo-utah"
|
||||
className="transition-colors inline-block py-0.5"
|
||||
>
|
||||
Provo, UT
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/vending-machines-sandy-utah" className="transition-colors inline-block py-0.5">
|
||||
<Link
|
||||
href="/vending-machines-sandy-utah"
|
||||
className="transition-colors inline-block py-0.5"
|
||||
>
|
||||
Sandy, UT
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/vending-machines-west-valley-city-utah" className="transition-colors inline-block py-0.5">
|
||||
<Link
|
||||
href="/vending-machines-west-valley-city-utah"
|
||||
className="transition-colors inline-block py-0.5"
|
||||
>
|
||||
West Valley City, UT
|
||||
</Link>
|
||||
</li>
|
||||
<li className="pt-2">
|
||||
<Separator className="mb-2" />
|
||||
<Link href="/service-areas" className="transition-colors font-medium inline-block py-0.5">
|
||||
<Link
|
||||
href="/service-areas"
|
||||
className="transition-colors font-medium inline-block py-0.5"
|
||||
>
|
||||
View all 20 cities →
|
||||
</Link>
|
||||
</li>
|
||||
|
|
@ -194,12 +250,20 @@ export function Footer() {
|
|||
|
||||
<div className="border-t border-border mt-12 pt-8">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4 text-sm text-muted-foreground">
|
||||
<p>© {currentYear} Rocky Mountain Vending LLC. All rights reserved.</p>
|
||||
<p>
|
||||
© {currentYear} Rocky Mountain Vending LLC. All rights reserved.
|
||||
</p>
|
||||
<nav className="flex gap-6" aria-label="Legal links">
|
||||
<Link href="/privacy-policy" className="transition-colors hover:text-foreground">
|
||||
<Link
|
||||
href="/privacy-policy"
|
||||
className="transition-colors hover:text-foreground"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link href="/terms-and-conditions" className="transition-colors hover:text-foreground">
|
||||
<Link
|
||||
href="/terms-and-conditions"
|
||||
className="transition-colors hover:text-foreground"
|
||||
>
|
||||
Terms & Conditions
|
||||
</Link>
|
||||
</nav>
|
||||
|
|
@ -209,11 +273,17 @@ export function Footer() {
|
|||
Free Vending Machines Utah
|
||||
</Link>
|
||||
{" | "}
|
||||
<Link href="/vending-machines" className="hover:text-foreground transition-colors">
|
||||
<Link
|
||||
href="/vending-machines"
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
Vending Machine Supplier
|
||||
</Link>
|
||||
{" | "}
|
||||
<Link href="/services" className="hover:text-foreground transition-colors">
|
||||
<Link
|
||||
href="/services"
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
Vending Machine Service & Repair
|
||||
</Link>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -30,36 +30,36 @@ type HeaderState = {
|
|||
}
|
||||
|
||||
type HeaderAction =
|
||||
| { type: 'TOGGLE_MENU' }
|
||||
| { type: 'TOGGLE_WHO_WE_SERVE' }
|
||||
| { type: 'TOGGLE_VENDING_MACHINES' }
|
||||
| { type: 'TOGGLE_FOOD_BEVERAGE' }
|
||||
| { type: 'TOGGLE_SERVICES' }
|
||||
| { type: 'TOGGLE_BLOG_POSTS' }
|
||||
| { type: 'TOGGLE_ABOUT' }
|
||||
| { type: 'SET_MODAL'; value: boolean }
|
||||
| { type: 'SET_CART'; value: boolean }
|
||||
| { type: "TOGGLE_MENU" }
|
||||
| { type: "TOGGLE_WHO_WE_SERVE" }
|
||||
| { type: "TOGGLE_VENDING_MACHINES" }
|
||||
| { type: "TOGGLE_FOOD_BEVERAGE" }
|
||||
| { type: "TOGGLE_SERVICES" }
|
||||
| { type: "TOGGLE_BLOG_POSTS" }
|
||||
| { type: "TOGGLE_ABOUT" }
|
||||
| { type: "SET_MODAL"; value: boolean }
|
||||
| { type: "SET_CART"; value: boolean }
|
||||
|
||||
// Reducer for header state - consolidates related mobile menu states
|
||||
function headerReducer(state: HeaderState, action: HeaderAction): HeaderState {
|
||||
switch (action.type) {
|
||||
case 'TOGGLE_MENU':
|
||||
case "TOGGLE_MENU":
|
||||
return { ...state, isMenuOpen: !state.isMenuOpen }
|
||||
case 'TOGGLE_WHO_WE_SERVE':
|
||||
case "TOGGLE_WHO_WE_SERVE":
|
||||
return { ...state, isWhoWeServeOpen: !state.isWhoWeServeOpen }
|
||||
case 'TOGGLE_VENDING_MACHINES':
|
||||
case "TOGGLE_VENDING_MACHINES":
|
||||
return { ...state, isVendingMachinesOpen: !state.isVendingMachinesOpen }
|
||||
case 'TOGGLE_FOOD_BEVERAGE':
|
||||
case "TOGGLE_FOOD_BEVERAGE":
|
||||
return { ...state, isFoodBeverageOpen: !state.isFoodBeverageOpen }
|
||||
case 'TOGGLE_SERVICES':
|
||||
case "TOGGLE_SERVICES":
|
||||
return { ...state, isServicesOpen: !state.isServicesOpen }
|
||||
case 'TOGGLE_BLOG_POSTS':
|
||||
case "TOGGLE_BLOG_POSTS":
|
||||
return { ...state, isBlogPostsOpen: !state.isBlogPostsOpen }
|
||||
case 'TOGGLE_ABOUT':
|
||||
case "TOGGLE_ABOUT":
|
||||
return { ...state, isAboutOpen: !state.isAboutOpen }
|
||||
case 'SET_MODAL':
|
||||
case "SET_MODAL":
|
||||
return { ...state, isModalOpen: action.value }
|
||||
case 'SET_CART':
|
||||
case "SET_CART":
|
||||
return { ...state, isCartOpen: action.value }
|
||||
default:
|
||||
return state
|
||||
|
|
@ -90,14 +90,23 @@ export function Header() {
|
|||
|
||||
const vendingMachinesItems = [
|
||||
{ label: "Machines We Use", href: "/vending-machines/machines-we-use" },
|
||||
{ label: "Machines for Sale in Utah", href: "/vending-machines/machines-for-sale" },
|
||||
{
|
||||
label: "Machines for Sale in Utah",
|
||||
href: "/vending-machines/machines-for-sale",
|
||||
},
|
||||
{ label: "Vending Machine Manuals", href: "/manuals" },
|
||||
]
|
||||
|
||||
const foodBeverageItems = [
|
||||
{ label: "Healthy Options", href: "/food-and-beverage/healthy-options" },
|
||||
{ label: "Traditional Options", href: "/food-and-beverage/traditional-options" },
|
||||
{ label: "Food & Beverage Suppliers", href: "/food-and-beverage/suppliers" },
|
||||
{
|
||||
label: "Traditional Options",
|
||||
href: "/food-and-beverage/traditional-options",
|
||||
},
|
||||
{
|
||||
label: "Food & Beverage Suppliers",
|
||||
href: "/food-and-beverage/suppliers",
|
||||
},
|
||||
]
|
||||
|
||||
const servicesItems = [
|
||||
|
|
@ -122,7 +131,10 @@ export function Header() {
|
|||
<div className="w-full px-4 lg:px-6 xl:px-8 2xl:px-12">
|
||||
<div className="flex h-20 items-center justify-between gap-4 lg:gap-6">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="flex items-center gap-2 flex-shrink-0 min-w-0">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 flex-shrink-0 min-w-0"
|
||||
>
|
||||
<Image
|
||||
src="/rmv-logo.png"
|
||||
alt="Rocky Mountain Vending"
|
||||
|
|
@ -235,6 +247,12 @@ export function Header() {
|
|||
>
|
||||
Products
|
||||
</Link>
|
||||
<Link
|
||||
href="/service-areas"
|
||||
className="text-sm font-medium transition-colors"
|
||||
>
|
||||
Service Areas
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact-us"
|
||||
className="text-sm font-medium transition-colors"
|
||||
|
|
@ -245,7 +263,9 @@ export function Header() {
|
|||
|
||||
{/* Desktop CTA */}
|
||||
<div className="hidden items-center gap-2 lg:gap-3 md:flex flex-shrink-0">
|
||||
<CartButton onClick={() => dispatch({ type: 'SET_CART', value: true })} />
|
||||
<CartButton
|
||||
onClick={() => dispatch({ type: "SET_CART", value: true })}
|
||||
/>
|
||||
<a
|
||||
href="tel:+14352339668"
|
||||
className="flex items-center gap-2 text-sm font-medium transition-colors whitespace-nowrap"
|
||||
|
|
@ -254,7 +274,7 @@ export function Header() {
|
|||
<span className="hidden lg:inline">(435) 233-9668</span>
|
||||
</a>
|
||||
<Button
|
||||
onClick={() => dispatch({ type: 'SET_MODAL', value: true })}
|
||||
onClick={() => dispatch({ type: "SET_MODAL", value: true })}
|
||||
className="bg-primary hover:bg-primary/90 whitespace-nowrap"
|
||||
size="sm"
|
||||
>
|
||||
|
|
@ -263,8 +283,16 @@ export function Header() {
|
|||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<button className="md:hidden" onClick={() => dispatch({ type: 'TOGGLE_MENU' })} aria-label="Toggle menu">
|
||||
{state.isMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
|
||||
<button
|
||||
className="md:hidden"
|
||||
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
{state.isMenuOpen ? (
|
||||
<X className="h-6 w-6" />
|
||||
) : (
|
||||
<Menu className="h-6 w-6" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -274,7 +302,7 @@ export function Header() {
|
|||
<Link
|
||||
href="/"
|
||||
className="text-sm font-medium py-1 transition-colors"
|
||||
onClick={() => dispatch({ type: 'TOGGLE_MENU' })}
|
||||
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
|
|
@ -282,14 +310,17 @@ export function Header() {
|
|||
{/* Who We Serve Mobile Section */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<button
|
||||
onClick={() => dispatch({ type: 'TOGGLE_WHO_WE_SERVE' })}
|
||||
onClick={() => dispatch({ type: "TOGGLE_WHO_WE_SERVE" })}
|
||||
className="flex items-center justify-between text-sm font-medium py-1 hover-brand"
|
||||
aria-label="Who We Serve menu"
|
||||
aria-expanded={state.isWhoWeServeOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
Who We Serve
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${state.isWhoWeServeOpen ? "rotate-180" : ""}`} aria-hidden="true" />
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 transition-transform ${state.isWhoWeServeOpen ? "rotate-180" : ""}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
{state.isWhoWeServeOpen && (
|
||||
<div className="flex flex-col gap-3 pl-4 border-l-2 border-secondary/30">
|
||||
|
|
@ -298,7 +329,7 @@ export function Header() {
|
|||
key={item.href}
|
||||
href={item.href}
|
||||
className="text-sm py-1 transition-colors"
|
||||
onClick={() => dispatch({ type: 'TOGGLE_MENU' })}
|
||||
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
|
|
@ -310,14 +341,17 @@ export function Header() {
|
|||
{/* Vending Machines Mobile */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<button
|
||||
onClick={() => dispatch({ type: 'TOGGLE_VENDING_MACHINES' })}
|
||||
onClick={() => dispatch({ type: "TOGGLE_VENDING_MACHINES" })}
|
||||
className="flex items-center justify-between text-sm font-medium py-1 hover-brand"
|
||||
aria-label="Vending Machines menu"
|
||||
aria-expanded={state.isVendingMachinesOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
Vending Machines
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${state.isVendingMachinesOpen ? "rotate-180" : ""}`} aria-hidden="true" />
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 transition-transform ${state.isVendingMachinesOpen ? "rotate-180" : ""}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
{state.isVendingMachinesOpen && (
|
||||
<div className="flex flex-col gap-3 pl-4 border-l-2 border-secondary/30">
|
||||
|
|
@ -326,7 +360,7 @@ export function Header() {
|
|||
key={item.href}
|
||||
href={item.href}
|
||||
className="text-sm py-1 transition-colors"
|
||||
onClick={() => dispatch({ type: 'TOGGLE_MENU' })}
|
||||
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
|
|
@ -338,14 +372,17 @@ export function Header() {
|
|||
{/* Food and Beverage Mobile */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<button
|
||||
onClick={() => dispatch({ type: 'TOGGLE_FOOD_BEVERAGE' })}
|
||||
onClick={() => dispatch({ type: "TOGGLE_FOOD_BEVERAGE" })}
|
||||
className="flex items-center justify-between text-sm font-medium py-1 hover-brand"
|
||||
aria-label="Food & Beverage menu"
|
||||
aria-expanded={state.isFoodBeverageOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
Food & Beverage
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${state.isFoodBeverageOpen ? "rotate-180" : ""}`} aria-hidden="true" />
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 transition-transform ${state.isFoodBeverageOpen ? "rotate-180" : ""}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
{state.isFoodBeverageOpen && (
|
||||
<div className="flex flex-col gap-3 pl-4 border-l-2 border-secondary/30">
|
||||
|
|
@ -354,7 +391,7 @@ export function Header() {
|
|||
key={item.href}
|
||||
href={item.href}
|
||||
className="text-sm py-1 transition-colors"
|
||||
onClick={() => dispatch({ type: 'TOGGLE_MENU' })}
|
||||
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
|
|
@ -366,14 +403,17 @@ export function Header() {
|
|||
{/* Services Mobile */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<button
|
||||
onClick={() => dispatch({ type: 'TOGGLE_SERVICES' })}
|
||||
onClick={() => dispatch({ type: "TOGGLE_SERVICES" })}
|
||||
className="flex items-center justify-between text-sm font-medium py-1 hover-brand"
|
||||
aria-label="Services menu"
|
||||
aria-expanded={state.isServicesOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
Services
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${state.isServicesOpen ? "rotate-180" : ""}`} aria-hidden="true" />
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 transition-transform ${state.isServicesOpen ? "rotate-180" : ""}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
{state.isServicesOpen && (
|
||||
<div className="flex flex-col gap-3 pl-4 border-l-2 border-secondary/30">
|
||||
|
|
@ -382,7 +422,7 @@ export function Header() {
|
|||
key={item.href}
|
||||
href={item.href}
|
||||
className="text-sm py-1 transition-colors"
|
||||
onClick={() => dispatch({ type: 'TOGGLE_MENU' })}
|
||||
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
|
|
@ -394,14 +434,17 @@ export function Header() {
|
|||
{/* Blog Posts Mobile */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<button
|
||||
onClick={() => dispatch({ type: 'TOGGLE_BLOG_POSTS' })}
|
||||
onClick={() => dispatch({ type: "TOGGLE_BLOG_POSTS" })}
|
||||
className="flex items-center justify-between text-sm font-medium py-1 hover-brand"
|
||||
aria-label="Blog Posts menu"
|
||||
aria-expanded={state.isBlogPostsOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
Blog Posts
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${state.isBlogPostsOpen ? "rotate-180" : ""}`} aria-hidden="true" />
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 transition-transform ${state.isBlogPostsOpen ? "rotate-180" : ""}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
{state.isBlogPostsOpen && (
|
||||
<div className="flex flex-col gap-3 pl-4 border-l-2 border-secondary/30">
|
||||
|
|
@ -410,7 +453,7 @@ export function Header() {
|
|||
key={item.href}
|
||||
href={item.href}
|
||||
className="text-sm py-1 transition-colors"
|
||||
onClick={() => dispatch({ type: 'TOGGLE_MENU' })}
|
||||
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
|
|
@ -422,14 +465,17 @@ export function Header() {
|
|||
{/* About Us Mobile */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<button
|
||||
onClick={() => dispatch({ type: 'TOGGLE_ABOUT' })}
|
||||
onClick={() => dispatch({ type: "TOGGLE_ABOUT" })}
|
||||
className="flex items-center justify-between text-sm font-medium py-1 hover-brand"
|
||||
aria-label="About menu"
|
||||
aria-expanded={state.isAboutOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
About Us
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${state.isAboutOpen ? "rotate-180" : ""}`} aria-hidden="true" />
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 transition-transform ${state.isAboutOpen ? "rotate-180" : ""}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
{state.isAboutOpen && (
|
||||
<div className="flex flex-col gap-3 pl-4 border-l-2 border-secondary/30">
|
||||
|
|
@ -438,7 +484,7 @@ export function Header() {
|
|||
key={item.href}
|
||||
href={item.href}
|
||||
className="text-sm py-1 transition-colors"
|
||||
onClick={() => dispatch({ type: 'TOGGLE_MENU' })}
|
||||
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
|
|
@ -450,14 +496,21 @@ export function Header() {
|
|||
<Link
|
||||
href="/products"
|
||||
className="text-sm font-medium py-1 transition-colors"
|
||||
onClick={() => dispatch({ type: 'TOGGLE_MENU' })}
|
||||
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
||||
>
|
||||
Products
|
||||
</Link>
|
||||
<Link
|
||||
href="/service-areas"
|
||||
className="text-sm font-medium py-1 transition-colors"
|
||||
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
||||
>
|
||||
Service Areas
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact-us"
|
||||
className="text-sm font-medium py-1 transition-colors"
|
||||
onClick={() => dispatch({ type: 'TOGGLE_MENU' })}
|
||||
onClick={() => dispatch({ type: "TOGGLE_MENU" })}
|
||||
>
|
||||
Contact Us
|
||||
</Link>
|
||||
|
|
@ -465,18 +518,21 @@ export function Header() {
|
|||
<div className="flex flex-col gap-4 pt-4 border-t border-border/40">
|
||||
<MobileCartButton
|
||||
onClick={() => {
|
||||
dispatch({ type: 'TOGGLE_MENU' })
|
||||
dispatch({ type: 'SET_CART', value: true })
|
||||
dispatch({ type: "TOGGLE_MENU" })
|
||||
dispatch({ type: "SET_CART", value: true })
|
||||
}}
|
||||
/>
|
||||
<a href="tel:+14352339668" className="flex items-center gap-2 text-sm font-medium transition-colors">
|
||||
<a
|
||||
href="tel:+14352339668"
|
||||
className="flex items-center gap-2 text-sm font-medium transition-colors"
|
||||
>
|
||||
<Phone className="h-4 w-4" />
|
||||
<span>(435) 233-9668</span>
|
||||
</a>
|
||||
<Button
|
||||
onClick={() => {
|
||||
dispatch({ type: 'TOGGLE_MENU' })
|
||||
dispatch({ type: 'SET_MODAL', value: true })
|
||||
dispatch({ type: "TOGGLE_MENU" })
|
||||
dispatch({ type: "SET_MODAL", value: true })
|
||||
}}
|
||||
className="bg-primary hover:bg-primary/90 w-full"
|
||||
>
|
||||
|
|
@ -486,8 +542,14 @@ export function Header() {
|
|||
</nav>
|
||||
)}
|
||||
</div>
|
||||
<GetFreeMachineModal open={state.isModalOpen} onOpenChange={(value) => dispatch({ type: 'SET_MODAL', value })} />
|
||||
<Cart isOpen={state.isCartOpen} onClose={() => dispatch({ type: 'SET_CART', value: false })} />
|
||||
<GetFreeMachineModal
|
||||
open={state.isModalOpen}
|
||||
onOpenChange={(value) => dispatch({ type: "SET_MODAL", value })}
|
||||
/>
|
||||
<Cart
|
||||
isOpen={state.isCartOpen}
|
||||
onClose={() => dispatch({ type: "SET_CART", value: false })}
|
||||
/>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ export function HeroSection() {
|
|||
<div className="grid gap-8 lg:grid-cols-2 lg:gap-12 items-center">
|
||||
{/* Left Content */}
|
||||
<div className="flex flex-col gap-6">
|
||||
<Badge variant="outline" className="inline-flex items-center gap-2 rounded-full border border-border px-3 py-1 text-xs font-medium w-fit">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border px-3 py-1 text-xs font-medium w-fit"
|
||||
>
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
|
||||
|
|
@ -21,44 +24,57 @@ export function HeroSection() {
|
|||
</Badge>
|
||||
|
||||
<h1 className="text-4xl font-bold tracking-tight text-balance md:text-5xl lg:text-6xl">
|
||||
Get a <span className="text-primary">FREE</span> Vending Machine for Your Utah Business
|
||||
Free Vending Machine Placement for Utah Businesses
|
||||
</h1>
|
||||
|
||||
<p className="text-lg text-muted-foreground text-pretty leading-relaxed">
|
||||
No cost. No hassle. Just fresh snacks and beverages for your employees and customers. We install, stock,
|
||||
and maintain everything at absolutely zero cost to you.
|
||||
If your breakroom, waiting area, or staff space needs a better
|
||||
snack-and-drink option, we can review the location, place the
|
||||
right machine, keep it stocked, and handle service after
|
||||
installation.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2 text-foreground">
|
||||
<CheckCircle2 className="h-5 w-5 text-primary flex-shrink-0" />
|
||||
<span className="font-semibold">100% FREE—No upfront costs or fees</span>
|
||||
<span className="font-semibold">
|
||||
Free placement for qualifying business locations
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-foreground">
|
||||
<CheckCircle2 className="h-5 w-5 text-primary flex-shrink-0" />
|
||||
<span>We stock, maintain, and service everything</span>
|
||||
<span>We review the location, then handle stocking and service</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-foreground">
|
||||
<CheckCircle2 className="h-5 w-5 text-primary flex-shrink-0" />
|
||||
<span>Modern machines with cashless payment options</span>
|
||||
<span>Snack, beverage, and combo machines with cashless payment</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-foreground">
|
||||
<CheckCircle2 className="h-5 w-5 text-primary flex-shrink-0" />
|
||||
<span>Healthy and traditional snack options</span>
|
||||
<span>Traditional favorites and healthier product options</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 pt-4">
|
||||
<Button asChild size="lg" className="bg-primary hover:bg-primary/90 text-lg h-14 px-8">
|
||||
<Link href="#request-machine">Get Your FREE Machine Now</Link>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="bg-primary hover:bg-primary/90 text-lg h-14 px-8"
|
||||
>
|
||||
<Link href="#request-machine">See If Your Location Qualifies</Link>
|
||||
</Button>
|
||||
<Button asChild size="lg" variant="outline" className="text-lg h-14 bg-transparent">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="text-lg h-14 bg-transparent"
|
||||
>
|
||||
<Link href="#how-it-works">See How It Works</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground pt-2">
|
||||
⭐ Trusted by 100+ Utah businesses | Serving Davis, Salt Lake & Utah Counties
|
||||
Serving Davis, Salt Lake, and Utah counties
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -77,12 +93,18 @@ export function HeroSection() {
|
|||
<CardContent className="p-0">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="text-3xl font-bold text-primary">100+</div>
|
||||
<div className="text-xs text-muted-foreground">Machines Installed</div>
|
||||
<div className="text-3xl font-bold text-primary">
|
||||
3
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Counties Served
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold text-primary">6+</div>
|
||||
<div className="text-xs text-muted-foreground">Years in Business</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Years in Business
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
|
|||
413
components/location-landing-page.tsx
Normal file
413
components/location-landing-page.tsx
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
import { businessConfig, socialProfiles } from "@/lib/seo-config"
|
||||
import {
|
||||
businessConfig,
|
||||
businessHours,
|
||||
getAllServiceAreasFormatted,
|
||||
socialProfiles,
|
||||
} from "@/lib/seo-config"
|
||||
|
||||
/**
|
||||
* Organization Schema Component
|
||||
* Implements comprehensive Organization schema for SEO
|
||||
* Helps establish business authority and trustworthiness (E-E-A-T)
|
||||
* Implements organization schema for the homepage.
|
||||
*/
|
||||
export function OrganizationSchema() {
|
||||
const structuredData = {
|
||||
|
|
@ -22,12 +26,21 @@ export function OrganizationSchema() {
|
|||
},
|
||||
description: businessConfig.description,
|
||||
foundingDate: businessConfig.openingDate,
|
||||
areaServed: getAllServiceAreasFormatted(),
|
||||
openingHoursSpecification: [
|
||||
{
|
||||
"@type": "OpeningHoursSpecification",
|
||||
dayOfWeek: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
|
||||
opens: businessHours.monday.open,
|
||||
closes: businessHours.monday.close,
|
||||
},
|
||||
],
|
||||
contactPoint: {
|
||||
"@type": "ContactPoint",
|
||||
telephone: businessConfig.phoneFormatted,
|
||||
contactType: "Customer Service",
|
||||
areaServed: "US",
|
||||
availableLanguage: ["English", "Spanish"],
|
||||
areaServed: "Utah",
|
||||
availableLanguage: ["English"],
|
||||
},
|
||||
sameAs: [
|
||||
socialProfiles.linkedin,
|
||||
|
|
@ -35,12 +48,6 @@ export function OrganizationSchema() {
|
|||
socialProfiles.youtube,
|
||||
socialProfiles.twitter,
|
||||
],
|
||||
address: {
|
||||
"@type": "PostalAddress",
|
||||
addressCountry: "US",
|
||||
addressRegion: "UT",
|
||||
// Service business - no specific street address
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -50,6 +57,3 @@ export function OrganizationSchema() {
|
|||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,47 +1,18 @@
|
|||
'use client'
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { Star } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { PublicInset, PublicPageHeader, PublicSurface } from "@/components/public-surface"
|
||||
|
||||
const reviews = [
|
||||
{
|
||||
title: "Excellent Service!",
|
||||
body: "Rocky Mountain Vending has been fantastic for our office. The machines are always well-stocked and working perfectly.",
|
||||
author: "Sarah M., Salt Lake City",
|
||||
},
|
||||
{
|
||||
title: "Professional and Reliable",
|
||||
body: "We've been working with Rocky Mountain Vending for over a year now. Their team is responsive and easy to work with.",
|
||||
author: "John D., Ogden",
|
||||
},
|
||||
{
|
||||
title: "Great Selection",
|
||||
body: "Our employees love the healthy snack options and variety available. The free installation and maintenance service is a huge plus.",
|
||||
author: "Michelle R., Provo",
|
||||
},
|
||||
{
|
||||
title: "Outstanding Customer Service",
|
||||
body: "Whenever we have an issue, the team responds quickly and resolves it the same day. It's rare to find this level of service today.",
|
||||
author: "David K., Sandy",
|
||||
},
|
||||
{
|
||||
title: "Highly Recommended",
|
||||
body: "Best vending service we've ever used. Free machines, regular restocking, and great communication.",
|
||||
author: "Lisa T., West Valley City",
|
||||
},
|
||||
{
|
||||
title: "Local Business We Trust",
|
||||
body: "Supporting a local, family-owned business feels good. They care about their customers and it shows in everything they do.",
|
||||
author: "Robert P., Bountiful",
|
||||
},
|
||||
]
|
||||
import {
|
||||
PublicInset,
|
||||
PublicPageHeader,
|
||||
PublicSurface,
|
||||
} from "@/components/public-surface"
|
||||
|
||||
export function ReviewsPage() {
|
||||
useEffect(() => {
|
||||
const existingScript = document.querySelector('script[data-rocky-reviews-widget="true"]')
|
||||
const existingScript = document.querySelector(
|
||||
'script[data-rocky-reviews-widget="true"]'
|
||||
)
|
||||
if (existingScript) {
|
||||
return
|
||||
}
|
||||
|
|
@ -65,39 +36,29 @@ export function ReviewsPage() {
|
|||
align="center"
|
||||
eyebrow="Customer Reviews"
|
||||
title="What Utah businesses say about working with Rocky Mountain Vending."
|
||||
description="See why Utah businesses trust Rocky Mountain Vending for free placement, dependable service, and fast local support."
|
||||
description="Browse the live Google review feed and see what Utah businesses say about placement, restocking, repairs, and service."
|
||||
/>
|
||||
|
||||
<div className="mt-10 grid gap-5 md:grid-cols-2 xl:grid-cols-3">
|
||||
{reviews.map((review) => (
|
||||
<PublicSurface key={review.author} className="h-full p-5 md:p-6">
|
||||
<div className="flex items-center gap-1 text-primary">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<Star key={index} className="h-5 w-5 fill-current" />
|
||||
))}
|
||||
</div>
|
||||
<h2 className="mt-4 text-xl font-semibold text-foreground">{review.title}</h2>
|
||||
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">{review.body}</p>
|
||||
<p className="mt-5 text-sm font-medium text-foreground">— {review.author}</p>
|
||||
</PublicSurface>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<section className="mt-12">
|
||||
<PublicSurface className="overflow-hidden p-5 md:p-7">
|
||||
<div className="flex flex-col gap-4 border-b border-border/60 pb-6 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">All Google Reviews</p>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
||||
All Google Reviews
|
||||
</p>
|
||||
<h2 className="mt-2 text-3xl font-semibold tracking-tight text-balance">
|
||||
Browse the full review feed from Rocky Mountain Vending customers.
|
||||
Browse the full review feed from Rocky Mountain Vending
|
||||
customers.
|
||||
</h2>
|
||||
<p className="mt-3 max-w-3xl text-sm leading-relaxed text-muted-foreground">
|
||||
See the full stream of Google reviews from businesses that rely on us for placement, restocking, repairs, and day-to-day support.
|
||||
See the full stream of Google reviews from businesses that rely
|
||||
on us for placement, restocking, repairs, and day-to-day
|
||||
service.
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="secondary" className="w-fit rounded-full px-3 py-1 text-sm">
|
||||
4.9 / 5.0 on Google
|
||||
</Badge>
|
||||
<div className="w-fit rounded-full border border-border/60 bg-background px-3 py-1 text-sm text-muted-foreground">
|
||||
Live review feed
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
|
|
@ -114,18 +75,35 @@ export function ReviewsPage() {
|
|||
|
||||
<section className="mt-12 grid gap-6 lg:grid-cols-[1.15fr_0.85fr]">
|
||||
<PublicSurface>
|
||||
<h2 className="text-3xl font-semibold tracking-tight text-balance">Why customers keep choosing Rocky Mountain Vending</h2>
|
||||
<h2 className="text-3xl font-semibold tracking-tight text-balance">
|
||||
What businesses usually want to verify before they choose a vendor
|
||||
</h2>
|
||||
<div className="mt-6 space-y-4">
|
||||
{[
|
||||
["100% free installation", "No upfront costs or hidden fees for qualifying businesses."],
|
||||
["Regular restocking and maintenance", "We keep machines full, clean, and working with ongoing service included."],
|
||||
["Wide product variety", "Healthy snacks, traditional favorites, beverages, and location-specific mixes."],
|
||||
["Fast local support", "Utah-based service means quicker response times and a more personal experience."],
|
||||
["Flexible service options", "From a single machine to multiple properties, we scale with your location."],
|
||||
[
|
||||
"Route consistency",
|
||||
"Whether the company actually keeps machines stocked, clean, and working after the initial install.",
|
||||
],
|
||||
[
|
||||
"Response time",
|
||||
"How quickly service issues, refunds, or machine problems get attention when something goes wrong.",
|
||||
],
|
||||
[
|
||||
"Product mix",
|
||||
"Whether the machine selection matches the people using the location instead of forcing a one-size-fits-all lineup.",
|
||||
],
|
||||
[
|
||||
"Communication",
|
||||
"Whether the provider is easy to reach when you need placement, repairs, moving help, or a change in product mix.",
|
||||
],
|
||||
].map(([title, body]) => (
|
||||
<PublicInset key={title}>
|
||||
<h3 className="text-lg font-semibold text-foreground">{title}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{body}</p>
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
{body}
|
||||
</p>
|
||||
</PublicInset>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -133,20 +111,42 @@ export function ReviewsPage() {
|
|||
|
||||
<PublicSurface className="flex flex-col justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">Next Step</p>
|
||||
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance">Want the same experience at your location?</h2>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
||||
Next Step
|
||||
</p>
|
||||
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance">
|
||||
Want to see whether your location qualifies?
|
||||
</h2>
|
||||
<p className="mt-3 text-base leading-relaxed text-muted-foreground">
|
||||
Whether you need free placement, service, or help buying a machine, our team is ready to help.
|
||||
Tell us about your traffic, breakroom, or customer area and
|
||||
we'll help you decide between free placement, machine sales,
|
||||
or service help.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 grid gap-4 sm:grid-cols-2">
|
||||
<Link href="/#request-machine" className="rounded-[1.5rem] border border-border/60 bg-white p-5 text-left transition hover:border-primary/30 hover:text-primary">
|
||||
<h3 className="text-lg font-semibold text-foreground">Free Placement</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">Start a request for free vending machine placement at your business.</p>
|
||||
<Link
|
||||
href="/#request-machine"
|
||||
className="rounded-[1.5rem] border border-border/60 bg-white p-5 text-left transition hover:border-primary/30 hover:text-primary"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
Free Placement
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
See whether your business qualifies for vending machine
|
||||
placement and ongoing service.
|
||||
</p>
|
||||
</Link>
|
||||
<Link href="/contact-us#contact-form" className="rounded-[1.5rem] border border-border/60 bg-white p-5 text-left transition hover:border-primary/30 hover:text-primary">
|
||||
<h3 className="text-lg font-semibold text-foreground">Service or Sales</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">Reach out about repairs, moving, manuals, parts, or machine sales.</p>
|
||||
<Link
|
||||
href="/contact-us#contact-form"
|
||||
className="rounded-[1.5rem] border border-border/60 bg-white p-5 text-left transition hover:border-primary/30 hover:text-primary"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
Service or Sales
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
Reach out about repairs, moving, manuals, parts, or machine
|
||||
sales.
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
</PublicSurface>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { Star } from "lucide-react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { ReviewSchema } from "@/components/review-schema"
|
||||
import { PublicPageHeader, PublicSurface } from "@/components/public-surface"
|
||||
|
||||
export function ReviewsSection() {
|
||||
|
|
@ -19,44 +16,26 @@ export function ReviewsSection() {
|
|||
}
|
||||
}, [])
|
||||
|
||||
// Review data based on website content (4.9 average, 50+ reviews)
|
||||
const aggregateRating = {
|
||||
ratingValue: 4.9,
|
||||
reviewCount: 50,
|
||||
bestRating: 5,
|
||||
worstRating: 1,
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReviewSchema aggregateRating={aggregateRating} />
|
||||
<section className="py-16 md:py-24 bg-card/30">
|
||||
<div className="container mx-auto px-4">
|
||||
<PublicPageHeader
|
||||
align="center"
|
||||
eyebrow="Google Reviews"
|
||||
title="What Our Customers Say"
|
||||
description="See what Utah businesses have to say about Rocky Mountain Vending and the service they count on every day."
|
||||
>
|
||||
<div className="mt-4 flex justify-center">
|
||||
<Badge variant="secondary" className="rounded-full px-4 py-1.5 text-sm">
|
||||
<Star className="mr-2 h-4 w-4 fill-current text-primary" />
|
||||
{aggregateRating.ratingValue} / {aggregateRating.bestRating} ({aggregateRating.reviewCount}+ Reviews)
|
||||
</Badge>
|
||||
</div>
|
||||
</PublicPageHeader>
|
||||
<section className="py-16 md:py-24 bg-card/30">
|
||||
<div className="container mx-auto px-4">
|
||||
<PublicPageHeader
|
||||
align="center"
|
||||
eyebrow="Customer Feedback"
|
||||
title="See what businesses say about Rocky Mountain Vending"
|
||||
description="Browse the live review feed from businesses that use Rocky Mountain Vending for placement, restocking, repairs, and day-to-day service."
|
||||
/>
|
||||
|
||||
<PublicSurface className="mx-auto mt-10 max-w-5xl overflow-hidden p-5 md:p-7">
|
||||
<iframe
|
||||
className="lc_reviews_widget min-h-[400px] w-full rounded-[1.5rem] border border-border/60 bg-background"
|
||||
src="https://reputationhub.site/reputation/widgets/review_widget/YAoWLgNSid8oG44j9BjG"
|
||||
frameBorder="0"
|
||||
scrolling="no"
|
||||
title="Customer Reviews"
|
||||
/>
|
||||
</PublicSurface>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
<PublicSurface className="mx-auto mt-10 max-w-5xl overflow-hidden p-5 md:p-7">
|
||||
<iframe
|
||||
className="lc_reviews_widget min-h-[400px] w-full rounded-[1.5rem] border border-border/60 bg-background"
|
||||
src="https://reputationhub.site/reputation/widgets/review_widget/YAoWLgNSid8oG44j9BjG"
|
||||
frameBorder="0"
|
||||
scrolling="no"
|
||||
title="Customer Reviews"
|
||||
/>
|
||||
</PublicSurface>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,13 @@ const services = [
|
|||
{
|
||||
title: "Vending Machines",
|
||||
items: [
|
||||
{ name: "Sales", href: "/vending-machines/machines-for-sale" },
|
||||
{
|
||||
name: "Machines for Sale",
|
||||
href: "/vending-machines/machines-for-sale",
|
||||
},
|
||||
{ name: "Machines We Use", href: "/vending-machines/machines-we-use" },
|
||||
{ name: "Custom Solutions", href: "/services/service-areas" },
|
||||
{ name: "Latest Technology", href: "/vending-machines/machines-we-use" },
|
||||
{ name: "Service Areas", href: "/service-areas" },
|
||||
{ name: "Free Placement", href: "/#request-machine" },
|
||||
],
|
||||
image: "/images/vending-bay-2-scaled.webp",
|
||||
},
|
||||
|
|
@ -26,18 +29,24 @@ const services = [
|
|||
{
|
||||
title: "Product Options",
|
||||
items: [
|
||||
{ name: "Coca-Cola Products", href: "/services" },
|
||||
{ name: "Pepsi Products", href: "/services" },
|
||||
{ name: "Healthy Snacks", href: "/services" },
|
||||
{ name: "Energy Drinks", href: "/services" },
|
||||
{
|
||||
name: "Healthy Vending Options",
|
||||
href: "/food-and-beverage/healthy-options",
|
||||
},
|
||||
{
|
||||
name: "Traditional Favorites",
|
||||
href: "/food-and-beverage/traditional-options",
|
||||
},
|
||||
{ name: "Supplier Overview", href: "/food-and-beverage/suppliers" },
|
||||
{ name: "Contact About Your Mix", href: "/contact-us#contact-form" },
|
||||
],
|
||||
image: "/variety-of-drinks-and-snacks.jpg",
|
||||
},
|
||||
{
|
||||
title: "Support & Resources",
|
||||
title: "Next Steps & Resources",
|
||||
items: [
|
||||
{ name: "Service Areas", href: "/service-areas" },
|
||||
{ name: "Contact Us", href: "/contact-us#contact-form" },
|
||||
{ name: "Talk to Our Team", href: "/contact-us#contact-form" },
|
||||
{ name: "Request Service", href: "/services/repairs#request-service" },
|
||||
{ name: "Order Parts", href: "/services/parts#how-to-order" },
|
||||
],
|
||||
|
|
@ -50,20 +59,32 @@ export function ServicesSection() {
|
|||
<section id="services" className="py-20 md:py-28 bg-muted/30">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">Explore</p>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
||||
Explore
|
||||
</p>
|
||||
<h2 className="mt-3 text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl text-balance">
|
||||
Our services and products
|
||||
</h2>
|
||||
<p className="mx-auto mt-3 max-w-2xl text-lg text-muted-foreground text-pretty leading-relaxed">
|
||||
From free placement to manuals and repairs, we make it easy to get the machines, service, and support your location needs.
|
||||
From free placement to repairs, manuals, and product mix planning,
|
||||
this is where to find the machines, service pages, and next steps
|
||||
your location may need.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 xl:grid-cols-4">
|
||||
{services.map((service) => (
|
||||
<PublicSurface key={service.title} className="overflow-hidden p-0 transition-all hover:-translate-y-0.5 hover:shadow-[0_26px_65px_rgba(0,0,0,0.12)]">
|
||||
<PublicSurface
|
||||
key={service.title}
|
||||
className="overflow-hidden p-0 transition-all hover:-translate-y-0.5 hover:shadow-[0_26px_65px_rgba(0,0,0,0.12)]"
|
||||
>
|
||||
<div className="relative aspect-[4/3] bg-[radial-gradient(circle_at_top_left,rgba(196,154,52,0.16),transparent_55%),linear-gradient(180deg,rgba(247,244,236,0.75),rgba(255,255,255,0.96))]">
|
||||
<Image src={service.image || "/placeholder.svg"} alt={service.title} fill className="object-cover" />
|
||||
<Image
|
||||
src={service.image || "/placeholder.svg"}
|
||||
alt={service.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-5 md:p-6">
|
||||
<h3 className="text-xl font-semibold mb-4">{service.title}</h3>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import { Card, CardContent } from "@/components/ui/card"
|
|||
export function StatsSection() {
|
||||
const stats = [
|
||||
{ value: "6+", label: "Years of Experience" },
|
||||
{ value: "100+", label: "Machines Installed" },
|
||||
{ value: "3", label: "Counties Served" },
|
||||
{ value: "20+", label: "Cities Served in Utah" },
|
||||
{ value: "100%", label: "FREE Installation" },
|
||||
{ value: "Full", label: "Service Support" },
|
||||
]
|
||||
|
||||
return (
|
||||
|
|
@ -15,8 +15,12 @@ export function StatsSection() {
|
|||
{stats.map((stat, index) => (
|
||||
<Card key={index} className="border-0 shadow-none bg-transparent">
|
||||
<CardContent className="p-0 text-center">
|
||||
<div className="text-4xl md:text-5xl font-bold text-primary mb-2">{stat.value}</div>
|
||||
<div className="text-sm md:text-base text-muted-foreground">{stat.label}</div>
|
||||
<div className="text-4xl md:text-5xl font-bold text-primary mb-2">
|
||||
{stat.value}
|
||||
</div>
|
||||
<div className="text-sm md:text-base text-muted-foreground">
|
||||
{stat.label}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,90 +1,16 @@
|
|||
import { businessConfig, serviceAreas, businessHours, socialProfiles } from "@/lib/seo-config"
|
||||
|
||||
interface StructuredDataProps {
|
||||
type?: "LocalBusiness" | "Service" | "Organization"
|
||||
additionalData?: Record<string, unknown>
|
||||
}
|
||||
import { businessConfig } from "@/lib/seo-config"
|
||||
|
||||
/**
|
||||
* Structured Data Component for LocalBusiness/Service/Organization
|
||||
* Implements JSON-LD schema for SEO and rich results
|
||||
* Enhanced with AggregateRating and Organization schema for better SEO
|
||||
* Home-page WebSite schema used for site name understanding.
|
||||
*/
|
||||
export function StructuredData({ type = "LocalBusiness", additionalData }: StructuredDataProps) {
|
||||
// Build opening hours specification
|
||||
const openingHoursSpecification = [
|
||||
{
|
||||
"@type": "OpeningHoursSpecification",
|
||||
dayOfWeek: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
|
||||
opens: businessHours.monday.open,
|
||||
closes: businessHours.monday.close,
|
||||
},
|
||||
]
|
||||
|
||||
// Build area served (detailed list of cities)
|
||||
const areaServed = serviceAreas.map((area) => ({
|
||||
"@type": "City",
|
||||
name: area.city,
|
||||
addressRegion: area.state,
|
||||
addressCountry: area.country,
|
||||
}))
|
||||
|
||||
export function StructuredData() {
|
||||
const structuredData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": type,
|
||||
"@type": "WebSite",
|
||||
name: businessConfig.name,
|
||||
legalName: businessConfig.legalName,
|
||||
description: businessConfig.description,
|
||||
alternateName: businessConfig.shortName,
|
||||
url: businessConfig.website,
|
||||
telephone: businessConfig.phoneFormatted,
|
||||
email: businessConfig.email,
|
||||
foundingDate: businessConfig.openingDate,
|
||||
// Service business - no physical address
|
||||
areaServed: areaServed,
|
||||
serviceArea: {
|
||||
"@type": "GeoCircle",
|
||||
// Approximate center of service area (Salt Lake City area)
|
||||
geoMidpoint: {
|
||||
"@type": "GeoCoordinates",
|
||||
latitude: 40.7608,
|
||||
longitude: -111.891,
|
||||
},
|
||||
// Approximate radius covering all service areas
|
||||
geoRadius: {
|
||||
"@type": "Distance",
|
||||
value: 50,
|
||||
unitCode: "mi",
|
||||
},
|
||||
},
|
||||
openingHoursSpecification: openingHoursSpecification,
|
||||
priceRange: "$$",
|
||||
// Aggregate rating based on website content (4.9 average, 50+ reviews)
|
||||
aggregateRating: {
|
||||
"@type": "AggregateRating",
|
||||
ratingValue: 4.9,
|
||||
reviewCount: 50,
|
||||
bestRating: 5,
|
||||
worstRating: 1,
|
||||
},
|
||||
// Social profiles
|
||||
sameAs: [
|
||||
socialProfiles.linkedin,
|
||||
socialProfiles.facebook,
|
||||
socialProfiles.youtube,
|
||||
socialProfiles.twitter,
|
||||
],
|
||||
// Logo
|
||||
logo: {
|
||||
"@type": "ImageObject",
|
||||
url: `${businessConfig.website}/rmv-logo.png`,
|
||||
},
|
||||
// Image
|
||||
image: {
|
||||
"@type": "ImageObject",
|
||||
url: `${businessConfig.website}/images/rocky-mountain-vending-service-area-926x1024.webp`,
|
||||
},
|
||||
// Additional data if provided
|
||||
...(additionalData || {}),
|
||||
inLanguage: "en-US",
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -94,4 +20,3 @@ export function StructuredData({ type = "LocalBusiness", additionalData }: Struc
|
|||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,59 @@
|
|||
'use client'
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { VendingMachinesShowcase } from "@/components/vending-machines-showcase"
|
||||
import { CheckCircle2, CreditCard, MonitorSmartphone, Package, Wrench } from "lucide-react"
|
||||
import {
|
||||
CheckCircle2,
|
||||
CreditCard,
|
||||
type LucideIcon,
|
||||
MonitorSmartphone,
|
||||
Package,
|
||||
Wrench,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { PublicInset, PublicPageHeader, PublicSurface } from "@/components/public-surface"
|
||||
import {
|
||||
PublicInset,
|
||||
PublicPageHeader,
|
||||
PublicSurface,
|
||||
} from "@/components/public-surface"
|
||||
import { GetFreeMachineCta } from "@/components/get-free-machine-cta"
|
||||
import { Breadcrumbs } from "@/components/breadcrumbs"
|
||||
|
||||
export function VendingMachinesPage() {
|
||||
const featureCards: Array<[LucideIcon, string, string]> = [
|
||||
[
|
||||
Package,
|
||||
"Healthy Options",
|
||||
"Wide selection of nutritious snacks and beverages.",
|
||||
],
|
||||
[
|
||||
CreditCard,
|
||||
"Cashless Payment",
|
||||
"Accept cards and mobile payments with modern hardware.",
|
||||
],
|
||||
[
|
||||
Wrench,
|
||||
"Easy Maintenance",
|
||||
"Remote monitoring and quick service response when issues come up.",
|
||||
],
|
||||
[
|
||||
MonitorSmartphone,
|
||||
"Smart Technology",
|
||||
"Track inventory and sales with better visibility.",
|
||||
],
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-10 md:py-14">
|
||||
<Breadcrumbs
|
||||
className="mb-6"
|
||||
items={[{ label: "Vending Machines", href: "/vending-machines" }]}
|
||||
/>
|
||||
<PublicPageHeader
|
||||
align="center"
|
||||
eyebrow="Machine Options"
|
||||
title="Modern vending machines built for reliable everyday use."
|
||||
description="Browse the machines we use, the features we prioritize, and the support that comes with every installation."
|
||||
description="Explore the snack, beverage, and combo machines we place, how we match them to a location, and what ongoing restocking and service look like after installation."
|
||||
/>
|
||||
|
||||
<div className="mt-10">
|
||||
|
|
@ -23,19 +62,40 @@ export function VendingMachinesPage() {
|
|||
|
||||
<section className="mt-12 grid gap-6 lg:grid-cols-[1.1fr_0.9fr]">
|
||||
<PublicSurface>
|
||||
<h2 className="text-3xl font-semibold tracking-tight text-balance">Why choose our machines?</h2>
|
||||
<h2 className="text-3xl font-semibold tracking-tight text-balance">
|
||||
Why businesses choose our vending setup
|
||||
</h2>
|
||||
<div className="mt-6 grid gap-5 md:grid-cols-2">
|
||||
{[
|
||||
["100% FREE service", "No upfront costs, installation fees, or monthly charges for qualifying businesses."],
|
||||
["Modern technology", "Current equipment with reliable performance and updated features."],
|
||||
["Cashless payments", "Integrated card readers and secure payment options for modern locations."],
|
||||
["Full service and support", "Maintenance, restocking, and local follow-up are part of the experience."],
|
||||
[
|
||||
"Machine planning by location",
|
||||
"We look at traffic, audience, and space before recommending a snack, beverage, or combo setup.",
|
||||
],
|
||||
[
|
||||
"Modern equipment",
|
||||
"We use dependable machines with updated payment hardware and features that work well in busy locations.",
|
||||
],
|
||||
[
|
||||
"Cashless-ready checkout",
|
||||
"Integrated card readers and mobile payment options make the machines easier for customers to use.",
|
||||
],
|
||||
[
|
||||
"Ongoing local service",
|
||||
"After installation, we stay responsible for restocking, routine service, and follow-up when machines need attention.",
|
||||
],
|
||||
].map(([title, body]) => (
|
||||
<div key={title} className="flex items-start gap-4 rounded-[1.5rem] border border-border/60 bg-white p-5 shadow-sm">
|
||||
<div
|
||||
key={title}
|
||||
className="flex items-start gap-4 rounded-[1.5rem] border border-border/60 bg-white p-5 shadow-sm"
|
||||
>
|
||||
<CheckCircle2 className="mt-0.5 h-6 w-6 shrink-0 text-primary" />
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-foreground">{title}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{body}</p>
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
{body}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -43,23 +103,35 @@ export function VendingMachinesPage() {
|
|||
</PublicSurface>
|
||||
|
||||
<PublicSurface>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">Get Started</p>
|
||||
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance">Ready to talk through the right machine setup?</h2>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">
|
||||
Next Step
|
||||
</p>
|
||||
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance">
|
||||
Need help choosing the right machine setup?
|
||||
</h2>
|
||||
<p className="mt-3 text-base leading-relaxed text-muted-foreground">
|
||||
We can help with free placement, sales questions, feature comparisons, and planning the best layout for your location.
|
||||
We can help you compare free placement and direct purchase options,
|
||||
narrow down the right machine type, and choose a layout that fits
|
||||
your space and traffic.
|
||||
</p>
|
||||
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
|
||||
<Button asChild size="lg" className="h-11 rounded-full px-6">
|
||||
<Link href="/contact-us#contact-form">Contact Us</Link>
|
||||
<Link href="/contact-us#contact-form">Talk to Our Team</Link>
|
||||
</Button>
|
||||
<GetFreeMachineCta buttonLabel="Get Free Machine" className="h-11 px-6" variant="outline" />
|
||||
<GetFreeMachineCta
|
||||
buttonLabel="See If Your Location Qualifies"
|
||||
className="h-11 px-6"
|
||||
variant="outline"
|
||||
/>
|
||||
</div>
|
||||
</PublicSurface>
|
||||
</section>
|
||||
|
||||
<section className="mt-12">
|
||||
<PublicSurface>
|
||||
<h2 className="text-3xl font-semibold tracking-tight text-balance">Our services around the machines</h2>
|
||||
<h2 className="text-3xl font-semibold tracking-tight text-balance">
|
||||
Our services around the machines
|
||||
</h2>
|
||||
<div className="mt-6 grid gap-5 md:grid-cols-3">
|
||||
{[
|
||||
{
|
||||
|
|
@ -79,18 +151,27 @@ export function VendingMachinesPage() {
|
|||
{
|
||||
icon: CreditCard,
|
||||
title: "Parts & Manuals",
|
||||
body: "Replacement parts, manuals, and support resources for major brands.",
|
||||
body: "Replacement parts, manuals, and machine resources for major brands.",
|
||||
href: "/manuals",
|
||||
cta: "Manuals",
|
||||
},
|
||||
].map((service) => (
|
||||
<PublicInset key={service.title} className="h-full p-5 text-center">
|
||||
<PublicInset
|
||||
key={service.title}
|
||||
className="h-full p-5 text-center"
|
||||
>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||
<service.icon className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="mt-4 text-xl font-semibold">{service.title}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{service.body}</p>
|
||||
<Button asChild variant="outline" className="mt-5 h-10 rounded-full px-4">
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
{service.body}
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="mt-5 h-10 rounded-full px-4"
|
||||
>
|
||||
<Link href={service.href}>{service.cta}</Link>
|
||||
</Button>
|
||||
</PublicInset>
|
||||
|
|
@ -101,18 +182,17 @@ export function VendingMachinesPage() {
|
|||
|
||||
<section className="mt-12">
|
||||
<PublicSurface>
|
||||
<h2 className="text-3xl font-semibold tracking-tight text-balance">Machine features customers ask about most</h2>
|
||||
<h2 className="text-3xl font-semibold tracking-tight text-balance">
|
||||
Machine features customers ask about most
|
||||
</h2>
|
||||
<div className="mt-6 grid gap-5 md:grid-cols-2 xl:grid-cols-4">
|
||||
{[
|
||||
[Package, "Healthy Options", "Wide selection of nutritious snacks and beverages."],
|
||||
[CreditCard, "Cashless Payment", "Accept cards and mobile payments with modern hardware."],
|
||||
[Wrench, "Easy Maintenance", "Remote monitoring and quick service response when issues come up."],
|
||||
[MonitorSmartphone, "Smart Technology", "Track inventory and sales with better visibility."],
|
||||
].map(([Icon, title, body]) => (
|
||||
{featureCards.map(([Icon, title, body]) => (
|
||||
<PublicInset key={title} className="h-full p-5 text-center">
|
||||
<Icon className="mx-auto h-10 w-10 text-primary" />
|
||||
<h3 className="mt-4 text-lg font-semibold">{title}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{body}</p>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
{body}
|
||||
</p>
|
||||
</PublicInset>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
19
components/website-schema.tsx
Normal file
19
components/website-schema.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { businessConfig } from "@/lib/seo-config"
|
||||
|
||||
export function WebsiteSchema() {
|
||||
const structuredData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
name: businessConfig.name,
|
||||
alternateName: businessConfig.shortName,
|
||||
url: businessConfig.website,
|
||||
inLanguage: "en-US",
|
||||
}
|
||||
|
||||
return (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
'use client'
|
||||
"use client"
|
||||
|
||||
import { ReactNode } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
|
|
@ -17,7 +17,9 @@ export function WhoWeServePage({ title, content }: WhoWeServePageProps) {
|
|||
<div className="container mx-auto px-4 py-8 md:py-12 max-w-5xl">
|
||||
{/* Header */}
|
||||
<header className="text-center mb-12 md:mb-16">
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-balance mb-4">{title}</h1>
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-balance mb-4">
|
||||
{title}
|
||||
</h1>
|
||||
<div className="w-24 h-1 bg-gradient-to-r from-[var(--link-hover-color)] to-[var(--link-hover-color-dark)] mx-auto rounded-full" />
|
||||
</header>
|
||||
|
||||
|
|
@ -33,9 +35,12 @@ export function WhoWeServePage({ title, content }: WhoWeServePageProps) {
|
|||
{/* Benefits Section */}
|
||||
<section className="mb-20">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl mb-4 text-balance">Why Choose Rocky Mountain Vending?</h2>
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl mb-4 text-balance">
|
||||
Why Choose Rocky Mountain Vending?
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto text-pretty leading-relaxed">
|
||||
We provide comprehensive vending solutions tailored to your specific needs
|
||||
We match the machine mix, product selection, and service schedule
|
||||
to the way your location actually runs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -45,9 +50,12 @@ export function WhoWeServePage({ title, content }: WhoWeServePageProps) {
|
|||
<CheckCircle2 className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-xl mb-2 text-foreground">100% FREE Service</h3>
|
||||
<h3 className="font-semibold text-xl mb-2 text-foreground">
|
||||
Placement for Qualifying Locations
|
||||
</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
No upfront costs, installation fees, or monthly charges
|
||||
If your location qualifies, we can place and service machines
|
||||
without pushing daily management onto your staff
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -57,9 +65,12 @@ export function WhoWeServePage({ title, content }: WhoWeServePageProps) {
|
|||
<CheckCircle2 className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-xl mb-2 text-foreground">Regular Maintenance</h3>
|
||||
<h3 className="font-semibold text-xl mb-2 text-foreground">
|
||||
Regular Maintenance
|
||||
</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
We stock, service, and maintain all machines at no cost to you
|
||||
We handle stocking, cleaning, and routine service so your team
|
||||
can stay focused on the work your location is there to do
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -69,9 +80,12 @@ export function WhoWeServePage({ title, content }: WhoWeServePageProps) {
|
|||
<CheckCircle2 className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-xl mb-2 text-foreground">Modern Technology</h3>
|
||||
<h3 className="font-semibold text-xl mb-2 text-foreground">
|
||||
Modern Technology
|
||||
</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
Cashless payment options and state-of-the-art equipment
|
||||
Cashless payment options and dependable equipment built for
|
||||
everyday use
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -81,9 +95,12 @@ export function WhoWeServePage({ title, content }: WhoWeServePageProps) {
|
|||
<CheckCircle2 className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-xl mb-2 text-foreground">Healthy Options</h3>
|
||||
<h3 className="font-semibold text-xl mb-2 text-foreground">
|
||||
Healthy Options
|
||||
</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
Wide variety of healthy snacks and beverages available
|
||||
Product mixes can include traditional favorites, drinks, and
|
||||
healthier options
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -93,9 +110,12 @@ export function WhoWeServePage({ title, content }: WhoWeServePageProps) {
|
|||
<CheckCircle2 className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-xl mb-2 text-foreground">Local Support</h3>
|
||||
<h3 className="font-semibold text-xl mb-2 text-foreground">
|
||||
Local Service
|
||||
</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
Family-owned business with dedicated local customer service
|
||||
Local service with responsive follow-up when a machine needs
|
||||
attention
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -105,9 +125,12 @@ export function WhoWeServePage({ title, content }: WhoWeServePageProps) {
|
|||
<CheckCircle2 className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-xl mb-2 text-foreground">Flexible Solutions</h3>
|
||||
<h3 className="font-semibold text-xl mb-2 text-foreground">
|
||||
Machine Options That Match Your Location
|
||||
</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
Customized vending solutions to fit your space and needs
|
||||
Machine types, product mixes, and service plans can be matched
|
||||
to your space, traffic, and audience
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -119,17 +142,23 @@ export function WhoWeServePage({ title, content }: WhoWeServePageProps) {
|
|||
<Card className="border-2 shadow-lg [&>div]:!bg-transparent bg-gradient-to-r from-[var(--primary)] to-[#1d7a35]">
|
||||
<CardContent className="p-10 md:p-12">
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-white mb-4 tracking-tight text-balance">
|
||||
Ready to Get Started?
|
||||
Tell us about your location
|
||||
</h2>
|
||||
<p className="text-white/90 mb-8 max-w-2xl mx-auto text-lg leading-relaxed">
|
||||
Contact us today to learn more about our free vending machine services and how we can help your business.
|
||||
Tell us about your location and we'll help you decide
|
||||
whether free placement, machine sales, or repair service makes
|
||||
the most sense.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button asChild size="lg" className="bg-white text-[var(--primary)] hover:bg-white/90 text-lg h-12 px-8 font-semibold">
|
||||
<Link href="/contact-us">Contact Us</Link>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="bg-white text-[var(--primary)] hover:bg-white/90 text-lg h-12 px-8 font-semibold"
|
||||
>
|
||||
<Link href="/contact-us">Talk to Our Team</Link>
|
||||
</Button>
|
||||
<GetFreeMachineCta
|
||||
buttonLabel="Get Free Machine"
|
||||
buttonLabel="See If Your Location Qualifies"
|
||||
className="h-12 border-2 border-white bg-transparent px-8 text-lg font-semibold text-white hover:bg-white/10"
|
||||
variant="outline"
|
||||
/>
|
||||
|
|
|
|||
346
lib/seo-registry.ts
Normal file
346
lib/seo-registry.ts
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
import { businessConfig } from "@/lib/seo-config"
|
||||
|
||||
export type StaticSeoPageKey =
|
||||
| "home"
|
||||
| "aboutUs"
|
||||
| "aboutLegacy"
|
||||
| "contactUs"
|
||||
| "services"
|
||||
| "serviceAreas"
|
||||
| "serviceAreasLegacy"
|
||||
| "vendingMachines"
|
||||
| "moving"
|
||||
| "repairs"
|
||||
| "parts"
|
||||
| "reviews"
|
||||
| "blog"
|
||||
| "manuals"
|
||||
| "healthyOptions"
|
||||
| "traditionalOptions"
|
||||
| "suppliers"
|
||||
| "warehouses"
|
||||
| "autoRepair"
|
||||
| "gyms"
|
||||
| "communityCenters"
|
||||
| "danceStudios"
|
||||
| "carWashes"
|
||||
| "faqs"
|
||||
| "privacyPolicy"
|
||||
| "terms"
|
||||
|
||||
export interface StaticSeoPageDefinition {
|
||||
path: string
|
||||
title: string
|
||||
description: string
|
||||
keywords: readonly string[]
|
||||
theme: string
|
||||
canonicalPath?: string
|
||||
noindex?: boolean
|
||||
}
|
||||
|
||||
export const ogImagePath =
|
||||
"/images/rocky-mountain-vending-service-area-926x1024.webp"
|
||||
|
||||
export const cornerstonePaths = [
|
||||
"/",
|
||||
"/services",
|
||||
"/service-areas",
|
||||
"/vending-machines",
|
||||
"/services/repairs",
|
||||
"/services/parts",
|
||||
"/services/moving",
|
||||
"/contact-us",
|
||||
] as const
|
||||
|
||||
export const seoPageRegistry: Record<StaticSeoPageKey, StaticSeoPageDefinition> =
|
||||
{
|
||||
home: {
|
||||
path: "/",
|
||||
title: "Free Vending Machines in Utah | Rocky Mountain Vending",
|
||||
description:
|
||||
"Free vending machine placement for qualifying Utah businesses, plus machine sales, repairs, and ongoing service. Rocky Mountain Vending serves Salt Lake City, Ogden, Provo, and nearby communities.",
|
||||
keywords: [
|
||||
"free vending machines Utah",
|
||||
"Utah vending machine company",
|
||||
"vending machines Salt Lake City",
|
||||
"vending machine repair Utah",
|
||||
],
|
||||
theme: "Utah vending services",
|
||||
},
|
||||
aboutUs: {
|
||||
path: "/about-us",
|
||||
title: "About Rocky Mountain Vending | Utah Vending Company",
|
||||
description:
|
||||
"Learn about Rocky Mountain Vending, the Utah service-area business behind our vending placement, machine sales, repairs, and ongoing service.",
|
||||
keywords: [
|
||||
"about Rocky Mountain Vending",
|
||||
"Utah vending company",
|
||||
"vending service company Utah",
|
||||
],
|
||||
theme: "about",
|
||||
},
|
||||
aboutLegacy: {
|
||||
path: "/about",
|
||||
canonicalPath: "/about-us",
|
||||
title: "About Rocky Mountain Vending | Utah Vending Company",
|
||||
description:
|
||||
"Learn about Rocky Mountain Vending, the Utah service-area business behind our vending placement, machine sales, repairs, and ongoing service.",
|
||||
keywords: ["about Rocky Mountain Vending", "Utah vending company"],
|
||||
theme: "about",
|
||||
noindex: true,
|
||||
},
|
||||
contactUs: {
|
||||
path: "/contact-us",
|
||||
title: "Contact Rocky Mountain Vending | Utah Vending Service",
|
||||
description:
|
||||
"Contact Rocky Mountain Vending about free placement, machine sales, repairs, moving, parts, and manuals across Utah.",
|
||||
keywords: [
|
||||
"contact Rocky Mountain Vending",
|
||||
"Utah vending service contact",
|
||||
"vending machine repair contact Utah",
|
||||
],
|
||||
theme: "contact",
|
||||
},
|
||||
services: {
|
||||
path: "/services",
|
||||
title: "Vending Machine Services in Utah | Rocky Mountain Vending",
|
||||
description:
|
||||
"Explore Rocky Mountain Vending services for Utah businesses, including free placement, repairs, moving, parts, and day-to-day vending service.",
|
||||
keywords: [
|
||||
"vending machine services Utah",
|
||||
"Utah vending repairs",
|
||||
"vending machine service Utah",
|
||||
],
|
||||
theme: "services",
|
||||
},
|
||||
serviceAreas: {
|
||||
path: "/service-areas",
|
||||
title: "Utah Service Areas | Rocky Mountain Vending",
|
||||
description:
|
||||
"See the Utah cities and counties Rocky Mountain Vending serves for free placement, machine sales, repairs, moving, parts, and day-to-day vending service.",
|
||||
keywords: [
|
||||
"Utah vending service areas",
|
||||
"vending machine service areas Utah",
|
||||
"Salt Lake City vending company",
|
||||
],
|
||||
theme: "service areas",
|
||||
},
|
||||
serviceAreasLegacy: {
|
||||
path: "/services/service-areas",
|
||||
canonicalPath: "/service-areas",
|
||||
title: "Utah Service Areas | Rocky Mountain Vending",
|
||||
description:
|
||||
"See the Utah cities and counties Rocky Mountain Vending serves for free placement, machine sales, repairs, moving, parts, and day-to-day vending service.",
|
||||
keywords: [
|
||||
"Utah vending service areas",
|
||||
"vending machine service areas Utah",
|
||||
],
|
||||
theme: "service areas",
|
||||
noindex: true,
|
||||
},
|
||||
vendingMachines: {
|
||||
path: "/vending-machines",
|
||||
title: "Vending Machines for Utah Businesses | Rocky Mountain Vending",
|
||||
description:
|
||||
"Browse vending machine options for Utah businesses, including snack, beverage, combo, and cashless-ready equipment.",
|
||||
keywords: [
|
||||
"vending machines Utah",
|
||||
"vending machine sales Utah",
|
||||
"business vending machines",
|
||||
],
|
||||
theme: "vending machines",
|
||||
},
|
||||
moving: {
|
||||
path: "/services/moving",
|
||||
title: "Vending Machine Moving in Utah | Rocky Mountain Vending",
|
||||
description:
|
||||
"Professional vending machine moving and relocation across Utah for snack, beverage, and combo machines.",
|
||||
keywords: [
|
||||
"vending machine moving Utah",
|
||||
"vending machine relocation Utah",
|
||||
"vending machine movers",
|
||||
],
|
||||
theme: "moving",
|
||||
},
|
||||
repairs: {
|
||||
path: "/services/repairs",
|
||||
title: "Vending Machine Repair in Utah | Rocky Mountain Vending",
|
||||
description:
|
||||
"Vending machine repair and maintenance for Utah businesses, including service calls, troubleshooting, and ongoing machine service.",
|
||||
keywords: [
|
||||
"vending machine repair Utah",
|
||||
"vending machine maintenance Utah",
|
||||
"Utah vending repair service",
|
||||
],
|
||||
theme: "repairs",
|
||||
},
|
||||
parts: {
|
||||
path: "/services/parts",
|
||||
title: "Vending Machine Parts in Utah | Rocky Mountain Vending",
|
||||
description:
|
||||
"Find vending machine parts, manuals, and parts help for common machine brands through Rocky Mountain Vending.",
|
||||
keywords: [
|
||||
"vending machine parts Utah",
|
||||
"vending machine manuals Utah",
|
||||
"vending parts help",
|
||||
],
|
||||
theme: "parts",
|
||||
},
|
||||
reviews: {
|
||||
path: "/reviews",
|
||||
title: "Rocky Mountain Vending Reviews | Utah Customer Feedback",
|
||||
description:
|
||||
"Read customer feedback and browse the live Google review feed for Rocky Mountain Vending's Utah placement, restocking, repairs, and service.",
|
||||
keywords: [
|
||||
"Rocky Mountain Vending reviews",
|
||||
"Utah vending company reviews",
|
||||
"vending machine service reviews",
|
||||
],
|
||||
theme: "reviews",
|
||||
},
|
||||
blog: {
|
||||
path: "/blog",
|
||||
title: "Utah Vending Blog | Rocky Mountain Vending",
|
||||
description:
|
||||
"Read Rocky Mountain Vending guides, updates, and Utah-focused vending insights for businesses and property managers.",
|
||||
keywords: [
|
||||
"Utah vending blog",
|
||||
"vending machine guides Utah",
|
||||
"Rocky Mountain Vending blog",
|
||||
],
|
||||
theme: "blog",
|
||||
},
|
||||
manuals: {
|
||||
path: "/manuals",
|
||||
title: "Vending Machine Manuals | Rocky Mountain Vending",
|
||||
description:
|
||||
"Browse vending machine manuals, machine resources, and documentation available through Rocky Mountain Vending.",
|
||||
keywords: [
|
||||
"vending machine manuals",
|
||||
"vending machine documentation",
|
||||
"Rocky Mountain Vending manuals",
|
||||
],
|
||||
theme: "manuals",
|
||||
},
|
||||
healthyOptions: {
|
||||
path: "/food-and-beverage/healthy-options",
|
||||
title: "Healthy Vending Options in Utah | Rocky Mountain Vending",
|
||||
description:
|
||||
"Explore healthier snack and beverage options available through Rocky Mountain Vending for Utah businesses, schools, and fitness locations.",
|
||||
keywords: [
|
||||
"healthy vending Utah",
|
||||
"healthy vending machine options",
|
||||
"healthy snacks for vending machines",
|
||||
],
|
||||
theme: "healthy vending",
|
||||
},
|
||||
traditionalOptions: {
|
||||
path: "/food-and-beverage/traditional-options",
|
||||
title: "Traditional Vending Options in Utah | Rocky Mountain Vending",
|
||||
description:
|
||||
"See traditional snack and beverage vending options available for Utah businesses through Rocky Mountain Vending.",
|
||||
keywords: [
|
||||
"traditional vending Utah",
|
||||
"snack vending options",
|
||||
"beverage vending options Utah",
|
||||
],
|
||||
theme: "traditional vending",
|
||||
},
|
||||
suppliers: {
|
||||
path: "/food-and-beverage/suppliers",
|
||||
title: "Snack and Beverage Supply Options | Rocky Mountain Vending",
|
||||
description:
|
||||
"Learn how Rocky Mountain Vending sources snack and beverage options for Utah vending placements and machine programs.",
|
||||
keywords: [
|
||||
"vending suppliers Utah",
|
||||
"snack and beverage suppliers",
|
||||
"vending product selection",
|
||||
],
|
||||
theme: "suppliers",
|
||||
},
|
||||
warehouses: {
|
||||
path: "/warehouses",
|
||||
title: "Warehouse Vending Service in Utah | Rocky Mountain Vending",
|
||||
description:
|
||||
"Warehouse vending machine placement, restocking, and service for Utah distribution and industrial facilities.",
|
||||
keywords: ["warehouse vending Utah", "industrial vending machines Utah"],
|
||||
theme: "warehouse vending",
|
||||
},
|
||||
autoRepair: {
|
||||
path: "/auto-repair",
|
||||
title: "Auto Repair Shop Vending in Utah | Rocky Mountain Vending",
|
||||
description:
|
||||
"Vending machine placement and service for Utah auto repair shops, service centers, and waiting areas.",
|
||||
keywords: ["auto repair shop vending Utah", "garage vending machines Utah"],
|
||||
theme: "auto repair vending",
|
||||
},
|
||||
gyms: {
|
||||
path: "/gyms",
|
||||
title: "Gym Vending Machines in Utah | Rocky Mountain Vending",
|
||||
description:
|
||||
"Healthy snack, beverage, and gym vending placement for Utah fitness centers and training facilities.",
|
||||
keywords: ["gym vending Utah", "fitness center vending machines"],
|
||||
theme: "gym vending",
|
||||
},
|
||||
communityCenters: {
|
||||
path: "/community-centers",
|
||||
title: "Community Center Vending in Utah | Rocky Mountain Vending",
|
||||
description:
|
||||
"Community center vending machine placement and service for Utah facilities serving families, staff, and visitors.",
|
||||
keywords: ["community center vending Utah"],
|
||||
theme: "community center vending",
|
||||
},
|
||||
danceStudios: {
|
||||
path: "/dance-studios",
|
||||
title: "Dance Studio Vending in Utah | Rocky Mountain Vending",
|
||||
description:
|
||||
"Vending machine placement and snack options for Utah dance studios, training spaces, and performance venues.",
|
||||
keywords: ["dance studio vending Utah"],
|
||||
theme: "dance studio vending",
|
||||
},
|
||||
carWashes: {
|
||||
path: "/car-washes",
|
||||
title: "Car Wash Vending in Utah | Rocky Mountain Vending",
|
||||
description:
|
||||
"Vending machine placement and service for Utah car washes, including customer-facing snacks and beverages.",
|
||||
keywords: ["car wash vending Utah"],
|
||||
theme: "car wash vending",
|
||||
},
|
||||
faqs: {
|
||||
path: "/about/faqs",
|
||||
title: "Rocky Mountain Vending FAQs | Utah Service Questions",
|
||||
description:
|
||||
"Answers to common questions about Rocky Mountain Vending's free placement, repairs, moving, and service-area coverage in Utah.",
|
||||
keywords: ["Rocky Mountain Vending FAQ", "Utah vending service questions"],
|
||||
theme: "faq",
|
||||
},
|
||||
privacyPolicy: {
|
||||
path: "/privacy-policy",
|
||||
title: "Privacy Policy | Rocky Mountain Vending",
|
||||
description:
|
||||
"Read the Rocky Mountain Vending privacy policy for website visitors, leads, and customers.",
|
||||
keywords: ["Rocky Mountain Vending privacy policy"],
|
||||
theme: "privacy policy",
|
||||
},
|
||||
terms: {
|
||||
path: "/terms-and-conditions",
|
||||
title: "Terms and Conditions | Rocky Mountain Vending",
|
||||
description:
|
||||
"Read the Rocky Mountain Vending terms and conditions for site usage, messages, and service requests.",
|
||||
keywords: ["Rocky Mountain Vending terms and conditions"],
|
||||
theme: "terms",
|
||||
},
|
||||
}
|
||||
|
||||
export function buildAbsoluteUrl(path: string): string {
|
||||
const normalizedPath = path === "/" ? "" : path.replace(/\/+$/, "")
|
||||
return `${businessConfig.website}${normalizedPath}`
|
||||
}
|
||||
|
||||
export function getSeoPageDefinition(key: StaticSeoPageKey) {
|
||||
return seoPageRegistry[key]
|
||||
}
|
||||
|
||||
export function buildLocationRoute(locationSlug: string): string {
|
||||
return `/vending-machines-${locationSlug}`
|
||||
}
|
||||
187
lib/seo.ts
187
lib/seo.ts
|
|
@ -1,143 +1,162 @@
|
|||
import type { Metadata } from 'next';
|
||||
import type { Metadata } from "next"
|
||||
import { businessConfig } from "@/lib/seo-config"
|
||||
import { buildAbsoluteUrl, ogImagePath } from "@/lib/seo-registry"
|
||||
|
||||
/**
|
||||
* Clean HTML entities from text (e.g., & -> &, " -> ")
|
||||
*/
|
||||
function cleanHtmlEntities(text: string): string {
|
||||
if (!text) return '';
|
||||
if (!text) return ""
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/ /g, ' ')
|
||||
.trim();
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/ /g, " ")
|
||||
.trim()
|
||||
}
|
||||
|
||||
export interface SEOData {
|
||||
title: string;
|
||||
description: string;
|
||||
excerpt?: string;
|
||||
date?: string;
|
||||
modified?: string;
|
||||
image?: string;
|
||||
title: string
|
||||
description: string
|
||||
excerpt?: string
|
||||
date?: string
|
||||
modified?: string
|
||||
image?: string
|
||||
robots?: Metadata["robots"]
|
||||
path?: string
|
||||
keywords?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SEO metadata for a WordPress page/post
|
||||
*/
|
||||
export function generateSEOMetadata(data: SEOData): Metadata {
|
||||
const { title, description, excerpt, date, modified, image } = data;
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
excerpt,
|
||||
date,
|
||||
modified,
|
||||
image,
|
||||
robots,
|
||||
path,
|
||||
keywords,
|
||||
} = data
|
||||
|
||||
// Clean title (max 60 chars for SEO)
|
||||
const cleanTitle = cleanHtmlEntities(title);
|
||||
const seoTitle = cleanTitle.length > 60
|
||||
? cleanTitle.substring(0, 57) + '...'
|
||||
: cleanTitle;
|
||||
|
||||
const fullTitle = `${seoTitle} | Rocky Mountain Vending`;
|
||||
const cleanTitle = cleanHtmlEntities(title)
|
||||
const fullTitle = cleanTitle.includes(`| ${businessConfig.name}`)
|
||||
? cleanTitle
|
||||
: `${cleanTitle} | ${businessConfig.name}`
|
||||
let seoDescription = cleanHtmlEntities(description || excerpt || "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
|
||||
// Clean description (150-160 chars)
|
||||
let seoDescription = description || excerpt || '';
|
||||
if (seoDescription.length > 160) {
|
||||
seoDescription = seoDescription.substring(0, 157) + '...';
|
||||
} else if (seoDescription.length < 120) {
|
||||
seoDescription = seoDescription || 'Rocky Mountain Vending provides quality vending machine services in Utah.';
|
||||
if (!seoDescription) {
|
||||
seoDescription =
|
||||
"Rocky Mountain Vending provides vending machine placement, service, repairs, and support across Utah."
|
||||
} else if (seoDescription.length > 165) {
|
||||
seoDescription = `${seoDescription.slice(0, 162).trim()}...`
|
||||
}
|
||||
|
||||
// Default image
|
||||
const ogImage = image || '/images/rocky-mountain-vending-service-area-926x1024.webp';
|
||||
const ogImage = image || ogImagePath
|
||||
const canonicalUrl = path ? buildAbsoluteUrl(path) : undefined
|
||||
|
||||
const openGraph: NonNullable<Metadata["openGraph"]> = {
|
||||
title: fullTitle,
|
||||
description: seoDescription,
|
||||
type: date || modified ? "article" : "website",
|
||||
...(canonicalUrl ? { url: canonicalUrl } : {}),
|
||||
images: [
|
||||
{
|
||||
url: ogImage,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: cleanTitle,
|
||||
},
|
||||
],
|
||||
siteName: businessConfig.name,
|
||||
...(date && { publishedTime: date }),
|
||||
...(modified && { modifiedTime: modified }),
|
||||
}
|
||||
|
||||
const metadata: Metadata = {
|
||||
title: fullTitle,
|
||||
description: seoDescription,
|
||||
openGraph: {
|
||||
title: fullTitle,
|
||||
description: seoDescription,
|
||||
type: 'website',
|
||||
images: [
|
||||
{
|
||||
url: ogImage,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: cleanTitle,
|
||||
},
|
||||
],
|
||||
siteName: 'Rocky Mountain Vending',
|
||||
},
|
||||
keywords,
|
||||
openGraph,
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
card: "summary_large_image",
|
||||
title: fullTitle,
|
||||
description: seoDescription,
|
||||
images: [ogImage],
|
||||
},
|
||||
};
|
||||
|
||||
// Add dates if available (via openGraph)
|
||||
if (date || modified) {
|
||||
metadata.openGraph = {
|
||||
...metadata.openGraph,
|
||||
...(date && { publishedTime: date }),
|
||||
...(modified && { modifiedTime: modified }),
|
||||
};
|
||||
alternates: canonicalUrl ? { canonical: canonicalUrl } : undefined,
|
||||
robots,
|
||||
}
|
||||
|
||||
return metadata;
|
||||
return metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate structured data (JSON-LD) for a page
|
||||
*/
|
||||
export function generateStructuredData(data: {
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
datePublished?: string;
|
||||
dateModified?: string;
|
||||
type?: 'Article' | 'WebPage';
|
||||
title: string
|
||||
description: string
|
||||
url: string
|
||||
datePublished?: string
|
||||
dateModified?: string
|
||||
type?: "Article" | "WebPage"
|
||||
}) {
|
||||
const { title, description, url, datePublished, dateModified, type = 'WebPage' } = data;
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
url,
|
||||
datePublished,
|
||||
dateModified,
|
||||
type = "WebPage",
|
||||
} = data
|
||||
|
||||
const structuredData: any = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': type,
|
||||
const structuredData: Record<string, unknown> = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": type,
|
||||
headline: cleanHtmlEntities(title),
|
||||
description: cleanHtmlEntities(description),
|
||||
url: url,
|
||||
};
|
||||
}
|
||||
|
||||
if (datePublished) {
|
||||
structuredData.datePublished = datePublished;
|
||||
structuredData.datePublished = datePublished
|
||||
}
|
||||
if (dateModified) {
|
||||
structuredData.dateModified = dateModified;
|
||||
structuredData.dateModified = dateModified
|
||||
}
|
||||
|
||||
if (type === 'Article') {
|
||||
if (type === "Article") {
|
||||
structuredData.author = {
|
||||
'@type': 'Organization',
|
||||
name: 'Rocky Mountain Vending',
|
||||
url: 'https://rockymountainvending.com',
|
||||
};
|
||||
"@type": "Organization",
|
||||
name: businessConfig.name,
|
||||
url: businessConfig.website,
|
||||
}
|
||||
structuredData.publisher = {
|
||||
'@type': 'Organization',
|
||||
name: 'Rocky Mountain Vending',
|
||||
legalName: 'Rocky Mountain Vending LLC',
|
||||
url: 'https://rockymountainvending.com',
|
||||
"@type": "Organization",
|
||||
name: businessConfig.name,
|
||||
legalName: businessConfig.legalName,
|
||||
url: businessConfig.website,
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: 'https://rockymountainvending.com/rmv-logo.png',
|
||||
"@type": "ImageObject",
|
||||
url: `${businessConfig.website}/rmv-logo.png`,
|
||||
width: 180,
|
||||
height: 45,
|
||||
},
|
||||
};
|
||||
}
|
||||
structuredData.mainEntityOfPage = {
|
||||
'@type': 'WebPage',
|
||||
'@id': url,
|
||||
};
|
||||
"@type": "WebPage",
|
||||
"@id": url,
|
||||
}
|
||||
}
|
||||
|
||||
return structuredData;
|
||||
return structuredData
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue