usiness`) to create a stable entity reference. This enables cross-page linking and knowledge graph association.
- Separate physical and service geography.
address defines where the business operates physically. areaServed defines where clients are accepted. Both can coexist without conflict.
Step 2: Structure Geographic and Temporal Data
Geographic precision requires three distinct properties:
PostalAddress: Standardized street, city, region, postal code, and country.
GeoCoordinates: Latitude and longitude for map pinning and proximity calculations.
areaServed: Array of cities, regions, or countries where services are delivered.
Temporal data must follow strict formatting rules. OpeningHoursSpecification requires full English day names and 24-hour time format. Violations silently disable rich result eligibility.
Step 3: Serialize and Inject
JSON-LD must be injected as a <script type="application/ld+json"> tag. The payload should be serialized with flags that prevent unnecessary escaping and preserve Unicode characters. Modern frameworks benefit from a factory pattern that generates the schema object, serializes it safely, and injects it into the document head.
TypeScript Implementation Example
interface BusinessProfile {
name: string;
description: string;
url: string;
telephone: string;
email: string;
priceRange: string;
socialProfiles: string[];
}
interface LocationData {
street: string;
city: string;
region: string;
postalCode: string;
country: string;
latitude: number;
longitude: number;
}
interface ServiceTerritory {
type: 'City' | 'State' | 'Country';
name: string;
}
interface OperatingWindow {
days: string[];
opens: string;
closes: string;
}
class LocalEntitySchemaBuilder {
private schema: Record<string, any>;
constructor(entityId: string, business: BusinessProfile) {
this.schema = {
'@context': 'https://schema.org',
'@type': 'ProfessionalService',
'@id': entityId,
name: business.name,
description: business.description,
url: business.url,
telephone: business.telephone,
email: business.email,
priceRange: business.priceRange,
sameAs: business.socialProfiles,
};
}
setPhysicalLocation(location: LocationData): this {
this.schema.address = {
'@type': 'PostalAddress',
streetAddress: location.street,
addressLocality: location.city,
addressRegion: location.region,
postalCode: location.postalCode,
addressCountry: location.country,
};
this.schema.geo = {
'@type': 'GeoCoordinates',
latitude: location.latitude,
longitude: location.longitude,
};
return this;
}
setServiceCoverage(territories: ServiceTerritory[]): this {
this.schema.areaServed = territories.map(t => ({
'@type': t.type,
name: t.name,
}));
return this;
}
setOperatingHours(windows: OperatingWindow[]): this {
this.schema.openingHoursSpecification = windows.map(w => ({
'@type': 'OpeningHoursSpecification',
dayOfWeek: w.days,
opens: w.opens,
closes: w.closes,
}));
return this;
}
serialize(): string {
return JSON.stringify(this.schema, null, 2);
}
toScriptTag(): string {
return `<script type="application/ld+json">${this.serialize()}</script>`;
}
}
// Usage Example
const businessData: BusinessProfile = {
name: 'Apex Engineering Solutions',
description: 'Commercial HVAC design and installation for industrial facilities in the Pacific Northwest.',
url: 'https://apexengineering.example.com',
telephone: '+12065550198',
email: 'contact@apexengineering.example.com',
priceRange: '$$$',
socialProfiles: [
'https://linkedin.com/company/apex-engineering',
'https://facebook.com/apexengineering',
],
};
const location: LocationData = {
street: '4820 Industrial Way',
city: 'Seattle',
region: 'Washington',
postalCode: '98134',
country: 'US',
latitude: 47.6062,
longitude: -122.3321,
};
const builder = new LocalEntitySchemaBuilder('https://apexengineering.example.com/#organization', businessData)
.setPhysicalLocation(location)
.setServiceCoverage([
{ type: 'State', name: 'Washington' },
{ type: 'State', name: 'Oregon' },
])
.setOperatingHours([
{ days: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], opens: '08:00', closes: '17:30' },
]);
console.log(builder.toScriptTag());
Step 4: Framework Integration
In modern applications, inject the serialized script tag into the document head using framework-specific head management. For Next.js, use next/head. For React, use react-helmet. For static sites, place the output directly in the <head> section. Always ensure the script tag is not duplicated across client-side navigation cycles.
Why this architecture works:
- Type safety prevents malformed JSON and missing required properties.
- Builder pattern enables incremental configuration without nested object mutation.
- Serialization flags (implicit in
JSON.stringify) preserve structure while avoiding escape character bloat.
- Separation of concerns keeps schema logic isolated from UI components, making it easier to audit and update.
Pitfall Guide
1. Duplicate Markup Conflicts
Explanation: Running an SEO plugin alongside manual JSON-LD injection creates multiple schema blocks on the same page. Crawlers may ignore the page entirely or merge conflicting properties, breaking rich result eligibility.
Fix: Audit the DOM for all <script type="application/ld+json"> tags. Disable auto-generation in plugins or restrict manual injection to pages where plugin output is insufficient. Use Google’s Rich Results Test to verify single-source truth.
2. NAP Fragmentation
Explanation: Name, Address, and Phone variations across your website, Google Business Profile, and third-party directories weaken entity matching. Even minor differences like "St." vs "Street" or missing country codes trigger disambiguation failures.
Fix: Centralize NAP data in a single configuration file or CMS field. Enforce exact string matching across all touchpoints. Use E.164 formatting for phone numbers (+12065550198).
3. Orphaned Entities (Missing @id)
Explanation: Without a canonical @id, each page’s schema is treated as an isolated entity. Blog posts, service pages, and team profiles cannot reference a central business node, preventing knowledge graph integration.
Fix: Implement a consistent @id pattern using a URL with a fragment identifier (e.g., https://domain.com/#business). Reference this ID across all related pages using sameAs or organization properties.
Explanation: OpeningHoursSpecification requires full English day names and 24-hour time format. Abbreviations (Mon, Tue) or 12-hour time (9:00 AM) silently break parsing without throwing visible errors.
Fix: Validate day arrays against the schema.org enum. Use ISO 8601 time strings (HH:MM). Implement a validation function that rejects malformed temporal data before serialization.
5. Geographic Ambiguity
Explanation: Mixing physical location and service area into a single field confuses proximity algorithms. Search engines use address for local pack ranking and areaServed for service eligibility. Overlapping or missing values degrade local targeting.
Fix: Strictly separate PostalAddress and areaServed. Include geo coordinates for physical precision. Use areaServed arrays for multi-city or regional service businesses.
6. Description Over-Optimization
Explanation: The description field is indexed and displayed in knowledge panels. Keyword stuffing or marketing fluff triggers spam filters and reduces credibility.
Fix: Treat the description as structured metadata. Keep it under 160 characters, focus on service scope and target audience, and avoid promotional language. Example: Commercial electrical contracting for industrial and municipal projects in the greater Chicago area.
7. Validation Complacency
Explanation: Assuming "no errors" in validation tools means production readiness. Warnings often indicate missing properties that gate specific rich result types. Crawlers may also ignore schema if it conflicts with visible page content.
Fix: Run both Google’s Rich Results Test and Schema.org’s validator. Cross-reference warnings with feature documentation. Ensure schema properties match visible on-page content to prevent crawler distrust.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Single-location retail store | Specific subtype + address + geo | Enables local pack, maps pinning, and category-specific features | Low (static injection) |
| Multi-city service provider | Specific subtype + areaServed array | Targets regional search without confusing physical proximity signals | Medium (dynamic territory config) |
| Enterprise with multiple branches | Canonical @id + branch-specific pages | Enables knowledge graph hierarchy and cross-page entity resolution | High (CMS integration required) |
| Legacy WordPress site | Disable plugin auto-schema + manual functions.php injection | Prevents duplicate markup and ensures precise property control | Low (one-time refactor) |
| Headless/SPA architecture | TypeScript factory + framework head injection | Maintains type safety, prevents hydration conflicts, and supports dynamic updates | Medium (build pipeline setup) |
Configuration Template
{
"@context": "https://schema.org",
"@type": "ProfessionalService",
"@id": "https://yourdomain.com/#organization",
"name": "Business Legal Name",
"description": "Clear, specific description of services and target market.",
"url": "https://yourdomain.com",
"telephone": "+1XXXXXXXXXX",
"email": "contact@yourdomain.com",
"priceRange": "$$",
"address": {
"@type": "PostalAddress",
"streetAddress": "123 Main Street",
"addressLocality": "City",
"addressRegion": "State",
"postalCode": "00000",
"addressCountry": "US"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 00.0000,
"longitude": -00.0000
},
"areaServed": [
{ "@type": "City", "name": "Primary City" },
{ "@type": "State", "name": "State" }
],
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "09:00",
"closes": "17:00"
}
],
"sameAs": [
"https://linkedin.com/company/yourbusiness",
"https://facebook.com/yourbusiness"
]
}
Quick Start Guide
- Define your entity model: Create a configuration object with business name, contact details, and service scope. Use the most specific
@type available on schema.org.
- Generate the payload: Use a TypeScript factory or static JSON template to build the schema object. Serialize it with
JSON.stringify and wrap it in a <script type="application/ld+json"> tag.
- Inject into the head: Place the script tag in your document
<head> using framework head management or static HTML. Ensure it loads on the homepage and primary service pages.
- Validate and monitor: Run the URL through Google’s Rich Results Test. Submit the page for indexing via Search Console. Monitor the Enhancements tab for 2-4 weeks to confirm rich result eligibility.