Most articles about local business schema markup stop at “add your name, address and phone number”. That is not enough if you are the person who actually has to ship the code. This guide is written for developers and technical SEOs: every property is explained with a copy-ready JSON-LD snippet, followed by placement rules, multi-location patterns, and a validation workflow you can run before the deploy.
What local business schema markup actually does
Local business schema markup is a block of structured data, written in JSON-LD, that tells search engines and AI answer engines what your business is, where it is located, when it is open, and how to contact it. It uses the LocalBusiness type from Schema.org, which covers restaurants, bank branches, medical practices, gyms, dealerships, bowling alleys and hundreds of other subtypes.
Concretely, well formed markup helps with three things:
- Entity clarity: Google can connect your website, your Business Profile and your citations to one single entity instead of guessing.
- Rich result eligibility: knowledge panel details, business hours, and in some verticals action links or menus.
- AI and assistant visibility: LLM based search surfaces parse structured data far more reliably than a styled address block in a footer.
Reality check: schema markup is not a ranking factor on its own. It is a machine readable description of facts that already exist on the page. If the data in your JSON-LD does not match the visible content, you get errors, not rankings.

Step 1: pick the most specific @type
Never default to the generic LocalBusiness if a subtype fits. A more specific type unlocks extra properties and gives search engines a stronger signal.
| Business | Recommended @type | Extra useful properties |
|---|---|---|
| Restaurant, cafe, food truck | Restaurant, CafeOrCoffeeShop, FoodEstablishment |
servesCuisine, hasMenu, acceptsReservations |
| Dentist, clinic, physio | Dentist, MedicalClinic, Physician |
medicalSpecialty, availableService |
| Law firm, accountant, agency | LegalService, AccountingService, ProfessionalService |
areaServed, hasOfferCatalog |
| Plumber, electrician, roofer | HomeAndConstructionBusiness, Plumber, Electrician |
areaServed, serviceArea |
| Store, dealership, salon | Store, AutoDealer, HairSalon |
currenciesAccepted, paymentAccepted |
| Hotel, B&B, campsite | Hotel, LodgingBusiness |
checkinTime, amenityFeature, starRating |
If none fits exactly, you can declare an array of types, for example "@type": ["LocalBusiness", "Organization"], which keeps you eligible for organization level features such as logo and sameAs. A fuller account is out there.
Step 2: the minimum viable snippet
Google requires very little to parse a local business entity: a type, a name and a postal address. Start here, then add layers.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "CafeOrCoffeeShop",
"@id": "https://example.com/#business",
"name": "Nordic Roast Coffee",
"url": "https://example.com/",
"telephone": "+13125551212",
"address": {
"@type": "PostalAddress",
"streetAddress": "148 W Kinzie St",
"addressLocality": "Chicago",
"addressRegion": "IL",
"postalCode": "60654",
"addressCountry": "US"
}
}
</script>
Three details that break more implementations than anything else:
- Always set a stable
@id. Use an absolute URL with a fragment. It is the anchor other schema nodes will reference. addressCountrymust be a two letter ISO 3166-1 alpha-2 code, not “United States”.telephoneshould be in international format with the country prefix, no spaces needed.

Step 3: field by field reference with code
Required and recommended properties at a glance
| Property | Status | Expected value |
|---|---|---|
@type |
Required | LocalBusiness or a subtype |
name |
Required | Exact business name, no keyword stuffing |
address |
Required | Nested PostalAddress object |
image |
Strongly recommended | Absolute URLs, multiple aspect ratios |
telephone |
Recommended | +countrycode number |
geo |
Recommended | GeoCoordinates, 5+ decimals |
openingHoursSpecification |
Recommended | Array of specification objects |
priceRange |
Recommended | Short string such as $$ or “$10-$50” |
url |
Recommended | Canonical URL of the location page |
sameAs |
Recommended | Array of official profile URLs |
aggregateRating / review |
Optional, conditional | Only for reviews visible on the page |
department |
Optional | Nested businesses at the same address |
areaServed |
Optional | Cities or regions for service businesses |
address: PostalAddress done correctly
"address": {
"@type": "PostalAddress",
"streetAddress": "148 W Kinzie St, Suite 300",
"addressLocality": "Chicago",
"addressRegion": "IL",
"postalCode": "60654",
"addressCountry": "US"
}
Put the suite or floor inside streetAddress. Do not repeat the city inside streetAddress, and never put the full one line address into a single field if you can split it. Anyone digging further should read Local Business Schema Markup for Better SEO 2026.
geo: coordinates and map link
"geo": {
"@type": "GeoCoordinates",
"latitude": 41.889370,
"longitude": -87.635260
},
"hasMap": "https://maps.google.com/?cid=1234567890123456789"
Use numbers, not strings, and at least five decimal places so the pin lands on the entrance rather than the block. Pull the coordinates from your own Business Profile listing so they match.
openingHoursSpecification: the four patterns you need
Schema.org accepts the shorthand openingHours string, but the specification object is far more expressive and easier to generate from a database.
1. Standard weekly hours
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "07:30",
"closes": "18:00"
},
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": "Saturday",
"opens": "09:00",
"closes": "14:00"
}
]
Days not listed are treated as closed. Times use 24 hour HH:MM format in the local time of the business.
2. Split shifts (lunch break)
[
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": "Wednesday",
"opens": "09:00",
"closes": "12:30"
},
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": "Wednesday",
"opens": "14:00",
"closes": "19:00"
}
]
3. Open 24 hours
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Saturday", "Sunday"],
"opens": "00:00",
"closes": "23:59"
}
4. Holiday closures and seasonal hours
"specialOpeningHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"opens": "00:00",
"closes": "00:00",
"validFrom": "2026-12-25",
"validThrough": "2026-12-25"
},
{
"@type": "OpeningHoursSpecification",
"opens": "10:00",
"closes": "16:00",
"validFrom": "2026-12-31",
"validThrough": "2027-01-02"
}
]
Setting opens and closes to the same value marks the day as closed. Automate these entries from your CMS so they never go stale, and remove past ranges during your next release.
priceRange: keep it short
"priceRange": "$$",
"currenciesAccepted": "USD",
"paymentAccepted": "Cash, Credit Card, Apple Pay, Invoice"
Google expects a compact value. Use one to four currency symbols, or a range like "$15-$40". Long sentences inside priceRange are a common warning in the Rich Results Test.
image and logo
"image": [
"https://example.com/img/storefront-1x1.jpg",
"https://example.com/img/storefront-4x3.jpg",
"https://example.com/img/storefront-16x9.jpg"
],
"logo": "https://example.com/img/logo.png"
Use absolute URLs, images that are crawlable (not blocked in robots.txt), and at least 720 px on the shortest side.
aggregateRating and review: read this before you copy
This is the property that gets sites into trouble. Google ignores self serving reviews for LocalBusiness pages, meaning star ratings that a business publishes about itself on its own site are generally not eligible for review rich results. The markup is still valid and still useful for entity understanding, but only add it when the ratings and reviews are genuinely displayed to users on that same page.
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.8",
"bestRating": "5",
"worstRating": "1",
"reviewCount": "327"
},
"review": [
{
"@type": "Review",
"author": { "@type": "Person", "name": "Maria L." },
"datePublished": "2026-08-04",
"reviewRating": {
"@type": "Rating",
"ratingValue": "5",
"bestRating": "5"
},
"reviewBody": "Fast service and the best flat white in River North."
}
]
Use a dot as the decimal separator even on non English sites. A comma will throw a parsing error.
sameAs, areaServed and services
"sameAs": [
"https://www.facebook.com/nordicroast",
"https://www.instagram.com/nordicroast",
"https://www.linkedin.com/company/nordicroast"
],
"areaServed": [
{ "@type": "City", "name": "Chicago" },
{ "@type": "City", "name": "Evanston" }
],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Services",
"itemListElement": [
{
"@type": "Offer",
"itemOffered": { "@type": "Service", "name": "Wholesale coffee supply" }
},
{
"@type": "Offer",
"itemOffered": { "@type": "Service", "name": "Barista training workshops" }
}
]
}
For service area businesses without a storefront open to customers, keep address but add areaServed, and consider "publicAccess": false.
department: several businesses at one address
"department": [
{
"@type": "AutoPartsStore",
"name": "Nordic Auto Parts Counter",
"telephone": "+13125551213",
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "08:00",
"closes": "17:00"
}
]
}
]
A department inherits the parent address unless you override it, which makes it perfect for a pharmacy inside a supermarket or a service desk with different hours.
Step 4: the complete production ready snippet
Here is everything assembled into one block, with a @graph so the business, the website and the page are linked as a coherent entity set.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "CafeOrCoffeeShop",
"@id": "https://example.com/locations/chicago/#business",
"name": "Nordic Roast Coffee Chicago",
"alternateName": "Nordic Roast River North",
"description": "Specialty coffee roaster and cafe in River North, Chicago.",
"url": "https://example.com/locations/chicago/",
"telephone": "+13125551212",
"email": "[email protected]",
"priceRange": "$$",
"currenciesAccepted": "USD",
"paymentAccepted": "Cash, Credit Card, Apple Pay",
"image": [
"https://example.com/img/chicago-1x1.jpg",
"https://example.com/img/chicago-16x9.jpg"
],
"logo": "https://example.com/img/logo.png",
"address": {
"@type": "PostalAddress",
"streetAddress": "148 W Kinzie St",
"addressLocality": "Chicago",
"addressRegion": "IL",
"postalCode": "60654",
"addressCountry": "US"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 41.889370,
"longitude": -87.635260
},
"hasMap": "https://maps.google.com/?cid=1234567890123456789",
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "07:00",
"closes": "18:00"
},
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Saturday", "Sunday"],
"opens": "08:00",
"closes": "15:00"
}
],
"specialOpeningHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"opens": "00:00",
"closes": "00:00",
"validFrom": "2026-12-25",
"validThrough": "2026-12-25"
}
],
"servesCuisine": "Coffee",
"acceptsReservations": "False",
"sameAs": [
"https://www.facebook.com/nordicroast",
"https://www.instagram.com/nordicroast"
],
"parentOrganization": { "@id": "https://example.com/#organization" },
"isPartOf": { "@id": "https://example.com/#website" }
},
{
"@type": "Organization",
"@id": "https://example.com/#organization",
"name": "Nordic Roast Coffee",
"url": "https://example.com/",
"logo": "https://example.com/img/logo.png"
},
{
"@type": "WebSite",
"@id": "https://example.com/#website",
"url": "https://example.com/",
"name": "Nordic Roast Coffee",
"publisher": { "@id": "https://example.com/#organization" }
}
]
}
</script>

Step 5: where to place the script
- Head or body both work. Google parses
application/ld+jsonanywhere in the document. Placing it in<head>keeps it out of the way of content edits. - Server rendered beats client rendered. Google can execute JavaScript, but injected markup depends on the render queue. If you inject via Google Tag Manager or a client side framework, verify the rendered HTML in Search Console URL Inspection.
- One canonical block per entity. Do not let a theme, a plugin and a hardcoded snippet all output a LocalBusiness node. Duplicated conflicting entities are the number one cause of “unparsable structured data” tickets.
- Never escape HTML entities inside JSON-LD. Values like
&should be written as a plain ampersand. Many CMS editors do this silently, so inspect the live source, not the template. - WordPress note: if you use an SEO plugin that already outputs a schema graph, extend it with its filters rather than adding a second script. The result is one merged
@graphwith consistent@idvalues.
Step 6: multi-location architecture
This is where most implementations fall apart. The rule is simple: one location, one URL, one LocalBusiness node.
- Create a dedicated, indexable page per location, for example
/locations/chicago/, with unique content, NAP details, hours and an embedded map. - Give each location a unique
@idbased on that page URL plus a fragment. - Create a single
Organizationnode on the site, referenced by every location throughparentOrganizationorbranchOf. - On the locations index page, you may list all branches, but keep the detailed markup on each individual page so hours and ratings stay location specific.
- Keep the JSON-LD generated from the same database that feeds the visible content, so a hours change in the CMS updates both at once.
{
"@context": "https://schema.org",
"@type": "Store",
"@id": "https://example.com/locations/austin/#business",
"name": "Nordic Roast Coffee Austin",
"branchCode": "TX-002",
"url": "https://example.com/locations/austin/",
"branchOf": {
"@type": "Organization",
"@id": "https://example.com/#organization",
"name": "Nordic Roast Coffee",
"url": "https://example.com/"
},
"address": {
"@type": "PostalAddress",
"streetAddress": "1100 E 5th St",
"addressLocality": "Austin",
"addressRegion": "TX",
"postalCode": "78702",
"addressCountry": "US"
}
}
| Scenario | Pattern to use |
|---|---|
| Chain with 40 branches | One page and one node per branch, all linked to a single Organization |
| Two businesses, same building | department inside the parent node |
| Franchise with separate domains | Independent nodes, connected with parentOrganization pointing to the franchisor URL |
| Mobile or service area business | Single node with areaServed and "publicAccess": false |

Step 7: validate before you deploy
Run this sequence on a staging URL or by pasting the rendered HTML. It takes five minutes and prevents the classic “we shipped broken schema to 300 pages” incident.
- Schema Markup Validator at
validator.schema.org: checks pure Schema.org syntax and vocabulary. Use it first, it flags misspelled properties that Google silently ignores. - Google Rich Results Test at
search.google.com/test/rich-results: tells you what Google actually detects and whether the page is eligible for enhancements. Test both the live URL and the code snippet. - Rendered HTML check: in the Rich Results Test, open the rendered HTML tab and search for
ld+json. If it is missing there but present in your template, your script is being stripped or injected too late. - Command line spot check:
curl -s https://example.com/locations/chicago/ | grep -A5 "ld+json"confirms the markup is in the raw server response. - Search Console: after deployment, monitor the enhancement reports and the URL Inspection tool for warnings across the whole template, not just the one page you tested.
- Consistency audit: compare name, address, phone and hours in the markup against your Google Business Profile and your main citations. Mismatches weaken the entity association.
Errors we see most often
| Mistake | Fix |
|---|---|
Keyword stuffed name such as “Best Plumber Chicago Cheap 24h” |
Use the real business name only |
| Hours in 12 hour format (“7:00 AM”) | Use 24 hour HH:MM |
| Markup data not visible on the page | Display hours, address and ratings in the HTML too |
Same @id reused on every location page |
Derive @id from the canonical URL |
| Relative image or URL paths | Always absolute, with https |
| Trailing commas or comments in the JSON | JSON-LD is strict JSON, lint it in your build |
| LocalBusiness added on every page of the site | Keep the full node on home and location pages, reference by @id elsewhere |
Deployment checklist
- Most specific
@typeselected - Unique
@idper location, absolute URL with fragment - Address split into proper PostalAddress fields, ISO country code
- Geo coordinates as numbers, matching the Business Profile pin
- Opening hours generated from the CMS, holidays scheduled ahead
- priceRange short, currencies and payment methods filled where relevant
- Ratings only where reviews are visible on the page
- sameAs pointing to official profiles you control
- No duplicate LocalBusiness nodes from plugins or tag manager
- Passed both validators, present in rendered HTML, monitored in Search Console
FAQ
Can you give me an example of a local business schema?
Yes, the minimum viable snippet above is a complete working example: a @context, a @type such as CafeOrCoffeeShop, a name, a nested PostalAddress, and a telephone. Wrap it in a <script type="application/ld+json"> tag and place it in the page head. Everything else, geo coordinates, hours, price range, is an enrichment layer on top.
How do I check if my website has schema markup?
Three quick methods: view the page source and search for ld+json, run the URL through the Rich Results Test, or paste it into the Schema Markup Validator. For a full site, crawl with a tool that extracts structured data so you can spot templates where the markup is missing or duplicated.
Does local business schema markup improve rankings?
Not directly. It improves how reliably search engines and AI assistants understand and reuse your business data, which drives better presentation in search features and more accurate entity matching. Rankings usually move because of the visibility and click through effects, not because of the code itself.
Should I use LocalBusiness or a more specific subtype?
Always the most specific subtype that accurately describes the business. A Dentist node carries more meaning than a generic LocalBusiness, and it unlocks vertical specific properties. If you are unsure between two, declare an array of types.
Do I still need schema if I have a Google Business Profile?
Yes. The Business Profile covers Google’s own ecosystem. Schema markup on your site feeds Bing, other crawlers, AI answer engines and Google’s understanding of your website as an entity. It also acts as corroborating data when Google cross checks profile information. See uberall.com for their take.
Can I put all my locations in one JSON-LD block?
Technically you can output an array of LocalBusiness objects, and it is acceptable on a locations index page. For anything beyond a handful of branches, dedicated location pages with one node each perform better because each page can rank for its own city query and carry its own hours and reviews.
Should I hand code the JSON-LD or use a generator?
Generators are fine for a one off single location site. For anything dynamic, generate the JSON-LD server side from the same data source that renders the visible content. That is the only way to guarantee the markup stays in sync when an address or a phone number changes.

