ELI5
Right now our event pages have no machine-readable "this is an event" tag, so Google can't show them in its events carousel. We add a hidden JSON block that tells Google "here's the title, date, venue, organiser" — and our events start appearing in rich search results.
Detailed proposal
Add a Schema.org JSON-LD emitter — either a Portal\Core\Schema\EventLdBuilder helper or a _core/templates/event-jsonld.php partial — invoked from event.php for any single-event view. Build a @type=Event payload from tblEvents (name, description, startDate, endDate, heroImage), with:
eventStatus mapped from our existing status column (Scheduled/Cancelled/Postponed → https://schema.org/Event{Scheduled,Cancelled,Postponed}).
eventAttendanceMode derived from tblEventLinks — OfflineEventAttendanceMode default; OnlineEventAttendanceMode when a Zoom/livestream link is present and no physical venue; MixedEventAttendanceMode for hybrid.
location as a Place with nested PostalAddress populated from the event's location fields (fall back to the site's address from Site::branding() when the event inherits venue).
organizer as an Organization built from hostOrgName plus any partnerOrgs, using Site::productName() and brand URL as the publisher fallback.
image as an absolute URL (resolve via Site::absoluteUrl()), url as the canonical event URL.
- Optional
offers block when tblEvents.priceText / paid flag is set (free events → price: 0, availability: InStock).
Series-aware: when an event belongs to tblEventSeries, add superEvent pointing at the series page; recurring instances from tblEventRecurrence each get their own JSON-LD on their own occurrence URL (no aggregation hack). Categories from tblEventCategories / tblEventTypes populate keywords. Wire it through AppRegistry so calendar opt-in/opt-out toggles it; gate behind a seo.jsonld.events setting (default on). Validate output against Google's Rich Results Test before merge.
Pros
- Pure additive output — no schema migration, no UI change, no auth surface.
- Unlocks Google events carousel eligibility (high CTR premium on public event pages).
- Reuses data already captured; brand-aware via
Site::branding() for ChurchMS / SchoolMS variants.
- Foundation for later
Course, Organization, LocalBusiness markup on other pages.
Cons
- Google rich-result eligibility is best-effort; not guaranteed indexing.
- Invalid markup can trigger Search Console warnings — requires validator step in CI ideally.
- Hybrid/recurring edge cases need careful mapping to avoid duplicate-event penalties.
How necessary?
Medium — high for public-facing church/community/school installs whose event discoverability matters; low for closed internal portals.
Acceptance criteria
Estimated effort
4-6 hours (one helper class + partial + admin toggle + manual validator pass).
Filed during The Events Calendar competitive analysis on 2026-06-16; decision pending.
ELI5
Right now our event pages have no machine-readable "this is an event" tag, so Google can't show them in its events carousel. We add a hidden JSON block that tells Google "here's the title, date, venue, organiser" — and our events start appearing in rich search results.
Detailed proposal
Add a Schema.org JSON-LD emitter — either a
Portal\Core\Schema\EventLdBuilderhelper or a_core/templates/event-jsonld.phppartial — invoked fromevent.phpfor any single-event view. Build a@type=Eventpayload fromtblEvents(name, description, startDate, endDate, heroImage), with:eventStatusmapped from our existing status column (Scheduled/Cancelled/Postponed→https://schema.org/Event{Scheduled,Cancelled,Postponed}).eventAttendanceModederived fromtblEventLinks—OfflineEventAttendanceModedefault;OnlineEventAttendanceModewhen a Zoom/livestream link is present and no physical venue;MixedEventAttendanceModefor hybrid.locationas aPlacewith nestedPostalAddresspopulated from the event's location fields (fall back to the site's address fromSite::branding()when the event inherits venue).organizeras anOrganizationbuilt fromhostOrgNameplus anypartnerOrgs, usingSite::productName()and brand URL as the publisher fallback.imageas an absolute URL (resolve viaSite::absoluteUrl()),urlas the canonical event URL.offersblock whentblEvents.priceText/ paid flag is set (free events →price: 0,availability: InStock).Series-aware: when an event belongs to
tblEventSeries, addsuperEventpointing at the series page; recurring instances fromtblEventRecurrenceeach get their own JSON-LD on their own occurrence URL (no aggregation hack). Categories fromtblEventCategories/tblEventTypespopulatekeywords. Wire it throughAppRegistryso calendar opt-in/opt-out toggles it; gate behind aseo.jsonld.eventssetting (default on). Validate output against Google's Rich Results Test before merge.Pros
Site::branding()for ChurchMS / SchoolMS variants.Course,Organization,LocalBusinessmarkup on other pages.Cons
How necessary?
Medium — high for public-facing church/community/school installs whose event discoverability matters; low for closed internal portals.
Acceptance criteria
event.phpemits a single<script type="application/ld+json">block for the rendered event.eventStatus,eventAttendanceMode,location,organizer,image,urlall populated correctly from existing tables.superEvent; cancelled/postponed events reflect status accurately.seo.jsonld.events(default on) toggles emission; respected byAppRegistry.Estimated effort
4-6 hours (one helper class + partial + admin toggle + manual validator pass).
Filed during The Events Calendar competitive analysis on 2026-06-16; decision pending.