概要
タイムラインページのナビタブ(ブログ・動画・ゲーム・写真)を切り替えたとき、ページ上部にあるアクティビティグラフ(いわゆる「草グラフ」)も連動してカテゴリ別の活動履歴に切り替わるよう実装した。
切り替えはCSSトランジションによるなめらかなフェードで表現されており、ユーザーは各カテゴリの活動パターンを視覚的に比較できる。
修正したバグ
HTMLコメント内の --> による文字化け
日付フィルタ(草グラフのマス目をクリックして特定の日の記録を表示する機能)を使うと、タイムラインの先頭に
/g, '-->').replace(/
という文字列が表示されていた。
原因
タイムラインのHTML断片を返すAPIのテンプレートに、以下のようなデバッグ用のHTMLコメントが埋め込まれていた。
<!-- diag: {JSON.stringify(debugData).replace(/-->/g, '-->').replace(/</g, '<')} -->
AstroのテンプレートでHTMLコメント(<!-- -->)内の {...} 式は評価されない。結果として、.replace(/--> という文字列がそのまま出力される。HTMLコメントは最初の --> で閉じられるため、その後に続く /g, '-->').replace(/ がコメント外のテキストとして描画されてしまっていた。
修正方法
この診断用コメントを削除した。デバッグ目的のコードが本番環境に残ったままだった点が根本的な原因であり、恒久的な解決策として削除が最適だった。
アクティビティグラフとフィルタの連動
設計方針
グラフ側(ActivityGraphコンポーネント)とタイムライン側(ページスクリプト)は、カスタムイベントで通信する。ページスクリプトはフィルタ状態を持つが、グラフはフィルタ状態を直接知らない。イベントを受け取ったときだけグラフを更新する、疎結合な設計にした。
- ページ側 → グラフ:
graph:filterイベントで切り替え先のカテゴリ名を渡す - グラフ → ページ側:
graph:dateFilteredイベントで日付フィルタ適用後のタイムライン置き換えを通知する
サーバーサイドでの事前計算
グラフコンポーネントは、レンダリング時にカテゴリごとのカウントマップを計算する。
- 全体: つぶやき+ログの日付ごとの件数
- ブログ: RSSフィードから取得したブログ記事の日付ごとの件数
- 動画: YouTube URLを含むつぶやきの日付ごとの件数
- ゲーム: ゲーム情報が紐づいているつぶやきの日付ごとの件数
- 写真: 画像が添付されているつぶやきの日付ごとの件数
これらのカウントマップを set:text ディレクティブで非表示の div 要素に埋め込む。set:text はHTMLエンティティを自動エスケープするため、JSONデータによるXSSリスクがない。
<div id="graph-category-data" hidden set:text={JSON.stringify({
all: allCounts,
blog: blogCounts,
youtube: youtubeCounts,
game: gameCounts,
photo: photoCounts
})}></div>
クライアントサイドでは textContent で読み出し、JSON.parse でパースする。
クライアントサイドの切り替え処理
graph:filter イベントを受信すると、対応するカウントマップを取得してすべてのセルに適用する。
document.addEventListener('graph:filter', (e) => {
const filterType = e.detail?.filter || 'all';
const countMap = categoryData[filterType] || categoryData['all'];
applyCountMap(countMap);
updateSummary(countMap);
});
applyCountMap は各セルの data-count 属性とレベルクラス(level-0 〜 level-4)を更新する。セルにCSSトランジション(transition: background-color 0.35s ease)を設定しているため、クラス変更だけでなめらかなフェード効果が得られる。
日付フィルタとカテゴリフィルタの競合回避
従来、日付フィルタ(グラフのマス目クリック)でタイムラインを置き換えたとき、アクティブなカテゴリフィルタが無効化される問題があった。
対応策として、タイムラインの置き換え完了後に graph:dateFiltered イベントを発行するようにした。ページスクリプト側でこのイベントを受け取り、アクティブなカテゴリフィルタを再適用することで、両フィルタが正しく共存する。
// タイムライン置き換え後
document.dispatchEvent(new CustomEvent('graph:dateFiltered'));
// ページスクリプト側でフィルタを再適用
document.addEventListener('graph:dateFiltered', () => {
if (blogFilterActive) {
// ブログカードのみ表示する処理
} else if (youtubeFilterActive) {
// 動画カードのみ表示する処理
}
// ...
});
まとめ
今回の実装では、コンポーネント間の通信にカスタムイベントを活用することで、グラフとタイムラインの独立性を保ちながら連動を実現した。サーバーサイドで事前計算したデータをJSON形式で埋め込み、クライアントでは切り替えのみを行う設計は、パフォーマンスとセキュリティの両面で堅牢だと思う。