DEV Community

Cover image for LocalBusiness structured data: the 20-minute fix most small business sites are missing
LeadReef Dev
LeadReef Dev

Posted on

LocalBusiness structured data: the 20-minute fix most small business sites are missing

Open the source of ten small business websites in your town and search for application/ld+json. On most of them you'll find nothing. On a few you'll find an Organization block a theme or plugin added years ago, with the wrong phone number and a logo URL that 404s.

LocalBusiness structured data is one of the cheapest fixes you can ship for a client. It is a single <script> tag, it does not touch the design, and you can write, test and deploy it in about twenty minutes once you have the business details in front of you. This post covers what Google actually asks for, how to pick the right type, how to get opening hours right (the part everyone gets wrong), and how to test it.

One expectation to set first, with yourself and with the client: structured data does not guarantee anything. Google's own guidelines say it "does not guarantee that your structured data will show up in search results." It helps Google understand the page. It is not a ranking switch, and you should never sell it as one.

What Google asks for

Google's LocalBusiness documentation has a short list of required properties and a longer list of recommended ones. As of this writing, only two are required: name and address. Everything else is recommended, which in practice means "fill in what's true and visible on the page."

Table of LocalBusiness properties from Google's documentation: required name and address, recommended properties with format notes and common gotchas

A few of the recommended ones have specific format rules that are easy to miss:

  • telephone: Google asks for the primary contact number including the country code and area code. Use +1-512-555-0142, not (512) 555-0142.
  • geo: latitude and longitude with at least 5 decimal places.
  • priceRange: must be shorter than 100 characters. "$$" or "$90 to $250" is fine. A sentence about free estimates is not.
  • url: the fully qualified URL of this specific location, and it has to work.
  • aggregateRating and review: Google only recommends these for sites that collect reviews about other businesses. More on that below, because it's the most common mistake we see.

The minimal working example

Here's a complete block for a fictional plumbing company. Put it in the <head> or anywhere in the <body>; Google reads both.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Plumber",
  "@id": "https://harborlaneplumbing.example/#business",
  "name": "Harbor Lane Plumbing",
  "url": "https://harborlaneplumbing.example/",
  "telephone": "+1-512-555-0142",
  "image": "https://harborlaneplumbing.example/img/van.jpg",
  "priceRange": "$$",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "100 Example Ave, Suite 2",
    "addressLocality": "Austin",
    "addressRegion": "TX",
    "postalCode": "78701",
    "addressCountry": "US"
  },
  "geo": {
    "@type": "GeoCoordinates",
    "latitude": 30.26715,
    "longitude": -97.74306
  },
  "openingHoursSpecification": [
    {
      "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
      "opens": "07:00:00",
      "closes": "19:00:00"
    },
    {
      "@type": "OpeningHoursSpecification",
      "dayOfWeek": "Saturday",
      "opens": "08:00:00",
      "closes": "14:00:00"
    }
  ],
  "sameAs": [
    "https://www.facebook.com/example-page"
  ]
}
</script>
Enter fullscreen mode Exit fullscreen mode

Harbor Lane Plumbing, its domain and address are a fictional example. The phone number uses the 555-01xx range reserved for fiction. Replace everything with the client's real, visible details.

Notes on the choices in there:

  • @id gives the business a stable identifier. It costs nothing and helps if you later reference the business from other markup on the site (a Service page, for example).
  • sameAs should list profiles the business actually owns. Don't add a Yelp URL you guessed.
  • The coordinates have 5 decimal places because Google asks for at least that. Grab them from a map by right-clicking the exact pin, not the city center.

Pick the most specific type

Google's docs say to use the most specific LocalBusiness subtype that fits. schema.org has a lot of them, and they sit in a hierarchy. Plumber, for instance, lives under LocalBusiness > HomeAndConstructionBusiness > Plumber. Dentist is under LocalBusiness directly and also under MedicalBusiness.

Map of common local business categories to schema.org types, with the parent type for each and a fallback when nothing fits

If a business really is two things, Google accepts multiple types as an array:

"@type": ["Plumber", "HVACBusiness"]
Enter fullscreen mode Exit fullscreen mode

Google's documentation also notes that additionalType is not supported for this, so stick to the array.

When nothing fits (a niche consultancy, a mobile pet groomer), use the closest parent, or plain LocalBusiness. A correct general type beats a wrong specific one.

A dental practice looks almost identical to the plumber block, with a different type and the fields that matter for patients:

{
  "@context": "https://schema.org",
  "@type": "Dentist",
  "name": "Example Family Dental",
  "url": "https://examplefamilydental.example/",
  "telephone": "+1-512-555-0187",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "200 Example Blvd",
    "addressLocality": "Austin",
    "addressRegion": "TX",
    "postalCode": "78702",
    "addressCountry": "US"
  }
}
Enter fullscreen mode Exit fullscreen mode

(Also fictional.)

Opening hours, done properly

Hours are where markup quietly goes wrong. The business changes its Saturday hours, the footer gets updated, and the JSON-LD still says the old thing for three years.

Google's format rules, from the LocalBusiness doc:

Situation How to mark it up
Normal hours opens / closes in hh:mm:ss, dayOfWeek as a name like "Monday" or an array
Open 24 hours "opens": "00:00", "closes": "23:59"
Closed all day "opens": "00:00", "closes": "00:00"
Open past midnight One entry, e.g. Friday 18:00:00 to 02:00:00
Seasonal or holiday hours Add validFrom and validThrough in YYYY-MM-DD
Same hours year-round Leave out validFrom and validThrough

A holiday closure, added on top of the regular hours, looks like this:

{
  "@type": "OpeningHoursSpecification",
  "dayOfWeek": "Thursday",
  "opens": "00:00:00",
  "closes": "00:00:00",
  "validFrom": "2026-11-26",
  "validThrough": "2026-11-26"
}
Enter fullscreen mode Exit fullscreen mode

If the site runs on a CMS, generate the hours block from the same data that renders the visible hours in the footer. That one change prevents most of the drift. On a static site, at least keep both in the same partial so whoever edits one sees the other.

Common mistakes

These are the ones we run into most when we look at local business sites. Most of them pass the validator and are still wrong.

Do and don't list of common LocalBusiness structured data mistakes and how to fix each one

The one that deserves a longer explanation is review stars. Lots of small business sites add aggregateRating with their Google rating hard-coded, hoping for stars in search results. Google's review snippet guidelines say that if the entity being reviewed controls the reviews about itself, its pages using LocalBusiness or any other Organization type are "ineligible for star review feature." A dentist marking up their own reviews on their own site is exactly that case. Leave it out. Show the reviews to humans instead.

The second one is marking up things the visitor can't see. Google's general structured data guidelines say: "Don't mark up content that is not visible to readers of the page." If the hours aren't on the page, either add them to the page or leave them out of the markup.

Test it in three passes

1. Check what's actually on the page. Plugins, themes and tag managers all inject JSON-LD. Before adding yours, paste this into the DevTools console to see every block already there and whether it parses:

[...document.querySelectorAll('script[type="application/ld+json"]')]
  .map((s, i) => {
    try {
      const data = JSON.parse(s.textContent);
      const items = [].concat(data['@graph'] || data);
      return { block: i, types: items.map(x => x['@type']) };
    } catch (e) {
      return { block: i, error: e.message };
    }
  });
Enter fullscreen mode Exit fullscreen mode

If you see two LocalBusiness-type blocks with different phone numbers, fix that first. Usually it means turning off a plugin's schema output or editing its settings.

2. Run Google's Rich Results Test. Paste the live URL or the code. Google's guidance is to fix any critical errors; non-critical warnings are optional, but they're usually missing recommended properties, so fill in the ones that are true.

3. Run the Schema Markup Validator at validator.schema.org. It checks against the full schema.org vocabulary rather than only what Google uses, and catches typos like adress that a Google-focused tool may just ignore.

After deploying, use the URL Inspection tool in Search Console (if the client has it set up) to confirm Google can fetch the page. The page can't be blocked by robots.txt or noindex. Google notes that recrawling can take several days, so don't promise the client anything by Friday.

How to pitch it to a client

Owners don't care about JSON-LD. They care about whether Google has their hours right. A short message works better than an explanation of schema.org:

I noticed your site doesn't tell Google your hours or service area in the format it reads directly. It's a small code addition, no design changes, and I can do it this week. It won't guarantee anything on its own, but it removes one way for Google to get your details wrong.

Notice the honesty in the middle sentence. Overselling structured data is how freelancers end up explaining in month three why nothing visibly changed.

It also pairs well with the rest of the basics: keeping the business's map listing, the footer and the markup in sync, and making sure the phone number on the page is a tappable tel: link. Fixing all three in one visit is an easy package.

Finding sites that are missing it

The manual way: pick a trade and a town, open the sites, and run the console snippet above on each. No JSON-LD, a stale Organization block or mismatched hours all tell you the site hasn't been looked after for a while, and that usually means other problems too.

If you'd like a shortcut for the finding part, LeadReef finds local businesses whose websites are outdated, broken on mobile or missing, shows what's wrong with each one, and helps with a first mockup and outreach message. You decide who to contact and you send everything yourself. There's a free plan with your first 10 leads. Full disclosure: we build LeadReef. Everything in this post works without it.


Sources: Google Search Central, Local business (LocalBusiness) structured data; Google Search Central, Introduction to structured data markup; Google Search Central, General structured data guidelines; Google Search Central, Review snippet guidelines; schema.org, Plumber and Dentist type pages; Schema Markup Validator; Google Rich Results Test.

Top comments (1)

Collapse
 
hyiptemplates profile image
Hyip Templates •

The step I see skipped most often is the first one in your three-pass test — checking what JSON-LD is already on the page before adding anything. Themes, plugins, and tag managers all inject their own blocks, so it is very easy to end up with two LocalBusiness blocks listing different phone numbers, which is worse than having none.

Your point about aggregateRating was also a useful warning. Hard-coding a Google rating to try for stars in search results is tempting, but as you explain, Google only recommends it for sites that collect reviews about other businesses. The small formatting details — telephone with the country code, geo coordinates to at least five decimal places, and a short priceRange like "$$" — are exactly the kind of thing that is quick to get right once and then easy to forget on the next client site.