0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

connpass API v2を使ってイベント取得してホームページ掲載したときのメモ

Posted at

技術コミュニティ「技術チャレンジ部」のホームページに、今後のイベント情報(connpass)を自動掲載する仕組みを作りました。

公式の connpass API v2 を使えば取得できますが、
実際に実装してみると APIキーの設定、期間絞り込み、サムネイルや説明文の取得などでいくつかハマりポイントがあったため、備忘録としてまとめます。

実装の全体像

  • scripts/fetch_connpass.js で connpass API からイベント情報を取得
  • _data/connpass.json にイベント情報を保存
  • Eleventy (11ty) のテンプレートから connpass.json を読み込んで表示
  • GitHub Actions でビルド時に API から最新情報を取得

APIキーの準備

connpass API v2 は X-API-Key ヘッダで認証します。
取得方法は、公式サポートに問い合わせる必要があります(2025年8月時点)。

https://connpass.com/about/api/v2/

私の場合、問い合わせから1週間以内にメールでAPIキーが発行されました。

ローカル実行時は .env に保存し(.env.gitignore に追記)、GitHub Actions の Secrets にも同名で登録します。

CONNPASS_API_KEY=xxxxxxxxxxxxxxxxx
CONNPASS_SUBDOMAIN=challenge-club

スクリプト例(scripts/fetch_connpass.js 抜粋)

主な仕様

  • 指定サブドメインのイベントを取得
  • 今から30日後までのイベントに絞り込み(環境変数で変更可)
  • APIに画像がない場合は null とし、規約上スクレイピングは行わない
  • 説明文は catch を優先、なければ description をHTML除去して要約
// scripts/fetch_connpass.js
/* eslint-disable no-console */
require("dotenv").config();

const fs = require("node:fs/promises");
// Node 18+ は globalThis.fetch がある。古い環境でも動くようにフォールバック。
const fetch = globalThis.fetch || require("node-fetch");

// === 環境変数 ===
const API_KEY   = process.env.CONNPASS_API_KEY;
const SUBDOMAIN = process.env.CONNPASS_SUBDOMAIN || "challenge-club";

// 出力先(Eleventy がビルド時に読み込む JSON)
const OUT   = "_data/connpass.json";
const COUNT = Number(process.env.CONNPASS_COUNT || 50);

// 取得ウィンドウ(デフォルト: 今〜+30日)
// 運用や検証で必要に応じて可変化
const DAYS_BACK  = Number(process.env.CONNPASS_RANGE_DAYS_BACK  || 0);
const DAYS_AHEAD = Number(process.env.CONNPASS_RANGE_DAYS_AHEAD || 30);

// === 事前チェック ===
if (!API_KEY) {
  console.error("ERROR: CONNPASS_API_KEY is not set.");
  process.exit(1);
}

// === 期間ウィンドウ ===
// JST だからといって +9h などの手動補正はしない。
// APIの ISO8601(+09:00 等) は Date() で適切に比較できる。
const now = new Date();
const windowStart = new Date(now); windowStart.setDate(windowStart.getDate() - DAYS_BACK);
const windowEnd   = new Date(now); windowEnd.setDate(windowEnd.getDate() + DAYS_AHEAD);

// HTMLタグを除去してプレーンテキストへ(APIの description から要約を作る用途)
function htmlToPlain(html) {
  if (!html) return "";
  return String(html).replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
}

// テキストを所定文字数で丸める(末尾に…)
function clampText(str, max = 140) {
  if (!str) return "";
  const s = String(str).replace(/\s+/g, " ").trim();
  return s.length > max ? s.slice(0, max - 1) + "" : s;
}

async function main() {
  // v2 イベント一覧エンドポイント(サブドメイン指定)
  const url = new URL("https://connpass.com/api/v2/events/");
  url.searchParams.set("subdomain", SUBDOMAIN);
  url.searchParams.set("count", String(COUNT));
  url.searchParams.set("order", "2"); // 開催日時順(昇順)

  const headers = {
    "X-API-Key": API_KEY,
    "User-Agent": "challenge-club-homepage/0.1 (+https://github.com/ChallengeClub/challenge-club-homepage)",
  };

  console.log(`[connpass] GET ${url.toString()}`);
  console.log(`[connpass] window ${windowStart.toISOString()} ~ ${windowEnd.toISOString()}`);

  const res = await fetch(url, { headers });
  if (!res.ok) {
    throw new Error(`connpass API error: ${res.status} ${res.statusText}`);
  }

  const data = await res.json();
  const raw = Array.isArray(data.events) ? data.events : [];

  // 期間で絞り、開始時刻でソート
  const list = raw
    .filter(ev => {
      if (!ev?.started_at) return false;
      const start = new Date(ev.started_at);
      return start >= windowStart && start <= windowEnd;
    })
    .sort((a, b) => new Date(a.started_at) - new Date(b.started_at));

  const events = [];

  for (const ev of list) {
    // URL は API の候補を優先。無ければ event_id から推測。
    const id = ev.event_id;
    const guessedUrl = id ? `https://connpass.com/event/${id}/` : null;
    const urlFromApi = ev.event_url || ev.public_url || ev.url || null;
    const safeUrl = urlFromApi || guessedUrl;

    // 概要は catch を優先、無ければ description をHTML除去して丸める
    const summary =
      clampText(ev.catch, 140) ||
      clampText(htmlToPlain(ev.description || ""), 140) ||
      null;

    // サムネイルは API で来ないことが多い。規約上スクレイピングはしないため null のまま許容。
    const thumbnail = ev.image_url || ev.thumb || null;

    events.push({
      id,
      title: ev.title,
      url: safeUrl,
      started_at: ev.started_at,
      ended_at: ev.ended_at,
      place: ev.place || ev.address || "",
      limit: ev.limit ?? null,
      accepted: ev.accepted ?? null,
      waiting: ev.waiting ?? null,
      thumbnail, // 多くの場合 null。テンプレート側でデフォルト画像を表示する想定。
      summary,   // テンプレート側で説明文として利用
    });
  }

  await fs.mkdir("_data", { recursive: true });
  await fs.writeFile(
    OUT,
    JSON.stringify({ updatedAt: new Date().toISOString(), events }, null, 2)
  );
  console.log(`[connpass] Saved ${events.length} events to ${OUT}`);
}

main().catch(err => {
  console.error(err);
  process.exit(1);
});

取得したイベント情報はi以下のように_data/connpass.jsonに保存されます。
これは.gitignoreに追記しておきます。

{
  "updatedAt": "2025-08-09T02:30:24.974Z",
  "events": [
    {
      "title": "もくもく読書会 in Challenge Club",
      "url": "https://challenge-club.connpass.com/event/365530/",
      "started_at": "2025-08-16T07:00:00+09:00",
      "ended_at": "2025-08-16T08:00:00+09:00",
      "place": "Discord",
      "limit": 45,
      "accepted": 1,
      "waiting": 0,
      "thumbnail": "https://media.connpass.com/thumbs/bf/1e/bf1e24ca29efc4089345efeb27339117.png",
      "summary": "ゆるーい読書会です。何を読んでもOK、話さなくてもOK、自分のペースでOK!"
    },
    {
      "title": "夏にチャレンジしたことLT会",
      "url": "https://challenge-club.connpass.com/event/364463/",
      "started_at": "2025-08-27T20:00:00+09:00",
      "ended_at": "2025-08-27T21:00:00+09:00",
      "place": "Zoom",
      "limit": 49,
      "accepted": 6,
      "waiting": 0,
      "thumbnail": "https://media.connpass.com/thumbs/16/92/1692fa9fd035d53f77859091f7b6930f.png",
      "summary": "チャレンジしたことをゆるーく共有する会です。とにかく褒め合いましょう。チャレンジしたいことでもOK!"
    }
  ]
}

Eleventy テンプレート例(index.html 抜粋)

<section class="connpass">
  <h2>今後のconnpassイベント</h2>
  {% if connpass.events.length %}
    <div class="news-list">
      {% for ev in connpass.events.slice(0, 3) %}
        <article class="news-item">
          <div class="news-thumbnail">
            <a href="{{ ev.url }}" target="_blank" rel="noopener">
              <img src="{{ ev.thumbnail or '/images/default.png' }}" alt="{{ ev.title }}" width="108" height="72" loading="lazy">
            </a>
          </div>
          <div class="news-content">
            <h3><a href="{{ ev.url }}" target="_blank" rel="noopener">{{ ev.title }}</a></h3>
            <div class="news-meta">
              <time datetime="{{ ev.started_at }}">{{ ev.started_at | date('yyyy/MM/dd HH:mm', 'Asia/Tokyo') }}</time>
              {% if ev.summary %}<br><span class="news-excerpt">{{ ev.summary }}</span>{% endif %}
            </div>
          </div>
        </article>
      {% endfor %}
    </div>
  {% else %}
    <p>現在、対象期間のイベントはありません。</p>
  {% endif %}
</section>
style.cssの内容(抜粋)
/* 一覧の外枠。幅を制限して中央寄せ */
.news-list {
  display: flex;
  flex-direction: column; /* 縦並び */
  gap: 0;                 /* 行間は区切り線で調整するため0 */
  margin: 0 auto;
  max-width: 800px;
}

/* 1カードの行 */
.news-item {
  display: flex;
  align-items: flex-start;
  gap: 0.8em;                 /* サムネと本文の間隔 */
  padding: 0.4em 0;           /* 行内の縦余白を控えめに */
  border-bottom: 1px solid #ddd;
}

/* サムネイルは固定サイズでトリミング */
.news-thumbnail img {
  width: 120px;
  height: 68px;
  object-fit: cover;
  border-radius: 4px;
  display: block;
}

/* 本文コンテナ */
.news-content {
  flex: 1;
  min-width: 0;               /* タイトル長い場合のオーバーフロー対策 */
}

/* タイトル行は詰め気味に */
.news-title {
  margin: 0 0 0.25em;
  font-size: 1em;
  font-weight: 700;
  line-height: 1.3;
}
.news-title a {
  color: #222;
  text-decoration: none;
}
.news-title a:hover {
  text-decoration: underline;
}

/* 日時・会場・参加数などのメタ情報 */
.news-meta {
  font-size: 0.9em;
  color: #666;
}

/* 説明文は改行して詰め気味に表示(PCのみ) */
.news-excerpt {
  display: block;
  margin-top: 0.2em;          /* 改行後の間隔を最小限に */
  line-height: 1.4;
  color: #555;
}

/* 「もっと見る」導線の体裁 */
.conpass-more,
.connpass-more {
  margin-top: 1em;
  text-align: center;
}

/* スマホでは説明文を非表示にしてコンパクト化 */
@media (max-width: 600px) {
  .news-excerpt {
    display: none;
  }
}

画像のような形で表示されます。

image.png

4. GitHub Actions ワークフロー

週1回(毎週日曜 24:00 JST=15:00 UTC)に、connpassのイベントを取得してビルド成果物(docs/)を更新するワークフローの例です。
Secrets 側に少なくとも CONNPASS_API_KEY を登録しておきます(任意で CONNPASS_SUBDOMAIN、GitHub App を使う場合は APP_IDPRIVATE_KEY も)。

name: Build and Commit to docs

on:
  push:
    branches: [ main ]
    paths-ignore:
      - 'docs/**'
      - 'Readme.md'
  pull_request:
    branches: [ main ]
    paths-ignore:
      - 'docs/**'
      - 'Readme.md'
  schedule:
    # 毎週日曜 24:00 JST(= 15:00 UTC)
    - cron: '0 15 * * 0'

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      # GitHub App のトークンを使う場合(任意)
      - name: Create GitHub App token
        id: app-token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ secrets.APP_ID }}            # 事前に登録
          private-key: ${{ secrets.PRIVATE_KEY }}  # 事前に登録

      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          # 上の GitHub App を使わない場合はこの行を削除
          token: ${{ steps.app-token.outputs.token }}

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      # ビルド前に connpass の最新データを生成
      - name: Fetch connpass events
        env:
          CONNPASS_API_KEY: ${{ secrets.CONNPASS_API_KEY }}       # 必須
          CONNPASS_SUBDOMAIN: ${{ secrets.CONNPASS_SUBDOMAIN }}   # 任意(未設定ならスクリプト側で 'challenge-club')
        run: |
          node scripts/fetch_connpass.js || echo "WARNING: connpass fetch failed; using last _data/connpass.json if exists."

      # Eleventy ビルド(必要に応じて `ELEVENTY_ENV=production` 等を付与)
      - name: Build with Eleventy
        env:
          CONNPASS_API_KEY: ${{ secrets.CONNPASS_API_KEY }}       # _data 側で環境変数に依存する処理がある場合の安全策
          CONNPASS_SUBDOMAIN: ${{ secrets.CONNPASS_SUBDOMAIN }}
        run: |
          npx @11ty/eleventy

      # docs/ を main にコミット(pushトリガーで Pages デプロイが走る構成)
      - name: Commit and push to docs
        if: github.event_name == 'push' || github.event_name == 'schedule'
        env:
          # GitHub Appを使わない場合は以下の2行と `with.token` を削除。
          GH_TOKEN: ${{ steps.app-token.outputs.token }}
        run: |
          git config --global user.name 'your-app-name[bot]'
          git config --global user.email 'your-app-id+your-app-name[bot]@users.noreply.github.com'
          git add docs/
          git diff --cached --quiet || git commit -m "ci: build site on $(date -u +'%Y-%m-%d %H:%M:%S')"
          # App未使用なら: git push
          git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }} HEAD:main

ハマりポイント

  • 生成AIを使って開発する場合、connpass API v2 では取得できない情報を生成してしまうことがあるため、必ず公式ドキュメントを確認するか、公式ドキュメントを生成AIに与えた上で作業する。
  • APIキーは必須(未設定だと ERROR: CONNPASS_API_KEY is not set. が出る)。GitHub Secrets への環境変数設定漏れが原因で取得できないことがあった。
  • スクレイピングは規約上NGのため、APIで取得できる範囲の情報のみを利用する。
  • GitHub Pages デプロイ時に in progress deployment エラーが出ることがあるが、再実行(リトライ)で回避できる。

まとめ

  • connpass API v2 だけでは不足する情報もあるが、規約上スクレイピングができないため、仕様の範囲内で補完する工夫が必要。
  • Eleventy の _data 機構を使えば、ビルド時にAPIデータをテンプレートへ埋め込めて便利。
  • GitHub Actions で定期的に更新すれば、常に最新のイベント一覧を自動表示できる。
  • 実装例のレポジトリは こちら
0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?