diff --git a/app/[...slug]/page.tsx b/app/[...slug]/page.tsx index 9909b9b6..bcbbd2ae 100644 --- a/app/[...slug]/page.tsx +++ b/app/[...slug]/page.tsx @@ -10,10 +10,18 @@ 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 { Breadcrumbs } from "@/components/breadcrumbs" +import { + PublicInset, + PublicPageHeader, + PublicProse, + PublicSurface, +} from "@/components/public-surface" import { generateLocationPageMetadata, LocationLandingPage, } from "@/components/location-landing-page" +import Link from "next/link" // Required for static export - ensures this route is statically generated export const dynamic = "force-static" @@ -48,6 +56,7 @@ const routeMapping: Record = { // Food & Beverage "food-and-beverage/healthy-options": "healthy-vending", + "food-and-beverage/snack-and-drink-delivery": "snack-and-drink-delivery", "food-and-beverage/traditional-options": "traditional-vending", "food-and-beverage/suppliers": "diverse-vending-options-with-rocky-mountain-vendings-exclusive-wholesale-accounts", @@ -348,6 +357,7 @@ export default async function WordPressPage({ params }: PageProps) { "vending-machines-for-your-car-wash", ] const isWhoWeServePage = whoWeServeSlugs.includes(pageSlug) + const routePath = `/${slugArray.join("/")}` return ( <> @@ -377,13 +387,58 @@ export default async function WordPressPage({ params }: PageProps) { pageSlug !== "contact-us" && pageSlug !== "about-us" && !isWhoWeServePage && ( -
-
-

- {page.title || "Page"} -

-
- {content} +
+ + + + {content} + + +
+
+

+ Need Help Choosing The Right Next Step? +

+

+ Talk with Rocky Mountain Vending +

+

+ Reach out about placement, machine sales, repairs, moving help, + manuals, or parts and we'll point you in the right direction. +

+
+
+ + Talk to Our Team + + + See If You Qualify + +
+
+
)} diff --git a/app/blog/abandoned-vending-machines/page.tsx b/app/blog/abandoned-vending-machines/page.tsx index 5a638a6b..0266b9fc 100644 --- a/app/blog/abandoned-vending-machines/page.tsx +++ b/app/blog/abandoned-vending-machines/page.tsx @@ -5,7 +5,14 @@ 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 { + PublicInset, + PublicPageHeader, + PublicProse, + PublicSurface, +} from "@/components/public-surface" import type { Metadata } from "next" +import Link from "next/link" const WORDPRESS_SLUG = "abandoned-vending-machines" @@ -81,17 +88,61 @@ export default async function AbandonedVendingMachinesPage() { type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} /> - -
- {content} +
+ + + + {content} + + +
+
+

+ Need Help With A Machine Situation? +

+

+ Get the right kind of support quickly +

+

+ Reach out if you need help with abandoned machines, service questions, + moving help, or figuring out the right next step for your location. +

+
+
+ + Talk to Our Team + + + Explore Repair Help + +
+
+
) diff --git a/components/dropdown-page-shell.tsx b/components/dropdown-page-shell.tsx index db9dc4be..3e107304 100644 --- a/components/dropdown-page-shell.tsx +++ b/components/dropdown-page-shell.tsx @@ -5,6 +5,7 @@ import { Breadcrumbs, type BreadcrumbItem } from "@/components/breadcrumbs" import { PublicInset, PublicPageHeader, + PublicProse, PublicSurface, } from "@/components/public-surface" import { cn } from "@/lib/utils" @@ -64,19 +65,19 @@ export function DropdownPageShell({ {contentIntro ? ( -
{contentIntro}
+
{contentIntro}
) : null} -
+
-
-
-
+
+
+

Location Guide @@ -87,15 +88,17 @@ export function DropdownPageShell({

-
{content}
+ + {content} +
- {sections ?
{sections}
: null} + {sections ?
{sections}
: null} {cta ? ( -
+
{cta.eyebrow ? ( diff --git a/components/location-landing-page.tsx b/components/location-landing-page.tsx index e185aff2..3a7c8e4f 100644 --- a/components/location-landing-page.tsx +++ b/components/location-landing-page.tsx @@ -114,11 +114,52 @@ export function LocationLandingPage({ }: { locationData: LocationData }) { + const isSaltLakeCity = locationData.slug === "salt-lake-city-utah" const countyName = getCountyName(locationData.slug) const industries = getIndustryFocus(locationData) const canonicalUrl = buildAbsoluteUrl(buildLocationRoute(locationData.slug)) const title = `Vending Machines in ${locationData.city}, ${locationData.stateAbbr}` const description = `Rocky Mountain Vending provides free placement for qualifying locations, machine sales, repairs, and vending service for businesses in ${locationData.city}, ${locationData.stateAbbr}.` + const comparisonRows = [ + ["Credit card readers", "Yes", "Maybe", "Yes"], + ["Locally owned", "Yes", "Yes", "Maybe"], + ["Fast service", "Yes", "Maybe", "Maybe"], + ["Large selection of products", "Yes", "Maybe", "Yes"], + ["Quality of equipment used", "Excellent", "Varies", "Excellent"], + ["Locked into Coke or Pepsi equipment", "No", "Maybe", "Probably"], + ] + const saltLakeServiceLinks = [ + { + title: "Traditional snacks and drinks", + body: "Stock the machine with the classic snacks, sodas, and convenience items most locations still want every day.", + href: "/food-and-beverage/traditional-options", + }, + { + title: "Healthy snacks and drinks", + body: "Offer protein bars, better-for-you snacks, and drink choices that fit health-conscious teams and customers.", + href: "/food-and-beverage/healthy-options", + }, + { + title: "Snack and drink delivery", + body: "Need product delivery or a broader refreshment setup beyond standard machine placement? We can help there too.", + href: "/food-and-beverage/snack-and-drink-delivery", + }, + { + title: "Vending machine sales", + body: "Compare purchase options if you want equipment ownership instead of a free-placement arrangement.", + href: "/vending-machines/machines-for-sale", + }, + { + title: "Parts, repairs, and moving", + body: "Get support for repair work, machine moving, replacement parts, and operational issues that need direct service help.", + href: "/services/parts", + }, + { + title: "Training and support", + body: "Browse the support pages and machine guides if you need help with specific models, manuals, or machine operation questions.", + href: "/blog", + }, + ] const structuredData = { "@context": "https://schema.org", @@ -188,15 +229,15 @@ export function LocationLandingPage({ -
+

- Vending service for businesses across {locationData.city} + A local vending partner for businesses across {locationData.city}

If your business is in {locationData.neighborhoods.join(", ")}, or @@ -219,11 +260,11 @@ export function LocationLandingPage({

Common business types we serve in {locationData.city}

-
+
{industries.map((industry) => ( {industry} @@ -236,6 +277,15 @@ export function LocationLandingPage({ Utah service area.

+
+ + + Talk to Our Team + +
@@ -243,7 +293,7 @@ export function LocationLandingPage({

Vending services available in {locationData.city}

-
+
{[ { title: "Free vending placement", @@ -270,7 +320,7 @@ export function LocationLandingPage({ cta: "View manuals and parts", }, ].map((service) => ( - +

{service.title}

{service.body} @@ -287,6 +337,95 @@ export function LocationLandingPage({

+ {isSaltLakeCity ? ( +
+ +

+ Why Rocky Mountain Vending +

+

+ What Salt Lake City businesses usually want to verify before they choose a vendor. +

+

+ Most businesses care about the same things: service speed, + product flexibility, local ownership, and whether the machines + feel modern and dependable after install. +

+
+
+ + + + + + + + + + + {comparisonRows.map((row, index) => ( + + + + + + + ))} + +
+ Comparison point + + Rocky Mountain Vending + + Small Vendor + + Large Vendor +
+ {row[0]} + + {row[1]} + + {row[2]} + + {row[3]} +
+
+
+
+ + +

+ Salt Lake City Services +

+

+ The service paths Salt Lake City businesses usually ask about first. +

+
+ {saltLakeServiceLinks.map((item) => ( + +

+ {item.title} +

+

+ {item.body} +

+ + Learn more + + +
+ ))} +
+
+
+ ) : null} +

@@ -405,7 +544,7 @@ export function LocationLandingPage({ vending help you need. We'll follow up with the next best option for your location.

-
+
([]) - const [cache, setCache] = useState(null) - const [isLoading, setIsLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - async function loadSuggestions() { - setIsLoading(true) - setError(null) - - try { - const query = [ - manual.manufacturer, - manual.category, - manual.commonNames?.[0], - manual.searchTerms?.[0], - "vending machine", - ] - .filter(Boolean) - .join(" ") - - const params = new URLSearchParams({ - keywords: query, - maxResults: "6", - sortOrder: "BestMatch", - }) - - const response = await fetch(`/api/ebay/search?${params.toString()}`) - const body = (await response.json().catch(() => null)) as - | ProductSuggestionsResponse - | null - - if (!response.ok || !body) { - throw new Error( - body && typeof body.error === "string" - ? body.error - : `Failed to load cached listings (${response.status})` - ) - } - - setSuggestions(Array.isArray(body.results) ? body.results : []) - setCache(body.cache || null) - setError(typeof body.error === "string" ? body.error : null) - } catch (err) { - console.error("Error loading product suggestions:", err) - setSuggestions([]) - setCache(null) - setError( - err instanceof Error ? err.message : "Could not load product suggestions" - ) - } finally { - setIsLoading(false) - } - } - - if (manual) { - loadSuggestions() - } - }, [manual]) - - if (isLoading) { - return ( -
-
- -
-
- ) - } - - if (error) { - return ( -
-
- - - {error} - - {cache?.lastSuccessfulAt ? ( - - Last refreshed {new Date(cache.lastSuccessfulAt).toLocaleString()} - - ) : null} -
-
- ) - } - - if (suggestions.length === 0) { - return ( -
-
- - - No cached eBay matches yet - - - {cache?.isStale - ? "The background poll is behind, so this manual is showing the last known cache." - : "Try again after the next periodic cache refresh."} - -
-
- ) - } - - return ( -
-
- -

- Related Products -

-
- {cache && ( -
- {cache.lastSuccessfulAt - ? `Cache refreshed ${new Date(cache.lastSuccessfulAt).toLocaleString()}` - : "Cache is warming up"} - {cache.isStale ? " • stale cache" : ""} -
- )} - - -
- ) -} interface ManualsPageClientProps { manuals: Manual[] @@ -272,7 +65,7 @@ function ManualCard({ const thumbnailUrl = getThumbnailUrl(manual) return ( - + {thumbnailUrl && (
)} - + {manual.filename.replace(/\.pdf$/i, "")} {manual.commonNames && manual.commonNames.length > 0 && (
- {manual.commonNames.map((name, index) => ( + {manual.commonNames.slice(0, 3).map((name, index) => ( ))} + {manual.commonNames.length > 3 && ( + + +{manual.commonNames.length - 3} more + + )}
)} {manual.searchTerms && manual.searchTerms.length > 0 && !manual.commonNames && (
- {manual.searchTerms.map((term, index) => ( + {manual.searchTerms.slice(0, 4).map((term, index) => ( ))} + {manual.searchTerms.length > 4 && ( + + +{manual.searchTerms.length - 4} more + + )}
)}
@@ -337,7 +146,7 @@ function ManualCard({ )}
-
+
{showManufacturer && (

Manufacturer: {manual.manufacturer} @@ -681,9 +490,28 @@ export function ManualsPageClient({ return (

{/* Search and Filter Controls */} - - -
+ + +
+
+
+

+ Start With Search +

+

+ Find the manual first, then narrow it down +

+

+ Search by model, manufacturer, or category. Use filters if + you already know the brand or want manuals with parts. +

+
+ + Showing {filteredManuals.length} of{" "} + {manuals.length} manuals + +
+ {/* Search Bar */}
@@ -697,12 +525,14 @@ export function ManualsPageClient({
{/* Filters Row */} -
+
+
Filters:
+