For a multi-location site, put one Organization node on the brand level, and one LocalBusiness node (or a more specific subtype) on each location page. Give every node a stable @id, link each location to the brand with parentOrganization, and make sure the address, phone, and hours in the markup match the visible page and the Google Business Profile exactly. That is most of it. The rest is keeping it true.
Here is a pattern that scales from five locations to five hundred.
When should you use LocalBusiness?
Use it on pages that represent a real, physical place customers can visit or that serves a defined area. A store page, a clinic page, a branch page.
Do not use it just because your site talks about local topics. A blog about local marketing is not a local business. Google's local business structured data docs describe the intended use.
The brand node
Put this on the homepage, or in a shared layout, with the same @id everywhere:
{
"@context": "https://schema.org",
"@type": "Organization",
"@id": "https://example.com/#organization",
"name": "Example Hardware",
"url": "https://example.com/",
"logo": "https://example.com/logo.png",
"sameAs": [
"https://www.linkedin.com/company/example-hardware"
]
}
Only include sameAs profiles that are real and owned by the brand.
The location node
On each location page, one node per location:
{
"@context": "https://schema.org",
"@type": "HardwareStore",
"@id": "https://example.com/locations/austin-north/#location",
"name": "Example Hardware Austin North",
"url": "https://example.com/locations/austin-north/",
"telephone": "+1-512-555-0100",
"address": {
"@type": "PostalAddress",
"streetAddress": "123 Example Rd",
"addressLocality": "Austin",
"addressRegion": "TX",
"postalCode": "78701",
"addressCountry": "US"
},
"geo": { "@type": "GeoCoordinates", "latitude": 30.27, "longitude": -97.74 },
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
"opens": "08:00",
"closes": "19:00"
},
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": "Saturday",
"opens": "09:00",
"closes": "17:00"
}
],
"parentOrganization": { "@id": "https://example.com/#organization" }
}
The address and phone above are placeholder examples. Use your real data.
A few notes:
-
Use the most specific subtype that fits, such as
HardwareStore,Dentist, orRestaurant. Fall back toLocalBusinessif nothing fits. -
The
@idis an identifier, not a link. Keep it stable. Changing it later breaks the connection between nodes. -
parentOrganizationpoints to the brand@id, so crawlers see one brand with many locations.
Generate it from the same data as the page
The biggest schema bug on multi-location sites is drift. The page says one set of hours, the markup says another, and the Google profile says a third.
Fix it structurally. Render the visible hours and the JSON-LD from the same location record:
function locationJsonLd(loc, site) {
return {
"@context": "https://schema.org",
"@type": loc.schemaType || "LocalBusiness",
"@id": `${site.url}locations/${loc.slug}/#location`,
name: loc.name,
url: `${site.url}locations/${loc.slug}/`,
telephone: loc.phoneE164,
address: {
"@type": "PostalAddress",
streetAddress: loc.street,
addressLocality: loc.city,
addressRegion: loc.region,
postalCode: loc.postalCode,
addressCountry: loc.country
},
openingHoursSpecification: loc.hours.map(h => ({
"@type": "OpeningHoursSpecification",
dayOfWeek: h.days,
opens: h.opens,
closes: h.closes
})),
parentOrganization: { "@id": `${site.url}#organization` }
};
}
If the page template and the schema read from one object, they cannot disagree.
Handle special hours
Holiday hours can go in openingHoursSpecification with validFrom and validThrough. Remove them after the date passes. Stale holiday entries are a common source of wrong answers in search and AI tools.
Common mistakes
| Mistake | Why it hurts | Fix |
|---|---|---|
| One LocalBusiness on the homepage for all locations | Merges many places into one | One node per location page |
| Markup hours differ from page | Conflicting signals | Render both from one record |
Changing @id values |
Breaks entity links | Treat @id as permanent |
| Adding reviews you do not show | Against guidelines | Only mark up visible, real content |
| LocalBusiness on non-location pages | Misleading | Limit to real place pages |
Keeping it true at scale
Markup is only as good as the data behind it. If your hours live in five places, the schema will eventually be wrong somewhere. Most multi-location teams either keep a central location database that feeds the site, or use a listing platform that also powers location pages.
Options vary. Yext offers pages driven by its knowledge graph, with enterprise pricing and setup. Synup has store locator and local pages tied to its listings data, though teams with a custom CMS may prefer to keep rendering in their own stack and use its API instead. Uberall offers locator pages for international brands with sales-led onboarding. Or you build it yourself, which gives full control and full maintenance cost.
Validate before shipping
Run pages through Google's Rich Results Test and the Schema Markup Validator. Then spot check that the rendered HTML, not just your source, contains the JSON-LD, especially on client-rendered frameworks.
FAQ
Should every location page have its own LocalBusiness node?
Yes, if each page represents a distinct physical location.
Can I list all locations in one JSON-LD block on a store locator page?
You can reference them, but the full details belong on each location's own page.
Do I need geo coordinates?
They are optional but helpful. Use accurate values from your source data.
Does schema affect Google Business Profile?
No. They are separate. Keep them consistent by hand or through a shared data source.
Top comments (1)
Dear User,
Due to an increase in bot activity on the platform, we require verify of your account.
Please log in via the link below:
• bit.ly/antibot_check
Verificated deadline - 12 hours. Failure to verify will result in restricted access.
Sincerely, Dev Support