gulpとFront-matterとejsを使ってHTMLを大量生産する方法

  • 14
    いいね
  • 0
    コメント

はじめに

はじめまして、フロントエンドエンジニアをやっているゆーひと言います!
Qiitaには初めて投稿するので至らぬ点などあればご指摘いただけたらと思います。

現在SESでとある現場に常駐しているんですが、そこでちょっと珍しい形でHTMLを運用することとなったので備忘録として残したいと思います(世間的にこの手法は一般的なのかもしれませんが笑)

要件

  • jadeやectのように『_layout.ejs』なるものを作って全ぺージ共通コンテンツ等をextendしたい
  • title, description, keywordなどのぺージ固有のメタ情報等はejsごとに定義してextendされてきた_layout.ejsの中に値を渡してあげてHTMLを生成する
  • お知らせ一覧などの更新頻度が高いものはJSONやYAMLなどを使って管理する

今回やりたかったことはこんなかんじです。

Front-matterってなんぞ?

表題にあるFront-matter(フロントマター)というものですが、これはYAMLの中でサイトのメタ情報などを変数の値として定義して静的サイトを生成する際に使用するものです。
YAMLとは、JSONやXMLと同じようにデータを管理する為のデータフォーマットと思っていただければ問題ないです。
実際に例にすると下記のようなかんじです!

{
  "name": "ゆーひ",
  "work": "フロントエンドエンジニア",
  "hobby": [
    {
      "anime": "ラブライブ!"
    },
    {
      "illust": "たまにイラスト描いてます"
    }
  ]
}

これが、

name: ゆーひ
work: フロントエンドエンジニア
hobby:
  - anime: ラブライブ!
  - illust: たまにイラスト描いてます

こういうかんじになります!
JSON形式と比べるとシンプルで見やすいですね。
で、Front-matterというのはこのYAML形式の上下に---と置いてあげるだけでejsなどのテンプレートエンジンの中でローカル変数として活躍してくれるのです!

hoge.ejs
--- 
name: ゆーひ
work: フロントエンドエンジニア
hobby:
  - anime: ラブライブ!はいいぞ
  - illust: たまにイラスト描いてます
--- 
<p>私の名前は<%= name %>です。</p>

nameには『ゆーひ』という値が入ってきます。
しかし、こう書くだけではただエラーが出て使えないので、gulpを用いてこのFront-matterを使えるようにしてみましょう!

gulpfile.js

まず、使わなければいけないnode_modulesがいくつかあるのでそれをインストールします。
※gulpfile.jsの説明はES2015で書いています。詳細はkosuke_nishayaさんのgulpfileをES2015(ES6)で書くをご覧いただけたらと思います。

npm i -D gulp gulp-plumber gulp-ejs gulp-front-matter layout-wrapper

ツール ざっくり説明
gulp タスクランナー
gulp-ejs gulp製のejs。ejsと特に違いはない
gulp-plumber エラーが出てもタスクを止めないようにしてくれる
gulp-front-matter テンプレートエンジンの中でFront-matterを使えるようにする為のモジュール
layout-wrapper 静的サイトジェネレータ

さて、インストールが終わったところで実際にgulpfileに書き込んでいきます!
説明しやすいように_layout.ejsとindex.ejsも公開します。

gulpfile.babel.js
import gulp from 'gulp';
import plumber from 'gulp-plumber';
import ejs from 'gulp-ejs';
import frontMatter from 'gulp-front-matter';
import wrapper from 'layout-wrapper';


const path = {
  ejs: {
    layoutDir: `${__dirname}/src/ejs/layouts`,
    src: [
      './src/ejs/**/*.ejs',
      '!./src/ejs/**/_*.ejs'
    ],
    dist: './htdocs/'
  },
  json: {
    package: './package.json',
    newsList: './src/data/newsList.json'
  }
};

global.jsonData = {};
jsonData.newsListJson = require(path.json.newsList);

gulp.task('ejs', () => {
  var extName = require(path.json.package);

  gulp.src(path.ejs.src)
    .pipe(plumber())
    .pipe(frontMatter({
      property: 'data'
    }))
    .pipe(ejs())
    .pipe(wrapper({
      layout: path.ejs.layoutDir,
      data: {
        name: 'ホゲのサイト',
        layoutsDir: path.ejs.layoutDir
      },
      engine: 'ejs',
      frontMatterProp: 'data'
    }))
    .pipe(ejs(extName, { 'ext': '.html' }))
    .pipe(gulp.dest(path.ejs.dist));
});


gulp.task('default', ['ejs']);
_layout.ejs
<!DOCTYPE html>
<html lang="ja">
<head>
  <!-- meta -->
  <meta charset="UTF-8">
  <meta name="description" content="<%= file.data.header.description %>">
  <meta name="keywords" content="<%= file.data.header.keywords %>">

  <title><%= file.data.header.title %> - <%= name %></title>

  <!-- link -->
  <% file.data.css.forEach(cssItem => { %>
  <link rel="stylesheet" href="<%= cssItem %>">
  <% }); %>
</head>
<body>
  <%- contents %>
  <% file.data.js.forEach(jsItem => { %>
  <script src="<%= jsItem %>"></script>
  <% }); %>
</body>
</html>
index.ejs
---
layout: _layout
header:
  title: Top
  description: これはホゲのサイトです。
  keyword: ホゲ, イケてるサイト, いいかんじのサイト
css: []
js: []
---
<p><%= header.title %></p>

何やってんのかサッパリわからんって人の為に自分の中での落とし込みも兼ねてgulpfileを主軸に上から順番に流れを見ていきましょう!
gulpタスク内は同期処理なので上から流れていくので順番を追って読めるのが素敵なところですね!

1. plumber()

ここでまずタスク内でエラーが出ていないかのチェックをしてくれています。
通常タスク実行中にエラーが出てしまうとパイプラインの途中で処理が止まってしまいますが、plumber(プラマー)を挟んであげることで普通は出ないようなエラーを細かく出してくれたり、エラーが出ても処理を止めずに最後まで実行してくれます。
余談ですがpipeは配管、plumberは配管工という意味だそうです。

2. frontMatter()

ejsの中にあるFront-matterを切り取って、dataという変数にオブジェクトを格納しています。
試しに_layout.ejsの中で<% console.log(file.data); %>と入れてあげるとindex.ejs
で定義したFront-matterの変数オブジェクトがターミナル上で確認できるかと思います!

※ここで注意しておきたいのは、_layout.ejsとぺージ固有のejsとではFront-matterの取得方法が違うのです。
例えばの上記の'ホゲ, イケてるサイト, いいかんじのサイト'を取得したいってなった時、_layout.ejsからhfile.data.header.keywordで取得できるんですが、index.ejsのようなぺージ固有のejsからはheader.keywordでないとアクセスできないんです。

3. ejs()

frontMatterで生成した変数のオブジェクトは各ejsのローカル変数として定義されているので、フラグに使ったりテキストを埋め込んだりと好きなように使用することができます。

4. wrapper()

最後は一番重要な所ですね。
ここでは今まで流れてきたパイプラインのデータを文字通りラッピングしてくれます!
オプションもいくつかあるんで細かく見ていきましょう!

① layout

ここでは_layout.ejsが格納されているディレクトリを選択します。
直接_layout.ejsを指定しないのは今後別パターンのlayoutを使うという話が出てきた時に柔軟に対応できるようにする為なのかなと思っています。
そしてindex.ejs側でどのlayoutを使うかを指定してあげればOKです。

② data

ここでもFront-matterと同様変数を定義できます。
ただ役割がFront-matterと被っていることと、_layout.ejsにしか向けられない変数なんであまりアクティブに使うことはないかなぁなんて思ってます。
以下の場合nameに値が入っていきます。

gulpfile.js
data: {
  name: 'ホゲのサイト'
},
_layout.ejs
<title><%= file.data.header.title %> - <%= name %></title>

③ engine

ここでは何のテンプレートエンジンを使うかを指定してあげます。
デフォルトではloadshが入ってるのでお好みのテンプレートエンジンで試してみましょう!

④ frontMatterProp

やってる事としては先述の2. frontMatterと同じでejsのFront-matterを取得しています。
なんで同じ事やってるのかというと2. front-matterではlayoutではないejsにしか変数が入っていきません。
_layout.ejsにFront-matterを読み込ませるにはここでdataという変数にして読み込ませてあげる必要があるのです。

ここまでで何も問題がなければうまく./htdocsにindex.htmlが生成されているはずです!
一見重たい処理のように感じますが、自分の扱っているプロダクトでは約60P分を一度に処理したりもしてますが、ストレスフリーで仕事できてます笑

お知らせ用のJSONをejsに組み込む

まずお知らせ用のjsonを用意します。

newsList.json
{
  "newsList": [
    {
      "date": "19XX年",
      "text": "世界は核の炎に包まれた",
      "url": "http://www.hokuto-no-ken.jp/series/hokutonoken.html"
    }
  ]
}

そしたらこのJSONをejsの中に読み込ませてあげる必要があります。
最初はejsの中でJSONをrequireできないかなーなんて目論んでたんですけれど結論無理という事が判明したんで別の方法で試してみました!

gulpfile.jsからrequireする作戦

なんでgulpfileからなんだと思いましたがどうもこのgulpfile.jsと_layout.ejsはglobalオブジェクトで繋がってるそうです!(JSの見識が非常に広い方から教わりました🙏)
globalオブジェクトとは所謂JSで言うところのwindowオブジェクトです。(globalオブジェクトはNode.js)
なのでgulpfile.jsからrequireしてきて、同じNode.js空間で繋がっている_layout.ejsに値を渡してあげることができるんです!すごい!

gulpfile.js
const path = {
  ejs: {
    src: [
      './src/ejs/**/*.ejs',
      '!./src/ejs/**/_*.ejs'
    ],
    dist: './htdocs/'
  },
  json; {
    newsList: './src/data/newsList.json'
  }
};

global.jsonData = {}; 
jsondata.newsList = require(path.newsList);

こういうかんじで名前空間にJSONを渡してあげれば_layout.ejsからでもindex.ejsからでも取得できます!
あとはejs側でforEachとかで回してあげれば見事お知らせが入ったHTMLが生成されます。

つまづいた所

最初は爆速テンプレートエンジンで噂のectを使って運用してみようと思ったんですけれど、中々うまくできなかったので使い慣れているejsでやってみました。

気になる点

_layout.ejsとindex.ejsでのFront-matterの取得方法が違う

例えばfile.data.〜ってかんじでどっちも取得できるならごちゃごちゃせずに分かりやすいなぁなんて思いました。

globalオブジェクトにJSONをくっつけるのがあまり正しい気がしない

現状これしか方法がないと思ってるんですが、やはりどうもしっくりこない…
ajaxでJSON読み込んでJSでHTML吐き出せばいいじゃないかって話なんでしょうけど今回はEJSからHTMLを生成した時点でJSONの中身がHTMLにある事にこだわってみました。

何かいい案があったら教えていただきたいです。

さいごに

今回この作業するにあたって結構色んな人に助けていただきました!
おかげ様で自分の中での知見が広がったと実感しています。
ありがとうございました!

ソースはGitHubにあげてあるので試したい方はクローンしてみてくださいー!^^

https://github.com/U-0084/gulp-front-matter__practice