Eleventy(SSG)をやってみた

11ty(Eleventy)をやってみた

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

https://www.11ty.dev/

インストール

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

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

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

package.json
{  "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による画像最適化もここで行う。

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

eleventy.config.js
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ファイルの中のデータを読み込んでいる。

metadataはmetadata.jsonのファイル名。

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

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

/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をレイアウトとして読み込んでいる。

/src/_layouts/post.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内にディレクトリを作りそこに配置する。


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

/src/content/index.html
---layout: base.htmltitle: HomeeleventyNavigation:  key: Home  order: 1pagination:  data: collections.posts  size: 9  alias: postspermalink: "{% 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

/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" %}のようにインクルードする。

/src/content/about.html
---layout: base.htmltitle: プロフィール---{% include "avatar.html" %}

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

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

/src/_includes/components/button.html
{% 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を呼び出す

/src/_includes/user_sitelist.html
{% 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を変更する設定も記述。

/src/content/posts/posts.11tydata.js
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に慣れてしまったせいかもしれない。