[Giphy API] 自分のアカウントの最新GIFをchangelogに自動表示する

[Giphy API] 自分のアカウントの最新GIFをchangelogに自動表示する

やりたかったこと

Giphyに新しいGIF・Stickerをアップロードしたとき、サイトのchangelogに自動表示させたかった。手動でのデプロイ操作なしに反映される仕組みが目標で、自動デプロイで動く形でよい。

changelogページにGIPHYの最新GIFが表示されている様子
完成系

Giphy APIを調べた

ユーザーのアップロード一覧を取得するエンドポイントは存在しない

公式ドキュメントに記載されているエンドポイントは以下のみで、自分がアップロードしたGIFの一覧を取得する専用APIは公開されていない

  • GET /v1/gifs/search — キーワード検索
  • GET /v1/gifs/trending — トレンド取得
  • GET /v1/gifs/{id} — ID指定で1件取得
  • GET /v1/gifs?ids= — ID複数指定で取得
  • POST upload.giphy.com/v1/gifs — アップロード

@username で検索できることを発見

ドキュメントに次の記述があった。

Users can add the @ sign before a GIPHY username to return content from a specific GIPHY channel. (rate-limited keyでは使用不可、approved appのみ対応)

q=@ofurousagi 形式のクエリはすでにサイト内の別ページで使っていたため、同じ仕組みをchangelogに流用することにした。

curl "https://api.giphy.com/v1/gifs/search?api_key={API_KEY}&q=%40ofurousagi&limit=5&sort=recent&rating=g"

レスポンス例:

{
  "data": [
    {
      "id": "7eKkGV8zNH71IDvkj9",
      "title": "Happy Illustration GIF",
      "import_datetime": "2026-02-26 14:20:38",
      "username": "ofurousagi"
    }
  ],
  "pagination": { "total_count": 66 }
}

import_datetime でアップロード日時も取得できる。StickerはURLの gifsstickers に替えるだけで同様に取得できる。

実装

型定義

export type GiphyGifItem = {
  giphyId: string;
  title: string;
  giphyUrl: string;
  gifUrl: string;
  isSticker: boolean;
};
 
export type GiphyEntry = {
  date: string;
  type: "giphy";
  title: string;
  gifs: GiphyGifItem[];
};

GIF・Stickerを並行取得し日付ごとにグループ化

const fetchGiphyMedia = async (
  apiKey: string,
  mediaType: "gifs" | "stickers",
  threshold: string,
): Promise<GiphyGifItemWithDate[]> => {
  const url = new URL(`https://api.giphy.com/v1/${mediaType}/search`);
  url.searchParams.set("api_key", apiKey);
  url.searchParams.set("q", `@ofurousagi`);
  url.searchParams.set("limit", "10");
  url.searchParams.set("sort", "recent");
  url.searchParams.set("rating", "g");
 
  const res = await fetch(url.toString());
  if (!res.ok) return [];
 
  const data = await res.json();
  return data.data.flatMap((gif) => {
    const date = parseGiphyImportDatetimeToIsoJst(gif.import_datetime);
    if (!date || date < threshold) return [];
    return [{ date, giphyId: gif.id, ... }];
  });
};

各GIFのURLは images.fixed_height.url(高さ200px)を使用。同日の複数投稿は Map で重複除外しながら1エントリにまとめる。

ISRで自動更新

changelogページに revalidate = 3600 を設定することで、Giphyにアップロードしてから最大1時間で自動反映される。

// src/app/changelog/page.tsx
export const revalidate = 3600;

Powered by GIPHY

Giphyのブランドガイドラインに従い、GIF表示箇所には「Powered by GIPHY」のロゴとリンクを付けている。

ハマったこと

本番でGIFが表示されない

原因: username フィールドによる絞り込みが厳しすぎる可能性を疑った。

gif.username === "ofurousagi"  // 本番では空で返ることがある?

username はレスポンスによって gif.usernamegif.user?.username・どちらも未定義のケースがあるため、フィルタを緩和した。しかしこれだけでは根本的に解決しなかった。

結局、本番でGIFが消えていた主因は「直近5日以内のGIFしか取得していなかった」こと(後述)であり、キャッシュ化による構成変更が実質的な対処になった。

onidazoページでGIFが永久に表示されない

/onidazorevalidate が未設定で完全静的ページになっていた。revalidate = 3600 を追加して解決。

一定期間が過ぎるとGIFが消える

原因: 初期実装では直近5日以内のGIFしかAPIから取得していなかった。

解決: ビルド時に取得した履歴を giphy-cache.json に蓄積し、静的データとして読み込む構成に変更した。

[ビルド時] fetch-giphy.mjs → giphy-cache.json(90日分を蓄積)
     ↓
[実行時] giphy-cache.json(静的)+ API直近14日(動的)→ マージして表示

scripts/fetch-giphy.mjs はAPIをページネーションしながら全件取得し、既存キャッシュに差分をマージして保存する。prebuild に組み込んでいるためデプロイのたびに自動更新される。

import giphyCacheJson from "@/generated/giphy-cache.json";
 
const GIPHY_DAYS_THRESHOLD = 14; // 動的フェッチは直近14日分
 
const getGiphyEntries = async (): Promise<GiphyEntry[]> => {
  // キャッシュをベースにマップ構築
  const byDate = new Map<string, Map<string, GiphyGifItem>>();
  for (const entry of cachedEntries) {
    byDate.set(entry.date, new Map(entry.gifs.map((g) => [g.giphyId, g])));
  }
 
  // APIから直近14日分をマージ
  if (apiKey) {
    const [gifs, stickers] = await Promise.all([...]);
    for (const { date, ...item } of [...gifs, ...stickers]) {
      if (!byDate.has(date)) byDate.set(date, new Map());
      byDate.get(date)!.set(item.giphyId, item);
    }
  }
 
  return Array.from(byDate.entries()).map(([date, gifsMap]) => ({
    date, type: "giphy" as const,
    title: "新しいGIFをアップロードしました",
    gifs: Array.from(gifsMap.values()),
  }));
};

制約: デプロイ間隔が14日を超えると、その間にアップロードしたGIFが一時的に消える可能性がある。

手動デプロイなしで更新する方法

手動デプロイなしに自動更新する方法として、以下を試したい。

GitHub Actions のスケジュール実行

.github/workflows/ に cron ワークフローを追加し、毎日 fetch:giphy を実行して差分があれば giphy-cache.json をコミット&プッシュする方法。Cloudflare の CD パイプラインが自動デプロイされるため、手動操作なしでchangelogが更新され続ける

on:
  schedule:
    - cron: "0 3 * * *"  # 毎日 JST 12:00
jobs:
  update-giphy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: node scripts/fetch-giphy.mjs
        env:
          GIPHY_API_KEY: ${{ secrets.GIPHY_API_KEY }}
      - run: |
          git config user.name "github-actions"
          git diff --quiet || git commit -am "chore: update giphy cache" && git push

差分がなければコミットもプッシュも発生しないため、無駄なデプロイは走らない。

GitHub Actions は存在は知っているものの実際に使ったことがなく、このユースケースはシンプルで試しやすそうなので良い機会になるかもしれない。

Cloudflare Cron Triggers + KV

Cloudflare Workers には Cron Triggers という定期実行の仕組みがある。Worker 内でGiphy APIを叩いて結果を KV に保存し、ページ描画時に KV から読み込む構成にすれば、デプロイなしで完全に動作する。

[Cron Trigger: 毎日] → Worker が Giphy API を呼ぶ → KV に保存
     ↓
[ISR revalidate] → Worker が KV から読む → ページに反映

ただし giphy-cache.json という静的ファイルベースの現構成から、KV を使った動的なデータ取得に切り替える必要があり、実装の変更は大きい。

まとめ

q=@username でユーザーアップロード一覧を代替取得し、ISRで自動反映させる仕組みを実装した。履歴の永続化にビルド時キャッシュを採用したため手動デプロイが前提になっているが、GitHub Actions で自動化すれば目標の「手動操作なしで反映」は達成できる。

注意点:

  • GIPHYレスポンスの username は揺れがあるため、単一条件で厳密に絞り込みすぎない
  • ISRを使うページは revalidate の設定漏れに注意(未設定だと完全静的になる)
  • .env.local に同じキーが複数ある場合、どの値が使われるかはパーサーの実装による。スクリプトで自前パースする際は特に注意

参考