calendar-link: one event object, five calendar link formats
📅 Calendar link generator for popular services
At a glance
- What is it?
- calendar-link takes a description of an event and turns it into a link that opens the right calendar application with the event already filled in. Google Calendar, Outlook, Office 365, Yahoo, and a plain iCalendar file each have their own URL shape, and the library hides the differences behind one event object. The interesting parts are the notes, which document where the calendars disagree with each other.
- Who is it for?
- calendar-link fits a web application that needs to hand a user a button which puts an event into their own calendar, without asking them to copy a title and a time into a form. It does not fit a product that needs the guest list to work everywhere, because the guest field is not supported by every target, and it does not fit a server side rendered page that forgets to set the document URL, since the default comes from the browser.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 27 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem, and the shape of the answer
The library's job is one sentence: generate an event link for Google Calendar, Yahoo! Calendar, Microsoft Outlook, and the rest. It is a JavaScript library, published on npm with type definitions, and the demo site is hosted on the author's pages.
The problem it solves is one everybody who has built a booking or a reminder feature has hit. There is no standard way to say add this event to my calendar. Every provider has a different URL, with a different query parameter for the title, a different one for the start, a different encoding, and a different set of optional fields. The result is usually a pile of string concatenation in the application, copied from whichever answer was on the internet that day.
The library's approach is to define the event once, as an object, and then to have one function per provider that turns it into a link.
// Usage with Node.js
const { google, outlook, office365, yahoo, ics } = require("calendar-link");
// Usage with TypeScript or ES6
import { google, outlook, office365, yahoo, ics } from "calendar-link";
// Set event as an object
const event = {
uid: "your-unqiue-id",
title: "My birthday party",
description: "Be there!",
start: "2019-12-29 18:00:00 +0100",
duration: [3, "hour"],
};
// Then fetch the link
google(event); // https://calendar.google.com/calendar/render...
outlook(event); // https://outlook.live.com/owa/...
office365(event); // https://outlook.office.com/owa/...
yahoo(event); // https://calendar.yahoo.com/?v=60&title=...
ics(event); // standard ICS file based on https://icalendar.orgThe comments after each call are the useful part, because each one shows the shape of the resulting link, and the fact that the personal and the work variants of Outlook have different hosts while sharing a path is visible right there.
There are five functions and the fifth is the odd one out. The first four return a link that opens a web interface; the fifth returns a standard iCalendar file, which is the interchange format that a calendar application can import. That one is the fallback for the case where you are not sure which calendar the user has, or where the application should offer a file rather than a redirect.
The project is MIT licensed, the last push was on 2026-09-26, and the release history shows a steady run of patch releases.
The event object, and the three ways to end it
The options table is the real specification, and it is worth reading as a contract rather than as a list.
Two fields are required: the title, and the start time. The start accepts a JavaScript date object, an ISO 8601 string, or a Unix timestamp, which is a generous set and covers the three ways a date arrives in application code.
Then there is the group of fields that determine when the event ends, and the readme's note about them is the part that will save you an hour: any one of the end, the duration, or the all-day flag is required. So you must say how long the event lasts in one of three ways, and you cannot leave all three out.
The duration is an array of a value and a unit, for example three hours, and the allowed units are listed as a link to the documentation of the date library the project uses. That link is the actual specification of the units, and it points at a library rather than at a hand-written list, which is a good sign about the project's general approach: where a standard exists, use it.
The all-day flag is a boolean, and it interacts with the end date in a way the readme explains with unusual care, which is the subject of a later section.
The remaining fields are the descriptive ones: a description with information about the event, a location in words, a busy flag for marking the time as busy on the calendar, a list of guest email addresses, a document URL, and a unique identifier for the event.
Two of those deserve a note of their own. The busy flag is a tri-state in practice even though it is described as a boolean, because the interesting value is the difference between free and unspecified, and different providers spell that difference differently. And the document URL is the one field with a default that depends on where the code is running, which is the subject of the next section.
The recurrence rule is the field that shows the limits of the approach. It accepts an iCalendar recurrence rule string, and the note says it is only supported by the Google and the iCalendar functions. That is the honest answer: a URL for a recurring event is much harder, and only the two targets that can express it properly do.
The notes, where the calendars disagree
The notes section is the most valuable part of the readme, and it is a list of the places where the providers do not agree with each other. A reader who has built this feature before will recognise every line.
The first note is about guests and the document URL. Not all calendars support those two fields. The list of the ones that do support guests is given: the Google function, the Outlook function, the mobile Outlook variant, the Office 365 function, the mobile Office 365 variant, and the Teams function. And then the honesty: the support for the Outlook and Office 365 functions was tested when a particular pull request was implemented, but the mobile variants could not be tested and might not work as expected. However, all the Outlook and Office calendar links have the same path and query string structure, so they were added anyway, and even if the query string is invalid it will simply be ignored and will not break anything.
That paragraph is a good piece of engineering communication. It says what was verified, what was not, and why the unverified thing was included anyway, and it explains the failure mode, which is that a bad parameter is ignored rather than fatal.
The second note is about time zones, and it is the one that bites hardest. If you do not pass the start and end time in UTC, Google will convert it to UTC but Outlook will not, so it is a good idea to use UTC when passing dates and times.
That is a genuine asymmetry between the two providers rather than a library bug, and it means the same event object can land at different moments in two calendars if the input is not in UTC. The library cannot fix that, because it does not know what you meant, so it documents it and recommends the discipline.
The third note covers the all-day case, and it is the hardest sentence in the readme. For all-day events, date-only strings and locally constructed midnight date values use their calendar date, and the end is the exclusive end date, so you use the next day for a one-day event. Other date times retain their UTC date.
So there are two different behaviours inside one flag, depending on how the date was constructed. A string that is only a date means a local calendar day, and a date object built at local midnight means the same thing, while any other date object is interpreted in terms of its UTC date. That is a deliberate choice, and the readme explains both halves.
The exclusive end date is the part that is easy to get wrong by a day, and it is a consequence of how the underlying format works: the end is the first moment not included in the event, so a one-day event on the first of the month ends on the second.
And then there is the acknowledged limitation. There are some known issues in Office 365 because of which a consistent link cannot be generated on all devices, with a reference to the issue. The readme does not pretend that Office 365 is fully solved.
Taken together, the notes are the argument for using the library rather than writing the URLs yourself. The differences are documented, dated, and specific, and the one that is genuinely unresolvable is flagged as such.
The identifier, and the deduplication plan
One field in the options table is different from the others, and the note about it is the most forward looking statement in the readme.
The unique identifier field is currently optional, and will generate a random one if you do not provide it. In a future version, this field will be required, to ensure proper event identification and deduplication.
That is a change with a real consequence, and it is worth understanding why a calendar library needs it. The typical use is a button on a booking page that adds the event to the user's calendar. If the user clicks it twice, or if the page is reloaded, you get two events. Without a stable identifier, the calendar application has no way of knowing that the two entries are the same event, and the user has to delete one by hand.
With an identifier, the second click updates the first event instead of creating a second one. The identifier has to be stable across requests, which means it has to come from your side, and it means you have to store it, and the readme is telling you now so that you do that from the start rather than retrofitting it.
The example event in the usage section includes the field, and the value it shows is a placeholder with a typo in it, which is worth noticing because it is the kind of detail that ends up in somebody's code by copy and paste. The comment on the field in the options table describes it as the unique identifier for the event, and it sits in the same category as the other event properties rather than being a technical afterthought.
The default of a random identifier is a reasonable interim behaviour. It is not stable across page loads, which is the case that produces duplicates, and that is the case the future change is aimed at.
So the practical advice is to treat the identifier as required today. The cost is a field in your event object that you have to generate and persist, and the benefit is that a future major version of the library will not break your integration, and that your users stop getting duplicate events in the meantime.
The browser, the server, and the URL default
There is one behaviour in the library that depends on where the code is running, and the readme handles it in a single sentence.
The document URL field defaults to the current page's address if a global document object exists. For server side rendering, you should supply the URL manually.
That is the whole of it, and it is a small design decision that would otherwise be a confusing bug. The field is the address of the document that the calendar link points back to, and in a browser there is exactly one sensible default for it. On a server there is no document, so the library has to do something, and doing nothing and using an empty value is better than guessing.
But a reader might wonder why the default exists at all, and the answer is in the surrounding design. The field is there so that the event in the user's calendar points back to the page it came from, which is what makes the calendar entry useful rather than a dead end. A booking page, a release announcement, a conference session listing, all of them want the calendar entry to lead back to the source.
For a server side rendered page the default is simply wrong, and the fix is one field in the event object. That is a much smaller problem than a broken link in production, because the failure mode is visible and the fix is local.
The compatibility note closes the loop on the browser side. The package uses the URL search parameters API, which is available in Node.js 10 and later and in 97 percent or more of browsers, and if you need to include a polyfill you can include a polyfill package first.
That is the whole browser support story, and it is a short one because the library does almost nothing that needs a browser. It builds a query string. The rest of its behaviour is string formatting and date arithmetic, which is why it runs identically on a server, in a browser, and in a test.
How the package is built and released
The manifest and the repository layout describe a small, carefully tooled package.
The published entry points are three, and the reason is worth understanding. The main field points at the CommonJS build, the types field at the declarations, and the source field at the TypeScript entry point, which is a convention that lets a bundler pick up the original source. The exports map then overrides the main one with a condition, so that an import gets the modern module build and a require gets the CommonJS build, with the types alongside. And a separate field named for the universal module definition format points at a UMD bundle, which is the one you load with a script tag in a page that is not using a bundler.
So the package serves three consumers: a bundler-aware application, a plain Node script, and a browser page that just wants a script tag. The build script reflects that, with one target that produces the modern and UMD formats through a microbundle invocation with nothing left external, and another that compiles the CommonJS build with the TypeScript compiler against a separate configuration, and a third that runs both in parallel.
The two TypeScript configurations exist because the two builds need different module output, and sharing one configuration between them is the kind of compromise that produces a package with a warning message in the console.
The test stack is Jest with the TypeScript transform, a global setup file, a separate configuration file, and a watch mode and a coverage script alongside. The coverage badge in the readme is fed by a service, so the coverage number is measured rather than claimed.
The release is driven by semantic release, with plugins for the changelog, the git tag and commit, the GitHub release, the npm publish, and a commit message format plugin. That last one is why the changelog reads the way it does, and the release configuration file at the top level of the repository is where those plugins are configured.
And there is a script that is worth mentioning because it is unusual: one that updates the template from the project URL. The script pulls in a template from the project URL, which is likely how the badges and the options table get refreshed. A readme with a generated table of options is a readme that cannot go stale, which is exactly what a library like this needs, since the table is the API.
The rest of the repository is the expected set: the source directory, the changelog, the licence, a site configuration file, a formatter configuration, and the ignore files that keep the tests and the templates out of the published package.
Editorial conclusion
calendar-link fits a web application that needs to hand a user a button which puts an event into their own calendar, without asking them to copy a title and a time into a form. It does not fit a product that needs the guest list to work everywhere, because the guest field is not supported by every target, and it does not fit a server side rendered page that forgets to set the document URL, since the default comes from the browser. Verify first which of the five targets you need and test the one you are least sure about, remember to pass times in UTC, since Google converts and Outlook does not, and remember that the unique identifier is optional today but is going to be required, which affects how you should be storing and re-creating events.
Frequently asked questions
What does the calendar-link library do?
It generates a link that opens a calendar application with an event already filled in. You describe the event as an object with a title, a start, and how it ends, and then call one of five functions, one each for Google Calendar, Outlook, Office 365, Yahoo, and a standard iCalendar file.
What fields does a calendar-link event need?
A title and a start time, which may be a date object, an ISO 8601 string, or a Unix timestamp. To determine the end you must supply one of three: the end time, a duration as a value and a unit, or the all-day flag. The recurrence rule, the guest list, and the document URL are optional and are not supported by every target.
What are the gotchas in the calendar-link documentation?
That Google converts times to UTC but Outlook does not, so pass UTC. That an all-day event's end date is exclusive, so a one-day event ends on the next day. That the guest list was verified for the desktop Outlook and Office 365 links but not the mobile ones, and that an invalid parameter is ignored rather than fatal. And that there are known Office 365 issues where a link cannot be made consistent across devices.
Why does the calendar-link identifier field matter?
Because the readme says it is currently optional and will generate a random value, and that in a future version it will be required to ensure proper event identification and deduplication. A stable identifier is what stops a user who clicks the button twice from getting two events in their calendar, so it is worth generating and storing one now.
How do I use calendar-link on a server side rendered page?
Supply the document URL yourself, because the field otherwise defaults to the current page address if a global document object exists, and on a server there is no document to read it from. The rest of the library is the same on a server as in a browser, since it is string formatting and date arithmetic, and its only browser dependency is the URL search parameters API.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/anandchowdhary-calendar-link)Was this useful?