DEV Community

Cover image for Implementing LocalBusiness Schema for Welsh Trade Sites: Real Examples That Survived a Year of Real Traffic
Jack Warner
Jack Warner

Posted on

Implementing LocalBusiness Schema for Welsh Trade Sites: Real Examples That Survived a Year of Real Traffic

Most Welsh trade sites I audit are invisible to Google Business Profile rich results despite ranking on page one for branded searches. The fix is almost always the same: there is no LocalBusiness schema, or it was generated by an SEO plugin three years ago and has gone stale.

This is the schema setup I now use across every client site at WebDev Wales. We host 42 small business client sites on Vercel, mostly local trade and service businesses across Neath, Bridgend, Swansea, Cardiff and the valleys. The patterns below have been running in production for over a year on the Next.js builds I migrate clients to.

Why schema markup specifically matters for trades

A plumber in Pontypridd does not compete with national chains on raw SEO. They compete with three other plumbers in Pontypridd. The decider is not who has the most pages, it is who Google trusts enough to show the rich result for "plumber in Pontypridd": the box with the rating, the phone number, the opening hours, the photo.

That rich result is driven by structured data, primarily LocalBusiness schema, aligned with the Google Business Profile listing. Get this right and Google starts surfacing your client where local-intent searches happen. Get it wrong (or skip it) and even a Lighthouse 95 site sits below the rich-result block.

The minimum viable LocalBusiness JSON-LD

This is the smallest schema block I will ship on a Welsh trade site. I drop it into a Next.js layout via the App Router's metadata API or a Script component with type application/ld+json.

{
  "@context": "https://schema.org",
  "@type": "Plumber",
  "name": "Example Plumbing Services",
  "url": "https://exampleplumbing.co.uk",
  "telephone": "+44-1639-595000",
  "image": "https://exampleplumbing.co.uk/og-image.jpg",
  "priceRange": "£60-£300",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "12 Example Street",
    "addressLocality": "Pontypridd",
    "addressRegion": "Rhondda Cynon Taf",
    "postalCode": "CF37 1AA",
    "addressCountry": "GB"
  },
  "geo": {
    "@type": "GeoCoordinates",
    "latitude": 51.5996,
    "longitude": -3.3431
  },
  "areaServed": [
    {"@type": "City", "name": "Pontypridd"},
    {"@type": "City", "name": "Treforest"},
    {"@type": "City", "name": "Llantwit Fardre"}
  ],
  "openingHoursSpecification": [{
    "@type": "OpeningHoursSpecification",
    "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
    "opens": "08:00",
    "closes": "17:00"
  }]
}
Enter fullscreen mode Exit fullscreen mode

The bits that earn the rich result are: matched name with the Google Business Profile, telephone in international format, address that matches the GBP address to the comma, opening hours that match the GBP hours, and a specific @type from schema.org's LocalBusiness subtypes (Plumber, Electrician, RoofingContractor, etc.) rather than a generic LocalBusiness.

Where I put it in the Next.js App Router

// app/layout.tsx
import Script from 'next/script';
import { LOCAL_BUSINESS_SCHEMA } from '@/lib/schema';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en-GB">
      <body>
        <Script
          id="local-business-schema"
          type="application/ld+json"
          strategy="beforeInteractive"
          dangerouslySetInnerHTML={{ __html: JSON.stringify(LOCAL_BUSINESS_SCHEMA) }}
        />
        {children}
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

I keep the schema object in lib/schema.ts so I can compose it per-client without rewriting the layout. The Script component with beforeInteractive ensures Google sees the JSON-LD on first crawl, which matters more than it should because some Welsh client sites get crawled fortnightly at most.

The Google Business Profile alignment audit

The schema only earns the rich result if it matches what Google already has on file. Before shipping, I run a quick alignment check against the GBP listing:

# Quick visual diff
curl -s https://client-site.co.uk | grep -A 100 'application/ld\+json' > /tmp/site-schema.json
# Compare against what GBP shows for: name, address, phone, hours
Enter fullscreen mode Exit fullscreen mode

Three fields commonly drift between schema and GBP:

  1. Phone number formatting. GBP usually stores 07700 900123 with spaces. Schema needs +44-7700-900123. Both must resolve to the same number but the format mismatch is fine, the resolution is what matters.
  2. Opening hours. GBP allows split shifts and bank holiday overrides. Schema needs the openingHoursSpecification array, repeated for each window. If your client closes 12 to 1 for lunch on Wednesdays, that needs to be in the JSON-LD too or Google flags inconsistency.
  3. Address line breaks. GBP shows the address on a single line. Schema needs streetAddress as one field, the rest split out. Easy to misalign during migration.

The multi-location pattern for service businesses

About a quarter of my clients serve multiple Welsh towns. The clean approach is one LocalBusiness schema for the primary registered address, plus a Service schema per town, linked via areaServed:

{
  "@context": "https://schema.org",
  "@type": "Service",
  "serviceType": "Emergency Plumbing",
  "provider": {
    "@type": "Plumber",
    "name": "Example Plumbing Services",
    "url": "https://exampleplumbing.co.uk"
  },
  "areaServed": {
    "@type": "City",
    "name": "Aberdare"
  }
}
Enter fullscreen mode Exit fullscreen mode

This goes on the town-specific page (eg /plumber-in-aberdare). The town pages do the long-tail work, the homepage LocalBusiness schema does the rich-result work, and Google reconciles the two via the shared provider reference.

What I have seen actually move in Search Console

For sites where I have implemented this correctly from scratch, the pattern in Search Console after about 12 weeks is consistent:

  • Impressions for "[service] in [town]" queries climb meaningfully
  • The rich-result count in the "Enhancements" tab fills in (you want LocalBusiness, Sitelinks, eventually Reviews)
  • The site starts appearing in the Local Pack for several town queries

I will not put numbers on it because client traffic patterns differ wildly and I do not want to promise specific growth. What I will say is that schema is the cheapest local SEO intervention available, takes maybe two hours to implement properly on a Next.js site, and is the single most common thing missing on the audits I run for Welsh small business sites.

The three mistakes I see repeatedly

  1. Schema for the wrong @type. Generic LocalBusiness when there is a specific subtype available. Plumber, Electrician, RoofingContractor, HVACBusiness, Carpenter, etc. The specific type is what triggers the trade-relevant rich result.

  2. No @id field. When a site has multiple schema blocks (LocalBusiness + breadcrumbs + Service), they all need an @id and they need to reference each other. Without @ids, Google often picks the wrong block as canonical.

  3. Stale schema after a phone change. A client changes their mobile number, updates GBP, forgets the website. Two weeks later the rich result disappears. I now check schema alignment quarterly as part of the maintenance retainer.

TL;DR for fellow devs building for UK local businesses

LocalBusiness schema is the highest-leverage SEO change you can ship on a Next.js client site. Use specific subtypes, align exactly with GBP, put it in the root layout via Script beforeInteractive, and audit alignment every quarter.

If you are running a similar setup for Welsh or UK trade clients and have a different pattern that works, I am genuinely interested to compare. Find me at webdevwales.com.

Top comments (0)