【個人開発】Laravel + Vue.jsで作る週刊少年ジャンプ年表アプリ【ジャンプなに読んでた?】
こんにちは。最近、「昔ジャンプで何読んでたっけ?」と思い出すことが増えたので、週刊少年ジャンプの歴代連載作品の年表サイトを作ってみました。
その名も…
ジャンプなに読んでた?

🔍 どんなサイト?
- 1968年創刊からの ジャンプ連載作品を年表形式で一覧表示
 - 縦スクロールで時代をさかのぼり、作品ごとの連載期間が一目でわかる
 - 気になる作品をクリックすると 作品紹介がポップアップ
 - さらに、コメントをニコニコ動画風に流せる機能も実装!
 
💡 技術スタック
| 技術 | 内容 | 
|---|---|
| Laravel | バックエンド(API、DB、ルーティング) | 
| Vue.js | フロントエンドUI(SPA) | 
| MySQL | データベース(Dockerで構築) | 
| Vite | フロントビルドツール | 
🛠 開発の工夫ポイント
今回このサイトを公開するにあたって、使用した技術や工夫した点をいくつか紹介します。
データベース
 作品テーブルworks
Wikipediaの週刊少年ジャンプ連載作品の一覧からデータを拝借。
このページの表をそのままテーブルにしました。
| カラム名 | 型 | 説明 | 
|---|---|---|
| id | bigint unsigned | 作品ID(自動採番) | 
| title | varchar(255) | 作品タイトル | 
| author | varchar(255) | 作者名 | 
| original | varchar(255) | 原作名(原作がある場合) | 
| start_number | varchar(255) | 連載開始号 | 
| end_number | varchar(255) | 連載終了号 | 
| note | varchar(255) | 備考(特記事項など) | 
 バックナンバーテーブルback_numbers
ゼブラック(集英社が運営する総合電子書籍サービス)の、週刊少年ジャンプのバックナンバーから情報を取得。
また、過去のバックナンバーはメディア芸術データベースというサイトからスクレイピングで取得したりもしました。
| カラム名 | 型 | 説明 | 
|---|---|---|
| number | varchar(255) | バックナンバー号数 | 
| release_date | date | 発売日 | 
この二つのテーブルを結合して、作品ごとの連載期間を算出しています。
クエリはシンプルでこんな感じ
$works = DB::table('works')
  ->select(
      'works.*',
      'start_back_numbers.release_date as start_release_date',
      'end_back_numbers.release_date as end_release_date'
  )
  ->leftJoin('back_numbers as start_back_numbers', 'start_back_numbers.number', '=', 'works.start_number')
  ->leftJoin('back_numbers as end_back_numbers', 'end_back_numbers.number', '=', 'works.end_number')
  ->orderBy('start_back_numbers.release_date', 'asc')
  ->get();
作品年表の表示
PHPでデータを整形して、Vue.jsで縦スクロールの年表を描画。
これが一番のポイントです。
日付の値をstrtotime()でタイムスタンプに変換し、その数値をもとに縦位置や連載期間(グラフの長さ)を描画しています。
まずは年表の開始日を指定します。
ジャンプの創刊は1968年8月1日なので、この年1月1日から始まるようにします。
$chronology_start_date = '1968-01-01';
次に、年表の開始日と、作品の連載開始日、連載終了日を全てタイムスタンプに変換し、
この数値をもとに作品の縦位置(top)と連載期間(serialization_period)を計算します。
// $chronology_start_date => '1968-01-01' // 年表の開始日
// $tmp_work['start_release_date'] => '1968-08-01' // 作品の連載開始日
// $tmp_work['end_release_date'] => '1968-12-26' // 作品の連載終了日
// この作品の年表の縦位置は、年表の開始日から6ヶ月後の位置、(1968-08-01 - 1968-01-01 )
// 作品の連載期間を表す縦線の長さは、約5ヶ月分の長さ(1968-12-26 - 1968-08-01 )
// これをそれぞれ、strtotime()でタイムスタンプに変換して計算します。
// 作品の縦位置
$tmp_work['top'] = strtotime($tmp_work['start_release_date']) - strtotime($chronology_start_date);
// => 18403200
// 作品の連載期間
$tmp_work['serialization_period'] = strtotime($tmp_work['end_release_date']) - strtotime($tmp_work['start_release_date']);
// => 12700800
これの値をもとに、Vue.jsで年表を描画します。
<template v-for="work in works" :key="work.id"><!-- 作品ごとにループ -->
    <div class="work" :style="workStyle(work)"><!-- 作品のスタイルを適用 -->
        <div class="title">{{ work.title }}</div><!-- 作品タイトル -->
    </div>
</template>
//作品のスタイル
workStyle(work) {
    // scaleは年表の縮尺、1画面にいい感じに収まるように調整するための値
    // 作品を表示する縦位置を計算
    const top = work.top / this.scale;
    // 作品の連載期間を高さに変換
    const height = work.serialization_period / this.scale;
    // 現在のスクロール位置と作品の位置を比較して、表示するかどうかを決定
    // → 連載終了した作品を非表示にするため
    const isVisible = top + height > this.scrollTop;
    return {
        top: top + 'px',
        height: height + 'px',
        opacity: isVisible ? 1 : 0, //非表示の場合は透明にする
        width: isVisible ? '14px' : '0', //非表示の場合は横幅も幅を0にする
        transition: 'opacity 0.3s,width 0.3s', // アニメーション効果
    };
},
これを描画した結果がこちらです。

コメント機能
作っている途中で、ただの年表では面白くないなと思い、ニコニコ動画風のコメント機能を実装しました。
(特許とか著作権は問題ないですよ!)
入力蘭にコメントを入力し、送信ボタンを押すと、コメントが年表の右から左へ流れていきます。
コメントの送信処理はこんな感じです。
sendComment() {
    if (!this.newComment.trim()) return; // 空のコメントは送信しない
    const id = self.crypto.randomUUID(); // UUIDを生成して一意のIDを作成
    const commentText = this.newComment.trim(); // 入力されたコメントを取得
    const chronologyHeight = this.$refs.chronology.clientHeight; // 年表の高さを取得
    const topOffset = this.scrollTop + chronologyHeight / 2; // 年表の中央にコメントを配置
    const distance = window.innerWidth + 200; // コメントが流れる距離(画面幅 + 200pxの余裕)
    const speed = 100; // コメントの流れる速度(px/s)
    const duration = distance / speed; // コメントが流れる時間(秒)
    const makeCommentStyle = () => ({
        position: 'absolute',
        whiteSpace: 'nowrap',
        top: `${topOffset}px`, // 現在表示されている年表の中央に配置
        animation: `move-left ${duration}s linear infinite`, // 左へ流れるアニメーション
    });
    // コメントオブジェクトを作成
    const comment = {
        id,
        text: commentText,
        style: makeCommentStyle(),
    };
    // コメントをVueのデータに追加
    this.comments.push(comment);
    this.newComment = '';
    // fetchでコメントをサーバーに送信
    fetch('/api/comments', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
        },
        body: JSON.stringify({
            text: commentText,
            position: topOffset
        })
    }).then(res => {
        // return res.json();
    });
}
実際にコメントが流れる様子は、ぜひ
こちらのサイトからご確認ください。
🤝 おわりに
今回は主にアプリケーションの実装内容を紹介しましたが、
他にも、スクレイピングでデータを取得したり、Dockerで環境構築をしたりと、いろいろな技術を使いました。
興味のある方は、ぜひコメントしてください。
Discussion