コンテンツにスキップ
Documents for MorningStatusApp
Esc
↑↓移動↵開く⌘Jプレビュー
このページの内容

TikTok投稿機能 データ設計書

最終更新: 2026-09-11

1. 概要

公式 TikTok アカウント(モーニング娘。公式 @morningmusume_uf・ミニモちゃん @minimochan_dapyon)の投稿を RapidAPI(tiktok-scraper7.p.rapidapi.com)経由で取得し、Neon(PostgreSQL)に保存する。対象アカウントは Vercel Blob(tiktok-official-accounts.json)で管理し、収集スクリプトはこの一覧をループしてアカウントごとに処理する(#1458)。アカウント一覧はデスクトップモード限定の管理UIから追加・編集できる(§3、#1487)。

上記に加えて、OGメンバー個人のTikTokアカウント(メンバー データ設計書 §3 の Member.tiktokChannel で管理)の投稿も収集する。YouTube OG収集(YouTube投稿一覧機能 データ設計書 §4-5)と同じ channelType('official' | 'og')+ memberId 方式で公式アカウントと区別する(#1478)。


2. Neon テーブル一覧

テーブル名 概要 更新タイミング
tiktok_posts 収集済み投稿メタデータ(公式アカウント・OGメンバー個人アカウント共通) 公式: collect-tiktok-posts ワークフロー実行時(差分追加、毎日)。OG: collect-tiktok-og ワークフロー実行時(差分追加、OG_SYNC_WEEKDAYS で指定した曜日のみ。#1478)
tiktok_sync_status 公式アカウント収集の同期実行状態(/sync-status画面向け)+TOP新着投稿セクションの新着ウィンドウ判定用のofficial/og独立ウィンドウ(常に1行。id=1 を upsert) collect-tiktok-posts(official用フィールドを更新)・collect-tiktok-og(og用ウィンドウのみ更新)ワークフロー実行時。詳細は§4「新着表示の判定基準」参照(#1489)

3. 型定義

型定義ファイル: types/tiktok.ts

OfficialTikTokAccount(公式アカウント管理、#1487)

公式アカウント一覧は Vercel Blob(tiktok-official-accounts.json)で管理する。デスクトップモード限定の管理UI(§6「TikTok投稿一覧画面」参照)から追加・編集できる(#1487)。

type OfficialTikTokAccount = {
  id: string;           // アカウントID(既存投稿の accountId・URLパスで使用。追加後は変更不可)
  name: string;         // 表示名(例: "モーニング娘。公式")
  username: string;     // TikTokユーザー名(@なし)
  tiktokUrl: string;    // TikTok公式アカウントURL
  accountType: 'normal' | 'group'; // 'normal': 投稿をそのままフラット表示 / 'group': メンバー・楽曲・グループ別に分類表示(§6参照)
};

tiktok-official-accounts.json の形式: OfficialTikTokAccount[]

Blobの検出方式: 専用の環境変数(URL固定)は用いず、@vercel/blobのlist({ prefix: 'tiktok-official-accounts.json' })でBlobを都度検出する(レーダーチャート時系列設定 §10-6-5のradar-chart-timeline-config.jsonと同じ方式)。新規Blobファイル追加のたびに環境変数を発行・登録するunits.json方式より運用コストが低い。

備考(CDNキャッシュ回避、#1487): Vercel Blobのput()はcacheControlMaxAgeを1分未満に設定できないため、管理UIで保存した直後にlist()で取得したURLへアクセスすると、CDNエッジキャッシュ経由で更新前のデータが返ることがある。これを避けるため、取得URLにblobs[0].etagをクエリパラメータとして付与し(保存のたびにetagが変わるため実質的に別URL扱いになる)、かつfetchはcache: 'no-store'で呼び出す。Instagram側(getInstagramOfficialAccountsFromBlob)も同じ方式。あわせて管理UIの保存後の画面反映は、ページ全体のリロードではなくPUTのレスポンスに含まれる最新の一覧でその場更新する方式を採る(ページリロードだとCDNキャッシュの影響を受けやすいため)。

API(app/api/tiktok-official-accounts/route.ts):

エンドポイント 用途 アクセス制御
GET /api/tiktok-official-accounts 一覧取得 制限なし
PUT /api/tiktok-official-accounts 追加・編集(id が既存と一致すれば上書き、一致しなければ追加) デスクトップモードでのみ使用可能

削除機能は設けない(登録数が少なく、削除の必要が生じた場合はBlobを直接編集する運用で足りるため)。

TikTokPost(投稿メタデータ)

type TikTokPost = {
  postId?: string;           // 投稿ID(video_id。TikTok URL の数値部分)
  postUrl: string;           // 投稿URL (https://www.tiktok.com/@{account.username}/video/{video_id})
  imageBlobUrl?: string;     // [未使用] Vercel Blob 上の画像 URL(将来用途のみ)
  thumbnailUrl?: string;     // サムネイル画像 URL(RapidAPI の cover フィールド。署名付き URL のため有効期限あり)
  caption: string;           // 投稿キャプション(RapidAPI の title フィールド。null は空文字に正規化)
  capturedAt: string;        // 取得日時(ISO 8601)
  createdAt?: string;        // 実際の投稿日時(ISO 8601。RapidAPI の create_time を Unix タイムスタンプから変換)
  mentionedMemberIds?: string[]; // 言及メンバーID一覧(収集時に `attributeTikTokPostToMembers` で算出。公式アカウントのみ。OGは空のまま。#1478)
  accountId?: string;        // 収集元の公式アカウントID(`OfficialTikTokAccount.id`。公式のみ。#1458)
  channelType?: 'official' | 'og'; // アカウント種別。既存行は 'official' がデフォルト(#1478)
  memberId?: string;         // OGメンバーのみ。公式アカウントの場合は undefined(#1478)
};

Neon カラムとのマッピング:

Neon カラム TikTokPost フィールド 型 備考
post_id postId VARCHAR(50) PK RapidAPI の video_id。数値文字列
post_url postUrl TEXT UNIQUE TikTok 投稿 URL
thumbnail_url thumbnailUrl TEXT NULL RapidAPI の cover。署名付き URL(有効期限あり)
image_blob_url imageBlobUrl TEXT NULL 将来用途(未使用)
caption caption TEXT 投稿キャプション・ハッシュタグ
captured_at capturedAt TIMESTAMPTZ 収集日時
created_at createdAt TIMESTAMPTZ NULL 実際の投稿日時
mentioned_member_ids mentionedMemberIds TEXT[] 言及メンバーID。公式アカウントのみ算出。OGは常に空配列(#1478)
account_id accountId VARCHAR(50) NULL DEFAULT morningmusume-official 収集元の公式アカウントID(OfficialTikTokAccount.id)。#1458 で追加。OGメンバー投稿は NULL(#1478 でカラムを nullable に変更。DEFAULT は既存行backfill用に残置、公式アカウント収集時は明示的に account.id を渡すため実質未使用)
channel_type channelType VARCHAR(20) DEFAULT official 'official' または 'og'。既存行はマイグレーション時にデフォルト値で自動補完される(#1478)
member_id memberId VARCHAR(50) NULL OGメンバーのみ。公式アカウントは NULL(#1478)

RapidAPI フィールドとのマッピング:

RapidAPI フィールド TikTokPost フィールド 備考
video_id postId 数値文字列。URL に使用する
aweme_id —(未保存) 英数混合の内部ID。postId には使わない
title caption 投稿キャプション・ハッシュタグを含む
cover thumbnailUrl 署名付き URL。有効期限(x-expires)あり
create_time createdAt Unix タイムスタンプ → new Date(t * 1000).toISOString()

TikTokSyncStatus(同期状態)

type TikTokSyncStatus = {
  lastSyncedAt: string;             // 最終同期日時(ISO 8601。/sync-status画面向け)
  syncStatus: 'valid' | 'expired'; // API 取得結果: 'valid'=投稿あり / 'expired'=投稿0件(/sync-status画面向け)
  capturedMembersCount: number;    // 取得対象アカウント数(`tiktok-official-accounts.json` の件数。#1458。/sync-status画面向け)
  totalTargetMembersCount: number; // 取得対象アカウント総数(`tiktok-official-accounts.json` の件数。#1458。/sync-status画面向け)
  retroCursor: string | null;      // [廃止] 遡及取得カーソル。初回遡及完了済みのため収集スクリプトでは使用しない(#1178)
  // TOP新着投稿セクションの新着ウィンドウ判定用(#1489)。詳細は§4「新着表示の判定基準」参照
  officialLastSyncedAt: string;         // official の直近の実行完了時刻(手動実行含む)
  officialLastAutoSyncedAt: string;     // official の直近の「定期実行」の完了時刻
  officialPreviousAutoSyncedAt: string; // official のその1つ前の「定期実行」の完了時刻
  ogLastSyncedAt: string;               // og の直近の実行完了時刻(手動実行含む)
  ogLastAutoSyncedAt: string;           // og の直近の「定期実行」の完了時刻
  ogPreviousAutoSyncedAt: string;       // og のその1つ前の「定期実行」の完了時刻
};

Neon カラムとのマッピング(tiktok_sync_status):

Neon カラム 型 備考
last_synced_at TIMESTAMPTZ 最終同期日時(/sync-status画面向け)
sync_status VARCHAR(20) valid または expired(/sync-status画面向け)
captured_members_count INT 取得対象アカウント数(tiktok-official-accounts.json の件数。#1458。/sync-status画面向け)
total_target_members_count INT 取得対象アカウント総数(tiktok-official-accounts.json の件数。#1458。/sync-status画面向け)
retro_cursor TEXT NULL [廃止] 遡及取得カーソル。初回遡及完了済みのため収集スクリプトでは使用しない(#1178)
official_last_synced_at TIMESTAMPTZ official 用(#1489)
official_last_auto_synced_at TIMESTAMPTZ official 用(#1489)
official_previous_auto_synced_at TIMESTAMPTZ official 用(#1489)
og_last_synced_at TIMESTAMPTZ og 用(#1489)
og_last_auto_synced_at TIMESTAMPTZ og 用(#1489)
og_previous_auto_synced_at TIMESTAMPTZ og 用(#1489)

4. 運用ルール

収集対象

公式・関連アカウント(channelType: 'official')

  • Vercel Blob(tiktok-official-accounts.json)に登録された公式・関連アカウントを対象とする。各アカウントは accountType('normal' | 'group')を持ち、/tiktok 画面での投稿の分類方法を左右する(§6参照)。2026-08-08 時点で以下の2件:
    • morningmusume-official(モーニング娘。公式、@morningmusume_uf。accountType: 'group' — メンバーが在籍する公式アカウント特有の扱いとして、投稿をメンバー・楽曲に分類する)
    • minimochan-dapyon(ミニモちゃん、@minimochan_dapyon。accountType: 'normal'(デフォルト)。#1458)
  • 収集スクリプト(collect-tiktok-posts.ts)は getTikTokOfficialAccountsFromBlob()(lib/blob.ts)で取得した一覧を順にループし、アカウントごとに新着取得を行う。収集処理(RapidAPIからの取得・保存・メンバー紐付け判定)自体は accountType によらず全アカウント共通。分類は表示側(§6)でのみ accountType により分岐する

OGメンバー個人アカウント(channelType: 'og'、#1478)

  • Member.tiktokChannel(メンバー データ設計書 §3)が設定された OG メンバーを対象とする。メンバー編集画面から設定する(YouTube OG収集と同じ運用方式。YouTube投稿一覧機能 データ設計書 §4参照)
  • 収集スクリプト(collect-tiktok-og.ts)を新設し、tiktokChannel が設定された OG メンバーをループして新着取得を行う。公式アカウント収集(collect-tiktok-posts.ts)とは別スクリプト・別ワークフローで完全に独立させる(実行スケジュールが異なるため。§「収集スケジュール」参照)

新着取得

公式アカウント

収集スクリプトは新着取得のみを行う。公式 TikTok アカウントの初回遡及取得は完了済みのため、遡及取得は実施しない(#1178)。

動作 API コスト
アカウントごとに最新ページから取得し、既存 postId に到達した時点で停止する。最大 MAX_PAGES(10)ページまで取得する アカウントあたり通常 1 ページ

postId(TikTok の video_id)はアカウント・チャンネル種別を問わずグローバルに一意なため、既存投稿の重複チェックは tiktok_posts テーブル全体に対して行う(アカウント別・OGメンバー別に絞り込まない)。

OGメンバー個人アカウント(#1478)

公式アカウントと異なり「既存 postId に到達するまでページを遡る」差分取得は行わない。保持件数の上限が5件(後述「保持件数」参照)であるため、既存の保存状況によらずメンバーごとに常に最新5件だけを取得し、DB側の保存内容をこの5件で置き換える(YouTube OG収集の fetchVideosFromPlaylist(playlistId, 5) と同じ考え方。§「保持件数」参照)。

動作 API コスト
メンバーごとに fetchUserPosts を1ページ(先頭5件)のみ呼び出す。ページを遡ることはしない メンバーあたり1ページ

保持件数(#1478)

チャンネル種別 保持ルール
公式アカウント(channelType: 'official') 全件保持(差分追加のみ・削除しない)
OGメンバー個人アカウント(channelType: 'og') メンバーごとに、毎回取得した最新5件で保存内容を置き換える(取得した5件に含まれない既存投稿を削除したうえで、取得した5件を保存する。skipDuplicates 付き保存のため既存の5件と変わらない場合は実質何も変化しない)。YouTube OG収集(YouTube投稿一覧機能 データ設計書 §5)と同じ方針

注意: 初回設計時、「今回の新着取得分(差分)に含まれない投稿を削除する」という記述だったが、この方式だと初回同期時に最新5件を超える投稿が保存されたり、2回目以降の同期で新着が見つかるたびにそれ以前の保存済み投稿が全て削除されてしまう不具合があったため、「常に最新5件を取得してDB側を置き換える」方式に設計を修正した(#1478 のレビューで検出)。

収集スケジュール(#1478)

チャンネル種別 スケジュール ワークフロー
公式アカウント 毎日 collect-tiktok-posts.yml
OGメンバー個人アカウント OG_SYNC_WEEKDAYS(環境変数)で指定した曜日のみ。YouTube OG収集と同じ環境変数を共用する collect-tiktok-og.yml(新設)

重複防止

  • 公式アカウント: 収集スクリプト実行時に既存の postId を DB から取得し、一致する投稿はスキップする
  • OGメンバー個人アカウント: 上記「保持件数」の置き換え処理(skipDuplicates 付き保存)によって重複が防止される。既存 postId に到達するまで遡る差分取得は行わないため、公式アカウントと異なり事前のスキップ判定はない

新着表示の判定基準(TOP新着投稿セクション、#1489)

TOP新着投稿セクションの新着判定は、YouTube投稿一覧機能 データ設計書 §6.2 と同じ考え方で、tiktok_sync_status(§2・§3)の official/og 独立ウィンドウ [xxxPreviousAutoSyncedAt, xxxLastSyncedAt] を使う。getRecentTikTokPosts()(lib/tiktok.ts)が channelType = 'official' AND capturedAt がofficial用ウィンドウ内 または channelType = 'og' AND capturedAt がog用ウィンドウ内 のいずれかに合致するレコードを取得する(PrismaのOR条件)。件数キャップは設けず、ウィンドウ内の該当レコードを全件返す。

書き込みロジック

official(collect-tiktok-posts.ts)・og(collect-tiktok-og.ts)は、updateTikTokSyncStatus(channelType, officialSummary?)(lib/tiktok.ts)にそれぞれ 'official' / 'og' を渡して呼び出し、自分のチャンネル用ウィンドウのみを更新する(もう一方のチャンネルのウィンドウには触れない。YouTubeのupdateYoutubeSyncStatusと同じ方式)。

  • 実行トリガーの判定・定期実行/手動実行でのスライド方式は YouTube(YouTube投稿一覧機能 データ設計書 §6.2)と同一
  • official 呼び出し時のみ、officialSummary(syncStatus・capturedMembersCount・totalTargetMembersCount)を渡して /sync-status 画面向けの既存フィールド(lastSyncedAt・syncStatus・capturedMembersCount・totalTargetMembersCount)も併せて更新する。og 呼び出し時はこれらの値には触れず既存値を据え置く
  • 初回実行時(テーブルが空)は、呼び出したチャンネル・呼び出していないチャンネルの双方が今回の実行時刻にフォールバックする

旧方式からの変更経緯(#1489)

初版(#1408)は「TikTok全チャンネル横断で最新の capturedAt を1件求め、そのJST日付と同日のレコードを新着候補としたうえで .slice(0, 10) で先頭10件に切り詰める」方式だった。この方式には以下の構造的な問題があった。

  • allTikTokPosts(getTikTokPosts())は公式アカウント・OGメンバー個人アカウントをchannelTypeで絞り込まず全件を対象にするため、複数アカウント・複数OGメンバーの投稿が全て同じ「同日」候補に混ざる
  • 「同日」判定は日付境界のみで、Ameba(#1387)・YouTube(#1450)のような実行区間ベースのウィンドウではなかった
  • .slice(0, 10) は候補の全件を単純に先頭10件へ切り詰めるだけで、実際の新着件数が10件を超えても警告なく黙って切り捨てる

このため、1回の収集で同日にまとまった件数の新着(複数アカウント・複数OGメンバーの初回同期、または通常運用でもたまたま複数アカウントに新着が重なった場合)が発生すると、実際には新着であるにもかかわらず10件を超えた分がホームページの新着投稿セクションから黙って消える不具合があった(ミニモちゃんTikTok公式アカウント収集の実運用開始時に200件超が初回同期され、10件しか表示されなかった事象で発覚)。

YouTube(#1450)と同じ official/og 独立ウィンドウ方式に変更し、件数キャップ自体を撤廃することでこの問題を解消した。tiktok_sync_status は元々 /sync-status 画面向けの公式収集ステータス(lastSyncedAt・syncStatus等)専用テーブルだったため、既存フィールドは変更せずに official/og 独立ウィンドウ用の6カラムを追加する形をとった(YouTubeのyoutube_sync_statusは本機能専用の新設テーブルだった点が異なる)。

メンバーの紐付け

公式アカウント(言及メンバーID)

  • 収集時(collect-tiktok-posts.ts)に attributeTikTokPostToMembers(lib/tiktok-attribution.ts)が caption を判定し、言及メンバーIDとしてDBに保存する
  • 表示側(TikTok投稿一覧画面・メンバー詳細画面・トップページ新着投稿セクション)は、保存済みの mentionedMemberIds をそのまま表示に用いる
  • 判定対象はキャプション内のハッシュタグ(# で始まるトークン)のみ。本文中に名前が書かれていてもハッシュタグに含まれていなければマッチしない(#1412)
    • 理由: メンバー分類は投稿の閲覧を絞り込むための補助的な機能であり、ハッシュタグでない言及の方が稀なため、多少の見落としより無関係な投稿を誤って特定メンバーの投稿として表示してしまう方を避ける
  • ハッシュタグ内でのマッチ規則:
    • メンバーのフルネーム(正規化後)が部分一致すればマッチ
    • フルネームが一致しない場合、苗字または名前(2文字以上のもの)が部分一致すればマッチ
    • 異体字(﨑/崎・髙/高・栁/柳等)は正規化してから比較する
  • Instagram はメンバー個別の公式アカウントを直接取得する方式のため、同様のキャプション文字列マッチングは行っていない

OGメンバー個人アカウント(#1478)

  • attributeTikTokPostToMembers によるハッシュタグ判定は行わない。channelType: 'og' + memberId で当該メンバー本人の投稿であることが自明なため(YouTube OG収集と同じ考え方)
  • mentionedMemberIds は常に空配列のまま保存する

サムネイル URL の有効期限・表示判定

  • RapidAPI の cover フィールドは署名付き URL であり、x-expires パラメータで有効期限が設定されている
  • DB には署名付き URL をそのまま保存する(有効期限後は表示できなくなる点に注意)
  • スライドショー(photomode)投稿のサムネイルは .heic 形式で返される場合がある
    • 収集時に isTikTokSlideshowThumbnail でサムネイル URL を検査し、isSlideshow: true として DB に保存する
    • デスクトップブラウザ(Chrome・Firefox)は HEIC を表示できないため、onError ハンドラでリンクのみ表示へフォールバックする
    • モバイル・デスクトップいずれの表示形態でも、スライドショー投稿には「Slide」ラベルを表示する(画像表示時はサムネイル左上にオーバーレイ、リンクカード表示時はキャプション上部に付与)

5. 環境変数

変数名 用途
RAPIDAPI_KEY RapidAPI アクセスキー。Repository secret に保存
DATABASE_URL Neon への接続 URL。Repository secret に保存
MEMBERS_BLOB_URL Vercel Blob のメンバーデータ URL(OG収集で tiktokChannel を参照)。Repository secret に保存(#1478)
OG_SYNC_WEEKDAYS OG収集を実行する曜日番号(カンマ区切り、デフォルト: 1)。YouTube OG収集と共用。Repository variable に保存(非機密な設定値のため)(#1478)

6. 画面設計(UI)

TikTok投稿一覧画面(/tiktok)

概要: 公式アカウント・OGメンバーそれぞれへのリンクカードのみを表示する画面(#1508)。アカウントごとの投稿一覧・分類ロジックは個別の詳細画面(後述)に集約されており、本画面自体は投稿を直接表示しない。

要素 内容
ナビゲーション 「← ホームへ戻る」リンク(/ へ)
タイトル 「TikTok」(固定表記)
説明文 「公式・関連アカウントの投稿一覧」
著作権表記 DisclaimerNote(「(画像は参考表示:権利は投稿者に帰属しています)」)
公式アカウントへのリンク(#1508) tiktok-official-accounts.json の登録順に、アカウントごとのリンクカード(アカウント名+@username)をグリッド表示。/tiktok/official/[accountId] へ遷移。YouTube投稿一覧画面(/youtube)の「公式チャンネル」セクションと同じ構成で、投稿件数によらず常に表示する
公式アカウント管理UI(デスクトップモードのみ、#1487) リンクカードのグリッドの下に、公式アカウントの追加・編集フォームを表示する(§3「OfficialTikTokAccount」参照)。一般ユーザーには表示しない
OGメンバーへのリンク(#1478) tiktokChannel が設定された OG メンバーのリンクカードをグリッド表示(メンバー名+ @username)。/tiktok/og/[memberId] へ遷移。YouTube投稿一覧画面(/youtube)の OGメンバーチャンネルセクションと同じ構成。対象 OG メンバーが0件の場合はセクションごと非表示

表示順(#1491): 公式アカウントへのリンクセクションを先に表示し、OGメンバーへのリンクセクションはその後に表示する。SNS投稿一覧の共通ルール「公式アカウント→メンバー(現役→OG)」に合わせるため。

TikTok公式アカウント詳細画面(/tiktok/official/[accountId]、#1508)

概要: tiktok-official-accounts.json の各アカウントの投稿一覧を表示する画面。投稿の分類方法はアカウントの accountType によって分岐する(#1458 で導入した分類ロジックをそのまま移植)。ページネーションは導入せず全件を表示する(YouTube公式チャンネル画面の「5件ずつ」ページングは踏襲しない)。

accountType 対象アカウント 表示方法
group モーニング娘。公式 投稿を「メンバーごとの投稿」「楽曲プロモーション投稿」「グループとしての投稿」の3グループに分類して表示する。メンバーが在籍する公式アカウント特有の扱い
normal(デフォルト) ミニモちゃん 分類を行わず、投稿をそのままグリッド表示する

「メンバーごとの投稿」「楽曲プロモーション投稿」という分類軸は、メンバーが在籍するモーニング娘。公式アカウント特有の概念であり、キャプションが偶然メンバー名や曲名に一致した場合だけ機械的に分類される汎用ロジック(groupTikTokPosts)に依存している。ミニモちゃんのようなマスコットキャラクターのアカウントに同じ分類軸を適用するのはアカウントの性質上不自然なため、accountType: 'normal'(デフォルト)のアカウントは分類せずフラット表示する(#1458 のレビューで指摘・是正)。

要素 内容
ナビゲーション 「← TikTok投稿一覧へ戻る」リンク(/tiktok へ)
タイトル アカウント名(例:「モーニング娘。公式」)
説明文 @username
著作権表記 DisclaimerNote
データなし表示 当該アカウントの投稿が0件の場合「投稿データが見つかりませんでした。」の glass カードを表示
accountId が tiktok-official-accounts.json に存在しない notFound()(404)

accountType: 'group' の表示(モーニング娘。公式):

要素 内容
メンバーごとの投稿 accountId で絞り込んだ投稿をメンバー別にアコーディオン(<details>)表示。サムネイル有効な投稿を含むメンバーを優先ソート。先頭のみデフォルト展開
楽曲プロモーション投稿 曲別にアコーディオン表示(曲名は /songs/{trackId} へリンク)。サムネイル有効な投稿を含む曲を優先ソート
グループとしての投稿 個別メンバー・楽曲に紐付かない投稿をグリッド表示

アコーディオン見出し(<summary>)の表示内容:

項目 内容
見出し名 メンバー名 or 曲タイトル
サムネイルありアイコン 📹(グループ内にサムネイル有効な投稿が1件以上ある場合のみ)
件数 ({posts.length}件)

曲別グルーピングの trackId 解決は、リリース全体から format: 'single' のトラックを優先して重複排除した trackInfos を組み立て(app/tiktok/official/[accountId]/page.tsx で normalizeForMatch を使用)、lib/tiktok-utils.ts の groupTikTokPosts/titleMatchesCaption で投稿キャプションと照合する。タイトルの正規化には ラジオオンエア データ設計書 §4-0「曲名正規化とハイフン文字種の吸収」 と同じ lib/track-match.ts を使うが、titleMatchesCaption 内では判定の型によって正規化強度を使い分ける。

  • ハッシュタグ完全一致の判定: ハッシュタグは構造上スペースを含み得ないため、normalizeForMatch(スペース・記号を全て除去する強い正規化)でタイトルとハッシュタグを比較する
  • ハッシュタグを除いた本文でのトークン一致の判定: マッチ箇所前後がCJK文字でないかという単語境界の安全確認にキャプション中の実際のスペースを使うため、normalizeSymbolVariantsForMatch(NFKC・小文字化・ハイフン/波ダッシュの文字種統一のみ行い、スペース等は残す軽量な正規化)を使う

旧仕様の記載(バグの経緯): 以前は trackKey・titleMatchesCaption が独自の簡易正規化(NFKCのみ、または全角/半角の一部記号のみ)しか行っておらず、ハイフン文字種・波ダッシュの違いを吸収できていなかった。そのため「Wake-up Call~目覚めるとき~」のような楽曲で、シングル優先の重複排除が機能せず、投稿がシングルではなくライブ盤等のトラックに誤ってグルーピングされる不具合があった(MorningStatusApp #1552で是正、lib/track-match.ts への統一により解消)。

accountType: 'normal' の表示(ミニモちゃん):

accountId で絞り込んだ投稿を分類せず、そのまま投稿カードのグリッドで表示する(アコーディオン・セクション見出しなし)。

投稿カード(TikTokPostGridCard、components/TikTokPostGridCard.tsx)の表示内容:

状態 表示内容
サムネイル有効(isTikTokThumbnailValid) 正方形サムネイル(imageBlobUrl 優先・なければ thumbnailUrl)+取得日時+キャプション(全文、pre-wrap)+登場メンバーへのリンクバッジ(mentionedMemberIds から解決)
サムネイル無効・期限切れ キャプション(またはキャプション未設定時は「TikTok投稿」)のみのglassリンクカード+登場メンバーへのリンクバッジ

サムネイルの有効・無効いずれの状態でも登場メンバーへのリンクバッジ(mentionedMemberIds から解決)を表示する(#1508のレビューで修正)。いずれもクリックで postUrl(TikTok投稿ページ)を新しいタブで開く。同じコンポーネントを /tiktok/og/[memberId] 画面(後述)でも共用する。

備考: 修正前は「サムネイル無効・期限切れ」の場合に登場メンバーへのリンクバッジを描画するコードがサムネイル有効時の分岐内にしかなく、TikTokのサムネイルURL(署名付きURL、有効期限あり)が期限切れになった投稿では mentionedMemberIds にデータがあってもバッジごと表示されなくなっていた。トップページ新着投稿セクションの TikTokNewPostCard(メンバー名表示をサムネイル分岐の外に置く実装)は影響を受けなかったため、新着投稿セクションでは表示されるのに /tiktok/official/[accountId]・/tiktok/og/[memberId] では表示されないという非対称な症状になっていた(#1508)。

メンバー詳細画面のTikTokセクション(components/member-detail/TikTokSection.tsx・TikTokPostCard.tsx)は本画面とは別実装で、スライドショー投稿向けの「Slide」ラベル表示を持つ。本画面(/tiktok/official/[accountId])の TikTokPostGridCard にはスライドショー判定は実装されていない。

TOP新着投稿セクション TikTok部分(components/NewPostsSection.tsx、#1507)

概要: トップページの新着投稿セクションでは、getRecentTikTokPosts()(§4「新着表示の判定基準」参照)で取得した新着ウィンドウ内の投稿を、投稿元アカウント単位で見出し分割して表示する。/tiktok 投稿一覧画面(本節冒頭)とは別実装だが、アカウント区分の考え方は共通。

見出し区分 対象 表示条件
モーニング娘。公式 accountId === 'morningmusume-official' の投稿 該当投稿が1件以上ある場合のみ見出しを表示
ミニモちゃん accountId === 'minimochan-dapyon' の投稿 同上
OGメンバー個別 channelType === 'og' の投稿を memberId でメンバーごとに分割 新着投稿があったメンバーのみ、メンバー名を見出しとして個別に表示(「OGメンバー」という束ねの見出しは設けない)
  • 各見出し内の投稿カードは、モーニング娘。公式・ミニモちゃん・OGメンバーいずれも共通のレンダリングロジックを使う。既存の mentionedMemberIds によるメンバー名ラベル表示(言及メンバーへのバッジ表示)は、タグがある投稿・ない投稿が混在する形でそのまま維持する
  • OGメンバー投稿へのハッシュタグ付与(例: 現役メンバーとの共演を#でタグ付けする等)は投稿者本人の裁量であり、アプリ側が対応の要否を判断する性質のものではない。表示側(本節のレンダリングロジック)はタグの有無に依存しない共通実装のため、収集側で mentionedMemberIds が設定されていればそのまま表示される
  • 既知の不具合: 収集スクリプト(collect-tiktok-og.ts)は公式アカウント側(collect-tiktok-posts.ts)と異なり attributeTikTokPostToMembers を呼び出さず、mentionedMemberIds を常に空配列で保存している。そのため投稿者が他メンバーをハッシュタグでタグ付けしても検出・表示されず、その情報が失われる(#1507の検討時に判明)。見出し自体はアカウント本人(投稿者)の識別を兼ねるが、タグ付けされた他のメンバーの情報はこの見出しでは代替できず欠落したままになる。収集スクリプト側の改修が必要(別Issueで対応)
  • 投稿が1件もないアカウント・OGメンバーの見出しは表示しない。トップレベルの「TikTok」見出し・/tiktok へのリンク自体は投稿数によらず常に表示されるため(開発ノート「一覧画面への常設導線を『データ0件なら非表示』の条件レンダリングに乗せない」#1404)、本節の見出しはその内側の分類であり、唯一の遷移経路ではない。全アカウント合計で新着が0件の場合の表示(「本日の新着投稿はありません」)は画面設計書 §1「新着投稿セクション」の表示条件に従う(本節固有の仕様ではない)
  • 背景(#1507): メンバータグ(mentionedMemberIds)による分類のみでは、モーニング娘。公式・ミニモちゃん・OGメンバー個人のどのアカウントの投稿か区別できなかった。モーニング娘。公式・ミニモちゃんはいずれもハッシュタグ経由でメンバータグが付与されるため、タグだけでは2アカウントを判別できない

TikTok OGアカウント画面(/tiktok/og/[memberId]、#1478)

概要: OGメンバー個人アカウントの保存済み最新5件をグリッド表示する(保持件数が最新5件のため、ページ切り替えなし)。YouTube OGチャンネル画面(YouTube投稿一覧機能 画面設計書 §5)と同じ構成。

要素 内容
ナビゲーション 「← TikTok投稿一覧へ戻る」リンク(/tiktok へ)
タイトル {メンバー名} TikTok
説明文 @username を表示
著作権表記 DisclaimerNote
投稿グリッド channelType: 'og' かつ memberId が一致する投稿を投稿日時降順で最大5件、TikTokPostGridCard(§6の/tiktok/official/[accountId]と同一実装)で表示
メンバー詳細へのリンク 「メンバー詳細へ」リンク(/members/{memberId} へ)
memberId が存在しない・tiktokChannel 未設定 notFound()(404)

メンバー詳細画面への表示(#1478)

メンバー詳細画面のTikTokセクション(components/member-detail/TikTokSection.tsx)には、以下の2種類の投稿をあわせて表示する。

投稿の種類 判定条件
公式アカウントでの言及投稿(既存) mentionedMemberIds に対象メンバーの id を含む
OGメンバー本人の投稿(#1478) channelType === 'og' かつ memberId が対象メンバーの id と一致する

YouTubeはメンバー詳細画面にOG動画を表示しない実装だが(判定ロジックが独立しており技術的な混在が起きないため)、TikTokはメンバー詳細画面に統合表示する。/tiktok/og/[memberId] 専用ページ(上記)は、/tiktok トップページからの独立した閲覧導線として別途維持する。


改訂履歴

版 更新日 変更内容
2.7 2026-09-11 公式アカウント一覧の管理方式をconstants/tiktok-official-accounts.tsのハードコード定数からVercel Blob(tiktok-official-accounts.json)に変更。デスクトップモード限定の追加・編集UI(/tiktok画面)とAPI(GET/PUT /api/tiktok-official-accounts)を新設。Blob保存直後のCDNエッジキャッシュ対策(etag付きURL取得・保存後のその場更新)を追記(#1487)
2.6 2026-08-23 /tiktok/official/[accountId]の曲別グルーピング(trackKey・titleMatchesCaption)が、ハイフン文字種・波ダッシュの違いを吸収できず投稿がシングルでなくライブ盤等に誤ってグルーピングされる不具合の経緯と、共有正規化関数lib/track-match.tsへの統一を追記。titleMatchesCaption内はハッシュタグ完全一致(強い正規化)と本文トークン一致(スペースを残す軽量な正規化)で強度を使い分けることも明記(MorningStatusApp #1552)
2.5 2026-08-16 TikTokPostGridCardで、サムネイル無効・期限切れの投稿では登場メンバーへのリンクバッジが表示されないバグの修正(#1508のレビューで検出)。サムネイルの有効・無効によらず登場メンバーバッジを表示するよう明記し、備考に不具合の原因を追記
2.4 2026-08-16 公式TikTokアカウントに詳細画面を追加(#1508)。/tiktok画面はアカウント単位のインライン投稿表示をやめ、公式アカウント・OGメンバーいずれもリンクカードのみを表示する構成に変更。投稿一覧・accountTypeによる分類ロジックは新設の/tiktok/official/[accountId]画面(YouTube公式チャンネル画面と異なりページングは導入しない)に移動
2.3 2026-08-16 TOP新着投稿セクションのTikTok部分(NewPostsSection.tsx)にアカウント単位の見出し分割を追加(#1507)。§6に新規サブセクション「TOP新着投稿セクション TikTok部分」を追加し、モーニング娘。公式/ミニモちゃん/OGメンバー個別の見出し区分・表示条件を明記
2.2 2026-08-12 SNS投稿一覧のルール統一(#1491): /tiktok 画面のタイトルを「モーニング娘。公式 TikTok」から媒体名のみの「TikTok」に変更。表示順をアカウント見出し(公式アカウント)セクション→OGメンバーへのリンクセクションの順に統一する旨を明記(実装ではOGメンバーが先に描画されていたため是正)
2.1 2026-08-09 TOP新着投稿セクションの新着判定を「同日capturedAt + slice(0, 10)」の件数キャップ方式から、YouTube(#1450)と同じofficial/og独立ウィンドウ方式に置き換え(§4「新着表示の判定基準」を新設)。TikTokSyncStatus/tiktok_sync_statusにofficial/og独立ウィンドウ用の6カラム(officialLastSyncedAt等)を追加。既存の/sync-status画面向けフィールドは変更しない。件数キャップにより同日にまとまった件数の新着(複数アカウント・複数OGメンバーの同時収集等)が黙って表示から漏れる不具合の修正(#1489)
2.0 2026-08-09 OGメンバー個人アカウントのTikTok収集を追加(#1478)。YouTube OG収集(channelType+memberId方式)と同じ方針を採用: TikTokPost/tiktok_postsにchannelType('official'|'og')・memberIdカラムを追加、accountIdをnullable化。アカウント情報はメンバー編集画面から設定するMember.tiktokChannel(詳細はメンバー データ設計書)で管理。OGは最新5件のみ保持、OG_SYNC_WEEKDAYSで指定した曜日のみ専用ワークフロー(collect-tiktok-og.ts)で収集し、tiktok_sync_statusは更新しない。/tiktok/og/[memberId]画面を新設し、メンバー詳細画面のTikTokセクションにはOG本人の投稿も統合表示する(YouTubeと異なり分離しない)
1.9 2026-08-08 複数公式アカウント対応(#1458): ミニモちゃん(@minimochan_dapyon)を収集対象に追加。TikTokPost/tiktok_posts に accountId フィールド・カラムを追加。OFFICIAL_TIKTOK_ACCOUNTS の各アカウントに accountType('normal'
1.8 2026-07-31 メンバーの紐付け(§4)に、表示側は保存済みの mentionedMemberIds をそのまま表示に用いる旨を明記(morning-status-blume#19)
1.7 2026-07-25 メンバーの紐付け(§4)を追加。判定対象をハッシュタグのみに限定する仕様変更を反映(#1412)。言及メンバーIDの「将来実装」表記を是正(実装済み)
1.6 2026-07-18 画面設計セクション(§6)を追加。/tiktok 画面の表示内容を反映(#1366)
1.5 2026-06-23 遡及取得(Phase 2)を廃止(#1178)。初回遡及完了済みのため新着取得のみに変更。retro_cursor を廃止フィールドとしてマーク
1.4 2026-06-20 スライドショー投稿の種別管理を追加(#1183): isSlideshow フィールドを DB・型定義・収集スクリプトに追加。表示側でモバイル(画像オーバーレイ)・デスクトップ(リンクカード)いずれも「Slide」ラベルを表示する仕様を明文化
1.3 2026-05-28 2フェーズ収集(新着 + 遡及)設計を追加(#933)。TikTokSyncStatus に retroCursor フィールドを追加。tiktok_posts 保存上限(200件)撤廃記述を削除(#924 で対応済み)
1.2 2026-05-15 ストレージを Vercel Blob → Neon に移行(#888): テーブル定義・カラムマッピング・syncStatus 改名
1.1 2026-05-15 RapidAPI レスポンスフィールド修正(#890): aweme_list→videos・desc→title・video.cover.url_list→cover・aweme_id→video_id(postId)に対応
1.0 2026-05-15 初版作成(#887): Playwright から RapidAPI への切り替えに伴い新規作成

このページは役に立ちましたか?