🐭

静的サイトジェネレータ 11ty を使いながら説明してみる

2023/02/27に公開

はじめに

静的サイトジェネレータをいろいろ調べているうちに 11ty というものを知り、1 度使ってみました。
その際、日本語ドキュメントが少なかったので公式ドキュメントを読んで学んだことをまとめます。
「よし!記事書くか!」となったところ何と 2 週間前にv2.0.0が登場したことを知り、もう一度ドキュメントを読む羽目になりました。
内容が間違っていないかできる限り確かめたつもりですが、間違いがあったらご指摘ください。

11ty とは

シンプルな静的サイトジェネレータです。
古くて温もりのある Jekyll の代替というのがコンセプトっぽいです。
2023 年 2 月 26 日時点で Github のスター数は 13,285 個で、Jamstack な SSG の中では 15 番目に多いです。(1 位は Next.js の 97,399 個、因縁の Jekyll は 4 位で 45,696 個)
登場は 2018 年 1 月です。現在のメインコントリビュータは開発者の@Zach Leatherman さん 1 人という小さめなフレームワークです。

どんな静的サイトジェネレータか

僕の感想ですが 11ty とはテンプレートエンジンをフレームワークとして最大活用したものです。

テンプレートエンジンとはざっくり説明すると、「データをテンプレートにぶち込んで文書を生成するもの」です。
後でブログみたいな静的サイトを作成しながら 11ty の機能を紹介します。
Template Lauguage
テンプレートエンジンとは(Wikipediaより抜粋)

11ty のいいところ

11ty の良いと思ったところは次の 3 つです。

  1. 10 種類以上のテンプレート言語を使用可能
  2. ビルドが爆速
  3. 学習コストが低い

1. 10 種類以上のテンプレート言語を使用可能

11ty はなんと 10 種類以上のテンプレート言語をデフォルトで使用可能です。テンプレート言語というのは例えば以下のものたちです。

  • HTML
  • Markdown
  • Nunjucks
  • Liquid
  • WebC
  • EJS
  • JavaScript
  • Handlebars
  • Mustache
  • Haml
  • Pug

template engines' logos
テンプレート言語のロゴ(どれがどれだかわかるかな?)

Markdown もテンプレート言語なの?と思われる方もいるかもしれませんが 11ty では Markdown もテンプレートになります。つまり Markdown にもデータを注入可能だということです。
これはなかなか良いじゃありませんか。
これほど多言語対応しているのは他には Hexo くらいです。
因縁の Jykell が対応しているテンプレート言語は Liquid だけですし、Next.js は React, Hugo は Go だけです。

もちろん他の SSG も拡張すれば多言語対応可能だと思いますが、間違いなくこの中にあなたのお気に入りのテンプレート言語があるはずです。

関係ないお話

Markdown にデータを埋め込むには Mustache 記法({{ こんな感じのやつ }})を用いますが、Mustasche のロゴを見たときになぜこのような カーリーブレイスでくくったものを Mustache(口ひげ) 記法と呼ぶのかわかりました。
カーリーブレイスって 90 度回転させたら立派な口ひげに見えますね。

2. ビルドが爆速

11ty のビルドは爆速です。11ty の開発者の Zach さんがどの SSG が Markdown を最も早くビルドするかというテストをしていました。
Markdown を 4,000 ファイル分ビルドするのにかかった時間がこれです。

which SSG build fastest graph

which SSG build fastest table
出典

Next.js でも 13.4 秒かかったのに対し 11ty は 1.93 秒しかかかっていません。
これを見ると 11ty がなかなか頑張っているのがわかりますね。
ちなみにタイトルを突き抜けているのはRemixです。

Hugo には劣るようですがなかなかのパフォーマンスでしょう。
私は実際に 11ty でビルドしましたがすぐ終わりました。

3. 学習コストが低い

内部が HTML や Markdown をデータ駆動にしたものという形なので学習コストは低いです。(個人の見解)
ですが 11ty は自分で関数などを設定可能なので複雑なことをすればするほど 11ty のメリットを失ってしまいます。ジレンマ。

11ty のよくないところ

  1. ES モジュールが使用不可能なところ
  2. テンプレートエンジンだというところ

ES モジュールが使用不可

11ty では現在 ES モジュールを使用不可能です。
Common JS 方式を使うほかありません。

テンプレートエンジンだというところ

個人の感想としてとらえていただきたいですが、僕はテンプレート言語よりも JSX のほうが好きです。
なぜかというと JSX のほうがソースコードが綺麗だからです。
テンプレート言語と JSX の違いの 1 つは、制御構文をテンプレートと JavaScript のどちらに押し付けるのか、ということです。
テンプレート言語に if 文や for 文を押し付けるテンプレートエンジンは、エンジンごとに特有の記法があったり、複雑なことをしようとするほどテンプレートのソースコードが肥大化します。
最終的にテンプレートが何を示しているのか分からなくなり混乱します。
それに対し JSX では制御構文を JavaScript(node)に委ねるので「人によって扱える記法が変わらず」「複雑な演算処理を行うことができる」というプログラミング言語の性質を有効に活用しています。
というわけで僕は JSX のほうが好きです。

11ty の簡単な紹介

ざっと紹介します。
ここを読んだだけで 11ty を 70%理解したと言ってもいいと思います。
11ty でナビゲーションを作ってみましょう。
素の HTML で以下のようなナビゲーションを作るとします。

<nav>
  <ul>
    <li><a href="/home/">Home</a></li>
    <li><a href="/about-us/">About us</a></li>
    <li><a href="/contact/">Contact</a></li>
  </ul>
</nav>

よくあるナビゲーションですね。
もしもナビゲーションのリンク先が増えたらどうでしょう?いちいち書き直すのは馬鹿らしいです。
さらにアクセシビリティの観点からaria-current="page"もつけるとします。
ページごとに「/home/では/home/のリンクにaria-current="page"をつける。/about-us/では/about-us/のリンクにaria-currentをつける。/contact/では/contact/のリンクに...(以下略)」なんてやるのはふくろねずみ(英語で Possum)君も絶句です。
11ty's mascot
11ty のマスコットのふくろねずみ君(名前なし)

そこでテンプレートエンジンの出番です。
以下のようなデータセットを用意します。

nav.json
{
  "items": [
    { "url": "/home/", "title": "Home" },
    { "url": "/about-us/", "title": "About Us" },
    { "url": "/contact/", "title": "Contact" }
  ]
}

これをテンプレート言語(ここでは Nunjucks)で次のようにします。

<ul>
{%- for item in nav.items %}
  <li>
    <a href="{{ item.title }}"
      {% if item.url == page.url %}
        aria-current="page"
      {% endif %}
    >
      {{ item.title }}
    </a>
  </li>
{%- endfor %}
</ul>

これをコンパイルするとpage-current="page"が適した位置に入ったものが自動生成されます。以下はその一例です。
これがテンプレートエンジン 11ty の基本です。

<nav>
  <ul>
    <li><a href="/home/" page-current="page">Home</a></li>
    <li><a href="/about-us/">About us</a></li>
    <li><a href="/contact/">Contact</a></li>
  </ul>
</nav>

11ty の用語

ドキュメントにて用語(Terminology)の説明があったので紹介します。ドキュメントで頻出するのと、少し一般感覚から離れているものがあるため紹介します。

テンプレート

HTML, Markdown, Liquid や Nunjucks のようなフォーマットで書かれたファイルのことです。

注意すべきは HTML や Markdown もテンプレート言語だということです。
先で{{ item.title }}のように変数を参照していました。これは Nunjucks や Liquid に限ったものではありません。
なんと Markdown や HTML でも同様にできるんです。

レイアウト

レイアウトというのはテンプレートを受け取る特別なテンプレートです。
つまりテンプレートの中身の値が他のテンプレートに注入されていたらその注入された側がレイアウトです。
例えばここに Markdown で書かれた記事(template)がレイアウトに埋め込まれます。

フィルター

データをより表示しやすい形式に変換するための関数のことです。

このフィルターというは関数です。
例えば{{ item.title | captalize }}とすると、item.titleを評価した値が先頭だけ大文字化します。このようにパイプでつなぐことで関数が実行されます。

ショートコード

コンテンツをテンプレートに流し込むための関数です。
React のコンポーネントとほぼ同じです。これは実例を見るのが早いでしょう。例えば設定ファイル.eleventy.jsに次のように設定します。引数の値を用いてテキストを作成できます。

.eleventy.js
module.exports = function (eleventyConfig) {
  eleventyConfig.addShortcode("user",
    function (name, account_name) {
      return `<div class="user">
        <div class="user-name">${name}</div>
        <div class="user-account-name">${account_name}</div>
      </div>`;
    }
  );
  ...
};

テンプレート内で次のように使用します。

{% user "Key5n", "@Key5n" %}

これで次のような HTML が作られます。

<div class="user">
  <div class="user-name">Key5n</div>
  <div class="user-account-name">@Key5n</div>
</div>

これこそテンプレート化って気がしますね。
ちなみに公式ではショートコードの代わりにペアードショートコード(Paired Shortcode)の使用が推奨されています。
ペアードショートコードというのはまさに children を使用している React コンポーネントです。

.eleventy.js
 module.exports = function (eleventyConfig) {
   eleventyConfig.addPairedShortcode("user",
    function (content, name, account_name) {
      return `<div class="user">
        <div class="user-name">${name}</div>
        <div class="user-account-name">${account_name}</div>
        <div class="user-bio">${content}</div>
      </div>`;
    }
  );
  ...
};
{% user "Key5n", "@Key5n" %}
ふくろねずみは英語で Possum と呼ぶらしい。
{% enduser %}

結果 ↓

<div class="user">
  <div class="user-name">Key5n</div>
  <div class="user-account-name">@Key5n</div>
  <div class="user-bio">ふくろねずみは英語で Possum と呼ぶらしい。</div>
</div>

ちなみにこのフィルターとショートコードは 2023 年 2 月 9 日のバージョン 2.0.0 で非同期関数に対応したそうです。それまではaddPairedAsyncShortcode(String, async () => {})のように用意されたものを使っていたらしいです。

コレクション

11ty ではテンプレートごとにタグを付けることが可能です。そしてタグを付けられたものはコレクションとして参照することが可能です。記事ごとにタグをつけそのタグが付いた記事をコレクションとして参照する、というように使います。

Nunjucks とは

Nunjucks というのは Firefox でお馴染みの Mozilla さんが開発したテンプレートエンジンです。拡張子はnjkです。
Nunjucks のドキュメントによると Nunjucks は基本的に jinja2 と同じだそうです。jinja2 っていうのは Python とかで使われるテンプレートエンジンです。これを使えば Python で HTML を出力するときに今まで紹介したようなテンプレート構文を出力できるらしいです。

11ty 内で{% if ~ %} {% endif %}のように書くことができるのは Nunjucks のおかげです。

いくつか Nunjucks で使えそうな 2 つの機能を紹介します。
これらはもちろん 11ty 内で使用可能です。

<!-- テンプレートの読み込み -->
<!-- Reactのように使いまわすソースコードを別ファイルに切り出すことができます. -->
{% include "hoge.html" %}

<!-- 変数の作成、代入 -->
<!-- もし`username`が"james"だった場合、"james joe"と出力される -->
{{ username }}
{% set username = "joe" %}
{{ username }}

実践

では 11ty を用いて「Markdown で記事を書いてビルドするブログサイト」を作ってみましょう。
Next.js のチュートリアルと似たようなことをします。

11ty の基本のファイル構造は下のような感じです。

.
├─ node_modules/
├─ _site/               # 出力先
├─ src/                 # 入力
│  ├─ _data             # グローバルデータディレクトリ
│  │  └─ *.json         # データファイル
│  ├─ _includes/        # レイアウトディレクトリ
│  │  └─ *.njk          # レイアウト
│  └─ index.md          # インデックスページ
├─ .eleventy.js         # 11tyの設定ファイル
├─ package-lock.json
└─ package.json

では始めましょう。
まずまっさらなディレクトリに 11ty をインストールします。

npm install @11ty/eleventy

こんな感じになりました。あとは自力でファイルを作成します。

.
├─ node_modules/
├─ package-lock.json
└─ package.json

設定ファイル

.eleventy.jsというのが 11ty の設定ファイルです。
まずは以下のように作成します。これはコンパイル対象をsrc/ディレクトリにし、Markdown テンプレートと html テンプレートのエンジンを Nunjucks にします。今回 HTML をテンプレートとして使用しませんが一応追加します。両者とも初期値はliquidです。

.eleventy.js
module.exports = function (eleventyConfig) {
  return {
    // 入力ディレクトリの設定
    // 出力ディレクトリはデフォルトの _site/
    dir: {
      input: "src",
    },
    // Markdown, HTMLのテンプレートエンジンをNunjucksに変更
    // デフォルトはliquid
    markdownTemplateEngine: "njk",
    htmlTemplateEngine: "njk"
  };
};

コンパイルは次のように行います。--formatsには使用するテンプレート言語名を入れます。

eleventy --formats=njk,md

レイアウト

ではレイアウトを書いてみましょう。レイアウトのファイル名は何でもいいですが、_includesフォルダ内になければなりません。

src/_includes/base.njk
<!DOCTYPE html>
<html lang="ja">
  <head>
    <title>
      {%- if title %}
        {{ title }}
      {%- else %}
        {{ metadata.title }}
      {%- endif %}
    </title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta
      name="description"
      content="
      {%- if description %}
        {{ description }}
      {%- else %}
        {{ metadata.description }}
      {%- endif %}"
    />
  </head>
  <body>
    {{ content | safe }}
  </body>
</html>

{{ content | safe }}というのは「その記事の中身はここに入れてね」ということを示しています。

safeというのは 11ty のデフォルトのフィルターで、特殊文字をエスケープしません。
このsafeがないと、例えば<p>はエスケープされ&lt;p&gt;となります。

データ

データを設定しましょう。
_dataディレクトリ内のファイルはすべてのテンプレート内で使用可能です。
そのためこの_dataディレクトリをグローバルデータディレクトリと呼びます。

<title>
  {%- if title %}
    {{ title }}
  {%- else %}
    {{ metadata.title }}
  {%- endif %}
</title>

このソースコードが意味するのはもし記事(テンプレート)内で title の指定があったらそれを使用し、なければグローバルデータのmetadataファイルのtitleを使用します。
ちなみにグローバルデータディレクトリのファイルはjsonだけでなくモジュール形式で.jsも使用可能です。

というわけでグローバルデータを設定しましょう。

src/_data/metadata.json
{
  "title": "ベストなSSG",
  "description": "SSGってどれを選べばいいの?"
}

グローバルデータを設定できました。以降はこのデータをテンプレート、レイアウト内で自由に使用できます。
では次にインデックスページを作成しましょう

src/index.njk
---
layout: base.njk
---
<main>
  <p>
    ベストなSSGを探しに我々はアマゾンの奥地へと向かった。
  </p>
</main>
<nav>
  <!-- あとでここに記事へのナビゲーションを付けます -->
</nav>

コンパイルすると次のようなファイルが生成されます。
コンパイルはこれでできます。

eleventy --formats=njk,md

ちなみに差分の自動コンパイルはこれで可能です。--watchではなく--serveとするとローカルにホスティングされながら自動コンパイルされます。ホットリロードなどが有効です。

# 差分の自動コンパイル
eleventy --watch --incremental

# 差分の自動コンパイル
# 同時にローカルサーバー起動
eleventy --serve --incremental

こんなファイルが生成されました。

_site/index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <title>
      ベストなSSG
    </title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta
      name="description"
      content="静的サイトジェネレータってどれを選べばいいの?"
    />
  </head>
  <body>
    <main>
      <p>ベストなSSGを探しに我々はアマゾンの奥地へと向かった。</p>
    </main>
    <nav>
    <!-- あとでここに記事へのナビゲーションを付けます -->
    </nav>
  </body>
</html>

テンプレート(src/index.njk)内でtitledescriptionを指定していないのでmetadata.jsonで指定したtitle, descriptionが使用されています。

記事

では記事を書きましょう
以下のようにファイルを追加します

 .
 ├─ node_modules/
 ├─ _site/               # 出力先
 ├─ src/                 # 入力
 │  ├─ _data             # グローバルデータディレクトリ
 │  │  └─ metadata.json  # サイトのメタデータ
 │  ├─ _includes/        # レイアウトディレクトリ
+│  │  │─ post.njk       # 記事のレイアウト
 │  │  └─ base.njk       # 基本のレイアウト
+│  ├─ posts/            # レイアウトディレクトリ
+│  │  └─ post1.md       # 追加する記事
 │  └─ index.md          # インデックスページ
 ├─ .eleventy.js         # 11tyの設定ファイル
 ├─ package-lock.json
 └─ package.json

記事用のレイアウト

記事用のレイアウトを作成しましょう。
当然レイアウトのネストは可能です。ドキュメントはこれをレイアウトチェインと言ってます。

src/_includes/post.njk
---
layout: base.njk
---

<main>
  <h1>{{ title }}</h1>
  <time>
  <!-- あとでここに記事の更新日を付けます -->
  </time>
  {{ content | safe }}
</main>
<nav>
  <!-- あとでここに記事へのナビゲーションを付けます -->
</nav>

実際に記事を書きましょう

src/posts/post1.md
---
title: Is 11ty the best SSG?
description: SSGの11tyを考察してみた
layout: post.njk
---

11ty って最適な静的サイトジェネレータなのでしょうか?

Front Matter でlayout: post.njkと記述しています。
これでこのファイルがコンパイルされるときにレイアウトとして_includes/post.njkを使用します。

ただ記事を書くたびに毎回レイアウトを指定するのは面倒ですね。
そんなposts/ディレクトリにあるんだからレイアウトはpost.njkに決まってんだろと思われる方に朗報です。
今はファイルごとに Front Matter を設定していますが、ディレクトリごとに設定可能です。
次のようにposts.jsonファイルを設定しましょう。

 .
 └─ src/                 # 入力
    └─ posts/            # レイアウトディレクトリ
       │─ post1.md       # 追加する記事
+      └─ posts.json     # ディレクトリデータファイル

(長くなってきたので不必要なものは省略)
src/posts/posts.json
{
  "layout": "post.njk",
}

これでsrc/posts/ディレクトリの記事(テンプレート)にはデフォルトでlayout: post.njkが適用されます。

出力先

この状態でコンパイルするとsrc/posts/post1.md_site/posts/post1/index.htmlに出力されます。
これだと「post1 って何の記事だっけ...」という事態を招いてしまいそうです。
出力先の記事のタイトルがわかるようにしましょう。

src/posts/posts.jsonpermalinkを設定します。
permalinkというのは出力先を決めるものです。

src/posts/posts/json
{
   "layout": "post.njk",
+  "permalink": "posts/{{ title | slug }}/",
}

post1 のタイトルは"Is 11ty the best SSG?"です。
post1.mdくんは以前_site/posts/post1/に出力されていましたが、これからは_site/posts/is-11ty-the-best-ssgに出力されるようになりました。

slug というのは引数を url フレンドリーな値にするものです。
今回post1.mdのタイトルは "Is 11ty the best SSG?"なので slug 化すると "is-11ty-the-best-ssg" となります。

ファイルがお引越ししたような感じがしますね。

フィルター

英語の記事って大文字で始まりまってますよね。
もしかしたらタイトルの初めをうっかり小文字にしてしまうかもしれません。
そんなことを防ぐためにフィルターを追加しましょう。
フィルターというのはただの関数です。

レイアウトを修正しましょう

src/_includes/post.njk
- <h1>{{ title }}</h1>
+ <h1>{{ title | capitalize }}</h1>

capitalizeは Nunjucks のデフォルトのフィルターです。
もしタイトルを間違えて "is 11ty the best SSG?"としても "Is 11ty the best ssg?"と先頭を大文字にするように変換してくれます。

日付

記事には日付はつきものです。
最新情報アンテナがびんびんの皆さまはきっと zenn で記事を見るたびに何年何月何日の記事なのかチェックしていらっしゃるでしょう。

11ty では日付として次のものをデフォルトで使用可能です。

  • Last Modified
    • ファイルの最終編集日
  • Created
    • ファイルが作られた日
  • git Last Modified
    • ファイルの最新のコミット日
      • もしそのファイルが git にチェックインされていないならDate.now()になる
  • git Created
    • v2.0.0で追加
      • もしそのファイルが git にチェックインされていないならDate.now()になる

これらをテンプレートで使用するには{{ page.date }}と参照します。
フィルターを何も設定していないとFri Feb 24 2023 03:38:28 GMT+0900 (Japan Standard Time)のように出力されます。(執筆時:2/24(金)午前 3 時 38 分)
これは JavaScript でDate(Date.now())またはnew Date()を実行したときの値ですね。
ここではYAML のタイムスタンプフォーマットに合わせてフォーマットはYYYY-MM-DDとしましょう。
というわけでフィルターをセットします。

.eleventy.js
module.exports = function (eleventyConfig) {
  eleventyConfig.addFilter("dateFormat", function (value) {
    const Year = value.getFullYear();
    const Month = (parseInt(value.getMonth()) + 1).toString().padStart(2, "0");
    const Date = value.getDate().toString().padStart(2, "0");
    return `${Year}-${Month}-${Date}`;
  });
  (省略)
};

これを次のように記事のレイアウトに追加します。

src/_includes/article.njk
---
layout: base.njk
---
 <main>
   <h1>{{ title | captalize }}</h1>
+  <time datetime="{{ page.date | dateFormat }}">
+    {{ page.date | dateFormat }}
  </time>
 {{ content | safe }}
 </main>
 <nav>
   <!-- あとでここに記事へのリンクを付けます -->
 </nav>

「急に出てきたpageって何?」と思われている方もいるでしょう。

pageは 11ty から与えられる特別なデータです。collectionも実は同じ類のものです。
pageは下のようなオブジェクトです。

let page = {
  // URL can be used in <a href> to link to other templates
  // Note: This value will be `false` if `permalink` is set to `false`.
  url: "/current/page/myFile/",

  // For permalinks: inputPath filename minus template file extension
  fileSlug: "myFile",

  // For permalinks: inputPath minus template file extension
  filePathStem: "/current/page/myFile",

  // JS Date Object for current page (used to sort collections)
  date: new Date(),

  // The path to the original source file for the template
  // Note: this will include your input directory path!
  inputPath: "./current/page/myFile.md",

  // Depends on your output directory (the default is _site)
  // You probably won’t use this: `url` is better.
  // Note: This value will be `false` if `permalink` is set to `false`.
  outputPath: "./_site/current/page/myFile/index.html",

  // Added in 1.0
  // Useful with `page.filePathStem` when using custom file extensions.
  outputFileExtension: "html",

  // Available in 2.0 with the i18n plugin
  // The default is the value of `defaultLanguage` passed to the i18n plugin
  lang: "",
};

このオブジェクトはテンプレート内で使い放題って訳です。

ナビゲーション

ではインデックスページや記事にナビゲーションを付けましょう。
まずはインデックスページから。

インデックスページにはすべての記事のリンクを貼りましょう。
collections.allはすべてのテンプレートのコレクションなのでこれを利用します。

src/index.njk
  <nav>
- <!-- あとでここに記事へのリンクをつけます -->
+   <ul>
+     {%- for post in collections.all %}
+     <li>
+       <a href="{{ post.url }}">{{ post.data.title }}</a>
+     </li>
+     {%- endfor %}
+   </ul>
  </nav>

ちなみにコレクションの要素postはこんなオブジェクトです。

let post = {
  page: {
    inputPath: "./test1.md",
    url: "/test1/",
    date: new Date(),
    // … and everything else in Eleventy’s `page`
  },
  data: {
    title: "Test Title",
    tags: ["tag1", "tag2"],
    date: "Last Modified" /* ... */,
  },
  content: "<h1>This is my title</h1>\n\n<p>This is content…",
  url: "/test1/",
  date: new Date(),
  /* ... */
};

なのでpost.data.titleを評価するとテンプレートのタイトルになるんですね。

ちなみにurldateは 2 つありますがどちらを使っても同じです。
ドキュメントは浅いほうのurlを使用しているのでそれに従います。

コンパイルする前に記事を増やしておきましょう。
適当に 2 つの記事を追加します。

src/posts/post2.md
---
title: Is jekyll the best SSG?
description: SSGのjekyllの考察してみた
---
src/posts/post3.md
---
title: Is Astro the best SSG?
description: SSGのAstroの考察をしてみた
---

これらをコンパイルするとこんな感じになります。ローカルでホスティングしてみると実際にルーティングが可能なことも確認できます。

_site/index.html
<nav>
  <ul>
    <li>
      <a href="/posts/is-11ty-the-best-ssg/">Is 11ty the best SSG?</a>
    </li>
    <li>
      <a href="/posts/is-jekyll-the-best-ssg/">Is jekyll the best SSG?</a>
    </li>
    <li>
      <a href="/post/is-astro-the-best-ssg/">Is Astro the best SSG?</a>
    </li>
  </ul>
</nav>

インデックスページのナビゲーションが完成したので今度は記事ごとにリンクを付けましょう。
先ほどのインデックスページとは異なり、ナビの現在位置にaria-current=pageを付けましょう。
記事のレイアウトをこんな風にします。

src/_includes/post.njk
<nav>
  <ul>
    {%- for post in collections.post %}
    <li>
      <a href="{{ post.url }}"
        {% if post.url == page.url %}
          aria-current="page"
        {% endif %}
      >
        {{ post.data.title }}
      </a>
    </li>
    {%- endfor %}
  </ul>
</nav>

page.urlというのはテンプレートごとの url を示します。詳しくは日付pageオブジェクトについてをご参照下さい。
ここではリンクの繰り返し中に自分の url と同じ url があったら<a>タグにaria-current=pageを付けるというわけです。

これが例えば次のようになります。

_site/posts/is-11ty-the-best-ssg.html
<nav aria-labelledby="my-pagination">
  <ul>
    <li>
      <a href="/posts/is-11ty-the-best-ssg/" aria-current="page" >
        Is 11ty the best SSG?
      </a>
    </li>
    <li>
      <a href="/posts/is-jekyll-the-best-ssg/" >
        Is jekyll the best SSG?
      </a>
    </li>
    <li>
      <a href="/posts/is-astro-the-best-ssg/" >
        Is Astro the best SSG?
      </a>
    </li>
  </ul>
</nav>

ファイル名とリンクが同じときには aria-current 属性がついてますね。
あとは css でaria-current属性つきの要素をスタイリングしましょう。

[aria-current] {
  background-color: #eee;
}

このような感じにリンクを生成できます。

CSS や画像

デフォルトの 11ty ではテンプレート言語以外のファイル(例:.css.png)はコンパイル時に出力されません。
ですので設定ファイルでaddPassthroughCopyメソッドを使ってディレクトリごとコピーしましょう。
なおこれらのフォルダを監視しておきましょう。

.eleventy.js
// Set directories to pass through to the _site folder
eleventyConfig.addPassthroughCopy("src/assets/");
eleventyConfig.addPassthroughCopy("src/styles/");
eleventyConfig.addPassthroughCopy("src/js/");

// Watch scss folder for changes
eleventyConfig.addWatchTarget("/src/assets/");
eleventyConfig.addWatchTarget("/src/styles/");
eleventyConfig.addWatchTarget("/src/js/");

終わり

いかがだったでしょうか。
個人的にはテンプレートエンジンとなめてかかったにもかかわらず、思ったよりも複雑なことができたので驚きました。
ですが複雑なことができるということはフレームワーク頼りになり、学習コストが上がるということだと思っています。
そうなると 11ty の「学習コストが少ないという長所(個人の感想)」もなくなってしまいます。
こうなると演算を JavaScript の記法通りに書くことが可能な React は立派ですね。

11ty の機能を実際に作りながら説明しました。
もし jekyll から乗り換えるのであれば 11ty は 1 つの選択肢になると思います。
それ以外の場合ですと個人的に、11ty は微妙な感じがします。

マスコットについて

関係ない話をしますが 11ty のマスコットのふくろねずみ君を紹介しました。
なぜふくろねずみなのでしょうか。そしてなぜ風船で飛んでいるのでしょうか。
もともとは 11ty の開発者の 1 人の James Williamson さんが 2018 年に赤い風船で吊るされている猫を考案したそうです。
ですが James さんは 2019 年に 6 年間に渡る ALS との闘病の末にお亡くなりになりました。
そして開発メンバーが James さんの思いを継ぎ、今も赤い風船に吊るされたふくろねずみくんが 11ty のドキュメントにいるそうです。

原案
原案の猫(名前なし)

v1
James さん考案 ふくろねずみの「エジソン」くん

v2
ふくろねずみの「Grumpy」くん

v3
現在のふくろねずみくん(名前なし)

GitHubで編集を提案

Discussion