1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「カレンダーに追加」は難しくない! 静的サイトだけで .ics を配るW杯日程ツールを作った話

1
Posted at

はじめに

普段は、言語勉強のアプリを作っている個人開発者です。

2026ワールドカップ楽しみなので、見逃さないためにWebサイトをつくりました。「自分が観たい試合だけカレンダーに入れたい」と思って、観たい試合をチェックして .ics でダウンロードできる静的サイトです。github pagesです。

output.gif

FIFAの公式サイトにあってもよさそうな機能ですがないようなので、バイブコーディング作りました。各種イベントサイトに「カレンダーに追加」が増えてほしいので記事を書きます。

「カレンダーに追加」は、、.ics(iCalendar)ファイルを生成して配るだけです。サーバーもAPIも一切いりません。HTML/CSS/JS だけの静的サイトで完結します。実際このツールも、単一のHTMLファイルを GitHub Pages に置いているだけです。

この記事では、その「意外と簡単」な部分を、実際のコードとともに解説します。とくにハマりがちなタイムゾーンの扱いを重点的に書きます。

そもそも .ics とは何か

.icsiCalendar という形式のプレーンテキストファイルです。RFC 5545 で標準化されていて、Google Calendar、Apple Calendar、Outlook など主要なカレンダーアプリがほぼすべて対応しています。

中身は単純で、このようなテキストです。

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//example//mytool//EN
BEGIN:VEVENT
UID:match-1@example.com
DTSTAMP:20260604T120000Z
DTSTART:20260611T190000Z
DTEND:20260611T210000Z
SUMMARY:🇲🇽 MEX v 🇿🇦 RSA
LOCATION:Estadio Azteca, Mexico City
END:VEVENT
END:VCALENDAR

VCALENDAR の中に VEVENT(予定)を並べるだけ。ユーザーがこのファイルを開くと、カレンダーアプリが「この予定を追加しますか?」と聞いてくれます。

つまり、この文字列を組み立ててダウンロードさせれば、それだけで「カレンダーに追加」機能が完成します。

最小実装:文字列を組み立ててダウンロードさせる

やることは2つだけです。

  1. .ics 形式の文字列を組み立てる
  2. その文字列を Blob にしてダウンロードリンクから落とす

1. 文字列を組み立てる

function buildICS(events) {
  const lines = [
    "BEGIN:VCALENDAR",
    "VERSION:2.0",
    "PRODID:-//example//worldcup//EN",
    "CALSCALE:GREGORIAN",
    "METHOD:PUBLISH",
  ];

  for (const ev of events) {
    lines.push(
      "BEGIN:VEVENT",
      `UID:${ev.id}@example.com`,
      `DTSTAMP:${icsStamp(new Date())}`,
      `DTSTART:${icsStamp(ev.start)}`,
      `DTEND:${icsStamp(ev.end)}`,
      `SUMMARY:${esc(ev.title)}`,
      `LOCATION:${esc(ev.location)}`,
      "END:VEVENT"
    );
  }

  lines.push("END:VCALENDAR");
  return lines.join("\r\n"); // ← 改行は CRLF
}

ポイントが2つあります。

改行は \r\n(CRLF)にする。 RFC 5545 は改行コードを CRLF と定めています。\n だけでも多くのアプリは寛容に読んでくれますが、Outlook など一部の実装は厳格なので、素直に CRLF にしておくのが安全です。

特殊文字はエスケープする。 , ; \ は予約文字なのでバックスラッシュでエスケープ、改行は \n(リテラルの2文字)に変換します。

function esc(s) {
  return String(s)
    .replace(/[\\;,]/g, (m) => "\\" + m)
    .replace(/\n/g, "\\n");
}

2. Blob にしてダウンロードさせる

ここがサーバー不要のキモです。生成した文字列を Blob にして、URL.createObjectURL で一時URLを作り、<a download> をプログラムからクリックするだけ。

function download(events) {
  const ics = buildICS(events);
  const blob = new Blob([ics], { type: "text/calendar;charset=utf-8" });
  const url = URL.createObjectURL(blob);

  const a = document.createElement("a");
  a.href = url;
  a.download = "worldcup.ics";
  document.body.appendChild(a);
  a.click();
  a.remove();

  setTimeout(() => URL.revokeObjectURL(url), 1000); // 後始末
}

これで完成です。

本題:タイムゾーンをどう扱うか

用意する時間は、UTCにすればOKです。.ics には、UTC をそのまま書ける記法があります。末尾に Z を付けるだけです。

DTSTART:20260611T190000Z

末尾の Z(Zulu time = UTC)が「これはUTCの絶対時刻だ」という意味になります。

Z 付きの UTC で書いておくと、カレンダーアプリ側が、ユーザーのローカルタイムゾーンに自動変換して表示してくれます。

UTC への変換コード

JavaScript の Date.UTC() を使えば、UTC の絶対時刻を簡単に作れます。

// 例:メキシコシティ現地 6/11 15:00 (CDT, UTC-5) のキックオフ
// → UTC では 6/11 20:00 ... のように、会場TZのオフセットを足して UTC を求める
const kickoffUTC = new Date(Date.UTC(2026, 5, 11, 20, 0, 0));
// 月は 0 始まりなので 5 = 6月

そして .ics に書き出すフォーマッタ。getUTCHours() など UTC 系のゲッターを使うのがポイントです(ローカル系の getHours() を使うと、生成する人の環境によって結果が変わってしまう)。

function pad(n) {
  return String(n).padStart(2, "0");
}

function icsStamp(d) {
  return (
    d.getUTCFullYear() +
    pad(d.getUTCMonth() + 1) +
    pad(d.getUTCDate()) +
    "T" +
    pad(d.getUTCHours()) +
    pad(d.getUTCMinutes()) +
    pad(d.getUTCSeconds()) +
    "Z" // ← これが UTC の目印
  );
}

これで、どの国の誰がいつダウンロードしても、生成される .ics は同一。そしてそれぞれのカレンダーアプリが、見る人のタイムゾーンに勝手に直してくれます。

つまずきやすいポイントまとめ

実際に作ってみて、ハマった/気をつけた点を挙げておきます。

  • 改行は CRLF(\r\n)。 厳格なアプリ対策。
  • , ; \ のエスケープ。 会場名にカンマが入ると壊れがち。
  • UTC 系ゲッター(getUTCHours 等)を使う。 ローカル系を使うと生成環境依存になる。
  • UID は予定ごとに一意に。 同じ UID だとカレンダー側で「同じ予定」と見なされ、再ダウンロード時に重複登録を防げる一方、UID が衝突すると上書きされる。用途に応じて設計する。
  • DTEND を忘れない。 終了時刻がないと終日予定扱いになるアプリがある。今回は一律「開始+2時間」にした。
  • 日付の繰り上がり。 「現地 深夜0:00」の試合を UTC に直すと日付がまたぐ。ここはデータ作成時にミスりやすいので要検算(私も最初いくつか1日ずれていました)。

主張:日付を扱うサービスは、.ics をつけてもいいのではないか

「カレンダー連携」と聞いて身構えていた過去の自分に、「.ics を文字列で吐くだけだよ」と言ってあげたいです。同じように身構えている人の参考になれば。

おわりに:期間限定のサイトです

最後に宣伝を少しだけ。今回作ったツールはこれです。
無料・広告もないです。

このサイトは 2026年7月19日(決勝の日)までしか意味がありません。賞味期限のあるサイトです。もしワールドカップを観る予定があるなら、ぜひ使ってみてください!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?