A SharePoint calendar event looks correct in SharePoint but appears one day early in Outlook, spans two days in Google Calendar, or duplicates every time the feed refreshes. ICS is a simple text format with several sharp edges.
The problem: all-day events are not midnight meetings
If an all-day event is exported as midnight in a local time zone, another calendar may convert it and move it to the prior day.
// Fragile for an all-day event
DTSTART:20261029T000000
DTEND:20261029T235900
The quirk: the all-day end date is exclusive
A one-day event on October 29 should end on October 30 in ICS. That does not make it a two-day event.
BEGIN:VEVENT
UID:sharepoint-calendar-481@example.edu
DTSTART;VALUE=DATE:20261029
DTEND;VALUE=DATE:20261030
SUMMARY:Faculty Flex Day
END:VEVENT
Another quirk: titles are not unique IDs
Two departments can both create “Staff Meeting.” I use the source site, list, and item ID to build a UID that survives title edits.
$uid = sprintf(
'sp-%s-%s-%d@example.edu',
$siteKey,
$listKey,
$itemId
);
The workaround: normalize before writing ICS
I first normalize start, end, all-day status, summary, location, description, and UID. Then I escape commas, semicolons, backslashes, and line breaks. When a source event is removed, its stable UID tells the consumer which event disappeared.
The file generation is the easy part. The real solution is getting identity and date semantics right before producing the file.
