11ty/eleventy × microCMS を使ったJamstackなサイトの作り方

この記事は[Jamstack Advent Calendar 2020](https://qiita.com/advent-calendar/2020/jamstack)の8日目の記事です。

## はじめに

SSGといえば、[東京都 新型コロナウイルス感染症対策サイト](https://stopcovid19.metro.tokyo.lg.jp/)で使われているVue製の[Nuxt.js](https://ja.nuxtjs.org/)やReact製の[Gatsby](https://www.gatsbyjs.com/)や[Next](https://nextjs.org/)が有名です。
しかし、VueやReactで書かなければいけないので、Web初学者からすると敷居が高くなかなか手が出しづらいと思います。

一方、11ty/eleventyの基本はHTML(テンプレートエンジン)なので、多少テンプレートエンジンの文法を覚える必要はありますが、
VueやReactと比較すると学習コストは低いと思いますので手を出しやすいかと思います。

## 11ty/eleventy とは :thinking:

- [11ty/eleventy](https://www.11ty.dev/)は**[zachleat](https://twitter.com/zachleat)**氏によって制作さいれている静的サイトジェネレーター(SSG)
- テンプレートエンジンの詰め合わせのようなもの
    - デフォルトで以下の言語に対応しています。
        - HTML（.html）
        - Liquid（.liquid）
        - EJS（.ejs）
        - Markdown（.md）
        - Handlebars（.hbs）
        - Mustache（.mustache）
        - Haml（.haml）
        - Pug（.pug）
        - Nunjucks（.njk）
        - JavaScript（.11ty.js）
- 今回は利用しませんが、ローカル開発用のサーバも組み込まれています。

## 事前準備
- [microCMS](https://microcms.io/) でユーザ登録しておく。（**無料**）
- [Github](https://github.com/) でユーザ登録し、(プライベート)リポジトリを作成しておく。（**無料**）
- [Netlify](https://www.netlify.com/) でユーザ登録し、Github連携しておく。（**無料**）

また、microCMSへのデータの投入は済ませておいてください。
(今回は[こちらの記事](https://qiita.com/ohnaka0410/items/c2f6d7c90330c1d97c69)で用意した~~ゴリラたち~~データを利用します。)

## 方針
- 11tyはHTMLのコンパイラとして利用する
    - 全体のワークフローはnpm scriptsで実行する
    - 11tyをメインにすると、他のワークフローが組み込みにくいため...
- テンプレートエンジンは`Handlebars`を利用する
    - [npm trends](https://www.npmtrends.com/ejs-vs-pug-vs-handlebars-vs-nunjucks-vs-mustache)で見てみても割と人気なため

## ディレクトリ構成
```
root/
　├ dist/				・・・ 出力ディレクトリ
　├ src/					・・・ 入力ディレクトリ
　│　├ components/		・・・ パーツ配置ディレクトリ
　│　│　└ row.hbs
　│　├ data/				・・・ データ配置ディレクトリ
　│　│　└ gorillas.js
　│　├ layouts/			・・・ レイアウト配置ディレクトリ
　│　│　└ default.hbs
　│　└ pages/				・・・ エントリポイント配置ディレクトリ
　│　 　└ index.hbs
　├ .bs-config.js		・・・ BrowserSync設定ファイル
　├ .eleventy.js			・・・ eleventy設定ファイル
　└ package.json
```

## プロジェクトの作成

```sh:console
# nodeのバージョン確認
% node -v
v14.0.0

# npm のバージョン確認
% npm -v
6.14.9

# プロジェクトの初期化
% npm init -y

% npm i -D @11ty/eleventy

# 11tyにも含まれていますが、今回は11tyをただのビルダーとして使いたかったため、別途インストール。
% npm i -D browser-sync

# npm-scriptsでタスクの直列、並列実行するため。
% npm i -D npm-run-all

# CLIからファイルの削除を行うため。 
% npm i -D rimraf

# microCMSからデータを取得するために利用
% npm i -D node-fetch
```

## 各種設定ファイルの作成
```json-doc:package.json
{
  // scriptsに追記する
  "scripts": {
    "build": "run-s clean build::*",
    "build::html": "eleventy",
    // "build::css"などを作ってdistフォルダへ出力ようにすればsassなども利用できます。
    "clean": "rimraf dist",
    "server": "browser-sync start --config \"./.bs-config.js\"",
    "start": "run-p watch server",
    "watch": "run-p watch::*",
    "watch::html": "eleventy --watch"
    // "watch::css"などを作ってdistフォルダへ出力ようにすればsassなども利用できます。
  }
}
```

```js:.bs-config.js
/**
 * Browser-sync Configuration
 */
module.exports = {
  ui: false,
  server: "dist",
  ghostMode: false,
  open: false,
  notify: false,
};

```

```js:.eleventy.js
/**
 * Eleventy Configuration
 */
module.exports = (eleventyConfig) => {
  return {
    dir: {
      // 入力ディレクトリ
      // input: ".",
      input: "src/pages",
      // インクルードのディレクトリ
      // includes: "_includes",
      includes: "../components",
      // レイアウトのディレクトリ
      // layouts: "_includes",
      layouts: "../layouts",
      // グローバルデータファイルのディレクトリ
      // data: "_data",
      data: "../data",
      // 出力ディレクトリ
      // output: "_site",
      output: "dist",
      // グローバルデータファイルのデフォルトのテンプレートエンジン
      dataTemplateEngine: false,
      // マークダウンファイルのデフォルトのテンプレートエンジン
      markdownTemplateEngine: "liquid",
      // HTMLファイルのデフォルトのテンプレートエンジン
      htmlTemplateEngine: "liquid",
      // テンプレートフォーマット
      templateFormats: [
        // "html",
        // "liquid",
        // "ejs",
        // "md",
        "hbs",
        // "mustache",
        // "haml",
        // "pug",
        // "njk",
        // "11ty.js",
      ],
    },
  };
};

```

```sh:環境の動作確認コマンド
# ビルドの実行
% npm run build

# 開発サーバの起動
% npm run start
```

## microCMSからデータ取得
```js:src/data/gorillas.js
const fetch = require("node-fetch");

module.exports = async () => {
  const res = await fetch("https://jamstack-sample.microcms.io/api/v1/gorilla", {
    headers: {
      // microCMSから取得したAPIキーを設定
      "X-API-KEY": "XXXXXXXXXXXXXXXXXXXX",
    },
  });
  return items;
};

```

## データを表示するテンプレートを作成
### ページ共通のレイアウト
````html:src/layouts/default.hbs
<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="robots" content="index, follow">
  <title>{{title}}</title>
  <meta name="description" content="{{description}}">
  <!--アイコンの設定-->
  <link rel="icon" href="/favicon.ico">
  <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
  <!--その他設定-->
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="format-detection" content="telephone=no,address=no,email=no">
  <meta name="theme-color" content="#fafafa">
  <!--外部ファイルの読み込み-->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/destyle.css@2.0.2/destyle.css">
</head>
<body>{{{content}}}</body>
</html>
```

`{{title}}`や`{{description}}`は、この後作成するページ側のhbsファイルより[front-matter](https://www.npmjs.com/package/front-matter)を使って投入します。

### ページのコンテンツ
```html:src/pages/index.hbs
---
layout: default
title: ここにページのタイトルが入ります
description: ここにページの説明が入ります
---
<table>
  <thead>
    <th>名称</th>
    <th>学名</th>
    <th>画像</th>
  </thead>
  <tbody>
  {{#each gorillas.contents}}
    {{> row name=name scientificName=scientificName image=image.url}}
  {{/each}}
  </tbody>
</table>
```

hbsの[Partials](https://handlebarsjs.com/guide/partials.html)という機能で、共通部品の`row.hbs`をパラメータ付きで呼び出すことができます。

### 共通部品
```html:src/components/row.hbs
<tr>
  <td>
    {{name}}
  </td>
  <td>
    {{scientificName}}
  </td>
  <td>
    <img src="{{image}}" alt={{name}}>
  </td>
</tr>
```

## ローカルで動作確認
ブラウザで以下にアクセスして確認してみる

```
http://localhost:3000/
```

![スクリーンショット 2020-12-08 22.58.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242655/a239c8cc-65c1-b6a6-e58b-500a28c07423.png)

いい感じなのでGithubにコミットします :ok_woman_tone1:

## Netlifyへデプロイ
連携したGithubアカウントのリポジトリを選択します。
※ Netlifyの権限が足りない場合は、一部のリポジトリしか表示されないため、今回作成したリポジトリへのアクセス権を追加しましょう。

![app.netlify.com_start (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242655/c316739b-7fb8-d518-5448-ef037cba7ad1.png)

ビルドの設定などを行います。
![app.netlify.com_start (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242655/04b65d11-072e-1c52-2d92-943c0edf02b5.png)

少し待てば以下のような表示になります。
![スクリーンショット 2020-12-08 23.18.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242655/3a025eea-8f0c-a0d7-d0a3-fbd31a00c755.png)

## Netlify上で確認
デプロイ先のURLを確認し完成です。
![スクリーンショット 2020-12-08 23.19.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242655/565d820e-cff3-97c2-12a6-4ed504867ba9.png)


## さいごに
11tyであれば、VueやReactほどの機能はいらないけれども、多少パーツの共通化や繰り返しを行いたい場合も、学習コスト少なめで導入できます。
また、生成されるHTMLはVueやReact製のSSGと比べてコントロールしやすいため、成果物にSSGを使った形跡を残したくない場合にもおすすめです。
なお、VueやReact製のSSGは、学習コストはかかるものの、様々な機能があったり、DX(Developer Experience)が良かったりするので、是非そちらも試してみていただければと思います。

<details><summary>生成されたHTML</summary><div>

```html:index.html

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="robots" content="index, follow">
  <title>ここにページのタイトルが入ります</title>
  <meta name="description" content="ここにページの説明が入ります">
  <!--アイコンの設定-->
  <link rel="icon" href="/favicon.ico">
  <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
  <!--その他設定-->
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="format-detection" content="telephone=no,address=no,email=no">
  <meta name="theme-color" content="#fafafa">
  <!--外部ファイルの読み込み-->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/destyle.css@2.0.2/destyle.css">
</head>
<body><table>
  <thead>
    <th>名称</th>
    <th>学名</th>
    <th>画像</th>
  </thead>
  <tbody>
    <tr>
      <td>
        ヒガシゴリラ
      </td>
      <td>
        Gorilla beringei
      </td>
      <td>
        <img src="https://images.microcms-assets.io/protected/ap-northeast-1:caccd6f4-3d77-4554-9063-9036c1d2f2df/service/jamstack-sample/media/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202020-01-30%2010.29.37.png" alt=ヒガシゴリラ height="150">
      </td>
    </tr>
    <tr>
      <td>
        ニシゴリラ
      </td>
      <td>
        Gorilla gorilla
      </td>
      <td>
        <img src="https://images.microcms-assets.io/protected/ap-northeast-1:caccd6f4-3d77-4554-9063-9036c1d2f2df/service/jamstack-sample/media/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202020-01-30%2010.28.34.png" alt=ニシゴリラ height="150">
      </td>
    </tr>
    <tr>
      <td>
        ニシローランドゴリラ
      </td>
      <td>
        Gorilla gorilla gorilla
      </td>
      <td>
        <img src="https://images.microcms-assets.io/protected/ap-northeast-1:caccd6f4-3d77-4554-9063-9036c1d2f2df/service/jamstack-sample/media/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202020-01-30%2010.27.28.png" alt=ニシローランドゴリラ height="150">
      </td>
    </tr>
    <tr>
      <td>
        マウンテンゴリラ
      </td>
      <td>
        Gorilla beringei beringei
      </td>
      <td>
        <img src="https://images.microcms-assets.io/protected/ap-northeast-1:caccd6f4-3d77-4554-9063-9036c1d2f2df/service/jamstack-sample/media/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202020-01-30%2010.23.05.png" alt=マウンテンゴリラ height="150">
      </td>
    </tr>
    <tr>
      <td>
        マウンテンゴリラ
      </td>
      <td>
        Gorilla beringei beringei
      </td>
      <td>
        <img src="https://images.microcms-assets.io/protected/ap-northeast-1:caccd6f4-3d77-4554-9063-9036c1d2f2df/service/jamstack-sample/media/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202020-01-30%2010.23.05.png" alt=マウンテンゴリラ height="150">
      </td>
    </tr>
  </tbody>
</table>
</body>
</html>

```

</div></details>