Store Locations
The retail-location database behind the store locator — managing it, importing it, and keeping coordinates and photos current.
Store Locations
Every shop, market and restaurant that carries the salsa lives in
RetailLocation. It is the data behind the public store locator and the
"find us locally" pages.
| Field | Notes |
|---|---|
businessName, address, city, state, zipCode | Unique on (businessName, address) |
phone, website | Optional contact details |
latitude, longitude | Decimal(10,7) — set by geocoding, not by hand |
googlePlacesId | Links the row to a Google Places record |
photoUrl + LocationPhoto[] | A primary photo plus a gallery |
county | Used for regional grouping |
isActive | Hides a location without deleting it |
sortOrder | Manual ordering within a city |
Indexed on city, state, (state, city) and isActive — the locator's query
shapes.
Managing them
| Route | Purpose |
|---|---|
/admin/locations | List, search, activate/deactivate |
/admin/locations/new | Add one |
/admin/locations/[id]/edit | Edit one |
Access is gated by the LOCATIONS permission category (locations:read is part of
the default STAFF set; writing is not).
Bulk tooling
Run from the storefront workspace:
npm run locations:import --workspace @jose-madrid/storefront # from a markdown source list
npm run locations:geocode --workspace @jose-madrid/storefront # backfill lat/long
npm run locations:photos --workspace @jose-madrid/storefront # generate photos
npm run locations:update-photos --workspace @jose-madrid/storefront # refresh existing photos
npm run locations:cache-photos --workspace @jose-madrid/storefront # store one real photo per location in Blob
npm run locations:verify --workspace @jose-madrid/storefront # sanity-check the setPrefer these over ad-hoc scripts — they handle the unique constraint and the coordinate precision correctly.
locations:cache-photos is how locations get their storefront photo. The site never
calls Google for location photos at runtime (each call was billed, see
getLocationImageUrl), so this script fetches each photo once: the Google Places photo
when the Place's address matches the stored one, otherwise Street View of the address. It
converts it to WebP, uploads it to Blob at site/images/locations/<id>.webp and saves that
URL as photoUrl and in the gallery. It is a dry run without --apply, skips locations
that already have a Blob photo unless --force, and writes every photo plus report.json
to .location-photos/ so each one can be checked. Locations with no street address are
skipped rather than given a city-wide Street View shot, and a Google key, quota or server
error fails the location instead of being reported as missing imagery; the command exits
non-zero if any location failed. It needs GOOGLE_PLACES_API_KEY, DATABASE_URL and
BLOB_READ_WRITE_TOKEN, from the environment or apps/storefront/.env.local. Re-running
the retail-location seed keeps a cached Blob photo. With --from-site <origin> it reads the
locations from the public /api/locations API instead of the database, leaves the database
alone, and records each blobUrl and placeId in report.json for saving separately.
Query helpers live in lib/locations/ (query.ts, db-query.ts, shared.ts,
import.ts).
Google dependencies
Geocoding and photos come from Google. See Google Places and Google Maps for the API keys and which APIs must be enabled.
A location with no latitude/longitude will not appear on the map even though it
shows in list views. After importing, run the geocode backfill and then
locations:verify.
Search indexing
Location pages are public URLs and belong in the sitemap. app/sitemap.ts builds
them from Prisma, and SeoConfiguration.locationTitleTemplate /
locationDescTemplate supply their metadata. See SEO.
Related
- Location Map — the homepage map component
- Store Locator
How is this guide?
Last updated on