Eleventy(SSG)をやってみた

11ty(Eleventy)をやってみた

名前をたまたま見かけただけ。

Eleventy is a simpler static site generator
Eleventy is a simpler static site generator.
Eleventy is a simpler static site generator faviconhttps://www.11ty.dev/
Eleventy is a simpler static site generator

インストール

11ty/EleventyはNode.jsで動作するのでインストールはnpmを使う

npm init -y
npm pkg set type="module"
npm install @11ty/eleventy

インストールしたパッケージ等

{
  "name": "eleventy-blog",
  "version": "0.0.1",
  "type": "module",
  "scripts": {
    "start": "eleventy --serve",
    "build": "eleventy"
  },
  "dependencies": {
    "@11ty/eleventy": "^3.0.0",
    "@11ty/eleventy-img": "^6.0.2",
    "@shikijs/markdown-it": "^3.2.2",
    "fast-glob": "^3.3.3",
    "image-size": "^2.0.2",
    "luxon": "^3.6.1",
    "markdown-it": "^14.1.0",
    "markdown-it-anchor": "^9.2.0",
    "markdown-it-attrs": "^4.3.1",
    "markdown-it-eleventy-img": "^0.10.2",
    "photoswipe": "^5.4.4",
    "sharp": "^0.34.1"
  }
}

eleventy.config.js で設定

Eleventyの設定やスクリプトは eleventy.config.js に記述する。

下の方のreturn dirでディレクトリの設定をする。ここでは/src/content以下にテンプレートを置く。

includes:layouts:のディレクトリはinput:のディレクトリから見た相対パス。

以下は eleventy.config.js全体。eleventy-imgによる画像最適化もここで行う。

とても長いのでほとんどの部分は省略した。

import fs from 'fs';
import path from 'path';
import Image from '@11ty/eleventy-img';
import Sharp from 'sharp';
import Shiki from '@shikijs/markdown-it';
import markdownIt from 'markdown-it';
import markdownItAnchor from 'markdown-it-anchor';
import markdownItAttrs from 'markdown-it-attrs';
import markdownItEleventyImg from 'markdown-it-eleventy-img';
import { DateTime } from 'luxon';

const md = markdownIt({
  html: true,
  breaks: true,
  linkify: true,
});

// Shiki の設定は省略

default function (eleventyConfig) {
  let markdownLibrary = md;
  markdownLibrary
    .use(markdownItEleventyImg)
    .use(markdownItAttrs)
    .use(markdownItAnchor);

  eleventyConfig.setLibrary('md', markdownLibrary);

  // ファイルのコピー
  eleventyConfig.addPassthroughCopy({ 'src/assets': 'assets' });
  eleventyConfig.addPassthroughCopy({ 'src/public': '/.' });
  /* 省略 */

  // ショートコード
  eleventyConfig.addShortcode('year', () => `${new Date().getFullYear()}`);
  /* 省略 */

  // フィルタ 日時を変換するなど
  eleventyConfig.addFilter('readableDate', dateObj => {
    return DateTime.fromJSDate(dateObj, { zone: 'utc' }).toFormat('yyyy年M月d日');
  });

  eleventyConfig.addFilter('htmlDateString', dateObj => {
    return DateTime.fromJSDate(dateObj, { zone: 'utc' }).toFormat('yyyy-LL-dd');
  });

  // タグのコレクション
  eleventyConfig.addCollection('tagList', function (collection) {
    let tagSet = new Set();
    collection.getAll().forEach(item => {
      if ('tags' in item.data) {
        let tags = item.data.tags;
        if (Array.isArray(tags)) {
          for (const tag of tags) {
            tagSet.add(tag);
          }
        }
      }
    });
    return [...tagSet].sort();
  });

  // postsのコレクション
  eleventyConfig.addCollection('posts', function (collection) {
    return collection.getFilteredByGlob('./src/content/posts/**/*.{md,mdx}').sort((a, b) => {
      return b.date - a.date;
    });
  });

  return {
    dir: {
      input: './src/content',
      output: '_site',
      includes: '../_includes',
      layouts: '../_layouts',
      data: '../_data',
    },
    templateFormats: ['md', 'mdx', 'njk', 'html', 'liquid'],
    markdownTemplateEngine: 'njk',
    htmlTemplateEngine: 'njk',
    dataTemplateEngine: 'njk',
  };
}

ディレクトリの図

大まかなツリーは以下の通り。

/
├── eleventy.config.js
├── package.json
└── src
    ├── _data
    ├── _includes
    ├── _layouts
    │   ├── base.html
    │   └── post.html
    ├── assets
    ├── content
    │   ├── index.html(トップページのテンプレート)
    │   ├── posts (markdownファイルがあるディレクトリ)
    └── public

レイアウト

AstroでいうLayoutは/src/_layouts ディレクトリに置く。(上記の設定ファイルで場所は自由)

画像やcssなどは/src/assets以下に配置。

{{ content | safe }}はAstroで言う<slot>にあたる。

{{ metada.SITE_LANG }} のような表記は /src/_data ディレクトリのjsonファイルの中のデータを読み込んでいる。

metadatametadata.jsonのファイル名。

{
  "SITE_TITLE": "へび右曲がり",
}

基本のレイアウト/src/_layouts/base.html

<!DOCTYPE html>
<html lang="{{ metadata.SITE_LANG }}">
  <head>
    <title>{% if title %}{{ title }} | {% endif %}{{ metadata.SITE_TITLE }}</title>
    <style>{% include "css/base.css" %}</style>
<!-- ------ 省略 ------- -->
  </head>

  <body>
    {% include "site_header.html" %}
      <main class="site-main">
        <div class="container">
          {{ content | safe }}
        </div>
      </main>
    {% include "site_footer.html" %}
  </body>
</html>

ブログ記事のレイアウト。/src/_layouts/post.html

frontmatter部分でbase.htmlをレイアウトとして読み込んでいる。

---
layout: base.html
---
<style>
  {% include "css/blog.css" %}
</style>

<article class="post">
  <header class="post-header">
    {% include "post_head.html" %}
  </header>
  <main id="blogbody" class="post-content">
    {{ content | safe }}
  </main>
  <footer class="post-footer">
    {% include "post_foot.html" %}
  </footer>
</article>

テンプレート

ここでは/src/contet以下にテンプレートを置いていく。

markdownファイルもテンプレートなのでcontent内にディレクトリを作りそこに配置する。


以下はサイトのトップページのテンプレート。

---
layout: base.html
title: Home
eleventyNavigation:
  key: Home
  order: 1
pagination:
  data: collections.posts
  size: 9
  alias: posts
permalink: "{% if pagination.pageNumber > 0 %}/{{ pagination.pageNumber + 1 }}/{% endif %}index.html"
---
{% include "blogcard.html" %}
{% include "page_nav.html" %}
<div style="display:none;">
  {% include 'avatar.html' %}
</div>
  • pagination: でブログのコレクション(data: collections.posts)を元にしたページネーションの設定をしている。
  • size: 9 で9項目ごとにページ分割される。
  • permalink: でページごとのURLを設定している。
  • alias: posts でコレクションにアクセスできる。({% for post in posts %} のような使い方)。

インクルード

別のファイルを特定の位置に読み込みたい場合は_includes以下にファイルを置く。

インクルードされる側のテンプレート/src/_includes/avatar.html

<div class="profile-card">
  <img src="/img/nyan4.jpg" loading="lazy" alt="ubanis nyan" />
  <p>{{ metadata.SITE_OWNER }}</p>
</div>

{% include "avatar.html" %}のようにインクルードする。

---
layout: base.html
title: プロフィール
---
{% include "avatar.html" %}

引数のあるコンポーネントを使いたい場合はショートコード以外の方法としてmacroを使う。

{{ caller() | safe }}がAstroでいうコンポーネントの中の<slot>

{% macro button(url, rounded=false, blank=false, color="base", border=false, extraAttributes="") %}
  {% set classList = ["button-base", "button-color-" + color] %}
  {% if rounded %}
    {% set classList = classList.concat(["button-rounded"]) %}
  {% endif %}
  {% if border %}
    {% set classList = classList.concat(["button-bordered"]) %}
  {% endif %}
  {% set classString = classList.join(" ") %}

  <a href="{{ url }}" class="{{ classString }}"
    {% if blank %} target="_blank" rel="noopener noreferrer"{% endif %}
    {{ extraAttributes | safe }}
  >
    {{ caller() | safe }}
  </a>
{% endmacro %}

呼び出し元では使いたいmacroをimportして call endcall ブロックを使ってmacroを呼び出す

{% import "components/button.html" as components %}

{% call components.button(url="https://example.com", rounded=true) %}
  <span>ボタンの中身</span>
{% endcall %}

レイアウトの指定とURLの変更

markdownがあるディレクトリ以下にレイアウト情報などを設定する。

ここでレイアウトを設定するとこのディレクトリ以下のレイアウトはすべて指定されたものになる。

posts.11tydata.jsを作成する。(コレクション名がnoteならばnote.11tydata.js)

元のAstroブログではblogはサイトのルートに配置されるようになっていたので以下のようにURLを変更する設定も記述。

export default {
  permalink: data => {
    const filePathStem = data.page.filePathStem;
    const slug = filePathStem.replace(`posts`, '');
    return `/${slug}/`;
  },
  layout: "post.html",
};

実際に表示させてみる。

npm run start

Astroからかなり移植したので見た目はほぼ同じ。

eleventyのサイト

11tyの結論

手間はかかりそうなもののかなりスクリプトでなんとかなる雰囲気。

Hugoと比べて融通は効くものの大変そうではある。画像変換はプラグインもあり便利。macroがイマイチ使いづらい。

Astroに慣れてしまったせいかもしれない。