つぶやき Web拍手を送る

実践編その⑤
モーダルで画像が表示されるギャラリーを作ろう

この項では、サンプルサイトの下半分――サムネイルをクリックすると大きい画像がモーダルウインドウで表示される形式のギャラリーを作っていきます。
最終的には表示させたいデータ(使用するサムネイル・原寸画像のパスやキャプションの文章など)を管理用の一つのファイルにまとめて記述しておいて、そこから必要なhtmlを生成する仕組みにしていきたいと思います。

モーダルウインドウを表示させるスクリプトには、doさんで公開されているfuwaimgを使用することにします(配布サイトが日本語で使い方が分かりやすいため)。他のモーダルウィンドウ表示スクリプトでも基本の考え方は共通だと思うので、お好きなものに応用してみてください。

まずは普通に書いてみる

いきなり11tyの機能を組み込んで作っていくのは大変なので、実践編その③ 展示ページの目次を作ろうの最後に述べたように、まずは完全手打ちで作るときと同様にギャラリー部分を作成し、後から自動でhtmlを生成するようなコードに書き換えていくという手順で進めていきます。

最初にfuwaimgの必要なスクリプトをサイト用フォルダに配置します。画像やcssと同様にそのまま_siteフォルダ内にコピーして欲しいので、srcフォルダ内にDL・解凍したfuwaimgフォルダを丸ごと入れておきましょう。

fuwaimgのファイル一式をsrcフォルダに置いたところ

これで必要なスクリプトの準備はできました。ここから、index.htmlにギャラリー部分を作っていきます。 まずは、index.html内で上記スクリプトの必要なファイルを読み込みます。詳しい説明は配布ページで書かれているのでそちらを参照していただくとして、ここでは実際のコードを示していきます。 まずはfuwaimg.css<head>~</head>内で読み込むために、index.htmlで使用しているレイアウトlayout.htmlに下記の記述を追加します。

_includes/layout.html
 <!-- 前略 -->
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <link rel="stylesheet" href="/src/style.css">
+    <link rel="stylesheet" href="/src/fuwaimg/css/fuwaimg.css"> <!-- この行を追加 -->
     <title>{{ title }} - Sample Site</title>
 </head>
 <!-- 後略 -->

次は必要なjavascriptファイル2つをindex.htmlの最下部で読み込みます。(layout.htmlに書いてもいいのですが、他のページでもlayout.htmlを使っていた場合、必要ないのにスクリプトを読み込むことになって無駄な感じがするので、使用するテンプレートファイルの方に記述しています この辺りはお好みで)

index.html
 <!-- 前略 -->
 <section>
     <h2>Gallery 2/モーダルを使用した例</h2>
     <p>モーダルウィンドウを使用した画像ギャラリーをここに作る予定</p>
 </section>
 
 <!-- 以下の2行を追加 -->  
+<script src="/src/fuwaimg/js/scrollbooster.min.js"></script>
+<script src="/src/fuwaimg/js/fuwaimg.js"></script>

これで必要なファイルは読み込めたので、ギャラリーを表示するためのコードを書いていきましょう。とりあえず動作確認も兼ねて3~4つ程度画像を置いてみます。こちらも詳細な説明はdoさんのfuwaimg配布ページにお任せするとして、実際のコードを以下に示します。(画像は各自お好きなものを用意してください。以下のコードでは、サムネイルに使用する画像は正方形にトリミングしてsrc/squareフォルダ内に格納してある前提で書いています。)

index.html
<!-- 前略 -->
<section>
    <h2>Gallery 2/モーダルを使用した例</h2>
    <ul class="gallery_2">
        <li>
            <a href="/src/001.jpg" data-fimg="gallery2" data-fcaption="ここにキャプションを入れることができます" class="fuwaimg">
                <img src="/src/square/001_sq.jpg" alt="">
            </a>
        </li>
        <li>
            <a href="/src/002.jpg" data-fimg="gallery2" data-fcaption="2枚目" class="fuwaimg">
                <img src="/src/square/002_sq.jpg" alt="">
            </a>
        </li>
        <li>
            <a href="/src/003.jpg" data-fimg="gallery2" data-fcaption="" class="fuwaimg">
                <img src="/src/square/003_sq.jpg" alt="">
            </a>
        </li>
        <li>
            <a href="/src/004.jpg" data-fimg="gallery2" data-fcaption="キャプションはなくても可" class="fuwaimg">
                <img src="/src/square/004_sq.jpg" alt="">
            </a>
        </li>
    </ul>
</section>
<!-- 後略 -->

基本は以下のようなコードを表示させたい画像の分だけ<ul></ul>内で繰り返しているだけです。

<li>
	<a href="表示したい画像のパス" data-fimg="グループ名(半角英数字)" data-fcaption="ここにキャプション" class="fuwaimg">
		<img src="サムネイル画像のパス">
	</a>
</li>

ではここで、一度11tyのローカルサーバーを起動して、ちゃんとギャラリーが表示できるか確認しておきましょう。

サムネイル画像が並んでいる

サムネイルが並んでいるのでクリックして……

モーダルで原寸画像が表示されたところ

このように表示されればOKです。それでは、自動でhtmlを生成する機能を組み込んでいきましょう。

データを管理するためのファイルを作ろう~Global Data Files~

今回は、データを管理するためのファイルとしてGlobal Data Filesという仕組みを利用します。 Global Data Filesとは、ある特定のフォルダ内(デフォルトでは_dataという名前のフォルダ)にJSONファイルを配置しておくことで、そのJSONファイルのデータをサイト内のどこからでも参照できるようになる仕組みです。
例えば、下記のように_dataフォルダの中にlist.jsonというファイルを作成し、

Global data filesの階層構造 このファイルの中身にJSON形式でデータを記述しておくと、サイトのどのテンプレートからでもlistというキーワードでその中身のデータを参照できるようになります。 この仕組みを利用して、ギャラリーに表示させるデータを1つのファイルで管理できるようにしてみます。

何をデータとして抜き出すのかまとめよう

ここで、ギャラリーを構成するためにどのようなデータを持っておけばいいのか、整理してみましょう。 まずは普通に書いてみるの項目でも述べたように、ギャラリーは以下のようなコードを表示させたいコンテンツの分だけ繰り返すことで構成されていました。

<li>
	<a href="表示したい画像のパス" data-fimg="グループ名(半角英数字)" data-fcaption="ここにキャプション" class="fuwaimg">
		<img src="サムネイル画像のパス">
	</a>
</li>

このコードを見てみると、1つのコンテンツにつき必要な情報は以下の3つであることが分かります。

つまり、1つのコンテンツにつき上記3つの情報を1つのまとまりとして、それをコンテンツの分だけ並べておけばよさそうです。以下の図のようなイメージです。 データファイルに持たせたいデータの構造はこんな感じ そして、上記のイメージをJSON形式に置き換えるとこのような形になります。

[
    {
        "imagePath": "表示したい画像①のパス",
        "caption": "①のキャプションの文章",
        "thumbnailPath": "サムネイル画像①のパス"
    },
    {
        "imagePath": "表示したい画像②のパス",
        "caption": "②のキャプションの文章",
        "thumbnailPath": "サムネイル画像②のパス"
    },
    //(...中略...)
	{
        "imagePath": "表示したい画像のパス",
        "caption": "キャプションの文章",
        "thumbnailPath": "サムネイル画像のパス"
    }
]

{}で囲まれた一塊が一つのコンテンツに対応しており、それをコンテンツの数だけ,(コンマ)で区切って一番外側の[]の中に並べていく感じです。 それでは、次の項でギャラリーを生成するための具体的なコードを記述していきましょう。

データからhtmlを出力するように改造しよう

まずは表示させたいコンテンツのデータをまとめるためのGlobal Data Fileを作成します。サイト制作用フォルダの直下に_dataという名前のフォルダを作り、その中にcontents.jsonというファイルを作成します。

contents.jsonという名前のGlobal data fileを作成 このcontents.jsonの中に表示させたいコンテンツのデータを記述していきます。画像のパスやキャプションの中身などは任意で設定してください。また、キャプションを書きたくないコンテンツがあるときは、"caption"の行を消すのではなく、対応する値を空の文字列""にしておきます。

_data/contents.json
[
    {
        "imagePath": "/src/001.jpg",
        "caption": "001.jpgのキャプション",
        "thumbnailPath": "/src/square/001_sq.jpg"
    },
    {
        "imagePath": "/src/002.jpg",
        "caption": "002.jpgのキャプション",
        "thumbnailPath": "/src/square/002_sq.jpg"
    },
    {
        "imagePath": "/src/003.jpg",
        // ↓キャプションが不要なときの例
        "caption": "",
        "thumbnailPath": "/src/square/003_sq.jpg"
    },
    //(...中略...)
    {
        "imagePath": "/src/XXX.jpg",
        "caption": "XXX.jpgのキャプション",
        "thumbnailPath": "/src/square/XXX_sq.jpg"
    }
]

次に、このcontents.jsonのデータをindex.html内に読み込んでいきます。いきなりギャラリー表示用のコードに組み込んでいくのは少し骨が折れるので、とりあえずデータを読み込んで文字列として表示させてみましょう。

Global Data Fileはファイル名をキーとしてデータを参照することができるのでした。ですので、今回は{{contents}}と書くことで上記のデータを参照することができるようになります。
さらに、今回の記述方法だとcontentsの中身は配列となっており、{{contents[順番を表す数値]}}とするとその位置に当たる要素(要素…ここでは{}で囲まれた1つの塊のこと)を参照することができます。この順番は0から始まって1,2,……とカウントするため、contents[0]とすると一番最初の要素が取り出せます。
そしてcontents[0]の後に.属性の名前と続けると、要素内のその属性に対応した値を取り出すことができます。今回の例では、imagePath, caption, thumbnailPathが属性に当たります。これを踏まえてcontents.json内で1番上に記述したコンテンツに対する画像のパス、キャプション、サムネイルのパスをindex.htmlで表示させてみます。

<!-- 前略-->
<section>
    <h2>Gallery 2/モーダルを使用した例</h2>
    <p>
        画像のパス:{{contents[0].imagePath}}<br>
        キャプション:{{contents[0].caption}}<br>
        サムネイルのパス:{{contents[0].thumbnailPath}}
    </p>
    <ul class="gallery_2">
<!-- 後略 -->

結果 20250218223738 無事にcontents.json内に記述したデータの、1番最初の要素を取り出すことが出来ました。また、上記のコードの[]内の数字を1,2,3,...と変えていくとcontents.json内の2番目の要素、3番目の要素、4番目の要素、……と取り出す要素を変えることができます。ローカルサーバーを起動した状態で数字を書き換え→上書き保存して、挙動を確認してみましょう。


これでデータの取り出し方は分かったので、いよいよこのデータを最初に記述したギャラリーのコードに組み込んでいきます。 前述の通り、ギャラリーは以下のようなコードを表示させたいコンテンツの分だけ繰り返すことで構成されています。

<li>
	<a href="表示したい画像のパス" data-fimg="グループ名(半角英数字)" data-fcaption="ここにキャプション" class="fuwaimg">
		<img src="サムネイル画像のパス">
	</a>
</li>

繰り返しということは、実践編その③ 展示ページの目次を作ろうでも使用したfor文が使えそうです。

{% for xxx in yyy %} 
 この間に繰り返したいコードを書く
{% endfor %}

ここで、繰り返しの対象となる配列はcontents.jsonから取り出したcontents、「繰り返したいコード」は上で示した<li>~</li>のコードですのでそのままfor文に当てはめると……

{% for item in contents %} 
	<li>
		<a href="表示したい画像のパス" data-fimg="グループ名(半角英数字)" data-fcaption="ここにキャプション" class="fuwaimg">
			<img src="サムネイル画像のパス">
		</a>
	</li>
{% endfor %}

そして、{% for item in contents %}{% endfor %}の中では、配列contents内の1つ1つの要素(上でcontents[0]contents[1]などとして取り出していたデータ)はitemという名前で扱うことができます。さらにこのデータ内に含まれる画像のパスやキャプションなどは.属性の名前と付けることで取り出すことが出来ました。 これによって上記のコード内の"表示したい画像のパス" "ここにキャプション"などの部分を置き換えると……

{% for item in contents %} 
 <li>
	<a href="{{item.imagePath}}" data-fimg="グループ名(半角英数字)" data-fcaption="{{item.caption}}" class="fuwaimg">
		<img src="{{item.thumbnailPath}}">
	</a>
  </li>
{% endfor %}

このように書けばいいことが分かります。あとは「グループ名」の部分を任意の文字列にしてやればOKです。 それでは実際にindex.htmlの該当部分を上記のコードに置き換えてみましょう。

index.html
<!-- 前略 -->
<section>
    <h2>Gallery 2/モーダルを使用した例</h2>
    <ul class="gallery_2">
        {% for item in contents %}
        <li>
            <a href="{{item.imagePath}}" data-fimg="gallery2" data-fcaption="{{item.caption}}" class="fuwaimg">
                <img src="{{item.thumbnailPath}}">
            </a>
        </li>
        {% endfor %}
    </ul>
</section>
<!-- 後略 -->

この状態で生成されたページをブラウザで確認してみると…… 20250218230954 contents.jsonに記述した分だけ、ちゃんとサムネイルが表示されました。さらにそれぞれクリックするとちゃんとモーダルウィンドウで原寸画像の表示・前後への移動もできるようになっているはずです。また、contents.json内に

{
	"imagePath": "/src/XXX.jpg",
	"caption": "XXX.jpgのキャプション",
	"thumbnailPath": "/src/square/XXX_sq.jpg"
}

この形式をひとまとまりとしてデータを追加したり削除したりすれば、それに伴ってギャラリーに表示されるコンテンツも増えたり減ったりします。ぜひ試してみましょう。

ここで、サムネイルの表示順を確認してみると、contents.json内で上に記述したものが先、下に記述したものが後になっています。データ内を上から下へ順番に処理しているということですね。もしもこれを逆にしたければ、日付データの扱い方にも使用した{% for ... reversed %}の書き方が使えます。もちろん、最初からcontents.json内の並びを希望の順番にしておくのもよいでしょう。この辺りは、更新の際にデータを上に追記していくのか下に追記していくのかも併せて、ご自身のやりやすい・分かりやすい方法を採用すればよいと思います。

これで、最初に示したサンプルサイトは完成です🎉🎉🎉お疲れさまでした!

おまけ:ソースコード(完成品)

サンプルサイト(完成品)のテンプレートファイル及び設定ファイルのソースコードを置いておきます
サイト制作用フォルダ直下の構成はこんな感じです

.eleventy.js
module.exports = function (eleventyConfig) {
    eleventyConfig.addGlobalData("permalink", () => {
        return (data) => `${data.page.filePathStem}.${data.page.outputFileExtension}`;
    });
    eleventyConfig.addPassthroughCopy("src");
};
index.html
---
title: Index
layout: layout.html
---
<img src="src/header.jpg">
<section>
    <h2>About</h2>
    <p>ここにサイトの説明とか注意書きとか書いておくといいですね</p>
</section>
<section>
    <h2>Gallery 1/個別ページの例</h2>
    <p>展示ページの目次がここに来る予定</p>
    <ul class="gallery_1">
        {% for item in collections.gallery reversed %}
        <li>
            <a href="{{ item.page.url }}">
                <img src="{{item.data.thumbnail}}" alt="">
                <span class="thumbnail_caption">
                    {{ item.data.title }}<br>
                    <small>{{ item.page.date | date: "%Y/%m/%d" }}</small>
                </span>
            </a>
        </li>
        {% endfor %}
    </ul>
</section>
<section>
    <h2>Gallery 2/モーダルを使用した例</h2>
    <ul class="gallery_2">
        {% for item in contents %}
        <li>
            <a href="{{item.imagePath}}" data-fimg="gallery2" data-fcaption="{{item.caption}}" class="fuwaimg">
                <img src="{{item.thumbnailPath}}">
            </a>
        </li>
        {% endfor %}
    </ul>
</section>
<script src="/src/fuwaimg/js/scrollbooster.min.js"></script>
<script src="/src/fuwaimg/js/fuwaimg.js"></script>
_data/contents.json
[
    {
        "imagePath": "/src/001.jpg",
        "caption": "001.jpgのキャプション",
        "thumbnailPath": "/src/square/001_sq.jpg"
    },
    {
        "imagePath": "/src/002.jpg",
        "caption": "002.jpgのキャプション",
        "thumbnailPath": "/src/square/002_sq.jpg"
    },
    {
        "imagePath": "/src/003.jpg",
        "caption": "003.jpgのキャプション",
        "thumbnailPath": "/src/square/003_sq.jpg"
    },
    // ...中略...
	// 実際のソースコードではコメント行は削除してください
    {
        "imagePath": "/src/011.jpg",
        "caption": "011.jpgのキャプション",
        "thumbnailPath": "/src/square/011_sq.jpg"
    }
]
_includes/layout.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="/src/style.css">
    <link rel="stylesheet" href="/src/fuwaimg/css/fuwaimg.css">
    <title>{{ title }} - Sample Site</title>
</head>
<body>
    {% include "partials/header.html" %}
    {{ content }}
    {% include "partials/footer.html" %}
</body>
</html>
_includes/gallery.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="/src/style.css">
    <title>{{ title }} - Sample Site</title>
</head>
<body>
    {% include "partials/header.html" %}
    {{ content }}
    {% assign previousPost = collections.gallery | getPreviousCollectionItem: page %}
    {% assign nextPost = collections.gallery | getNextCollectionItem: page %}
    <section>
        <nav class="navInCollection">
            <ul>               
                {% if nextPost %}
                <li class="navPrev">
                    <a href="{{ nextPost.page.url }}">{{ nextPost.data.title }}</a>
                </li>
                {% endif %}
                {% if previousPost %}
                <li class="navNext">
                    <a href="{{ previousPost.page.url }}">{{ previousPost.data.title }}</a>
                </li>
                {% endif %}
            </ul>
        </nav>
    </section>
    {% include "partials/footer.html" %}
</body>
</html>
_includes/partials/header.html
<header>
    <h1><a href="/">Sample Site</a></h1>
</header>
_includes/partials/footer.html
<footer>
    <h4>Sample Site</h4>
    <p>
        <small>© 2024 yoshimura</small>
    </p>
    <ul class="link_footer">
        <li>
            <a>
                <span class="material-symbols-outlined">home</span>
            </a>
        </li>
        <li>
            <a>
                <span class="material-symbols-outlined">mail</span>
            </a>
        </li>
        <li>
            <a>
                <span class="material-symbols-outlined">favorite</span>
            </a>
        </li>
        <li>
            <a>
                <span class="material-symbols-outlined">onsen</span>
            </a>
        </li>
    </ul>
</footer>
gallery/001.html
---
title: 展示ページ①
thumbnail: /src/thumbnails/001.jpg
date: 2024-01-23
---
<section>
    <img src="/src/001.jpg" alt="">
    <h2>{{ title }}</h2>
    <p>けれどもぼくは、そんなことを習ったろうと思いながら答えました。僕いま苹果のことを考えているんですそうですか。けれども、誰だって、ただそう感じているのでした。つりがねそうか野ぎくかの花があちこち咲いていました。時計屋の店には明るくネオン燈がついて、たくさんのきいろな底をもった人に出しました。</p>
</section>
<!-- ファイルの数や内容は任意 -->
gallery/gallery.json
{
    "layout": "gallery.html",
    "tags": "gallery"
}

おわりに

ここまで目を通していただきありがとうございました。 当サイトの制作に使用しているSSGの11tyについて、必要なツールのDLからちょっとしたサイトを構築するまでの流れを一通り解説してみました。基本的なことや創作系の個人サイトで使いそうな機能にしぼって説明したため、ここで紹介できていない機能や説明が十分でない概念なども色々あります。気になる部分や深く知りたい部分などあれば、ぜひググったり、文中で紹介している公式ドキュメントを参照したりしてみてください。(公式ドキュメントは英語ですが、今どきは優れた翻訳ツールもあるので恐れることはないぞい)
この解説が、サイト制作・改装に関する何らかのヒントや足掛かりになれば幸いです。