ブライダル

ブライダルフェアの予約確認に「カレンダーに追加」を付ける実装。.ics(iCalendar)の書き方、日本時間の扱い、日時の変更と取り消しの送り直し、確認メールへの添付と予約完了ページからの配り方

フェアや見学の予約を、お客様のカレンダーに入れてもらう .ics ファイルの作り方です。RFC 5545 の折り返しとエスケープ、日本時間をUTCで書く理由、変更と取り消しの送り直しを、手元で動かしたコードと出力で確かめました。

この記事の結論:フェアや見学の予約確認に「カレンダーに追加」を付けるなら、予約ごとに1つの .ics(iCalendar形式のファイル)を作って、確認メールに添付し、予約完了ページからもダウンロードできるようにします。書くときに外せないのは4つです。①改行はCRLFにし、1行75オクテットを超える行は、日本語の文字の途中で切らずに折り返す。②開始と終了は、日本時間をUTCに直して末尾にZを付ける(タイムゾーンなしの時刻は、端末の地域で読まれてずれる)。③UIDは予約ごとにランダムなUUIDを1つ作って保存し、日時を変えたらSEQUENCEを上げて送り直す。④説明欄にお客様の個人情報を入れない。そのうえで、.ics は補助と割り切り、日時と変更の方法はメール本文と予約確認ページに必ず書きます。

この記事の内容のご相談は株式会社bundlyzeへbundlyzeのWEB戦略チーム代行について見てみる

この記事は、結婚式場でフェアや見学の予約を受け付けている担当の方と、予約の仕組みや確認メールを作る開発の方に向けています。リマインドをメール・SMS・LINEのどれで送るかは見学予約のリマインドの記事で扱ったので、ここでは、予約した日時をお客様自身のカレンダーに入れてもらうための、ファイルの作り方と配り方に絞ります。コードとテストは2026年10月6日に手元のMacで動かし、出力をそのまま載せています。お客様の実際のスマホやメールアプリでの見え方は確かめていないので、その範囲は最後に分けて書きました。

「カレンダーに追加」は、来館の予定をお客様の手元に残す仕組み

予約確認メールは、受信箱の中でほかのメールに埋もれていきます。お客様が普段使っているカレンダーに予定が入っていれば、来館の日が近づいたときに、式場の連絡を開かなくても予定が目に入ります。.ics は、そのための共通の形式です。

.ics の中身は、RFC 5545(iCalendar)で決められたテキストです。拡張子 ics は、この形式のファイルに使うと RFC 5545 に書かれています。Googleカレンダー・Appleのカレンダー・Outlookなど多くのカレンダーが読めるので、式場が1つの形式で作れば、お客様がどのカレンダーを使っていても渡せます。

一方で、予定を追加するかどうかはお客様が決めることで、届いた .ics の見え方はメールアプリやカレンダーごとに違います。Googleカレンダーのヘルプで案内されている .ics の読み込みも、パソコンで設定の「インポート / エクスポート」から行う手順です。「.ics を付けたから案内は足りている」とはせず、次の3か所に同じ情報を置く前提で設計します。

置く場所 載せる内容 役割
予約確認メールの本文 日時・場所・所要時間・変更と取り消しの方法 正本。ここだけ読めば来館できる
添付の .ics 日時・場所・所要時間・式場からの案内 お客様のカレンダーに入れてもらう補助
予約完了ページ・予約確認ページ 日時・場所と、.ics のダウンロードのボタン メールが届かない・消えたときの入口

予約確認メールそのものが届かなければ、どれも意味がありません。送信元の認証など、確認メールを届ける実装の点検はフェアの予約フォームの記事にまとめています。

.ics の最小の中身と、RFC 5545 で決まっていること

フェア予約1件ぶんの .ics は、VCALENDAR の中に VEVENT を1つ入れた短いテキストです。手元のスクリプトで、2026年11月14日(土)10時から3時間のフェアの予約を書き出すと、次のようになりました(実際の改行はCRLFです)。

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//example//fair-booking//JA
METHOD:PUBLISH
BEGIN:VEVENT
UID:6f1c2b9e-3d4a-4f5b-8c7d-0e1f2a3b4c5d
SEQUENCE:0
DTSTAMP:20261006T121500Z
DTSTART:20261114T010000Z
DTEND:20261114T040000Z
SUMMARY:ブライダルフェア(試食つき)のご来館
LOCATION:サンプル会場(〒000-0000 サンプル県サンプル市1-2
 -3)
DESCRIPTION:所要時間は約3時間です。\n駐車場:あり(30台
 )\n日時の変更・取り消しは、予約確認メールのリンク
 か、お電話でお知らせください。
ORGANIZER;CN="サンプル会場 ブライダルサロン":mailto:fair@exam
 ple.jp
STATUS:CONFIRMED
URL:https://example.jp/fair/booking/
END:VEVENT
END:VCALENDAR

会場名・住所・アドレスはすべて例です。行ごとに、RFC 5545 と、メールで予定を配る手順を決めた RFC 5546(iTIP)で何が決まっているかを並べます。

行 決まっていること この例での書き方
VERSION・PRODID VCALENDAR に必ず1つずつ書く PRODID は作ったシステムを表す文字列
METHOD 書くと、メールなどで配る予定として扱われる。PUBLISH は、相手に出欠を求めずに予定を配る使い方 PUBLISH(取り消しは CANCEL)
UID・DTSTAMP VEVENT に必ず1つずつ書く。DTSTAMP はUTCで書く 予約ごとのUUIDと、ファイルを作った時刻
DTSTART METHOD を書かない場合は必須。PUBLISH では必須 UTCに直して末尾にZ
DTEND DURATION と同時には書かない 開始+所要時間
ORGANIZER PUBLISH では必須。メールで配る場合は mailto: で書く 式場の予約窓口のアドレス
ATTENDEE PUBLISH では書かない 書かない(お客様のアドレスを入れない)
SEQUENCE 0より大きいときは書く 予約時0、変更のたびに1増やす

METHOD を REQUEST にして ATTENDEE にお客様のアドレスを入れると、会議の招待と同じ「出欠の返事を求める予定」になります。フェアの予約は受付の時点で確定しているので、この記事では、返事を求めない PUBLISH にしています。

1行75オクテットの折り返しは、日本語の文字の途中で切らない

RFC 5545 では、1行は改行(CRLF)を除いて75オクテットより長くしないこと(SHOULD NOT)、長い行は「CRLFと空白1文字」を挟んで折り返すこととされています。日本語はUTF-8で1文字3バイトになることが多いので、文字数で数えると、すぐに75オクテットを超えます。

RFC 5545 には、簡単な実装ではUTF-8の複数バイトの文字の途中で折り返してしまうことがある、という注意も書かれています。読む側が正しく戻せる前提にせず、書く側で文字の境目で折り返します。次のコードは、1文字ずつバイト数を数え、75オクテット(2行目からは先頭の空白の分を引いて74)を超える手前で改行を入れます。

js
// fair-ics.mjs(抜粋)
const CRLF = "\r\n";
const enc = new TextEncoder();

// TEXT の値のエスケープ(RFC 5545 3.3.11):\ ; , と改行
export function escapeText(s) {
  return String(s)
    .replace(/\\/g, "\\\\")
    .replace(/;/g, "\\;")
    .replace(/,/g, "\\,")
    .replace(/\r\n|\r|\n/g, "\\n");
}

// 1行を75オクテット以内で折り返す(RFC 5545 3.1)。
// 文字の途中(UTF-8の複数バイトの途中)では切らない
export function fold(line) {
  const out = [];
  let cur = "";
  let bytes = 0;
  for (const ch of line) {
    const n = enc.encode(ch).length;
    const limit = out.length === 0 ? 75 : 74; // 2行目以降は先頭の空白1文字ぶんを引く
    if (bytes + n > limit) {
      out.push(cur);
      cur = "";
      bytes = 0;
    }
    cur += ch;
    bytes += n;
  }
  out.push(cur);
  return out.join(CRLF + " ");
}

書き出した .ics の各行のバイト数を数えると、いちばん長い行が75でした。for...of は文字(コードポイント)ごとに回るので、3バイトの文字を2バイトと1バイトに分けることがありません。

説明欄や場所の文字は、エスケープも必要です。RFC 5545 では、TEXT の値の中のバックスラッシュ・セミコロン・カンマは前にバックスラッシュを付け、改行は \n で書くと決められています。コロンはエスケープしません。住所や案内文にはカンマや改行がよく入るので、エスケープしてから折り返す順番にします。

開始時刻は、日本時間をUTCに直してZを付ける

日時は、日本時間をUTCに直し、末尾にZを付けて書きます。日本時間の10時は、UTCの1時なので DTSTART:20261114T010000Z です。タイムゾーンを付けない書き方は、遠方や海外にいるお客様のカレンダーで時刻がずれます。

RFC 5545 では、時刻の書き方が3つあります。Zを付けたUTC、TZID でタイムゾーンを指定する書き方、どちらも付けない書き方です。どちらも付けない時刻は「浮動時刻」と呼ばれ、どのタイムゾーンにも結びつかず、受け取った人がいる地域の時刻として読むよう書かれています。決まった時刻を伝えるには、UTCか、タイムゾーンを指定した時刻を使わなければなりません。

浮動時刻で書いた場合とUTCで書いた場合を、ical.js(カレンダーのファイルを読むJavaScriptのライブラリ)で、端末のタイムゾーンだけを変えて読み比べました。表示はどれも日本時間に直しています。

TZ=Asia/Tokyo
  浮動 20261114T100000  → 2026/11/14 10:00:00(日本時間)
  UTC  20261114T010000Z → 2026/11/14 10:00:00(日本時間)
TZ=Asia/Bangkok
  浮動 20261114T100000  → 2026/11/14 12:00:00(日本時間)
  UTC  20261114T010000Z → 2026/11/14 10:00:00(日本時間)
TZ=America/Los_Angeles
  浮動 20261114T100000  → 2026/11/15 3:00:00(日本時間)
  UTC  20261114T010000Z → 2026/11/14 10:00:00(日本時間)

浮動時刻は、日本の端末では正しく見えるため、作った側の確認では気づけません。海外に住んでいて帰国して式を挙げるお二人や、旅行中に予約したお客様の端末で、時刻がずれます。UTCで書いた行は、どの設定でも日本時間の10時のままでした。

TZID を使う書き方もありますが、RFC 5545 では、TZID を使うときは同じファイルの中にそのタイムゾーンの定義(VTIMEZONE)を書くよう決められています。日本時間には夏時間がなく、UTCに直すだけで済むので、この記事ではUTCを選びました。予約のシステムの中で日本時間とUTCをどう持ち分けるかは、予約の空き枠と締め切りを日本時間で扱う記事で書いています。

UIDは予約ごとのUUIDにして、変更はSEQUENCEを上げて送り直す

UIDは、予約を受け付けたときにランダムなUUIDを1つ作り、予約の行に保存して、その予約の .ics を作るたびに使い回します。日時を変えたら、同じUIDのままSEQUENCEを1増やし、PUBLISH で送り直します。取り消しは、METHOD を CANCEL、STATUS を CANCELLED にして、SEQUENCEをさらに上げます。

UIDに予約番号や式場のドメインを入れたくなりますが、RFC 7986 で、UIDには利用者・ホスト・ドメインなどを特定できる情報を入れてはいけないと改められ、ランダムなUUIDを使うことがすすめられています。予約番号は推測されやすい連番のことも多いので、UIDとは別に持ちます。

RFC 5546 では、PUBLISH で配った予定は、配った側が PUBLISH で更新し、CANCEL で取り消せると書かれています。予約・日時の変更・取り消しの3通を作って、ical.js と Python の icalendar(どちらも作ったコードとは別の実装)で読み直した結果です。

$ node parse-check.mjs
1-booked.ics     PUBLISH  seq=0 CONFIRMED  2026/11/14 10:00:00 180分 "駐車場:あり(30台)"
2-changed.ics    PUBLISH  seq=1 CONFIRMED  2026/11/21 13:00:00 180分 "駐車場:あり(30台)"
3-cancelled.ics  CANCEL   seq=2 CANCELLED  2026/11/21 13:00:00 180分 "駐車場:あり(30台)"

$ python parse_check.py
1-booked.ics     PUBLISH  0 CONFIRMED  2026-11-14 10:00 サンプル会場(〒000-0000 サンプル県サンプル市1-2-3)
2-changed.ics    PUBLISH  1 CONFIRMED  2026-11-21 13:00 サンプル会場(〒000-0000 サンプル県サンプル市1-2-3)
3-cancelled.ics  CANCEL   2 CANCELLED  2026-11-21 13:00 サンプル会場(〒000-0000 サンプル県サンプル市1-2-3)

折り返した日本語の場所は、2つのライブラリとも元どおりに戻りました。説明欄の折り返しとエスケープした改行は、ical.js の出力(説明の2行目を取り出したもの)で、改行として読めていることを確かめました。

ここで注意したいのは、受け取った側のカレンダーが、2通目で前の予定を置き換えるか、3通目で予定を消すかは、アプリしだいだという点です。RFC 5546 の CANCEL の決まりには、予定全体を取り消すときは出席者(ATTENDEE)の一部または全部を含めると書かれていますが、PUBLISH では ATTENDEE を書かないので、PUBLISH で配った予定の取り消しをどう扱うかは、アプリによって違う余地があります。変更と取り消しは、必ずメール本文で「前の予定は削除してください」と伝え、.ics はそれを手伝うものと考えます。

確認メールには text/calendar の添付として付ける

確認メールに付けるときは、Content-Type を text/calendar にし、method に .ics の中の METHOD と同じ値、charset に UTF-8 を入れます。RFC 6047(メールで予定を配る決まり)で、method の値は .ics の中の METHOD と同じにすること、US-ASCII で書けない文字を含むなら charset を UTF-8 で付けることが決められています。

メールの送信は、多くの場合、メールの送信サービスのAPIに「添付ファイル」として渡します。その前に、MIMEの形が正しいかを手元で確かめるため、本文と .ics の添付を持つメールを組み立てて、Python の email パッケージで読み直しました。

js
// mail.mjs(抜粋)
const mail = [
  `From: ${mimeWord("サンプル会場 ブライダルサロン")} <[email protected]>`,
  "To: [email protected]",
  `Subject: ${mimeWord("【ご予約確認】ブライダルフェア 11月14日(土)10:00")}`,
  "MIME-Version: 1.0",
  `Content-Type: multipart/mixed; boundary="${boundary}"`,
  "",
  `--${boundary}`,
  'Content-Type: text/plain; charset="UTF-8"',
  "Content-Transfer-Encoding: base64",
  "",
  b64(body),
  `--${boundary}`,
  'Content-Type: text/calendar; charset="UTF-8"; method=PUBLISH',
  'Content-Disposition: attachment; filename="fair.ics"',
  "Content-Transfer-Encoding: base64",
  "",
  b64(ics),
  `--${boundary}--`,
  "",
].join("\r\n");
$ python mail_check.py
Subject: 【ご予約確認】ブライダルフェア 11月14日(土)10:00
text/plain {'charset': 'UTF-8'} None
text/calendar {'charset': 'UTF-8', 'method': 'PUBLISH'} fair.ics
  添付を取り出したら元の .ics と同じ: True

ファイル名は Content-Disposition で付けられますが、RFC 6047 には、受け取った側は拡張子ではなく Content-Type で扱いを決めること、とも書かれています。送信サービスのAPIで添付の種類を指定できる場合は、拡張子まかせにせず text/calendar を明示します。

送るメールの種類によって、送ってよい内容も変わります。予約確認と、その予約に付ける .ics は予約に伴う連絡です。ほかのフェアの案内を .ics の説明欄に足すと、広告の性格を持つ配信と混ざります。配信の同意を分ける考え方は追客メール・LINEの同意と配信停止の記事にまとめています。

予約完了ページからも、同じ .ics をダウンロードできるようにする

メールが迷惑メールに入った人や、メールを開く前に予約完了の画面を見ている人のために、予約完了ページと予約確認ページに「カレンダーに追加」のボタンを置き、同じ .ics を返します。返すときのレスポンスの見出しは、次のようにしました(curl の出力のうち、ここで設定した行だけを載せています)。

$ curl -s -D - -o /dev/null 'http://localhost:8931/fair/booking/calendar.ics?t=tkn_9c2f0d'
HTTP/1.1 200 OK
Content-Type: text/calendar; charset=utf-8
Content-Disposition: attachment; filename="fair.ics"
Cache-Control: private, no-store
X-Robots-Tag: noindex

予約ごとのファイルなので、キャッシュさせない(private, no-store)、検索に出さない(X-Robots-Tag: noindex)ようにしています。手元のサーバーでは、間違ったトークンを付けたリクエストには404を返すことも確かめました。

ダウンロードのURLには、予約番号ではなく、推測されにくいトークンを使います。連番の予約番号をそのままURLに入れると、番号を変えるだけで他の人の予約の日時が見えてしまいます。

説明欄に入れるのは式場の案内だけにする

.ics の説明欄(DESCRIPTION)と場所(LOCATION)には、式場の側の案内だけを書き、お客様の名前・電話番号・人数・予算などは入れません。カレンダーの予定は、家族との共有カレンダーや職場の端末への同期で、予約した本人以外の目に触れることがあるからです。

説明欄に入れる内容は、来館の当日に役立つものに絞ると、カレンダーを開いたときにそのまま使えます。

  • 所要時間と、受付の場所(建物の入口が分かりにくい場合)
  • 駐車場の有無、最寄り駅からの道順の要点
  • 日時の変更と取り消しの方法(予約確認ページか電話)
  • 式場の電話番号(当日、遅れそうなときの連絡先)
  • 予約確認ページのURL(本人確認のトークン付き。開くと予約の詳細が見える)

予約確認ページのURLを入れる場合、そのURLを知っている人は予約の詳細を見られます。トークンの有効期限を来館日の後までに区切る、ページには氏名を伏せて表示するなど、URLが共有されても困らない範囲を決めておきます。

実装の前に、式場側で決めておくこと

.ics の作り方そのものは小さな実装ですが、どの予約に付けるか、変更や取り消しの連絡とどうそろえるかは、式場の運用で決めることです。予約のシステムや確認メールを作る側に渡す前に、次の表を埋めておくと、作ってからの手戻りが減ります。

決めること 選択肢の例 決める理由
付ける予約の種類 フェア・見学・打ち合わせのどこまでに付けるか 打ち合わせは日程の変更が多く、送り直しの回数が増える
予定の名前(SUMMARY) 式場名を入れるか、フェアの名前だけか お客様のカレンダーの一覧で、何の予定か分かるように
終了時刻 フェアの所要時間で書くか、目安として長めに書くか お客様がその後の予定を入れる判断に使う
変更・取り消しのときに送り直すか 送り直す/メール本文で知らせるだけ 送り直す場合はUIDとSEQUENCEを予約の行に持つ
電話で受けた予約 電話のあとに確認メールを送るか 電話の予約にも .ics を届けるなら、メールアドレスを聞く運用が要る

電話とWebの予約を1つの台帳にまとめる作り方は見学予約の台帳の記事で扱っています。予約の行にUIDとSEQUENCEの列があれば、窓口がどこでも同じ .ics を送れます。

株式会社bundlyzeでは、結婚式場のWeb集客の一部として、フェアやプランのLP運用も行っています。フェアの予約フォームから確認メール・予約確認ページまでを、式場の運用に合わせて見直したいときは、WEB戦略代行で受け持つ範囲を紹介しています。

確かめたことと、確かめていないこと

ここまでのコードは、手元のMacで動かしたものです。確認日は2026年10月6日です。

  • macOS 26.6.2、Node.js 22.23.2(node:test)、Python 3(email パッケージ)、curl 8.7.1
  • 読み直しに使ったライブラリ:ical.js 2.2.1、icalendar 6.3.2(Python)
  • node --test で、次の6件のテストが通りました:改行がすべてCRLFであること、どの行も75オクテット以内であること、折り返しを戻すと日本語が元どおりになること、TEXT のエスケープ、日本時間10時の開始がUTCの01:00Zになること、変更でUIDが変わらずSEQUENCEが上がり、取り消しが CANCEL と CANCELLED になること
ok 1 - 改行はすべてCRLFで、単独のLFが無い
ok 2 - どの行も75オクテット以内
ok 3 - 折り返しを戻すと、日本語が元どおりになる(文字の途中で切っていない)
ok 4 - TEXTのエスケープ:\\ ; , 改行。コロンはそのまま
ok 5 - 日本時間10:00の開始は、UTCの01:00Zで書く
ok 6 - 日時の変更はUIDを変えずにSEQUENCEを上げる。取り消しはCANCELとCANCELLED

確かめていないことは次のとおりです。導入するときは、お客様が使う端末とメールアプリで、実際に予約から取り消しまでを一度通して確かめてください。

  • 実際のメールの送信と、iPhoneの標準のメール・Gmailのアプリ・Outlookなどで、添付の .ics がどう表示され、どう追加されるか
  • 2通目(変更)で前の予定が置き換わるか、3通目(取り消し)で予定が消えるか。アプリによって違う可能性があります
  • 予約完了ページのボタンを、スマホのブラウザで押したときの動き

出典

この記事の決まりの説明は、次の資料に基づいています。どれも2026年10月6日に原文を確認しました。

  • RFC 5545(iCalendar):CRLFと75オクテットの折り返し、UTF-8の文字の途中で折り返してしまう実装への注意(3.1)、時刻の3つの書き方と浮動時刻(3.3.5)、TEXT のエスケープ(3.3.11)、VCALENDAR の PRODID・VERSION、VEVENT の UID・DTSTAMP と METHOD がないときの DTSTART、DTEND と DURATION を同時に書かないこと(3.6・3.6.1)、METHOD がない場合は予定のやり取りとして扱わないこと(3.7.2)、TZID ごとの VTIMEZONE(3.6.5)、DTSTAMP はUTC(3.8.7.2)、拡張子 ics(8.1)
  • RFC 5546(iTIP):PUBLISH の意味と、ORGANIZER は必須・ATTENDEE は書かないこと、SEQUENCE、PUBLISH で配った予定の更新と CANCEL での取り消し(3.2.1)、REQUEST が出席者を招く使い方であること(3.2.2)、CANCEL の決まり(3.2.5)
  • RFC 6047(iMIP):Content-Type の method と charset、ORGANIZER の mailto:、Content-Disposition より Content-Type で扱いを決めること(2.3・2.4・2.6)
  • RFC 7986:UID に利用者やドメインを特定できる情報を入れないこと、ランダムなUUIDをすすめること(5.3)
  • Google カレンダーに予定を読み込む(Google カレンダー ヘルプ):パソコンの設定の「インポート / エクスポート」から .ics を読み込む手順

よくある質問

予約確認メールに .ics を添付すれば、お客様のカレンダーに自動で予定が入りますか?

自動では入らないと考えて設計します。.ics は「この予定を追加できます」と渡すファイルで、追加するかどうかと、どう見えるかは、お客様のメールアプリとカレンダーで決まります。Googleカレンダーのヘルプで案内されている読み込みの手順も、パソコンの設定画面から行うものです。日時や持ち物の案内は、メール本文と予約確認ページに必ず書き、.ics はその補助として付けます。

.ics の開始時刻は、日本時間のまま書いてよいですか?

タイムゾーンを付けずに「20261114T100000」とだけ書くと、RFC 5545 では受け取った人がいる地域の時刻で読む「浮動時刻」になります。ical.js(カレンダーのファイルを読むライブラリ)で、タイムゾーンをロサンゼルスにして読むと、日本時間の翌日3時の予定になりました。UTCに直して末尾にZを付けるか、TZIDとVTIMEZONEを書きます。この記事のコードはUTCで書いています。

フェアの日時を変えたときは、新しい .ics を送ればよいですか?

同じUIDのまま、SEQUENCEを1増やした .ics を送ります。UIDを変えると、カレンダーからは別の予定に見え、古い日時の予定が残ることがあります。UIDは予約を受けたときに一度だけ作り、予約の行に保存して使い回します。ただし、受け取った側のカレンダーが前の予定を置き換えるかは、アプリによって違うので、変更の連絡はメール本文でも行います。

.ics の説明欄に、お客様の名前や電話番号を入れてもよいですか?

入れないことをおすすめします。カレンダーの予定は、家族との共有や別の端末への同期で、予約した本人以外の目に触れることがあります。説明欄には、所要時間・駐車場・変更の方法など式場側の案内だけを書き、予約の詳細は、本人だけが開ける予約確認ページで見せます。

この記事の書き方と確認の方針