文書番号:XSS-SPEC-001
版:1.0
更新日:2025-12-12(JST)
対象:Google Chrome(Manifest V3)
X(旧Twitter)のタイムラインは SPA(無限スクロール)であり、意図しない更新・リロード・スクロールにより「さっき見ていたツイート/画像」が流れて見失われることがある。
本拡張は、X 上でユーザーが 実際に viewport に表示(視認)したツイート/画像(将来:動画)を短期ログとしてローカルに保存し、Side Panel / Feed UI で「自分専用の履歴フィード」として再表示する。
- 直近 TTL=10分 の範囲で、視認済みのツイート/メディアのURL・メタ情報を保存する
- 拡張UIで 新しい順 にカード表示し、フィルタ(All/Tweets/Media)を提供する
- データは ローカルのみ(外部送信なし)
- DOM変化に強いセレクタ戦略(data-testid)と、パフォーマンス劣化しない収集方式(バッチ送信)を採用する
- 収集対象:tweet / image / video(video は当面「存在判定+tweetUrl」のみでも可)
- ストレージ:
chrome.storage.local - UI:Side Panel(推奨)+ feed.html(同一UIを再利用可能)
- 動画の実体URL・サムネ抽出とプレビュー
- ピン留め(TTLを超えた永続保存領域)
- JSON/CSVエクスポート
- 仮想スクロール(500件を超える運用)
- ツイート内複数画像の「グルーピング表示」
- Chrome 拡張:Manifest V3
- 対象ドメイン:
https://x.com/*,https://twitter.com/* - X は SPA / DOM差し替えが頻繁(クラス名依存は避ける)
- 本拡張はユーザーの閲覧体験を阻害しない(軽量動作)
責務:
- DOM 監視(MutationObserver)で新規ツイート要素を検出
- 視認判定(IntersectionObserver)で「見えた」タイミングのみ抽出
- data-testid を起点にツイートURL・本文・画像URL・動画存在を抽出
- 収集結果をバッチ化して Background に送信
責務:
- Content Script から LOG_BATCH を受信
- 書き込み競合防止(Promiseキュー / Mutex)
- 既存データとマージ(重複排除、seenAt更新、フィールド単位で情報劣化防止)
- TTL期限切れ削除・上限500件のカット
chrome.alarmsによる定期クリーンアップ(再起動耐性あり)- Side Panel 起動制御(推奨:openPanelOnActionClick)
責務:
chrome.storage.localのfeed_logを読み込み、カード形式で表示- フィルタ(All/Tweets/Media)
chrome.storage.onChangedによるリアルタイム反映- DOM API による安全なレンダリング(
innerHTML排除)
- Content Script が DOM 変化を監視し、ツイート要素を監視対象に追加
- IntersectionObserver が閾値を満たした要素を「視認」と判定
- Content Script が LogItem を生成し、内部キューに蓄積
- 一定周期/件数で
LOG_BATCHとして Background に送信 - Background が
feed_logを取得 → マージ → TTL/上限適用 → 保存 - Feed UI が
storage.onChangedを受けて再描画(または手動Reload)
- Header
- Filter: All / Tweets / Media
- Reload(任意)
- Feed List
- Card(tweet/image/video)
- Empty State
- 「No items found in last 10 mins.」
+---------------------+
| X (x.com) tab |
| (scroll/refresh) |
+----------+----------+
|
| (視認ログが貯まる)
v
+---------------------+ action click / Side Panel open
| Background (SW) |-------------------------------------+
| merge + TTL + cap | |
+----------+----------+ v
| +-------------------+
| storage.local feed_log update | Side Panel UI |
| (array) | feed.html/feed.js |
+---------------------+ +---------+---------+
| |
| |<-------------------------------------+
+---------------------+ storage.onChanged
- Tweet container:
[data-testid="tweet"] - Tweet text:
[data-testid="tweetText"] - Photo wrapper:
[data-testid="tweetPhoto"] - Video player:
[data-testid="videoPlayer"]
注記:
- クラス名はビルドごとに変動し得るため依存しない
- data-testid の変更リスクはゼロではないため、定数へ集約し保守点を局所化する
type LogItem = {
key: string; // dedupe キー(例:tweet:123..., image:https://pbs...)
type: 'tweet'|'image'|'video';
url: string; // typeごとの対象URL(tweetはtweetUrlでも可)
tweetUrl?: string; // 元ツイートURL
text?: string; // 取得できる範囲で(空可)
author?: string; // 取得できる範囲で(空可)
timeISO?: string; // time[datetime]等(空可)
seenAt: number; // epoch ms(最後に見た時刻)
source?: { pageUrl?: string };
};-
キー:
feed_log -
値:
LogItem[](新しい順にソート済み) -
制約:
- TTL:
now - seenAt < TTL_MS - 上限:
MAX_ITEMS
- TTL:
| Key | Default | 意味 | 影響 |
|---|---|---|---|
| TTL_MS | 600,000 (10分) | ログ保持期間 | 長いほどノイズ増、短いほど“取りこぼし” |
| MAX_ITEMS | 500 | 最大保存件数 | UI描画/容量の上限 |
| STORAGE_KEY | feed_log |
保存キー | UI/Background で共通 |
| CLEANUP_ALARM | cleanup |
アラーム名 | 定期削除の識別 |
| Key | Default | 意味 | 影響 |
|---|---|---|---|
| IO_THRESHOLD | 0.6 | 視認判定閾値 | 低いほど誤検知増、高いほど取りこぼし |
| FLUSH_INTERVAL_MS | 2000 | バッチ送信間隔 | 小さいほどリアルタイムだが通信増 |
| FLUSH_MAX_ITEMS | 10 | 送信トリガ件数 | 小さいほど小刻み、大きいほど遅延 |
| KEY_COOLDOWN_MS | 30000 | 同一キー再送抑止 | 小さいほど再送増、大きいほど更新抑制 |
LOG_BATCH
{
"type": "LOG_BATCH",
"items": [
{
"key": "tweet:1234567890",
"type": "tweet",
"url": "https://x.com/user/status/1234567890",
"tweetUrl": "https://x.com/user/status/1234567890",
"text": "sample text",
"author": "@user",
"timeISO": "2025-12-12T01:23:45.000Z",
"seenAt": 1765500000000,
"source": { "pageUrl": "https://x.com/home" }
}
]
}chrome.storage.localのget → setを複数バッチで並列実行すると取りこぼしが発生し得る- 対策:Promise キューで
processBatch()を直列化する
-
同一キーの既存
prevと新規nextがある場合:seenAtはmax(prev.seenAt, next.seenAt)author/text/timeISO/tweetUrl/url/typeは next が空でない時のみ上書き(情報劣化を防止)
- 保存時に必ず TTL フィルタと上限カットを実施(cleanup 依存禁止)
-
1分ごとに期限切れを削除
-
SW再起動耐性:
- 起動時に
ensureCleanupAlarm()を呼び、存在しなければ作成
- 起動時に
- All:全件
- Tweets:
type === 'tweet' - Media:
type === 'image' || type === 'video'
chrome.storage.onChangedを監視し、feed_log更新時にデバウンス(例:100ms)して再描画
- DOM API で安全にレンダリングし、
innerHTMLを使用しない - 外部リンクは
rel="noopener noreferrer"
loading="lazy",decoding="async"referrerPolicy="no-referrer"
課題:
pbs.twimg.comはname=small/large等のパラメータ揺れがあり、同一画像が重複する可能性がある。
方針(推奨):
- 表示URLは変更しない
- 重複排除キーのみ正規化(例:
nameを削除)
適用箇所:
- Content 側の key 生成時、または Background の
processBatch()前処理で key を補正
[
{
"key": "tweet:1234567890",
"type": "tweet",
"url": "https://x.com/user/status/1234567890",
"tweetUrl": "https://x.com/user/status/1234567890",
"text": "This is a sample tweet text.",
"author": "@user",
"timeISO": "2025-12-12T01:23:45.000Z",
"seenAt": 1765500000000,
"source": { "pageUrl": "https://x.com/home" }
},
{
"key": "image:https://pbs.twimg.com/media/ABCDEF12345?format=jpg",
"type": "image",
"url": "https://pbs.twimg.com/media/ABCDEF12345?format=jpg&name=large",
"tweetUrl": "https://x.com/user/status/1234567890",
"seenAt": 1765500000123,
"source": { "pageUrl": "https://x.com/home" }
}
]- X 上でスクロールすると、直近10分の tweet/image が
feed_logに保存される - Feed UI に All/Tweets/Media のフィルタがあり、内容が正しく切り替わる
- 期限切れ(10分超)データが削除される(保存時・定期cleanupの両方で担保)
- 上限 500 件を超えない
- 高速スクロールでも体感劣化がない(バッチ送信が機能)
- UI は
innerHTMLを使用せず、外部リンクは noopener を付与
x-stream-saver/
manifest.json
src/
content.js
background.js
feed/
feed.html
feed.js
feed.css (任意:CSS分離する場合)
README.md (任意:インストール手順)
- ストレージ:chrome.storage.local
- TTL:10分
- 上限:500件
- セレクタ:data-testid 基準
- 通信:LOG_BATCH(バッチ送信)
- UI:Side Panel を基本(feed.html を default_path に設定)
- Background:Promiseキューで競合防止、フィールド単位マージ、Alarm 再起動耐性