DEV Community

Hello Nakama
Hello Nakama

Posted on

Store Locators That Crawlers Can Actually Read

If your store locator only renders locations after JavaScript runs, many crawlers will see an empty page. The fix is architectural: give every location its own server-rendered URL, link to all of them from plain HTML, and treat the interactive map as an enhancement on top. The widget is for humans. The static pages are for everything else.

This post covers the failure mode, a URL structure that works, and how to generate the pages without hand-maintaining hundreds of files.

What is wrong with a typical locator?

A common locator is one route, /locations, that loads an empty shell, calls an API, and draws pins and a list client-side. The search box filters in the browser. There are no individual location URLs, or they exist only as hash fragments.

For a person with a modern browser, it works. For a crawler, the page is a script tag and a spinner.

Do crawlers really not run JavaScript?

Some do, some do not, and the ones that do are slower about it. Googlebot renders JavaScript, but rendering can happen later than the initial crawl. Vercel published an analysis in late 2024 of AI crawler traffic across its network and found that the major AI crawlers it measured, including OpenAI's and Anthropic's, fetched JavaScript files but did not execute them.

So if you want ChatGPT, Claude, or Perplexity to know your store hours, those hours need to be in the HTML response.

What URL structure works best?

One indexable URL per location, organized in a shallow hierarchy:

/locations/                          all regions
/locations/texas/                    state or region
/locations/texas/austin/             city, lists stores
/locations/texas/austin/south-congress/   one store
Enter fullscreen mode Exit fullscreen mode

Each store page should include, in server-rendered HTML:

  • Business name, full address, and phone number.
  • Hours, including a visible note for holiday hours.
  • Services or departments specific to that store.
  • A static map image or link, not only an embedded interactive map.
  • LocalBusiness JSON-LD that matches the visible content.

The region and city pages are crawl paths, so link to every child with a normal <a href>. Do not rely on a sitemap alone.

How do you generate the pages?

Treat location data as content with one source of truth, and build pages from it. With a framework that supports static generation or server rendering, this is a small amount of code. A Next.js example using the App Router:

// app/locations/[state]/[city]/[store]/page.tsx
import { getAllStores, getStore } from "@/lib/locations";

export async function generateStaticParams() {
  const stores = await getAllStores();
  return stores.map((s) => ({ state: s.state, city: s.city, store: s.slug }));
}

export const revalidate = 3600; // refresh hours hourly

export default async function StorePage({ params }) {
  const { store: slug } = await params; // params is async in Next.js 15
  const store = await getStore(slug);
  return (
    <main>
      <h1>{store.name}</h1>
      <address>{store.street}, {store.city}, {store.region} {store.postalCode}</address>
      <a href={`tel:${store.phone}`}>{store.phoneDisplay}</a>
      <ul>
        {store.hours.map((h) => <li key={h.day}>{h.day}: {h.open} to {h.close}</li>)}
      </ul>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Revalidation matters for hours. A statically built page that never refreshes will eventually show last year's holiday schedule.

Where should location data live?

In one system that feeds both your listings and your pages. The failure you want to avoid is a website that says 9 to 6 while Google says 8 to 5, because someone updated one and not the other.

Options, with their tradeoffs:

Approach Strength Weakness
Headless CMS Full control, fits your stack You build the sync to listings yourself
Yext Pages Tight link to Yext's listing data Tied to Yext's platform and pricing
Synup locator and pages Location pages and listings share one data source Less flexible than a fully custom build
Custom database plus scripts Cheapest at small scale Maintenance grows with every location

Synup and similar platforms also expose APIs, so a common pattern is to keep the platform as the source of truth and pull data into your own static build. That keeps full control of the front end without a second copy of hours to maintain.

How do you check that it works?

Fetch a store page the way a non-rendering crawler would:

curl -s https://example.com/locations/texas/austin/south-congress/ | grep -i "south congress"
Enter fullscreen mode Exit fullscreen mode

If the address and hours are not in that raw response, a crawler that skips JavaScript will not see them either. Also run a sample of store URLs through Google's URL Inspection tool in Search Console to confirm Google renders and indexes them.

FAQ

Can I keep my interactive map?

Yes. Render it on top of the static list. Just make sure the list and links exist without it.

Is a sitemap of location URLs enough?

It helps discovery, but internal links from region and city pages are what give crawlers a clear path and context.

Should closed stores keep their pages?

Redirect them to the nearest open location or the city page, so old links and bookmarks still land somewhere useful.

How many locations before this matters?

Even a few. A single JavaScript-only locator page can hide every location's hours from crawlers that skip scripts.

Top comments (0)