Documentation menu

Add to calendar block

A one-tap "add this to my calendar" block, and the practical fix for cross-timezone invites: since a mailed email has no JavaScript to detect a recipient's own timezone, this block encodes the event's start and end as an absolute instant rather than a pre-computed local-time string, so every attendee's own calendar app displays it correctly in their own local time automatically.

How it works

The block is pure content — like Venue, there's nothing to answer, so it never appears in the responses dashboard or CSV export, and all three links it renders ride the same generic click-tracking slot every plain link-bearing block uses rather than a bespoke capability.

The links are gated on having a valid startsAt: an event with no start time authored renders just the title and description text, with no dead links pointing nowhere. If startsAt is set but endsAt isn't (or resolves to something before startsAt), the block defaults the duration to one hour after the start — filling in only "when it starts" is treated as the common case, and every downstream consumer (the .ics file, Google Calendar, Outlook.com) needs some end instant to exist. location is a plain free-typed field you fill in by hand from your Venue or Meeting link block's own text; the three blocks aren't linked automatically in this first cut, so update it yourself if the venue changes.

From those props, three links render, sharing one blockId for click-tracking purposes (the heatmap buckets by block, not by individual link, the same convention Social links uses for its multiple icons):

  • Download .ics — a signed link into GET /api/calendar that streams a hand-rolled RFC 5545 (iCalendar) file. There's no npm dependency behind it — an .ics file is plain text, built the same "pure from-scratch" way the countdown GIF encoder is. The file's DTSTART/DTEND are emitted as absolute UTC instants (a trailing Z, no TZID/VTIMEZONE component needed) — that's the actual cross-timezone fix, not something computed at render time, just a consequence of the block's startsAt/endsAt being stored as epoch milliseconds in the first place. The event's UID is a deterministic sha256 hash of its title/description/location/start/end, so re-downloading the same signed link updates one calendar entry instead of creating a duplicate. Free-typed text is escaped per RFC 5545 §3.3.11 (backslashes, commas, semicolons, literal newlines) and long lines are folded at the 75-octet limit the spec requires, so an organizer's long description can't produce a technically invalid file. The URL itself is signed the same way the countdown/formula/chart endpoints are — the whole event payload serializes into the query string (no Firestore read on the GET path), domain-separated from every other signed-URL family in the app via a "calendar:" prefix baked into the HMAC input, so a signature minted for this endpoint can never verify against another.
  • Add to Google Calendar — a plain calendar.google.com/calendar/render?action=TEMPLATE&... deep link built at emit time, no server round-trip.
  • Add to Outlook.com — a plain outlook.live.com/calendar/0/action/compose?... deep link, also built at emit time.

Title, description, and location are each defensively sliced to fixed maximum lengths (200 / 2,000 / 300 characters) before any of the three links are built, so a very long free-typed field can't produce a broken .ics link or an oversized query string on the two web links.

Configurable fields:

  • Event title.
  • Description (optional).
  • Starts at — required for any links to render.
  • Ends at (defaults to 1 hour after start).
  • Location (optional, free text).
  • Background color, padding, border and border radius on the card.
  • Event title color/size, and the three links' shared color/background overrides.

Examples

Subject

Save the date: our fall meetup

Every "Event" purpose project starts with an Add to calendar block already in place alongside Meeting link, Venue, and RSVP — see Events & webinars for the full invite-to-reminder pattern this block is part of. A sports club reuses the same block week over week for practice and match times, so every family's calendar reflects the schedule correctly regardless of which timezone they're traveling in — see Sports teams & clubs. A webinar series can drop this into a confirmation email sent right after registration, so the recipient adds it to their calendar in the same moment they signed up, rather than relying on them to remember later.

What the static fallback looks like

<div style="margin:12px 0;background-color:transparent">
  <p style="margin:0 0 8px;font-weight:600;font-size:16px;color:#111827">Q3 Product Launch</p>
  <a href="https://mailinapp.com/api/calendar?d=eyJ0aXRsZSI6...&s=abc123..."
     style="display:inline-block;padding:10px 20px;background-color:#4f46e5;color:#ffffff;
            border-radius:6px;text-decoration:none;font-size:14px;font-weight:600;margin:0 8px 8px 0">
    Download .ics
  </a>
  <a href="https://calendar.google.com/calendar/render?action=TEMPLATE&text=Q3+Product+Launch&dates=20260901T170000Z%2F20260901T180000Z"
     style="display:inline-block;padding:10px 20px;background-color:#4f46e5;color:#ffffff;
            border-radius:6px;text-decoration:none;font-size:14px;font-weight:600;margin:0 8px 8px 0">
    Add to Google Calendar
  </a>
</div>

This is the entire mailed block — three plain links (the third, Outlook.com, omitted here for brevity) built from the same event details, no image or gated content involved.