iToverDose/Software· 14 JUNE 2026 · 04:04

Static site calendar feeds: Build-time ICS generation without a server

Serving valid .ics calendar feeds from a static site sounds complex, but a post-build emitter handles it elegantly without infrastructure. Learn how to generate compliant ICS files at build time and avoid RFC 5545 pitfalls that break calendar clients.

DEV Community4 min read0 Comments

Static site generators excel at serving HTML, CSS, and JavaScript, but what about calendar feeds that update automatically? For a site like Calendana—built entirely with next build and deployed to Cloudflare Pages—adding subscribable .ics feeds required a different approach: file generation at build time.

The solution turned out to be simpler than expected: treat the calendar feed as a static artifact, not a live endpoint. After the site builds, a Node script reads the same data source used for rendering pages and writes .ics files directly into the out/ directory. No server, no API routes, and no risk of drift between content and calendar events. The emitter produces four types of files: yearly feeds, single-event feeds, an aggregated subscription feed, and a JSON API—all derived from a single source of truth.

A post-build emitter keeps feeds in sync

The emitter runs as part of the deployment pipeline. After next build completes, the script processes holiday data and generates static files that calendar clients poll on a schedule. This ensures the .ics files always reflect the latest events without requiring real-time processing.

The generated files include:

  • A yearly feed for each locale and year (e.g., holidays-de-2026.ics)
  • A single-event feed for individual holidays (for "download this day" buttons)
  • An aggregated subscription feed (accessible via webcal:// links)
  • A JSON API under /api/ for programmatic access

This approach eliminates the need for new pages or routes—just static files that integrate seamlessly with the existing static site architecture.

RFC 5545 compliance: Avoid silent calendar failures

Generating valid .ics files requires attention to RFC 5545, a specification that governs calendar data interchange. Subtle violations can cause calendar clients to silently ignore events or render them incorrectly.

Key requirements include:

  • Exclusive DTEND for all-day events: An event on January 1 must use DTSTART:20260101 and DTEND:20260102, not DTEND:20260101. The DTEND value is exclusive, meaning the event spans from the start date up to but not including the end date.
  • CRLF line endings: Calendar files must use `

line endings, not `. Some clients reject files with LF-only line breaks.

  • 75-octet line folding: Lines longer than 75 bytes must be folded, with continuation lines starting with a single space. This is especially critical for multilingual content, where non-ASCII characters can span multiple bytes.
  • TEXT value escaping: Commas, semicolons, backslashes, and newlines in fields like SUMMARY or DESCRIPTION must be escaped (\,, \;, \\, \n).
  • Stable UIDs: The UID field must remain consistent across rebuilds to prevent duplicate events in subscribers’ calendars. A deterministic UID like {locale}-{year}-{key}@domain ensures stability.

The following function handles line folding correctly by accounting for multi-byte UTF-8 sequences:

function foldLine(line) {
  const bytes = new TextEncoder().encode(line);
  if (bytes.length <= 75) return line;

  const dec = new TextDecoder();
  const out = [];
  let start = 0;
  let limit = 75;

  while (start < bytes.length) {
    let end = Math.min(start + limit, bytes.length);
    // Back off if the cut lands mid-codepoint (UTF-8 continuation = 10xxxxxx)
    while (end < bytes.length && (bytes[end] & 0xc0) === 0x80) end--;

    out.push((start === 0 ? "" : " ") + dec.decode(bytes.slice(start, end)));
    start = end;
    limit = 74; // Continuation lines spend 1 octet on the leading space
  }

  return out.join("\r\n");
}

Deep links: Adding events without downloading files

While .ics files cover "download" and "subscribe" actions, the most user-intent action—"Add to Google Calendar"—requires no file at all. Google Calendar and Outlook accept URLs that pre-fill event details directly in their interfaces.

For Google Calendar, the URL follows this format:

The function below constructs this URL dynamically:

function googleHref({ name, date }) {
  const compact = (iso) => iso.replace(/-/g, "");
  const next = nextDay(date); // DTEND is exclusive
  const p = new URLSearchParams({
    action: "TEMPLATE",
    text: name,
    dates: `${compact(date)}/${compact(next)}`,
  });
  return `
}

A similar approach works for Outlook, but its deep-link structure differs slightly:

Omitting the deeplink segment in the URL will cause the prefill to fail silently.

Serving the correct MIME type on a static host

Serving .ics files as text/plain can cause some calendar clients to reject them. To ensure compatibility, configure the Content-Type header in a _headers file within the public/ directory:

/*.ics
  Content-Type: text/calendar; charset=utf-8
  Cache-Control: public, max-age=86400

/api/*
  Content-Type: application/json; charset=utf-8
  Access-Control-Allow-Origin: *

This setup ensures calendar clients recognize the files correctly and cache them appropriately.

The multilingual challenge: One source, many locales

Calendana supports 15 locales, and calendar terminology varies significantly across regions. Machine translation can create thin, near-duplicate pages that search engines may penalize, or worse, mislabel events for users.

For example:

  • Mexican Spanish uses "Agregar a Google Calendar" for "holidays"
  • European Spanish uses "Añadir"
  • Argentine Spanish uses "feriados"

Each locale’s microcopy must be hand-curated to ensure accuracy and cultural relevance. The solution was a shared strings file that both the Node emitter and React components read, guaranteeing consistency between the UI and the calendar feeds.

This single-source approach—where one holiday JSON feeds the pages, the feeds, and the API, and one strings file feeds the emitter and the UI—prevented the feature from becoming a maintenance burden.

Where to see it in action

Calendana provides printable calendars and public holiday data for multiple countries, all served as a static site with no backend. The calendar-export functionality is now live on every holiday page, offering users the ability to download or subscribe to events directly from their calendar applications.

For developers looking to implement similar functionality, the key takeaway is this: calendar feeds are static files, not live APIs. By generating them at build time, you can deliver a seamless experience without the complexity of a server.

AI summary

Learn how to create valid .ics calendar feeds for static sites using a post-build emitter. Avoid RFC 5545 pitfalls and serve events via webcal links or deep-link URLs.

Comments

00
LEAVE A COMMENT
ID #BKJNIN

0 / 1200 CHARACTERS

Human check

7 + 6 = ?

Will appear after editor review

Moderation · Spam protection active

No approved comments yet. Be first.