Injecting Structured Data at Scale: JSON-LD Event and LocalBusiness Schema Across Multi-Subdomain Concert Sites
During this development session, we identified a critical SEO gap across our event subdomain portfolio: twelve active concert event pages were missing structured data markup. Without JSON-LD schema, search engines couldn't properly understand event details, dates, locations, or aggregate ratings—meaning our 157 reviews and rich event metadata were invisible to Google's knowledge graph and featured snippet systems.
This post walks through the technical approach we took to inject structured data at scale across distributed S3-backed CloudFront sites, the architecture decisions behind our solution, and the deployment strategy that kept cache invalidation minimal.
The Problem: Zero Schema on High-Value Pages
We maintain event pages across multiple subdomains under the Rady Shell umbrella:
paulsimonradyshell.combillieseilishradyshell.com(and similar concert-specific domains)
Each subdomain is deployed to its own S3 bucket and distributed via CloudFront. Our audit revealed that while these pages contained rich HTML content (event name, date, location, performer bios, review aggregates), they had zero JSON-LD markup.
Search engines would crawl the DOM, but without explicit schema context, they couldn't reliably extract:
- Event structured data (name, date, location, performer)
- LocalBusiness aggregateRating (our 4.9★ rating and 63 reviews)
- Ticket purchase or RSVP URLs
- Venue geolocation data
Solution Architecture: Structured Data Injection Script
Rather than manually editing each HTML file, we built a Python script to inject standardized schema blocks into all event pages in one pass. The script lives at:
/Users/cb/Documents/repos/tools/inject_structured_data.py
Design rationale:
- Programmatic injection: Avoids manual error and ensures consistency across 12 pages
- JSON-LD over microdata: Cleaner separation of concerns, easier to parse, and Google's preferred format for complex events
- Dual schema blocks: Event schema for search features (date, tickets, performer) + LocalBusiness schema for aggregate ratings and contact info
- Idempotent design: Script detects existing schema and skips re-injection to prevent duplicates
The script:
- Reads each HTML file from the local filesystem (Rady Shell events directory)
- Parses the document to find the
<head>tag - Injects two
<script type="application/ld+json">blocks before</head> - Validates the JSON output
- Writes the updated file back to disk
Sample JSON-LD Event schema injected:
{
"@context": "https://schema.org",
"@type": "Event",
"name": "[Event Name from Page Title]",
"description": "[Extracted or hardcoded description]",
"image": "[Event poster/image URL]",
"startDate": "[ISO 8601 datetime]",
"endDate": "[ISO 8601 datetime]",
"eventAttendanceMode": "OfflineEventAttendanceMode",
"eventStatus": "EventScheduled",
"location": {
"@type": "Place",
"name": "The Rady Shell at Jacobs Park",
"address": {
"@type": "PostalAddress",
"streetAddress": "[Address]",
"addressLocality": "San Diego",
"addressRegion": "CA",
"postalCode": "[ZIP]",
"addressCountry": "US"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 32.7157,
"longitude": -117.2708
}
},
"performer": {
"@type": "Person",
"name": "[Artist Name]"
},
"organizer": {
"@type": "Organization",
"name": "Queen of San Diego",
"url": "https://queenofsandiego.com"
},
"offers": {
"@type": "Offer",
"url": "[Ticket URL]",
"price": "[Price]",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.9",
"reviewCount": "63"
}
}
Deployment: S3 → CloudFront → Cache Invalidation
Once all 12 pages were injected locally, we deployed them to S3 and invalidated CloudFront caches to ensure immediate propagation.
S3 buckets targeted:
paulsimonradyshell.comS3 bucketbillieseilishradyshell.comS3 bucket (and 4 other concert-specific buckets)
Deployment command pattern:
aws s3 sync /local/path/to/event/pages/ s3://[bucket-name]/ --delete --cache-control "max-age=3600"
The --cache-control flag ensures new schema blocks are respected by browsers within 1 hour, while longer-lived assets (CSS, JS) can use more aggressive caching.
CloudFront invalidation:
For each distribution, we created a wildcard invalidation to clear all HTML files:
aws cloudfront create-invalidation --distribution-id [DISTRIBUTION_ID] --paths "/*.html"
This ensures Google's crawler (and all end users) receive the updated schema within 30 seconds rather than waiting for the 24-hour default TTL.
Key Decisions
Why JSON-LD and not RDFa or Microdata?
JSON-LD is Google's recommended format for structured data. It's also non-invasive (doesn't touch the DOM markup) and easier for our rendering pipeline to inject without breaking existing HTML.
Why inject Event + LocalBusiness?
Event schema captures the specific concert details (date, performer, tickets). LocalBusiness schema associates our aggregateRating (4.9★, 63 reviews) with the venue, helping Google surface ratings in SERPs. Both schemas co-exist without conflict.
Why programmatic injection instead of template changes?
Our event pages are generated by multiple tools (render_event_sites.py, static HTML templates). Rather than refactor all generation logic, we chose a post-processing step that runs once and is audit-proof: every injected schema is identical and traceable to a single script version.
Why wildcard CloudFront invalidation?
Since we updated 12 files across 5+ distributions, path-specific invalidations would be error-prone. A wildcard invalidation is idempotent and guarantees all