1. 都道府県別CoderDojoカレンダー
CoderDojoは子どものためのプログラミング道場です。全国各地で開催されています。
開催予定をGoogleカレンダーで見られるようにしました。2025年2月24日現在、30都道府県のカレンダーを公開しています(リンクになっている都道府県です)。
北海道、青森県、岩手県、宮城県、秋田県、山形県、福島県、茨城県、栃木県、群馬県、埼玉県、千葉県、東京都、神奈川県、新潟県、富山県、石川県、福井県、山梨県、長野県、岐阜県、静岡県、愛知県、三重県、滋賀県、京都府、大阪府、兵庫県、奈良県、和歌山県、鳥取県、島根県、岡山県、広島県、山口県、徳島県、香川県、愛媛県、高知県、福岡県、佐賀県、長崎県、熊本県、大分県、宮崎県、鹿児島県、沖縄県
カレンダーを購読すると、自分のGoogleカレンダー上に表示して、スケジュール調整がしやすくなります。購読するには、カレンダーの右下にある「+ Google Calendar」というアイコンをクリックしてください。

最新のカレンダー一覧はこちらのスプレッドシートに掲載しています。
2. カレンダーを作った背景
CoderDojoでは、プログラミングを楽しみたい子どもたちをボランティアのメンターが支援します。
CoderDojo は7〜17歳を主な対象とした非営利のプログラミング道場です。2011年にアイルランドで始まり、世界では100カ国・2,000の道場、日本には216以上の道場があります。CoderDojo は日本各地で毎年1,000回以上開催されており、延べ10,000人以上の子ども達がプログラミングを学んでいます
https://coderdojo.jp/partnership より引用
私も一緒に楽しみたいなと思い、CoderDojoのメンターとして活動しはじめました。今は様々な道場にお邪魔して支援の仕方を学んでいるところです。
全国各地に存在する道場の情報はCoderDojo Japanのサイトに集まっています。直近の開催予定は近日開催の道場で確認できます。
この開催予定を「普段使っているGoogleカレンダーで見たい」と思い、公開カレンダーを自動生成してみました。都道府県別にカレンダーを分けているので、興味のある都道府県だけ購読できます。
@yasulabさんのご協力でcoderdojo.jpのEvent APIが拡張され、CoderDojo Japanと同じ開催予定をカレンダーに掲載できるようになりました 
この記事では、自動生成した都道府県別のCoderDojoカレンダーを最初にご紹介します。そして、自動生成に使った技術と、その詳細について解説していきます。
以前は勉強会サービスのconnpassとDoorkeeperでCoderDojoを検索していました。その時の記事はこちらです。
3. 使っている技術
Google Apps Scriptを毎朝起動し、coderdojo.jpのEvent APIから予定を取得し、公開カレンダーに追加・更新しています。
3.1 coderdojo.jpのEvent APIで開催予定を取得
https://coderdojo.jp/events.json?all_events=true で、その日以降に開催予定のイベントをJSON形式で取得できます。
| 項目 | 説明 | 例 | 
|---|---|---|
| id | DojoのID。Dojo APIと同じ | 289 | 
| name | Dojoの名前 | "たまち" | 
| url | DojoのURL | "https://coderdojo-tamachi.connpass.com/" | 
| event_id | 開催予定のID | 5722 | 
| event_title | 開催予定のタイトル | "第8回CoderDojoたまち" | 
| event_date | 開始時刻 | "2023-07-23T14:00:00.000+09:00" | 
| event_end_at | 終了時刻 | "2023-07-23T16:30:00.000+09:00" | 
| event_url | 開催予定のURL | "https://coderdojo-tamachi.connpass.com/event/288359/" | 
| prefecture | 都道府県 | "東京" | 
| participants | 参加者数 | 6 | 
| event_update_at | 更新時刻 | "2023-06-25T19:02:06.000+09:00" | 
| address | 住所 | "東京都港区芝三丁目41番8号" | 
| place | 場所 | "駐健保会館 中会議室" | 
| limit" | 定員数 | 12 | 
3.2 Google Apps Scriptを使って無料で実現
実行環境はGoogle Apps Scriptです。いいところがたくさんあります。
- 無料で使える
- 定期実行できるのもありがたいです
- Googleスプレッドシートをデータベース代わりに使います
 
- GoogleカレンダーやGoogleマップのAPIを呼び出せる
- 共有カレンダーを作るだけでなく、一般に公開することもできます
- Geocoderで緯度経度から都道府県を取得できます(ただし、一定時間内にアクセスできる回数に制限があります)
 
4. 実装の詳細
APIで予定を検索し、新しい予定はスプレッドシートに追加、更新された予定はスプレッドシートを更新します。新規と更新された予定をカレンダーに設定します。
4.1 開催予定を収集しシートに記録
APIで収集した開催予定は、「イベント一覧」シートに記録します。
4.1.1 APIの呼び出しでエラーが発生した場合にリトライ
APIを呼び出した結果が、正常(HTTPレスポンスコードが200)ではなかった場合、少し時間をおいてリトライすることにします。
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 500;
function fetchWithRetry_(url) {
  for (let i = 0; i < MAX_RETRIES; i++) {
    const response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
    if (response.getResponseCode() === 200) {
      return response.getContentText();
    }
    Utilities.sleep(RETRY_DELAY_MS * (i + 1));
  }
  throw new Error(`Failed to fetch URL: ${url}`);
}
4.1.2 Event APIで開催予定を取得
Event APIから取得できる都道府県名には、都・道・府・県がついていないので、それをつけてあげます。また、住所が空で、タイトルにオンラインがあるものは、オンライン開催としています。
const SPECIAL_PREFECTURES = {
  '東京': '東京都',
  '北海道': '北海道',
  '大阪': '大阪府',
  '京都': '京都府'
};
function fetchAllEvents_() {
  function onlineIfBlank(prop, title) {
    return (!prop && title.includes('オンライン')) ? 'オンライン' : prop;
  }
  function normalize(event) {
    const event_date = new Date(event.event_date);
    const event_end_at = new Date(event.event_end_at);
    const event_update_at = new Date(event.event_update_at);
    const prefecture = SPECIAL_PREFECTURES[event.prefecture] || event.prefecture + '県'
    const address = onlineIfBlank(event.address, event.event_title);
    const place = onlineIfBlank(event.place, event.event_title);
    return {
      ...event, event_date, event_end_at, event_update_at, prefecture, place, address
    };
  }
  const url = 'https://coderdojo.jp/events.json?all_events=true';
  const events = JSON.parse(fetchWithRetry_(url));
  return events.map(normalize);
}
4.1.3 収集した開催予定をシートに記録
APIで取得した開催予定は、スプレッドシートに記録します。イベントIDを使ってシートを探し、なければsheet.appendRow()で追記し、あれば更新日を確認して新しいものならsheet.getRange().setValues()で更新します。
const SHEET_NAME_CODER_DOJO_EVENTS = 'イベント一覧';
function toEvent_(row) {
  const [event_id, prefecture, name, event_title, event_url, event_date, event_end_at, event_update_at, limit, participants, place, address ] = row;
  return { event_id, prefecture, name, event_title, event_url, event_date: new Date(event_date), event_end_at: new Date(event_end_at), event_update_at: new Date(event_update_at), limit, participants, place, address };
}
function toRow_(event) {
  const { event_id, prefecture, name, event_title, event_url, event_date, event_end_at, event_update_at, limit, participants, place, address } = event;
  return [event_id, prefecture, name, event_title, event_url, event_date, event_end_at, event_update_at, limit, participants, place, address];
}
function updateEventsSheet(forceUpdate = false) {
  const updatedEvents = [];
  const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME_CODER_DOJO_EVENTS);
  const oldEvents = sheet.getDataRange().getValues().map(row => toEvent_(row));
  fetchAllEvents_().forEach(newEvent => {
    const oldEventIndex = oldEvents.findIndex(e => e.event_id === newEvent.event_id);
    if (oldEventIndex === -1) {
      updatedEvents.push(newEvent);
      sheet.appendRow(toRow_(newEvent));
      console.log('追加', newEvent);
    } else {
      const oldEvent = oldEvents[oldEventIndex];
      if (forceUpdate || oldEvent.event_update_at < new Date(newEvent.event_update_at)) {
        updatedEvents.push(newEvent);
        const newEventRow = toRow_(newEvent);
        sheet.getRange(oldEventIndex + 1, 1, 1, newEventRow.length).setValues([newEventRow]);
        console.log('更新', newEvent);
      }
    }
  });
  return updatedEvents;
}
4.2 Googleカレンダーの操作
開催予定の収集が終わったら、その情報をGoogleカレンダーに掲載します。
都道府県別の公開カレンダーは「カレンダー一覧」シートで、カレンダーに掲載する予定は「カレンダーイベント一覧」で、それぞれ管理します。
4.2.1 都道府県別の公開カレンダーを作成
「カレンダー一覧」シートには、都道府県名とカレンダーIDを記載します。
新しい都道府県が見つかった場合、新しくカレンダーを作り、そのカレンダーを公開します。カレンダーを公開するためにCalendar APIでACLを操作します。
const SHEET_NAME_CALENDARS = 'カレンダー一覧';
// https://developers.google.com/apps-script/advanced/calendar?hl=en
// https://developers.google.com/calendar/api/v3/reference/acl?hl=en
function makePublic_(calendarId) {
  Calendar.Acl.insert({
    kind: 'calendar#aclRule',
    role: 'reader',
    scope: {
      value: '__public_principal__@public.calendar.google.com',
      type: 'default'
    },
  }, calendarId);
}
function fetchOrCreateCalendar_(prefecture) {
  function asCalendar_(row) {
    const [ prefecture, calendarId, url ] = row;
    return { prefecture, calendarId, url };
  }
  if (!prefecture) throw 'invalid prefecture';
  const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME_CALENDARS);
  const calendars = sheet.getDataRange().getValues().slice(1).map(row => asCalendar_(row));
  const calendar = calendars.find(c => c.prefecture === prefecture);
  if (calendar) {
    return CalendarApp.getCalendarById(calendar.calendarId);
  }
  const name = `CoderDojo ${prefecture}`;
  const description = `${prefecture}で開催されるCoderDojoの予定です。connpassとDoorkeeperから取得しています。`;
  // timeZone を指定しないと GMT になる
  const newCalendar = CalendarApp.createCalendar(name, {
    color: '#e6e6fa', // lavender
    timeZone: 'Asia/Tokyo'
  });
  newCalendar.setDescription(description);
  const newCalendarId = newCalendar.getId();
  makePublic_(newCalendarId);
  console.log('新しいカレンダーです', name, description);
  const url = `https://calendar.google.com/calendar/embed?src=${newCalendarId}&ctz=Asia%2FTokyo`;
  sheet.appendRow([prefecture, newCalendarId, url]);
  return newCalendar;
}
4.2.2 カレンダーイベントを作成
開催予定に対応するカレンダーイベントを作り、カレンダーに追加します。開催予定のイベントIDと、カレンダーイベントIDの組は予定を更新するときに必要になるため、「カレンダーイベント一覧」に追記しておきます。
const SHEET_NAME_CALENDAR_EVENT_IDS = 'カレンダーイベント一覧';
function getCalendarOption_(event) {
  return {
    description: [
      `URL: ${event.event_url}`,
      event.place ? `開催場所: ${event.place}`: '',
      `参加人数: ${event.participants}` + ((event.limit > 0) ? ` / ${event.limit}` : ''),
    ].filter(elem => elem).join('\n'),
    location: event.address
  }
}
function createCalenderEvent_(calendar, event) {
  return calendar.createEvent(
    event.event_title,
    new Date(event.event_date),
    new Date(event.event_end_at),
    getCalendarOption_(event)
  );
}
function updateCalenderEvent_(calendar, calendarEvenId, event) {
  const calendarEvent = calendar.getEventById(calendarEvenId);
  calendarEvent.setTitle(event.event_title);
  calendarEvent.setTime(event.event_date, event.event_end_at);
  const { description, location } = getCalendarOption_(event);
  calendarEvent.setDescription(description);
  calendarEvent.setLocation(location);
}
let CACHE_CALENDER_EVENTS;
function fetchCalenderEventId_(eventId) {
  function asCalendarEvent_(row) {
    const [ eventId, calendarId, calendarEventId ] = row;
    return { eventId, calendarId, calendarEventId };
  }
  if (!CACHE_CALENDER_EVENTS) {
    CACHE_CALENDER_EVENTS = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME_CALENDAR_EVENT_IDS).getDataRange().getValues().slice(1).map(row => asCalendarEvent_(row));
  }
  const calendarEvent = CACHE_CALENDER_EVENTS.find(ce => ce.eventId === eventId);
  if (!calendarEvent) return null;
  return calendarEvent.calendarEventId;
}
function createOrUpdateCalenderEvents_(updatedEvents) {
  const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME_CALENDAR_EVENT_IDS);
  for (let event of updatedEvents) {
    if (!event.prefecture) continue;
    const calendar = fetchOrCreateCalendar_(event.prefecture);
    const calenderEventId = fetchCalenderEventId_(event.event_id);
    if (!calenderEventId) {
      const calenderEvent = createCalenderEvent_(calendar, event);
      sheet.appendRow([event.event_id, calendar.getId(), calenderEvent.getId()]);
    } else {
      updateCalenderEvent_(calendar, calenderEventId, event);
    }
  }
}
4.3 一連の動作を実行
毎朝、APIの検索からカレンダーの更新まで、一連の動作を定期実行します。
更新があった開催予定だけ、カレンダーを更新しています。
function updateEventsSheetAndCalendars(forceUpdate = false) {
  const updatedEvents = updateEventsSheet(forceUpdate);
  createOrUpdateCalenderEvents_(updatedEvents);
}
5. まとめ
この記事では、CoderDojoの開催予定をGoogleカレンダーで管理する方法について説明しました。具体的には、coderdojo.jpのEvent APIからCoderDojoの開催情報を取得し、それをGoogleカレンダーに自動的に追加する方法を示しました。これにより、CoderDojoの開催情報を一覧で確認しやすくなり、自分のスケジュールとの調整も容易になります。
また、このプロジェクトはGoogle Apps Scriptを使用して実装されており、無料で利用できること、Googleスプレッドシートをデータベース代わりに使用できること、Googleカレンダーを呼び出せることなど、Google Apps Scriptの利点を活かしています。
このプロジェクトはCoderDojoのメンターだけでなく、参加者やその保護者にも役立つと思います。興味のある方はぜひ試してみてください。
