Instagram投稿機能 データ設計書
最終更新: 2026-08-12
1. 概要
Instagram 投稿のメタデータを取得し、サムネイル画像を直接参照する方式をとる。 当初の「画像キャプチャによる Vercel Blob 保存」から、ストレージ容量削減と実行速度向上のため「外部 URL 参照」へ移行した。
2. Blob ファイル一覧
| ファイルパス | 概要 | 更新タイミング |
|---|---|---|
instagram/{memberId}/posts.json |
メンバー単位の全投稿メタデータ | collect-instagram-posts ワークフロー実行時(差分追加) |
instagram/sync-status.json |
同期実行状態(同期状況画面向け) | collect-instagram-posts ワークフロー実行時(毎回上書き) |
instagram/daily-new-posts.json |
当日新規取得投稿一覧(トップ画面向け) | collect-instagram-posts ワークフロー実行時(毎回上書き) |
注意:
instagram/{memberId}/{postId}.jpgへの画像保存は廃止された。
{postId}は投稿 URL のパス末尾(/p/xxxxxxxx/のxxxxxxxx部分)を使用する。
3. 型定義
型定義ファイル: types/instagram.ts
InstagramPost(投稿メタデータ)
type InstagramPost = {
postUrl: string; // 投稿URL (https://www.instagram.com/p/xxxx/)
thumbnailUrl?: string; // サムネイル画像 URL (外部参照: OGP og:image)。Live投稿は undefined
imageBlobUrl?: string; // [レガシー] Vercel Blob 上の画像 URL
isLive?: boolean; // Live アーカイブ投稿フラグ。true の場合 thumbnailUrl は存在しない
isReel?: boolean; // Reel投稿フラグ。true の場合UI上に「Reel」ラベルを表示する
caption: string; // キャプション全文(空文字の場合あり。null は空文字に正規化)
capturedAt: string; // 取得日時(ISO 8601)
memberId: string; // メンバーID
instagramUrl?: string; // 投稿元の Instagram アカウント URL(複数アカウント対応)
};
caption の取得: OGP og:description メタタグから取得し、cleanCaption() で不要な接頭辞を除去した後全文(複数行を含む)を保存する。取得できなかった場合は空文字列。Zod スキーマ側で null/undefined を空文字列に正規化するため、レガシーデータの null caption も安全に読み込める。
1行目のみ保存する仕様からの変更(#10): 従来は
cleanCaption()が1行目のみを切り出して保存していたが、ハッシュタグは通常キャプション末尾(2行目以降)に書かれるため、1行目のみの保存ではメンバー判定(§8)に必要な情報が失われていた。全文保存に変更したことで、収集済みの caption からハッシュタグを抽出できるようになる。
- 過去に収集済みの投稿(1行目のみの旧データ)への遡及的な再取得は行わない。本変更以降に新規収集する投稿から全文保存が適用される
- UI表示側(
InstagramPostCard等)は既存の CSS(whiteSpace: nowrap+textOverflow: ellipsis)で1行に省略表示しているため、caption が長くなっても表示上の変更は不要
instagram/{memberId}/posts.json の形式: InstagramPost[]
InstagramSyncStatus(同期状態)
type InstagramSyncStatus = {
lastSyncedAt: string; // 最終同期日時(ISO 8601)
sessionStatus: 'valid' | 'expired'; // Instagram セッションの有効状態
capturedMembersCount: number; // 今回実行で取得できたメンバー数
totalTargetMembersCount: number; // 取得対象メンバー総数
};
instagram/sync-status.json の形式: InstagramSyncStatus
DailyNewPost(当日新規投稿)
type DailyNewPost = {
postUrl: string; // 投稿URL
thumbnailUrl?: string; // サムネイル画像 URL (外部参照: OGP og:image)。Live投稿は undefined
imageBlobUrl?: string; // [レガシー] Vercel Blob 上の画像 URL
isLive?: boolean; // Live アーカイブ投稿フラグ
isReel?: boolean; // Reel投稿フラグ。true の場合UI上に「Reel」ラベルを表示する
caption: string; // キャプション全文(null は空文字に正規化)
capturedAt: string; // 取得日時(ISO 8601)
memberId: string; // メンバーID
instagramUrl?: string; // 投稿元の Instagram アカウント URL(複数アカウント対応)
memberName: string; // メンバー表示名(トップ画面で別途Blob参照しないよう埋め込む)
};
instagram/daily-new-posts.json の形式: DailyNewPost[]
4. 運用ルール
収集対象
officialSnsにinstagram.comを含む URL が登録されており、active: trueのメンバーが対象active: falseのエントリは収集をスキップする- 公式グループアカウント:
constants/instagram-official-accounts.tsのOFFICIAL_INSTAGRAM_ACCOUNTSに定義されたアカウントも収集する(現在:morningmusume_official)- 投稿は
instagram/{account.id}/posts.jsonに保存する(例:instagram/morningmusume-official/posts.json) memberIdにはアカウントID(例:morningmusume-official)を使用する- UI では Instagram 一覧ページの「公式」セクションに表示する
- 投稿は
重複防止
- 収集スクリプト実行時に既存の
posts.jsonを読み込み、取得済みのpostUrlはスキップする daily-new-posts.jsonには当該実行で初めて取得した投稿のみを記録する
投稿削除時の扱い
- Blob 上のデータは削除しない(履歴として保持)
- 削除された投稿の
postUrlにアクセスすると 404 になる点を UI 側で考慮すること
Session 失効時
- Instagram へのアクセスが以下のいずれかのパターンにリダイレクトされた場合、セッション失効と判定する(#749):
/accounts/loginを含む URL/challenge/を含む URL/privacy/checks/を含む URL/accounts/suspended/を含む URL/accounts/one_tap/を含む URLis_from_rleパラメータを含む URL
- セッション失効と判定した場合、
sessionStatus: 'expired'をsync-status.jsonに書き込み、非ゼロ終了コードで終了する - GitHub Actions が失敗扱いになり、リポジトリオーナーにメール通知が届く
5. 認証状態のセットアップ手順
INSTAGRAM_AUTH_STATE Secret は、初回セットアップ時および Session 失効時に以下の手順で登録する。
前提: このスクリプトは macOS で実行すること。Windows では Playwright のブラウザ起動が動作しない。
# 0. playwright のブラウザをインストール(初回のみ)
bunx playwright install chromium
# 1. ヘッドフルブラウザで Instagram にログインし、認証状態を保存する
bun run instagram:auth
# → auth_state.json が生成される(60 秒以内に手動ログインすること)
# 2. 生成された auth_state.json を GitHub Actions Secret に登録する
gh secret set INSTAGRAM_AUTH_STATE < auth_state.json
# 3. auth_state.json を削除する(機密情報のため必ず削除)
rm auth_state.json
注意:
auth_state.jsonには Cookie 等の認証情報が含まれるため、Git にコミットしてはならない(.gitignore済み)。
6. 環境変数
| 変数名 | 用途 |
|---|---|
INSTAGRAM_AUTH_STATE |
Playwright 認証状態(JSON文字列)。Repository secret に保存 |
BLOB_READ_WRITE_TOKEN |
Vercel Blob への読み書きトークン(既存) |
MEMBERS_BLOB_URL |
メンバーデータの Blob URL(既存) |
INSTAGRAM_SYNC_STATUS_BLOB_URL/INSTAGRAM_DAILY_NEW_POSTS_BLOB_URLは、MEMBERS_BLOB_URLからパスを導出するフォールバック実装に切り替え済みのため廃止(#1351)。lib/blob.tsのgetInstagramSyncStatusFromBlob/getDailyNewPostsFromBlobを参照。
7. 画面設計(UI)
7-1. Instagram投稿一覧画面(/instagram)
概要: 公式アカウント・メンバー個人アカウントの最新投稿をカバー表示するハブ画面。
| 要素 | 内容 |
|---|---|
| ナビゲーション | 「← ホームへ戻る」リンク(/ へ) |
| タイトル | 「Instagram」 |
| 説明文 | 「公式アカウントおよびメンバーの最新投稿」 |
| 公式アカウントセクション | OFFICIAL_INSTAGRAM_ACCOUNTS(複数アカウント対応)ごとの最新投稿カバーをグリッド表示。0件の場合はセクション自体を非表示 |
| メンバー最新投稿セクション | officialSns に active: true かつ instagram.com を含むURLを登録している全メンバーの最新投稿カバーをグリッド表示(sortMembers で現役→OG順に並び替え、#1491) |
| 著作権表記 | 各セクションに DisclaimerNote(「(画像は参考表示:権利は投稿者に帰属しています)」)を表示 |
| データなし表示 | メンバー最新投稿が0件の場合「投稿データが見つかりませんでした。」の glass カードを表示 |
カバーカード(InstagramCoverCard)の表示内容:
| 項目 | 内容 |
|---|---|
| サムネイル | thumbnailUrl。Live投稿で thumbnailUrl がない場合は専用プレースホルダー表示 |
| アカウント/メンバー名 | memberName |
| クリック動作 | latestPostUrl(Instagram投稿ページ)を新しいタブで開く |
7-2. Instagramアカウント別画面(/instagram/[accountId])
概要: 公式アカウント1件分の投稿一覧を表示する画面。accountId は OFFICIAL_INSTAGRAM_ACCOUNTS の id。
| 要素 | 内容 |
|---|---|
| ナビゲーション | 「← Instagram へ戻る」リンク(/instagram へ) |
| タイトル | {account.name} |
| アカウントリンク | @{username} ↗(Instagram公式アカウントページへの外部リンク、ユーザー名を抽出できた場合のみ) |
| 著作権表記 | DisclaimerNote |
| 投稿一覧 | OfficialInstagramSection(hideHeader 指定)で投稿を表示。isReel: true の投稿はサムネイル左下に「Reel」ラベルを表示する |
アクセス制御: accountId が OFFICIAL_INSTAGRAM_ACCOUNTS に存在しない場合は notFound()。
7-3. メンバー詳細画面(/members/{id}): 公式アカウント投稿の表示(#10)
概要: メンバー詳細画面の InstagramSection(components/member-detail/InstagramSection.tsx)は、メンバー本人の officialSns 登録アカウントの投稿に加え、公式アカウント(OFFICIAL_INSTAGRAM_ACCOUNTS)の投稿のうち当該メンバーが判定された投稿(§8)も表示する。
アカウントバケットの列挙対象拡張:
- 従来:
officialSnsに登録された、メンバー自身の Instagram アカウント(active: trueかつinstagram.comを含む URL)のみを列挙 - 拡張後: 上記に加えて、
OFFICIAL_INSTAGRAM_ACCOUNTSのうち、そのメンバーが言及されている投稿(mentionedMemberIdsに該当・サムネイルあり)を1件以上持つアカウントもバケットとして列挙する - 判定結果(言及メンバーIDの算出)はページ側(サーバーコンポーネント)で計算済みの投稿配列を渡す形とし、
InstagramSection自身は判定ロジックを持たない(既存の「表示専用コンポーネント」という役割分担を維持する)
表示順・ラベル:
| 項目 | 内容 |
|---|---|
| 表示順 | 本人の officialSns 登録アカウント(登録順)を先に表示し、公式アカウントでの言及投稿はその後ろに表示する。公式アカウントバケットが複数該当する場合は OFFICIAL_INSTAGRAM_ACCOUNTS の定義順に列挙する |
| ラベル | 本人アカウントは従来通り @{username} ↗。公式アカウントバケットは本人のアカウントと混同しないよう {account.name}(公式アカウントでの言及) @{username} ↗ の形式で表示する |
| リンク先 | 他のバケットと挙動を統一するため、/instagram/{accountId}(アプリ内の公式アカウント一覧画面)ではなく、他バケット同様に外部の Instagram 投稿ページ(account.instagramUrl)へのリンクとする |
| バケット数が1件以下の場合 | 従来通り「単一アカウント: 全投稿を一覧表示」の表示(アカウント見出しなし)にフォールバックする |
8. メンバー判定(公式アカウント投稿、都度計算・#10)
公式アカウント(OFFICIAL_INSTAGRAM_ACCOUNTS)の投稿に写っている(言及されている)メンバーを判定し、メンバー詳細画面(§7-3)から参照できるようにする。YouTube(YouTube投稿一覧機能 データ設計書 §6.1)と同様、判定結果は保存せず表示のたびに都度計算する。Instagram の投稿データは Neon ではなく Vercel Blob(instagram/{accountId}/posts.json)に保存されているため、判定結果を永続化するには過去投稿分を含めた書き戻しバッチが別途必要になる。過去投稿は §3 の caption 全文化以前のデータでハッシュタグ判定の対象にできないため、書き戻しの実益が薄く、都度計算を採用する。
判定関数
- ファイル:
lib/instagram-attribution.ts(新設) - 関数:
attributeInstagramPostToMembers(posts: InstagramPost[], members: Member[]): (InstagramPost & { mentionedMemberIds: string[] })[] - 判定対象は TikTok(
lib/tiktok-attribution.ts)と同じくキャプション内のハッシュタグ(#で始まるトークン)のみとする。ハッシュタグ抽出の正規表現#[^\s#]+は空白・改行いずれの区切りにも対応しており、Instagram のハッシュタグが本文と別ブロック(複数行)に書かれる書式差異があっても TikTok 版をそのまま流用できる - マッチング規則も TikTok 版を踏襲する: フルネーム(正規化後・空白除去版含む)の部分一致を優先し、一致しない場合は姓・名(2文字以上のもの)の部分一致にフォールバックする
- TikTok・YouTube 同様の異体字正規化(
﨑→崎・髙→高・栁→柳・𠮷→吉)を流用する - 返り値の型は実行時限定の拡張型(
InstagramPost & { mentionedMemberIds: string[] })とする。都度計算のためInstagramPost型本体(§3)にはmentionedMemberIdsフィールドを追加しない(Blob 上の実データに存在しないフィールドを型定義に持たせると誤解を招くため)
データ取得
lib/blob.tsにgetAllOfficialInstagramPosts()を新設する。OFFICIAL_INSTAGRAM_ACCOUNTSの各アカウントに対してgetInstagramPostsFromBlob(account.id)を呼び出し、結果を結合して返す(getAllOfficialYoutubePosts()の Instagram 版)- メンバー詳細画面(
app/members/[id]/page.tsx)でgetAllOfficialInstagramPosts()の結果をattributeInstagramPostToMembers()で判定し、当該メンバーのmentionedMemberIdsに一致する投稿のみを本人のgetInstagramPostsFromBlob(id)の結果とマージしてInstagramSectionに渡す(youtubePostsと同じ「ページ側で判定・フィルタ済みの配列を渡す」パターン)
改訂履歴
| 版 | 更新日 | 変更内容 |
|---|---|---|
| 1.10 | 2026-08-12 | SNS投稿一覧のルール統一(#1491): §7-1 メンバー最新投稿セクションの表示順に sortMembers による現役→OG順の並び替えを追加。見出し・公式アカウント→メンバーの表示順は既に統一ルールに適合済みのため変更なし |
| 1.9 | 2026-07-29 | §3 の caption 保存範囲を1行目のみ→全文に変更(#10)。§7-3・§8 を追加し、公式アカウント投稿のメンバー判定(都度計算)を設計(#10) |
| 1.8 | 2026-07-22 | InstagramPost・DailyNewPost に isReel フィールドを追記。§7-2 に Reel ラベル表示を追記(#1373) |
| 1.7 | 2026-07-18 | 画面設計セクション(§7)を追加。/instagram・/instagram/[accountId] 画面の表示内容を反映(#1366) |
| 1.6 | 2026-07-18 | INSTAGRAM_SYNC_STATUS_BLOB_URL / INSTAGRAM_DAILY_NEW_POSTS_BLOB_URL 環境変数の廃止を反映(#1351) |
| 1.5 | 2026-06-20 | InstagramPost・DailyNewPost に isLive・instagramUrl フィールドを追記(#1166, #1186) |
| 1.4 | 2026-06-15 | 公式グループアカウント収集を追加(#1143)。OFFICIAL_INSTAGRAM_ACCOUNTS 定数・収集ルール・UI 表示(「公式」セクション)を追記 |
| 1.3 | 2026-04-30 | caption の取得元(OGP og:description)と null 正規化を明記、セッション失効判定パターンを詳細化(#732, #749, #754) |
| 1.2 | 2026-04-29 | サムネイル取得方式(外部参照)への移行対応(#751) |
| 1.1 | 2026-04-26 | 認証状態セットアップ手順を追加(#720) |
| 1.0 | 2026-04-26 | 初版作成(#715) |