# Spatie's calendar-links package is mostly about getting timezones right

> calendar-links generates add to calendar links for Google, Yahoo, Outlook and ics files, and its documentation spends most of its length on timezone semantics: which zone names a calendar service will accept, what happens to an event that crosses zones, and why the Etc tree of zone names is refused outright. That is the interesting part of a small package.

**spatie/calendar-links** — Generate add to calendar links for Google, iCal and other calendar systems

- Repository: https://github.com/spatie/calendar-links
- Website: https://spatie.be/opensource
- Stars: 1,017 · Forks: 157
- Language: PHP
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/spatie-calendar-links

## One object, several calendar targets

The package generates links that add events to calendar systems, and the shape of it is a single Link object with a method per target. The quick example gives the whole idea:

```php
use Spatie\CalendarLinks\Link;

Link::create(
    'Birthday party',
    new DateTime('2027-03-15 10:00', new DateTimeZone('Europe/Brussels')),
    new DateTime('2027-03-15 17:00', new DateTimeZone('Europe/Brussels')),
)->google();
```

which produces a Google calendar template URL with the action, the dates, the zone and the text. The full usage adds a description and an address, and then calls the method for each service you want to offer: google, yahoo, webOutlook for the personal Outlook service, webOffice for the organisational one, and ics for a data URI holding an ics file that opens in iCal, Apple Calendar and Outlook. The ics method also takes a format option, and with the file format it returns the raw ics body rather than a data URI, which is what you want when you attach the file to an email.

And there is a formatWith method that takes your own generator, so the set of targets is not fixed by the package. That matters more than it sounds, because every calendar service encodes the same event differently, and the ones the package does not know about will encode it differently again.

The audience is a PHP application that shows events, typically a Laravel one, and the licence is MIT, with the package published on Packagist as spatie/calendar-links and the current release 2.1.0 from 2026-08-20.

## The zone has to be a place, not an offset

The documentation is unusually emphatic about timezones, and the reason is that a calendar service needs somewhere to resolve the offset, not just a number.

Give both dates an explicit timezone. The ctz parameter of the generated link is taken from the start date, so without one your links follow the date.timezone setting of whichever machine generated them, which is the classic bug where a link works on the developer's laptop and lands at the wrong hour for the user.

Then name a place, Europe/Brussels or an alias of one such as Japan or US/Pacific, rather than an offset. The reason given is precise: parsing an ISO 8601 string leaves you with a zone named +01:00, and neither an offset nor an abbreviation such as CEST gives a calendar service a place to resolve.

The same goes for the IANA entries that stand for no place, and the documentation names them. The POSIX rule sets, EST and MST7MDT, are out. So are the other spellings of UTC. And the whole Etc/GMT plus or minus N family is out, because Google rejects it outright and, worse, its sign runs the opposite way from the offset it names, so a naive reading puts the event in the wrong hemisphere by several hours.

What the package does with those cases is worth reading, because it is a deliberate fallback rather than an error. Events in a refused zone are written in UTC instead: the dates parameter carries a Z suffix and there is no ctz, and the ics file gets a UTC DTSTART, so they land at the right instant everywhere. The trade-off is stated: the calendar has no zone to follow when the daylight saving rules of that place change.

The Etc tree is refused in full, Etc/UTC included, and the documentation says to pass UTC when you want a UTC endpoint alongside a named zone, since a refused zone at either end of a two zone event sends both ends to the fallback.

## An event that starts and ends in different zones

Most events start and end in the same place, and some do not, and the flight is the obvious case.

The documentation's example creates a link with a DateTime in Asia/Tokyo and a DateTime in America/Los_Angeles, and the difference is kept rather than flattened. On the Google side, each end is named with stz and etz, and those take priority over ctz, so ctz is left out of the link entirely:

```
https://calendar.google.com/calendar/render?action=TEMPLATE&dates=20270315T090000/20270315T093000&stz=Asia/Tokyo&etz=America/Los_Angeles&text=NH+106+Tokyo+to+Los+Angeles
```

The ics side uses the TZID parameter instead of flattening both ends to UTC:

```
DTSTAMP:20270315T000000Z
DTSTART;TZID=Asia/Tokyo:20270315T090000
DTEND;TZID=America/Los_Angeles:20270315T093000
```

The timestamp of the message itself stays in UTC, as RFC 5545 requires, and that is the detail that shows the care: the same file mixes a UTC stamp with two local times in named zones.

Each zone named this way also gets its own VTIMEZONE component, placed before the event and holding the offsets in force around it, so the file stands on its own for a client that does not resolve bare IANA names, the documentation says, giving older Outlook desktop as the example. The component lists every change the zone makes in the year either side of the event, and the last change of each kind carries a yearly recurrence rule whenever the zone repeats it on a fixed weekday of a fixed month, so the component keeps describing the zone for occurrences well beyond that year.

That recurrence rule detail is the difference between a file that works this month and a file that works next March, and it is the sort of thing that is rarely written down anywhere except here.

## The ics output, and plugging in your own generator

The ics output for a simple event is short enough to read in full, which is a good way to see what the package decided to include.

```
BEGIN:VCALENDAR
VERSION:2.0
PRODID:Spatie calendar-links
BEGIN:VEVENT
UID:dc01d073bb7e6c2bfae1bd5c43283062
SUMMARY:Birthday party
DTSTAMP:20270315T090000Z
DTSTART:20270315T090000Z
DTEND:20270315T160000Z
DESCRIPTION:A full day of hands-on testing. Bring a laptop.
LOCATION:Samberstraat 69D\, 2060 Antwerp\, Belgium
END:VEVENT
END:VCALENDAR
```

The producer identifier is the package itself, the identifier of the event is a generated hash, the timestamps are UTC in the single zone case, and the location escapes the commas, which is the iCalendar rule that most hand written files get wrong.

Two extension points matter here. The ics method takes an options array, and with the file format it returns the body rather than a data URI, which is the form you attach to an email. And formatWith takes any generator of your own, so a team supporting a calendar service the package does not know about writes a small class rather than reverse engineering the URL format of a service that might change it.

The package also generates ics files that can be opened in several email and calendar programs, including Microsoft Outlook, Google Calendar and Apple Calendar, which is the check worth doing before you rely on it: open the generated file in each of the clients your users have, because the differences between them are exactly where a link that looks fine in the documentation fails in practice.

## Quality gates, and a major version that broke things

The repository is small and conventional for a Spatie package, and the tooling is visible in the badges.

There is a run-tests workflow, a Scrutinizer quality score badge, and two badges from a service that reports type coverage and the Psalm level the code is held to. In the repository that means a Psalm configuration file, a PHPUnit configuration, a PHP-CS-Fixer configuration, an editor configuration, and a changelog.

There is also an UPGRADE.md file, which is the part worth noticing. The version history shows 2.0 on 2026-02-10, 2.0.1 on 2026-05-29 and 2.1.0 on 2026-08-20, and the existence of an upgrade guide alongside a 2.0 release means the major version changed something in the public API.

The timezone work in this version is the most likely candidate for what changed, because the behaviour described in the documentation is subtle: the ctz parameter, the refusal of the Etc tree, the UTC fallback, the stz and etz parameters, the VTIMEZONE component with yearly recurrence rules. A package that has just been through that much care is not one to pin at an old major version and expect the same output.

For an adopter, the practical checklist is short. Read the upgrade guide if you are moving from 1.x, pass named zones explicitly to both dates, and check the generated link for a couple of your real events against the calendar clients your users actually have.

## Conclusion

calendar-links suits a Laravel or PHP application that offers an event and wants the visitor to add it to their own calendar in one click, and it is a good choice if your events are in a specific city, because the zone handling is deliberate rather than incidental. It is the wrong choice if you need the event to travel across zones and land at the same wall clock time everywhere, since a named zone means the local time shifts when the destination changes their clocks, and a poor match if your events are in a region with no IANA zone name you trust. Before using it, pass explicit named zones to both dates, and read the sections on the POSIX rule sets and the Etc family, because those are the cases the package handles by falling back to UTC.

## FAQ

### How do I generate a Google Calendar link with spatie/calendar-links?

Create a Link with a title and a start and end DateTime, then call the google method. The description and address methods add more detail, and the yahoo, webOutlook, webOffice and ics methods produce the same event for the other targets, while formatWith accepts a generator of your own.

### Why must I pass a named timezone instead of an offset?

The ctz parameter is taken from the start date, and a calendar service needs a place to resolve rather than a number, so an ISO 8601 string that parses to a zone named +01:00 or an abbreviation such as CEST will not work. Name a place such as Europe/Brussels, or an alias like Japan or US/Pacific.

### What happens when I use a POSIX zone or an Etc/GMT zone?

The POSIX rule sets such as EST and MST7MDT, the other spellings of UTC and the whole Etc family including Etc/UTC are refused, and Google rejects the Etc family outright with a sign that runs the opposite way from the offset it names. Such events are written in UTC instead, with a Z suffix and no ctz, so they land at the right instant, but they have no zone to follow when the local daylight saving rules change.

### How does the package handle an event that starts and ends in different timezones?

It keeps the difference rather than flattening it. The Google link names each end with stz and etz, which take priority over ctz, and the ics file uses TZID parameters with a VTIMEZONE component for each zone holding the offsets in force around the event, including yearly recurrence rules, while the message timestamp itself stays in UTC as RFC 5545 requires.

## Sources

- [License: MIT](https://github.com/spatie/calendar-links/blob/master/LICENSE)
- [Project website](https://spatie.be/opensource)
- [README](https://github.com/spatie/calendar-links/blob/master/README.md)
- [Releases](https://github.com/spatie/calendar-links/releases)
- [spatie/calendar-links on GitHub](https://github.com/spatie/calendar-links)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/spatie-calendar-links
