DEV Community

Cover image for Local Business Schema Markup: The Complete Guide to Structured Data for Physical Businesses
Roman Popovych
Roman Popovych

Posted on

Local Business Schema Markup: The Complete Guide to Structured Data for Physical Businesses

Search "dentist near me" on your phone. The first results aren't websites — they're a map with three pins and a list with ratings, hours, and a phone number you can tap to call right there. That's the Local Pack. Getting into it is mostly about Google Business Profile, but Local Business schema on your website is the supporting signal that connects your GBP listing to your domain and strengthens the whole thing.

For businesses with a physical location or a defined service area, this is the schema type with the most direct connection between implementation and visible search behavior. Address, hours, phone, star ratings — all of it comes from data you provide.

What's covered: the right business subtype (there are over 100 options), opening hours edge cases that trip up most implementations, how multi-location businesses should structure their schema, and code examples for Next.js, React, and WordPress.


What Is Local Business Schema?

LocalBusiness is a schema.org type that describes a physical business or service area business — any organization that serves customers at or from a geographic location. It's a subtype of Organization, which means it inherits all Organization fields and adds location-specific ones.

{
  "@context": "https://schema.org",
  "@type": "LocalBusiness",
  "name": "Corner Coffee Roasters",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "42 Market Street",
    "addressLocality": "Portland",
    "addressRegion": "OR",
    "postalCode": "97201",
    "addressCountry": "US"
  },
  "telephone": "+15034567890",
  "openingHours": "Mo-Fr 07:00-18:00"
}
Enter fullscreen mode Exit fullscreen mode

Unlike Organization schema which describes a brand entity, LocalBusiness schema describes a specific location. A chain with five locations should have five separate LocalBusiness schema blocks — one per location page.


LocalBusiness Subtypes: Choose the Most Specific One

Schema.org has over 100 specific subtypes of LocalBusiness. The more specific your type, the better Google can categorize your business. Always use the most specific type that accurately describes what you do.

Category Subtypes
Food & drink Restaurant, Bakery, Bar, CafeOrCoffeeShop, FastFoodRestaurant
Health Dentist, MedicalClinic, Pharmacy, Optician, Physician
Automotive AutoDealer, AutoRepair, GasStation, ParkingFacility
Finance AccountingService, InsuranceAgency, LegalService
Retail ClothingStore, ElectronicsStore, GroceryStore, BookStore
Home services HVACBusiness, Plumber, Electrician, RoofingContractor
Beauty HairSalon, BeautySalon, NailSalon, SpaOrHealthClub
Sports GolfCourse, GymOrHealthClub, SkiResort
Accommodation Hotel, Motel, BedAndBreakfast, Hostel
Professional AccountingService, LegalService, FinancialService, RealEstateAgent

If your specific business type isn't in the list, go one level up. A tattoo studio might use LocalBusiness directly since there's no TattooStudio type. A software development agency is better served by ProfessionalService than LocalBusiness.

If your business genuinely belongs to more than one category — for example a coffee shop that is also a bookstore — schema.org supports multiple types as an array:

{
  "@context": "https://schema.org",
  "@type": ["CafeOrCoffeeShop", "BookStore"],
  "name": "Read & Brew"
}
Enter fullscreen mode Exit fullscreen mode

Use this sparingly and only when both types genuinely apply.


What Rich Results Local Business Schema Enables

Feature What appears Requirement
Address in results Street, city, region address with PostalAddress
Phone click-to-call Tap to call on mobile telephone
Opening hours "Open now" / "Closes at 6pm" openingHours or openingHoursSpecification
Star rating ⭐ 4.3 (127 reviews) aggregateRating
Map pin Location marker in search maps geo with GeoCoordinates
Price range $, $$, $$$ indicator priceRange
Menu / services Link to menu page hasMenu (restaurants)
Amenities Parking, Wi-Fi, accessibility amenityFeature

These features appear in Local Pack results (the map + three business listings), Google Knowledge Panels for local businesses, and standard search result snippets.


The Complete Local Business Schema

{
  "@context": "https://schema.org",
  "@type": "Restaurant",
  "name": "Corner Coffee Roasters",
  "description": "Specialty coffee roaster and café serving single-origin pour-overs, espresso drinks, and housemade pastries in Portland's Pearl District.",
  "url": "https://cornercoffeeroasters.com",
  "image": [
    "https://cornercoffeeroasters.com/images/cafe-exterior.jpg",
    "https://cornercoffeeroasters.com/images/interior.jpg",
    "https://cornercoffeeroasters.com/images/coffee.jpg"
  ],
  "logo": "https://cornercoffeeroasters.com/logo.png",
  "telephone": "+15034567890",
  "email": "hello@cornercoffeeroasters.com",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "42 Market Street",
    "addressLocality": "Portland",
    "addressRegion": "OR",
    "postalCode": "97201",
    "addressCountry": "US"
  },
  "geo": {
    "@type": "GeoCoordinates",
    "latitude": 45.5231,
    "longitude": -122.6765
  },
  "openingHoursSpecification": [
    {
      "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
      "opens": "07:00",
      "closes": "18:00"
    },
    {
      "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Saturday"],
      "opens": "08:00",
      "closes": "17:00"
    },
    {
      "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Sunday"],
      "opens": "09:00",
      "closes": "15:00"
    }
  ],
  "priceRange": "$$",
  "servesCuisine": "Coffee, Pastries",
  "hasMenu": "https://cornercoffeeroasters.com/menu",
  "menu": "https://cornercoffeeroasters.com/menu",
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.6",
    "reviewCount": "218",
    "bestRating": "5",
    "worstRating": "1"
  },
  "sameAs": [
    "https://www.google.com/maps/place/corner-coffee-roasters",
    "https://www.yelp.com/biz/corner-coffee-roasters",
    "https://www.instagram.com/cornercoffeeroasters",
    "https://www.facebook.com/cornercoffeeroasters"
  ],
  "paymentAccepted": "Cash, Credit Card, Apple Pay",
  "currenciesAccepted": "USD",
  "amenityFeature": [
    {
      "@type": "LocationFeatureSpecification",
      "name": "Free Wi-Fi",
      "value": true
    },
    {
      "@type": "LocationFeatureSpecification",
      "name": "Wheelchair accessible",
      "value": true
    },
    {
      "@type": "LocationFeatureSpecification",
      "name": "Outdoor seating",
      "value": true
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Field Reference

Field Type Priority Notes
name Text Required Business name as customers know it — match your Google Business Profile exactly
address PostalAddress Required Full address with all sub-fields
telephone Text Required E.164 format: "+15034567890"
url URL Required Your website's homepage or location page
openingHoursSpecification Array Strongly recommended More flexible than openingHours string — use this
geo GeoCoordinates Strongly recommended Exact lat/lng — copy from Google Maps for precision
image URL or array Strongly recommended Real photos of the business, not stock images
aggregateRating AggregateRating Strongly recommended Enables star display — only include with real review data
priceRange Text Recommended "$", "$$", "$$$", "$$$$"
description Text Recommended 100–200 characters, what you offer and who you serve
logo URL or ImageObject Recommended Business logo
sameAs Array of URLs Recommended Google Maps, Yelp, social profiles, directory listings
email Text Optional Public contact email
paymentAccepted Text Optional Payment methods accepted
amenityFeature Array Optional Wi-Fi, parking, accessibility features
hasMap URL Optional Google Maps URL for this location

Opening Hours: Two Formats Explained

Google supports two ways to express opening hours. Use openingHoursSpecification (the structured object array) — it's more flexible and handles edge cases like split shifts and holiday hours.

Format 1: openingHours string (simple, limited)

"openingHours": [
  "Mo-Fr 09:00-18:00",
  "Sa 10:00-16:00"
]
Enter fullscreen mode Exit fullscreen mode

The string format uses a compact notation: two-letter day abbreviations (Mo, Tu, We, Th, Fr, Sa, Su), dash for ranges, 24-hour time. For simple Monday-Friday hours, this is fine.

Format 2: openingHoursSpecification (flexible, handles edge cases)

"openingHoursSpecification": [
  {
    "@type": "OpeningHoursSpecification",
    "dayOfWeek": ["Monday", "Tuesday", "Wednesday"],
    "opens": "09:00",
    "closes": "18:00"
  },
  {
    "@type": "OpeningHoursSpecification",
    "dayOfWeek": ["Thursday", "Friday"],
    "opens": "09:00",
    "closes": "20:00"
  },
  {
    "@type": "OpeningHoursSpecification",
    "dayOfWeek": ["Saturday"],
    "opens": "10:00",
    "closes": "16:00"
  }
]
Enter fullscreen mode Exit fullscreen mode

Use openingHoursSpecification when:

  • Different days have different hours
  • You have split shifts (morning and evening sessions)
  • You have seasonal hours that change

24/7 businesses:

"openingHoursSpecification": {
  "@type": "OpeningHoursSpecification",
  "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],
  "opens": "00:00",
  "closes": "23:59"
}
Enter fullscreen mode Exit fullscreen mode

Closed on a specific day (e.g. Sundays):

{
  "@type": "OpeningHoursSpecification",
  "dayOfWeek": ["Sunday"],
  "opens": "00:00",
  "closes": "00:00"
}
Enter fullscreen mode Exit fullscreen mode

Setting both opens and closes to "00:00" is Google's documented way to indicate a day is closed. Don't omit the day or skip the entry — include it explicitly.

Temporarily closed:

"openingHours": "closed"
Enter fullscreen mode Exit fullscreen mode

Or use temporarilyClosed: true as a boolean flag.


Geo Coordinates: Getting Them Right

The geo field with exact latitude and longitude is one of the most impactful fields for local search accuracy. Google uses coordinates to determine proximity in "near me" searches.

"geo": {
  "@type": "GeoCoordinates",
  "latitude": 45.52310,
  "longitude": -122.67650
}
Enter fullscreen mode Exit fullscreen mode

How to get precise coordinates:

  1. Open Google Maps and find your business location
  2. Right-click on the exact spot (center of your building entrance works best)
  3. The coordinates appear at the top of the context menu — click to copy
  4. Use 5+ decimal places for accuracy

Don't use your city center coordinates or a rounded approximation. The precision matters for "near me" proximity calculations, especially in dense urban areas where multiple competitors may be within a block.


Service Area Businesses: When You Don't Have a Storefront

A plumber, house cleaner, or mobile dog groomer serves customers at their location, not at a fixed address. These are Service Area Businesses (SABs). The schema approach differs:

{
  "@context": "https://schema.org",
  "@type": "Plumber",
  "name": "Quick Fix Plumbing",
  "telephone": "+15034567890",
  "url": "https://quickfixplumbing.com",
  "areaServed": [
    {
      "@type": "City",
      "name": "Portland"
    },
    {
      "@type": "City",
      "name": "Beaverton"
    },
    {
      "@type": "AdministrativeArea",
      "name": "Multnomah County"
    }
  ],
  "serviceArea": {
    "@type": "GeoCircle",
    "geoMidpoint": {
      "@type": "GeoCoordinates",
      "latitude": 45.5231,
      "longitude": -122.6765
    },
    "geoRadius": "30000"
  }
}
Enter fullscreen mode Exit fullscreen mode

Key differences for SABs:

  • address is optional — many SABs prefer not to show their home address publicly
  • Use areaServed to list cities and regions where you work
  • Use serviceArea with GeoCircle to define a radius in meters
  • Don't include geo if you're not showing a physical address

In Google Business Profile, SABs set their service area rather than a storefront address. Your schema should mirror this setup.


Multi-Location Businesses

A chain with multiple locations should have a separate LocalBusiness JSON-LD block on each location's individual page. Don't try to express multiple locations in a single schema block.

Root organization page (e.g., brand.com):

{
  "@context": "https://schema.org",
  "@type": "Corporation",
  "name": "Corner Coffee Roasters",
  "url": "https://cornercoffeeroasters.com",
  "numberOfLocations": 5
}
Enter fullscreen mode Exit fullscreen mode

Individual location page (e.g., brand.com/portland):

{
  "@context": "https://schema.org",
  "@type": "CafeOrCoffeeShop",
  "name": "Corner Coffee Roasters — Portland",
  "parentOrganization": {
    "@type": "Corporation",
    "name": "Corner Coffee Roasters",
    "url": "https://cornercoffeeroasters.com"
  },
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "42 Market Street",
    "addressLocality": "Portland",
    "addressRegion": "OR",
    "postalCode": "97201",
    "addressCountry": "US"
  },
  "telephone": "+15034567890",
  "openingHoursSpecification": [...]
}
Enter fullscreen mode Exit fullscreen mode

parentOrganization links each location to the parent brand. This helps Google understand the relationship between the locations and the overarching organization.


Implementation in Next.js (App Router)

For a single-location business with a dedicated location page:

// app/location/page.tsx (or just app/page.tsx for single-location)

export default function LocationPage() {
  const localBusinessSchema = {
    '@context': 'https://schema.org',
    '@type': 'LocalBusiness',
    name: 'Acme Services',
    description: 'Professional web development and consulting services.',
    url: 'https://acmeservices.com',
    telephone: '+15034567890',
    address: {
      '@type': 'PostalAddress',
      streetAddress: '42 Market Street',
      addressLocality: 'Portland',
      addressRegion: 'OR',
      postalCode: '97201',
      addressCountry: 'US',
    },
    geo: {
      '@type': 'GeoCoordinates',
      latitude: 45.5231,
      longitude: -122.6765,
    },
    openingHoursSpecification: [
      {
        '@type': 'OpeningHoursSpecification',
        dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
        opens: '09:00',
        closes: '18:00',
      },
    ],
    sameAs: [
      'https://linkedin.com/company/acmeservices',
      'https://www.google.com/maps/place/acmeservices',
    ],
  }

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(localBusinessSchema) }}
      />
      {/* page content */}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

For dynamic multi-location pages, fetch location data from your CMS and map it to the schema structure in the server component.


Implementation in React (with react-helmet-async)

import { Helmet } from 'react-helmet-async'

function LocationPage({ location }) {
  const schema = {
    '@context': 'https://schema.org',
    '@type': location.businessType || 'LocalBusiness',
    name: location.name,
    description: location.description,
    url: location.url,
    telephone: location.phone,
    address: {
      '@type': 'PostalAddress',
      streetAddress: location.street,
      addressLocality: location.city,
      addressRegion: location.state,
      postalCode: location.zip,
      addressCountry: location.country,
    },
    geo: {
      '@type': 'GeoCoordinates',
      latitude: location.lat,
      longitude: location.lng,
    },
    openingHoursSpecification: location.hours,
    aggregateRating: location.rating
      ? {
          '@type': 'AggregateRating',
          ratingValue: location.rating.value.toString(),
          reviewCount: location.rating.count.toString(),
        }
      : undefined,
  }

  return (
    <>
      <Helmet>
        <script type="application/ld+json">
          {JSON.stringify(schema)}
        </script>
      </Helmet>
      {/* page content */}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Implementation in WordPress

For a single-location business, add to functions.php:

function add_local_business_schema() {
  if ( ! is_front_page() && ! is_page( 'contact' ) ) return;

  $schema = [
    '@context'    => 'https://schema.org',
    '@type'       => 'LocalBusiness',
    'name'        => get_bloginfo( 'name' ),
    'description' => get_bloginfo( 'description' ),
    'url'         => home_url( '/' ),
    'telephone'   => '+15034567890',
    'address'     => [
      '@type'           => 'PostalAddress',
      'streetAddress'   => '42 Market Street',
      'addressLocality' => 'Portland',
      'addressRegion'   => 'OR',
      'postalCode'      => '97201',
      'addressCountry'  => 'US',
    ],
    'geo' => [
      '@type'     => 'GeoCoordinates',
      'latitude'  => 45.5231,
      'longitude' => -122.6765,
    ],
    'openingHours' => [
      'Mo-Fr 09:00-18:00',
      'Sa 10:00-16:00',
    ],
    'sameAs' => [
      'https://linkedin.com/company/your-business',
      'https://www.google.com/maps/place/your-business',
    ],
  ];

  echo '<script type="application/ld+json">' . wp_json_encode( $schema ) . '</script>';
}
add_action( 'wp_head', 'add_local_business_schema' );
Enter fullscreen mode Exit fullscreen mode

If you use a page builder or custom fields plugin, pull the address and contact info from ACF or custom meta fields instead of hardcoding them.


The 7 Most Common Local Business Schema Mistakes

1. Name doesn't match Google Business Profile

Google cross-references your schema name against your Google Business Profile listing. If they differ — even slightly ("Joe's Plumbing" vs "Joe's Plumbing LLC") — it weakens the entity signal. Use the same name everywhere.

2. Incorrect or outdated telephone format

// Wrong  Google may not link this for click-to-call
"telephone": "503-456-7890"

// Correct  E.164 international format
"telephone": "+15034567890"
Enter fullscreen mode Exit fullscreen mode

3. Not including geo coordinates

For "near me" queries, proximity is determined by coordinates, not address parsing alone. A business without geo relies entirely on Google's geocoding of the address — which is usually accurate but never as precise as explicit coordinates.

4. Fabricated aggregateRating

Only include aggregateRating if it reflects real reviews from real customers. Google can cross-reference this against your Google Business Profile rating and other review platforms. Fabricated ratings are a policy violation.

5. Same schema block for all locations

If you have multiple locations and serve them all from one schema block with one address, Google can only associate structured data with one location. Each location page needs its own, unique schema block.

6. openingHours not kept up to date

If your hours change seasonally or for holidays and your schema still shows the old hours, Google may display incorrect "Open now" / "Closed" status in search results. Use specialOpeningHoursSpecification for temporary hour changes.

7. Using LocalBusiness for a fully remote business

A SaaS company, a freelance consultant with no fixed office, or an e-commerce store with no customer-facing location should use Organization or Corporation, not LocalBusiness. Using LocalBusiness incorrectly for remote businesses can confuse Google's local index.


Local Business Schema vs Google Business Profile

A common question: do I need both? Yes, and they serve different functions.

Google Business Profile Local Business Schema
Source Google's own platform Your website
Controls Map pack ranking, reviews, photos Rich result data, entity signals
Indexing Managed directly by Google Discovered when Google crawls your site
Updates Near real-time Next crawl cycle (days to weeks)
Reviews Native review system Aggregates from schema data

Google Business Profile is the primary driver of Local Pack (map results) performance. Local Business schema on your website is a supporting signal that reinforces the entity connection between your GBP listing and your website.

Always have both. Prioritize keeping your GBP listing complete and current — then let Local Business schema on your site reinforce those signals.


Summary

Local Business schema has the most direct connection between implementation and visible search results of any schema type covered in this series. Get it right and Google has the data to show your address, hours, phone, and star rating in search results. Miss the key fields and you're leaving that display space to competitors.

The practical checklist:

  • Use the most specific @type subtype for your business category
  • Match name exactly to your Google Business Profile
  • Include geo coordinates from Google Maps — copy them precisely
  • Use openingHoursSpecification (not the string format) for anything beyond simple Mon–Fri hours
  • Only include aggregateRating with real review data
  • For multiple locations: one schema block per location page, use parentOrganization to link to the brand
  • For SABs: use areaServed and serviceArea instead of a physical address

If you want to generate Local Business schema through a structured form with live preview and missing field alerts, there's a free generator here: Local Business Schema Generator. No signup, no backend, runs locally in the browser.


Running into issues with your schema not matching your GBP listing? Drop the specifics in the comments — the discrepancy cases are often more instructive than the happy path.

Top comments (0)