You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Originally scoped in transformstudios/zakat.org#2329; moved here since all the work is in this repo.
Agreed approach. One blocking dependency on transformstudios/prime (see bottom). Targets main (7.0); 6.x maintenance continues on the 6.x branch.
Context
Events needs to express two independent things about where an event happens: an online URL (Zoom, livestream) and a location that may be a plain string ("Outside the side exit") or an address ("123 Main St, Surrey, BC"). Both optional, both settable at once — a hybrid event is in a room and on Zoom.
Today neither is a real field. resources/fieldsets/event.yaml declares recurrence and timing only. The addon reads four undeclared handles off the entry and guesses at their meaning in src/Types/Event.php: eventUrl() returns link, else location if Str::isUrl(); icsAddress() returns address ?? location, if it's a string.
So location is overloaded to mean both a place and a URL, resolved by sniffing, and the docs contradict themselves about it — DOCUMENTATION.md line 45 lists address/coordinates/description, line 266 lists location/description/link. The is_string() guard came from #184, merged the same day the original issue was filed: a group-shaped location was reaching spatie's address(string $address) and throwing a TypeError, i.e. a 500 on a public download route.
Outcome: location, online_url and coordinates become declared, documented, first-class fields with an unambiguous ICS mapping, and the sniffing is deleted.
What the research settled
location is unanimous across RFC 5545, the Google Calendar API, MS Graph and schema.org — always free text. That validates this issue's framing (one field, plain string or address) and kills address, which nothing else uses.
url and link are both wrong for a join link. RFC 5545 URL means "a location where a more dynamic rendition of the calendar information can be found" — the event's page, not the join link. Systems that model a join link name it for joining (joinUrl, hangoutLink, conferenceData). Hence online_url, which also leaves url free to mean the real ICS URL later.
No dedicated conference property is worth emitting. Google discards it on import: "When you import an event, guests and conference data for that event are not imported." RFC 7986's CONFERENCE is inert in every mainstream client. Fantastical and friends simply pattern-match provider URLs out of the event content across 30+ services — no property involved. So what matters is the URL landing in a field clients actually render.
Decisions
Release
7.0 breaking, with an update script that migrates existing entries. The current contract is sniffing on undeclared fields with self-contradicting docs — there's no coherent published API to preserve, and a compat shim would outweigh the feature.
address, link, and the Str::isUrl($location) sniff
Non-string location
Ignored silently at runtime, never fatal. The addon never reaches into nested shapes — computed values stay the documented escape hatch.
ICS mapping
Event
LOCATION:
URL:
GEO:
Physical only
location
—
coordinates
Online only
online_url
online_url
—
Hybrid
location
online_url
coordinates
The LOCATION fallback is the point: it makes an online-only event's join link clickable in Apple Calendar and auto-detected by Fantastical, with no vendor properties. It's what Fantastical's own Zoom integration does ("Add meeting URL to location"). No CONFERENCE, no X-GOOGLE-CONFERENCE, no X-APPLE-STRUCTURED-LOCATION.
Work
1. Fieldset — resources/fieldsets/event.yaml. Append a Location section following the existing times_sections pattern: location_section (type: section); location (type: text, localizable: true, instructions covering both the plain-description and address cases); online_url (type: text, input_type: url, validate: [nullable, url], localizable: false, display "Online URL"); coordinates (type: group of latitude/longitudefloat fields — Statamic 6 core has no map fieldtype, and a group matches what the addon already documents and reads).
2. Resolution — src/Types/Event.php. Delete eventUrl() and icsAddress(). Replace with icsUrl() returning online_url, and icsLocation() returning location and falling back to online_url when location is empty. Read via $this->event->get() throughout rather than today's mix of raw get() and augmented __get. Keep an is_string() guard on location — someone can always override the field type in their blueprint, and a TypeError on a public route is the worst failure mode.
3. Fix multi-day, and de-duplicate. The address/coordinates/description/url block is copy-pasted three times — Event.php:116-130, MultiDayEvent.php:67-81, RecurringEvent.php:36-50 — and a fourth path is missing it entirely. Bug:MultiDayEvent::toICalendarEvents() (the whole-event download, no date param) maps days through Day::toICalendarEvent(), which sets only UID/start/end — so location, URL, description and coordinates are silently dropped for every multi-day event downloaded that way. The single-date route works, which is why it's gone unnoticed. Add one protected function decorate(ICalendarEvent $iCalEvent): ICalendarEvent on the base Event and call it from all four sites. Day stays ignorant of location — it's about dates and times; MultiDayEvent decorates what Day returns. Fixing the bug and removing the duplication is the same edit. While in here, guard coordinates before passing to spatie's coordinates(float, float) — a partial or non-numeric value is #184's exact crash in a different spot.
4. Update script — src/UpdateScripts/MigrateLocationFields.php. Follows the existing ConvertConfigToSettings: shouldUpdate() returns $this->isUpdatingTo('7.0'), auto-discovered from src/UpdateScripts, reports via $this->console(). isUpdatingTo is a crossing check (7.0 <= new && 7.0 > old), so a 5.x → 7.0 jump correctly fires both the existing 6.0 script and this one. Iterate entries in the collections from Events::setting('collections'), across all localizations, applying only to handles Events owns: address → location; link → online_url; a location that is a string and passes Str::isUrl() → online_url. Then remove the old handles.
Never touch a non-string location — an array-shaped location belongs to another package (see Dependency below) and reshaping it here would destroy data the owning package still renders. Log those entry IDs and move on. Two further ambiguous cases log and skip rather than guess, listing entry IDs for manual resolution: address and a non-URL location both set (today address wins for LOCATION, so the location value would be lost); and link and a URL-valued location both set. This rewrites content files across collections and sites, so the upgrade notes need a back-up-first warning, and it can't help anyone who mapped these fields via computed values.
5. Tests — tests/Http/Contollers/IcsControllerTest.php. Rewrite the beforeEach fixture onto the new handles, then cover the mapping table directly: physical only (LOCATION: is the location, no URL:); online only (LOCATION: and URL: both the join URL); hybrid (LOCATION: the place, URL: the join URL); coordinates → GEO:, and a partial/non-numeric value not fatal; non-string location ignored rather than fatal, collapsing #184's three near-identical tests into the one invariant that matters; and a multi-day whole-event download carrying location/URL/description as the regression test for §3. Add tests/UpdateScripts/MigrateLocationFieldsTest.php for §4: each migration row, both ambiguous cases logging and leaving data untouched, and an array-shaped location left completely untouched.
6. Docs — DOCUMENTATION.md. Rewrite the ICS Downloads field list (~line 43) to location, online_url, coordinates, description; fix the events:download_link list (~line 266) so it agrees; add a "Location & Online URL" subsection under Fields with the mapping table, noting the two are independent and combinable and that a non-string location is skipped, plus the computed-value pointer for custom shapes; and add 7.0 upgrade notes covering the renames, what the update script does automatically, what it skips, and the back-up-first warning.
Dependency — Prime must rename its location group
Tracked separately in Prime. 7.0 cannot ship into a Prime site until it's done.
Statamic's Fields::resolveFields() does ->keyBy->handle(), so duplicate handles collapse silently — last import wins. Today Events declares no location, so Prime owns the handle uncontested. Once Events 7 declares location as text, any blueprint importing both prime::location and events::event has two location fields and one loses its data. That is exactly this site's event blueprint.
Prime's group isn't disposable — it renders location:details and location:coordinates:latitude in card_event.antlers.html and list_event.antlers.html. So Prime renames its group to a non-colliding handle and ships its own migrator; details stays a Prime feature. prime::location and simple::location are byte-identical, so one shape covers both.
Checked sacair while scoping this: its events have no location data at all (0 of 11 entries use location, address, link or coordinates), and the group-with-coordinates shape lives in its separate locations "Map Point" collection, which Events never touches. It's also on transformstudios/events: ^5.0 and statamic/cms: ^5.73, so it can't take Events 7 until Statamic 5→6 and Simple→Prime happen anyway.
Deliberately out of scope
venue / spatie's addressName() — would unlock X-APPLE-STRUCTURED-LOCATION (a map pin in Apple Calendar) when paired with coordinates. Real value, but new scope.
description as a declared field — still documented-but-undeclared, as coordinates was. Worth doing, but not what this issue asks for. Note an augmented bard/markdown description reaching description(string) is the same crash class as Only pass string addresses to ICS downloads #184.
Verify
vendor/bin/pest in the events repo, then confirm against a real site. The events sandbox site symlinks the checkout via a composer path repo and imports events::event but no Prime fieldsets, so it's a clean test of Events standing alone. Walk the mapping table — create a physical-only, an online-only and a hybrid event, download each .ics from events:download_link, and check LOCATION:/URL:/GEO: match. Open the online-only one in Calendar.app to confirm the link is clickable. Then a multi-day event downloaded without a date param, which is the §3 regression.
Steps
Each lands independently with its own tests and docs.
Originally scoped in transformstudios/zakat.org#2329; moved here since all the work is in this repo.
Agreed approach. One blocking dependency on
transformstudios/prime(see bottom). Targetsmain(7.0); 6.x maintenance continues on the6.xbranch.Context
Events needs to express two independent things about where an event happens: an online URL (Zoom, livestream) and a location that may be a plain string ("Outside the side exit") or an address ("123 Main St, Surrey, BC"). Both optional, both settable at once — a hybrid event is in a room and on Zoom.
Today neither is a real field.
resources/fieldsets/event.yamldeclares recurrence and timing only. The addon reads four undeclared handles off the entry and guesses at their meaning insrc/Types/Event.php:eventUrl()returnslink, elselocationifStr::isUrl();icsAddress()returnsaddress ?? location, if it's a string.So
locationis overloaded to mean both a place and a URL, resolved by sniffing, and the docs contradict themselves about it —DOCUMENTATION.mdline 45 listsaddress/coordinates/description, line 266 listslocation/description/link. Theis_string()guard came from #184, merged the same day the original issue was filed: a group-shapedlocationwas reaching spatie'saddress(string $address)and throwing a TypeError, i.e. a 500 on a public download route.Outcome:
location,online_urlandcoordinatesbecome declared, documented, first-class fields with an unambiguous ICS mapping, and the sniffing is deleted.What the research settled
locationis unanimous across RFC 5545, the Google Calendar API, MS Graph and schema.org — always free text. That validates this issue's framing (one field, plain string or address) and killsaddress, which nothing else uses.urlandlinkare both wrong for a join link. RFC 5545URLmeans "a location where a more dynamic rendition of the calendar information can be found" — the event's page, not the join link. Systems that model a join link name it for joining (joinUrl,hangoutLink,conferenceData). Henceonline_url, which also leavesurlfree to mean the real ICSURLlater.No dedicated conference property is worth emitting. Google discards it on import: "When you import an event, guests and conference data for that event are not imported." RFC 7986's
CONFERENCEis inert in every mainstream client. Fantastical and friends simply pattern-match provider URLs out of the event content across 30+ services — no property involved. So what matters is the URL landing in a field clients actually render.Decisions
location(text,localizable: true),online_url(text/url,localizable: false),coordinates(group oflatitude/longitudefloats)address,link, and theStr::isUrl($location)snifflocationICS mapping
LOCATION:URL:GEO:locationcoordinatesonline_urlonline_urllocationonline_urlcoordinatesThe
LOCATIONfallback is the point: it makes an online-only event's join link clickable in Apple Calendar and auto-detected by Fantastical, with no vendor properties. It's what Fantastical's own Zoom integration does ("Add meeting URL to location"). NoCONFERENCE, noX-GOOGLE-CONFERENCE, noX-APPLE-STRUCTURED-LOCATION.Work
1. Fieldset —
resources/fieldsets/event.yaml. Append a Location section following the existingtimes_sectionspattern:location_section(type: section);location(type: text,localizable: true, instructions covering both the plain-description and address cases);online_url(type: text,input_type: url,validate: [nullable, url],localizable: false, display "Online URL");coordinates(type: groupoflatitude/longitudefloatfields — Statamic 6 core has no map fieldtype, and a group matches what the addon already documents and reads).2. Resolution —
src/Types/Event.php. DeleteeventUrl()andicsAddress(). Replace withicsUrl()returningonline_url, andicsLocation()returninglocationand falling back toonline_urlwhenlocationis empty. Read via$this->event->get()throughout rather than today's mix of rawget()and augmented__get. Keep anis_string()guard onlocation— someone can always override the field type in their blueprint, and a TypeError on a public route is the worst failure mode.3. Fix multi-day, and de-duplicate. The address/coordinates/description/url block is copy-pasted three times —
Event.php:116-130,MultiDayEvent.php:67-81,RecurringEvent.php:36-50— and a fourth path is missing it entirely. Bug:MultiDayEvent::toICalendarEvents()(the whole-event download, nodateparam) maps days throughDay::toICalendarEvent(), which sets only UID/start/end — so location, URL, description and coordinates are silently dropped for every multi-day event downloaded that way. The single-date route works, which is why it's gone unnoticed. Add oneprotected function decorate(ICalendarEvent $iCalEvent): ICalendarEventon the baseEventand call it from all four sites.Daystays ignorant of location — it's about dates and times;MultiDayEventdecorates whatDayreturns. Fixing the bug and removing the duplication is the same edit. While in here, guardcoordinatesbefore passing to spatie'scoordinates(float, float)— a partial or non-numeric value is #184's exact crash in a different spot.4. Update script —
src/UpdateScripts/MigrateLocationFields.php. Follows the existingConvertConfigToSettings:shouldUpdate()returns$this->isUpdatingTo('7.0'), auto-discovered fromsrc/UpdateScripts, reports via$this->console().isUpdatingTois a crossing check (7.0 <= new && 7.0 > old), so a 5.x → 7.0 jump correctly fires both the existing 6.0 script and this one. Iterate entries in the collections fromEvents::setting('collections'), across all localizations, applying only to handles Events owns:address→location;link→online_url; alocationthat is a string and passesStr::isUrl()→online_url. Then remove the old handles.Never touch a non-string
location— an array-shapedlocationbelongs to another package (see Dependency below) and reshaping it here would destroy data the owning package still renders. Log those entry IDs and move on. Two further ambiguous cases log and skip rather than guess, listing entry IDs for manual resolution:addressand a non-URLlocationboth set (todayaddresswins forLOCATION, so thelocationvalue would be lost); andlinkand a URL-valuedlocationboth set. This rewrites content files across collections and sites, so the upgrade notes need a back-up-first warning, and it can't help anyone who mapped these fields via computed values.5. Tests —
tests/Http/Contollers/IcsControllerTest.php. Rewrite thebeforeEachfixture onto the new handles, then cover the mapping table directly: physical only (LOCATION:is the location, noURL:); online only (LOCATION:andURL:both the join URL); hybrid (LOCATION:the place,URL:the join URL);coordinates→GEO:, and a partial/non-numeric value not fatal; non-stringlocationignored rather than fatal, collapsing #184's three near-identical tests into the one invariant that matters; and a multi-day whole-event download carrying location/URL/description as the regression test for §3. Addtests/UpdateScripts/MigrateLocationFieldsTest.phpfor §4: each migration row, both ambiguous cases logging and leaving data untouched, and an array-shapedlocationleft completely untouched.6. Docs —
DOCUMENTATION.md. Rewrite the ICS Downloads field list (~line 43) tolocation,online_url,coordinates,description; fix theevents:download_linklist (~line 266) so it agrees; add a "Location & Online URL" subsection under Fields with the mapping table, noting the two are independent and combinable and that a non-stringlocationis skipped, plus the computed-value pointer for custom shapes; and add 7.0 upgrade notes covering the renames, what the update script does automatically, what it skips, and the back-up-first warning.Dependency — Prime must rename its
locationgroupTracked separately in Prime. 7.0 cannot ship into a Prime site until it's done.
Statamic's
Fields::resolveFields()does->keyBy->handle(), so duplicate handles collapse silently — last import wins. Today Events declares nolocation, so Prime owns the handle uncontested. Once Events 7 declareslocationas text, any blueprint importing bothprime::locationandevents::eventhas twolocationfields and one loses its data. That is exactly this site's event blueprint.Prime's group isn't disposable — it renders
location:detailsandlocation:coordinates:latitudeincard_event.antlers.htmlandlist_event.antlers.html. So Prime renames its group to a non-colliding handle and ships its own migrator;detailsstays a Prime feature.prime::locationandsimple::locationare byte-identical, so one shape covers both.Checked sacair while scoping this: its events have no location data at all (0 of 11 entries use
location,address,linkorcoordinates), and the group-with-coordinates shape lives in its separatelocations"Map Point" collection, which Events never touches. It's also ontransformstudios/events: ^5.0andstatamic/cms: ^5.73, so it can't take Events 7 until Statamic 5→6 and Simple→Prime happen anyway.Deliberately out of scope
venue/ spatie'saddressName()— would unlockX-APPLE-STRUCTURED-LOCATION(a map pin in Apple Calendar) when paired with coordinates. Real value, but new scope.descriptionas a declared field — still documented-but-undeclared, ascoordinateswas. Worth doing, but not what this issue asks for. Note an augmented bard/markdowndescriptionreachingdescription(string)is the same crash class as Only pass string addresses to ICS downloads #184.Verify
vendor/bin/pestin the events repo, then confirm against a real site. Theeventssandbox site symlinks the checkout via a composer path repo and importsevents::eventbut no Prime fieldsets, so it's a clean test of Events standing alone. Walk the mapping table — create a physical-only, an online-only and a hybrid event, download each.icsfromevents:download_link, and checkLOCATION:/URL:/GEO:match. Open the online-only one in Calendar.app to confirm the link is clickable. Then a multi-day event downloaded without adateparam, which is the §3 regression.Steps
Each lands independently with its own tests and docs.
6.x6.x, forward-mergeonline_urlfor Zoom and livestream links #193 — Addonline_urlfor Zoom and livestream links ·maincoordinatesin the fieldset #194 — Declarecoordinatesin the fieldset ·mainlocation, removeaddress,linkand URL sniffing #195 — Addlocation, removeaddress,linkand URL sniffing ·main, blocked on transformstudios/prime#834main#191, #192, #193 and #194 are unblocked, so most of the work can proceed while Prime sorts out its rename. Only #195 and #196 wait.