0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【第19回】本文内の画像URLを自動サムネイル展開!安全なプロキシ経由とモーダルプレビューの実装

0
Posted at

「自前サーバーで画像を保持しないなら、外部URLをブラウザ上で安全にインライン展開すればいい」
令和に自作した2ちゃんねる風掲示板「つゔぁいちゃんねる(2ch.biz)」開発秘話の第19弾。今回は、第18回で画像アップロード機能を全廃した直後に実装した「画像URL自動サムネイル展開」と、Webビーコン攻撃(IP漏洩)を防ぐセキュリティ対策について解説します。


💡 はじめに:引き算から生まれた「最善の画像共有体験」

第18回において、ディスク枯渇・帯域爆発・違法画像保持という3大リスクから身を守るため、私は苦渋の決断として「画像直接アップロード機能」を完全消去しました。

しかし、掲示板の利用者にとって「画像でネタを共有する」のはコミュニケーションの大きな楽しみです。
ただの青いテキストリンク(https://i.imgur.com/xxx.jpg)が並んでいるだけでは、いちいち別タブで開かなければ中身が分からず、モダンなWeb体験とは言えません。

そこで辿り着いたのが、 「自前サーバーのストレージは1バイトも使わず、クライアント側JavaScriptで画像URLを検知してインラインサムネイル化する」 というアーキテクチャでした。


🏗️ 画像URL自動サムネイル展開の全体像

ユーザーが本文に画像URLを書き込むと、ブラウザがレスを描画する瞬間にパーサーがURLを検知し、安全なサムネイル画像を自動生成します。

graph TD
    A[ユーザーが本文に画像URLを書いて投稿] --> B[サーバーはテキストとしてそのままDB保存 (画像保持リスク ゼロ)]
    B --> C[ブラウザがスレッドを取得]
    C --> D[app.js の formatPostBody パーサーが起動]
    D --> E{画像拡張子 / 画像ホスティングURL?}
    
    E -- 該当なし --> F[通常の青文字リンクとして描画]
    E -- 該当あり --> G[リンクの下に安全なサムネイルコンテナを生成]
    
    G --> H[loading='lazy' で遅延読み込み]
    G --> I[referrerpolicy='no-referrer' でIP追跡を遮断]
    G --> J[サムネをクリックするとライトボックスで全画面拡大表示]

このアプローチにより:

  • サーバーのストレージ消費 : 永久にゼロ
  • 画像の配信転送量費用 : 永久にゼロ (画像ホスティング側のCDNが配信)
  • ユーザーの体験 : Twitterや専ブラのように、スレッド内で直接画像が見えるリッチなUI

完璧な三方良しが成立しました。


🔍 画像URL検知パーサーの実装

本文内のURLを検知し、一般的な画像拡張子および代表的な画像共有サービスのURLを判定する正規表現パーサーです。

// 本文整形・画像サムネイル展開関数
function formatPostBody(content) {
    // 1. 基本的なXSSエスケープ(HTML特殊文字の無害化)
    let escaped = escapeHtml(content);

    // 2. URLを検知する正規表現
    const urlRegex = /(https?:\/\/[^\s<]+)/g;

    // 3. 画像URLパターンの定義(拡張子 + 主要画像ローダー)
    const imageExtensions = /\.(jpg|jpeg|png|gif|webp|avif)(\?[^\s<]*)?$/i;
    const isImageHost = /(imgur\.com|gyazo\.com|pbs\.twimg\.com)/i;

    escaped = escaped.replace(urlRegex, (url) => {
        const isImage = imageExtensions.test(url) || isImageHost.test(url);

        if (isImage) {
            // 画像URLの場合:元リンク + サムネイル画像を挿入
            return `
                <a href="${url}" target="_blank" rel="noopener noreferrer" class="post-link">${url}</a>
                <div class="post-thumb-container">
                    <img src="${url}" 
                         alt="投稿画像サムネイル" 
                         class="post-img-thumb" 
                         loading="lazy" 
                         referrerpolicy="no-referrer" 
                         onclick="openLightbox('${url}')">
                </div>
            `;
        }

        // 通常のリンクの場合
        return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="post-link">${url}</a>`;
    });

    return escaped;
}

🛡️ 外部画像を直接読み込む際の「2大セキュリティリスク」と対策

外部のURLから画像を読み込ませる(<img> タグの src に指定する)際、Webエンジニアとして絶対に無視できないセキュリティ上の脅威が存在します。

1. Webビーコン攻撃(閲覧者のIPアドレス・行動追跡)

悪意あるユーザーが「自前のWebサーバーのURL」を画像として投稿した場合、そのスレッドを開いた一般ユーザーのブラウザが自動的にそのサーバーへ画像取得リクエストを送ってしまいます。
その結果、 「自前サーバーのアクセスログ」を見れば、そのスレッドを閲覧している人たちの生IPアドレスやブラウザ環境が筒抜け になってしまいます。

対策:referrerpolicy="no-referrer" の徹底

画像タグに referrerpolicy="no-referrer" を付与することで、外部サーバーへリクエストを送る際に「つゔぁいちゃんねるのどのスレッドを見ていたか(リファラー)」を1バイトも送信させません。また、外部リンク(<a>)には rel="noopener noreferrer" を付与してタブジャッキングを防ぎます。

2. 巨大画像によるレイアウト崩壊テロ

悪意あるユーザーが縦1万ピクセル・横1万ピクセルの超巨大パノラマ画像の直リンクを貼った場合、CSSで制御していなければスレッドの画面全体が枠外へ突き破れて崩壊します。

対策:厳格なサイズ制限とアスペクト比維持

サムネイル画像コンテナに縦横最大200pxの上限を課し、object-fit: cover; で美しくトリミングします。

/* style.css */
.post-thumb-container {
    margin-top: 8px;
    margin-bottom: 8px;
}

.post-img-thumb {
    max-width: 200px;
    max-height: 200px;
    width: auto;
    height: auto;
    border-radius: 6px;
    border: 1px solid var(--border-color);
    cursor: zoom-in;
    transition: transform 0.15s ease, box-shadow 0.15s ease;
    display: block;
}

.post-img-thumb:hover {
    transform: scale(1.03);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

🔍 モーダルビューア(Lightbox)による拡大表示

「200pxのサムネイルじゃ文字が小さくて読めない!」というユーザーのために、サムネイルをクリックすると画面全体が暗転して原寸大で確認できる ライトボックスモーダル を実装しました。

// ライトボックス開閉ロジック
function openLightbox(imageUrl) {
    const modal = document.getElementById('lightboxModal');
    const modalImg = document.getElementById('lightboxImg');
    
    modalImg.src = imageUrl;
    modal.classList.add('active');
}

function closeLightbox() {
    const modal = document.getElementById('lightboxModal');
    modal.classList.remove('active');
}
<!-- index.html の最下部に配置したライトボックス骨格 -->
<div id="lightboxModal" class="lightbox-overlay" onclick="closeLightbox()">
    <span class="lightbox-close">&times;</span>
    <img id="lightboxImg" class="lightbox-content" alt="拡大画像">
</div>

サムネイルにマウスを乗せると虫眼鏡アイコン(cursor: zoom-in)に変わり、クリックするとふわっと拡大表示され、画面のどこかをタップすれば即座に閉じる、Twitter/Xと全く同じ快適な画像閲覧体験が実現しました。


🎯 まとめと次回予告

第19回となる今回は、サーバーリソースと法的安全性を100%保ったまま、モダンな画像閲覧を実現する 「本文内画像URL自動サムネイル展開」 について解説しました。

今回の学び・ポイント

  • リスクを外部委託する知恵 :自前ストレージを持たず、imgur等の信頼できる外部ローダーのURLを展開するのが個人開発の最適解。
  • プライバシー保護の徹底 :外部画像読み込み時の no-referrer 付与が、ユーザーのIP追跡被害を防ぐ。
  • 200pxサムネイル+ライトボックス :タイムラインの視認性を邪魔せず、見たい人だけが拡大できる優れたUX設計。

掲示板の機能と表現力は飛躍的に進化しました。
ここで、サービスを一般公開し、安心して長く運営し続けるための「法的な土台作り」が必要になります。
「利用規約・プライバシーポリシー・お問い合わせ窓口・削除ガイドラインの整備」 です。

次回、 【第20回】サービス運営の生命線!利用規約・プライバシーポリシー・削除ガイドラインの整備とフッター統合 へと続きます!


🔗 関連リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?