
 CMSを構築するほどでもないけれど、複数のページをさくっと作りたい。もしくは、PHPが使えない・インクルードができないサーバーでもページを量産したい。そういったとき、手軽にHTMLを生成できる「EJS」がとても便利です。EJSを活用すると、JSONファイルでコンテンツを一括管理して、それを元に静的HTMLファイルを生成する、なんてこともできるようになります。

EJSを用いてページ内のヘッダーやフッターといったコンテンツをインクルード化し、それらのソースを合体したHTMLを生成できます。今回はgulpとEJS、さらにデータの管理がしやすいようJSONファイルを用いてページの量産を試みたいと思います。ひとつのJSONファイルでコンテンツを一括管理して、それを元に静的HTMLファイルを量産できるようになるので、ぜひチェックしてみてくだささい!
※記事内の画像はすべてフリー素材サイト「Unsplash」「Pixabay」よりダウンロードしています。
目次
今回作るものは
今回はgulp+EJS+JSONを用いて大量のhtmlを生成しやすくしてみようと思います。
・同じテンプレートの商品紹介ページが大量にあり、WordPressを使用しないサイト
 ・セキュリティ上どうしてもHTMLを使用しないといけないサイト
 ・CMSを構築するほどでもないけれど、複数のページをさくっと作らないといけないとき
上記のような場面で活用するととても便利です。
それでは、まずはベースとなる言語やツールについて簡単に紹介します。
gulpとは
gulpはNode.jsのビルドシステムです。CSSやJavaScript、画像などのさまざまな種類のファイルを、どのように加工してどのように出力するかを指定し、自動でビルドしてくれるように設定できます。プロジェクトごとに、CSSは圧縮したほうが良いか否か、画像は軽量化するか、特定の言語を使うかなど、出力方法を選べる柔軟性も備えています。
EJSとは
EJSとは、JavaScriptを用いてHTMLを生成できるテンプレート言語で、拡張子は「.ejs」です。Effective JavaScript Templatingの頭文字をとっており、直訳すると「効率的なJavaScriptのテンプレート」になります。簡単に導入でき、複雑な書き方などもないので素早く適応できるとても便利なものとなっています。
参考までに、メリットとデメリットをまとめてみました。
EJSのメリット
 ・ヘッダーやフッターなどの共通パーツをインクルード化できるため、修正や改修などが簡単におこなえる
 ・JavaScriptをベースにしているので、PHP(CMS)が使えない環境であっても導入することが可能
EJSのデメリット
 ・EJSはシンプルな造りになっているため、他のテンプレートエンジンに比べると機能が少ない
 ・node.jsのインストールが必要のため、導入までに多少時間がかかる
詳しい使い方は記事の中で紹介していきます。
ファイル構成

gulp+EJSでインクルード化し、JSONファイルからサイト内のデータを読み込みgulpで展開できるようにします。記事データをJSON形式でまとめることで、同じ作りのページの量産が簡単にできます。また、コンテンツ内の造りを変えることになっても、記事データとHTMLを切り離しているのでスムーズに改修ができます。
手順
記事内で使うNode.jsとGulpのバージョンは以下の通りです。
| Node.js | v16.13.1 | 
|---|---|
| gulp | CLI version: 2.3.0 | 
1. Node.jsをインストールする
gulpはNode.jsのライブラリのため、利用するにはNode.jsのインストールが必要です。
2. プロジェクトファイルを作成
任意のフォルダを作成し、ターミナルから以下を叩きます。
1 2 3  | $ npm init -y  | 
3. npmでパッケージのインストール
npmでパッケージのインストールをします。
1 2 3 4 5  | $ npm install パッケージ名 // もしくは $ npm i パッケージ名  | 
今回使うパッケージは以下の通りです。
| gulp | gulp用 | 
|---|---|
| gulp-ejs | EJS用 | 
| gulp-rename | ファイル出力時にファイル名を変える | 
| gulp-dart-sass | sass用 | 
| gulp-postcss | 生成されたcssを操作する | 
| autoprefixer | 自動でベンダープレフィックスを付与 | 
| gulp-html-beautify | HTML生成後のコードを綺麗にする | 
| gulp-plumber | エラーによるタスクの強制停止を防止 | 
| gulp-notify | デスクトップ通知 | 
| browser-sync | 変更を即座にブラウザへ反映 | 
| fs | JSONファイル操作用 | 
| del | データ削除用 | 
4. gulpfile.jsの編集
さきほど作成したプロジェクトの中にgulpfile.jsというものが自動で作られていると思うので、こちらを編集していきましょう。まずは、先ほどインストールしたプラグインを読み込みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18  | // gulpプラグインの読み込み const gulp = require('gulp'); const ejs = require('gulp-ejs'); //EJS const rename = require('gulp-rename'); //ファイル出力時にファイル名を変える const sass = require('gulp-dart-sass'); //sass const postcss = require('gulp-postcss'); //生成されたcssを操作する const autoprefixer = require('autoprefixer'); //自動でベンダープレフィックスを付与 const htmlbeautify = require("gulp-html-beautify"); //HTML生成後のコードを綺麗にする const plumber = require("gulp-plumber"); //エラーによるタスクの強制停止を防止 const notify = require("gulp-notify"); //デスクトップ通知 const browserSync = require("browser-sync").create(); //変更を即座にブラウザへ反映 const fs = require('fs');//JSONファイル操作用 const del = require('del'); //データ削除用  | 
次に、入出力するフォルダを指定しやすいよう、変数にまとめます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21  | const srcBase = './src'; const distBase = './dist'; const srcPath = {   'scss': srcBase + '/scss/**/*.scss',   'img': srcBase + '/img/**/*',   'js': srcBase + '/js/**/*.js',   'json': srcBase + '/**/*.json',   'ejs': srcBase + '/**/*.ejs',   '_ejs': '!' + srcBase + '/_inc/**/*.ejs', }; const distPath = {   'css': distBase + '/css',   'html': distBase + '/**/*.html',   'img': distBase + '/img',   'js': distBase + '/js',   'item': distBase + '/item', };  | 
生成される前に、一度生成した中身を削除するためのタスクを用意しておきます。
1 2 3 4 5 6  | /* clean */ const clean = () => {   return del([distBase + '/**'], { force: true }); }  | 
scssを自動で生成するようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25  | /* sass */ const cssSass = () => {   return gulp.src(srcPath.scss, {     sourcemaps: false   })     .pipe(       //エラーが出ても処理を止めない       plumber({         errorHandler: notify.onError('Error:<%= error.message %>')       }))     .pipe(postcss([       autoprefixer({         browsers: ['last 2 versions', 'iOS >= 12', 'Android >= 8']       })     ]))     .pipe(sass({ outputStyle: 'expanded' }))     .pipe(gulp.dest(distPath.css)) //コンパイル先     .pipe(browserSync.stream())     .pipe(notify({       message: 'Sassをコンパイルしました!',       onLast: true     })) }  | 
EJSからHTMLを自動で生成するようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17  | /* EJS */ const ejsFunc = () => {   var jsonFile = srcBase + '/data/pages.json',     json = JSON.parse(fs.readFileSync(jsonFile, 'utf8'));   return gulp     .src([srcPath.ejs, srcPath._ejs])     .pipe(ejs({json:json}))     .pipe(rename({       basename: 'index', //ファイル名       extname: '.html' //拡張子     }))     .pipe(gulp.dest(distPath.item)) }  | 
※ファイル名について
1 2 3 4 5 6 7 8  | gulp-rename .pipe(rename({   dirname: , // ディレクトリ名   basename: 'index', // ファイル名   extname: '.html'  // 拡張子 }))  | 
上記のように、出力するファイル名を指定することができます。
画像とJavaScriptの出力先を指定します。
 これらは今回特別な操作をしないので、シンプルなソースになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13  | /* image */ const imgFunc = () => {   return gulp.src(srcPath.img)     .pipe(gulp.dest(distPath.img)) } /* js */ const jsFunc = () => {   return gulp.src(srcPath.js)     .pipe(gulp.dest(distPath.js)) }  | 
ローカルサーバーを立ち上げるようにします。
 ファイルの更新があれば自動でブラウザがリロードされるようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24  | /* ローカルサーバー立ち上げ */ const browserSyncFunc = () => {   browserSync.init(browserSyncOption); } const browserSyncOption = {   server: distBase } /* リロード */ const browserSyncReload = (done) => {   browserSync.reload();   done(); } /* ファイルの変更時にbrowserSyncReloadする */ const watchFiles = () => {   gulp.watch(srcPath.scss, gulp.series(cssSass))   gulp.watch(srcPath.img, gulp.series(imgFunc, browserSyncReload))   gulp.watch(srcPath.ejs, gulp.series(ejsFunc, browserSyncReload))   gulp.watch(srcPath.js, gulp.series(jsFunc, browserSyncReload)) }  | 
css、EJS、img、jsを出力します。
1 2 3 4 5 6 7 8 9 10 11 12  | exports.default = gulp.series(   clean,   gulp.parallel(cssSass, ejsFunc, imgFunc, jsFunc),   gulp.parallel(watchFiles, browserSyncFunc) ); exports.build = gulp.series(   clean,   gulp.parallel(cssSass, ejsFunc, imgFunc, jsFunc), );  | 
以上がpackage.jsの設定になります。
5. data.json の作成
記事に流し込むデータをJSONに登録します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33  | {,   "category01": {     "title": "category01"     "description": "カテゴリーのディスクリプションです",     "keywords": "カテゴリーのキーワードです"   },   "item01": {     "id": "item01",     "category": "category01",     "title": "りんご",     "price": "1,000",     "itemAbout": "りんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキスト",     "itemInfo": "りんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキスト",     "hasInfo": true,     "productInfo": "りんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキスト",     "itemTtl01": "甘酸っぱくておいしい",     "itemCont01": "りんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキスト",     "itemTtl02": "みずみずしい",     "itemCont02": "りんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキストりんごのテキスト",     "description": "りんごのディスクリプションです",     "keywords": "りんごのキーワードです"   },   "item02": {     "id": "item02",     "category": "category01",     "title": "オレンジ",   ⁝   ⁝   以下略   } }  | 
6. EJSの作成
EJSファイルを作成します。
 基本的にはhtmlファイルと同じですが、任意のテキストを挿入したい箇所に、先ほどのJSONファイルで指定した変数を入れておきます。
 フッター用のインクルードファイル_footer.ejsもこのファイルで読み込んでいます。
_contents.ejs
_contents.ejsでは、<%= page['category'] %>というような、EJS用のタグ<% %>で囲われた箇所をいくつか確認できるかと思います。ここで、JSONから任意のデータ(<%= page['category'] %>ではcategory欄)を参照し、テキストを表示させています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87  | <div id="wrapper">   <main id="main" class="main">     <div class="contents-body">       <ul class="bread pc">         <li>           <a href="<%= baseUrl %>/index.html" class="item">TOP</a>         </li>         <li><a href="<%= baseUrl %>/<%= page['category'] %>/" class="item"><%= page['category'] %></a>         </li>         <li>           <span class="item"><%= page['title'] %></span>         </li>       </ul>       <div class="wrap-head">         <div class="img anm-right js-scroll">           <img src="<%= baseUrl %>/img/item/<%= page['category'] %>/<%= page['id'] %>/main.jpg" alt="">         </div>         <div class="inner">           <div class="box anm-up js-scroll">             <ul class="bread sp">               <li>                 <a href="<%= baseUrl %>/index.html" class="item">TOP</a>               </li>               <li>                 <a href="<%= page['category'] %>" class="item"><%= page['category'] %></a>               </li>               <li>                 <span class="item"><%= page['title'] %></span>               </li>             </ul>             <h1 class="box-ttl js-anm"><%= page['title'] %></h1>             <p class="box-price"><span class="price">¥<%= page['price'] %></span><span class="tax">(税込)</span></p>             <p class="box-txt"><%= page['itemAbout'] %></p>             <ul class="box-list">               <li>                 <a href="" class="detail-btn">                   <span class="txt">購入する</span>                 </a>               </li>             </ul>           </div>         </div>       </div><!-- wrap-head -->       <div class="wrap-main">         <div class="item-product anm-up js-scroll">           <div class="product-inner">             <h2 class="product-ttl">               商品詳細             </h2>             <div class="inn-max">               <ul class="product-list">                 <li class="item">                   <div class="list-img"><img src="<%= baseUrl %>/img/item/<%= page['category'] %>/<%= page['id'] %>/product01.jpg" alt=""></div>                   <p class="list-txt"><%= page['itemInfo'] %></p>                 </li><!-- item -->               </ul>             </div>             <% if( page['hasInfo']) { %>               <div class="product-info"><%- page['productInfo'] %></div>             <% } %>           </div>         </div><!-- item-product -->         <div class="item-detail">           <ul class="item-detail-inner">             <li class="anm-up js-scroll">               <div class="detail-img"><img src="<%= baseUrl %>/img/item/<%= page['category'] %>/<%= page['id'] %>/scene01.jpg" alt=""></div>               <div class="detail-body">                 <h2 class="detail-ttl js-anm"><%= page['itemTtl01'] %></h2>                 <p class="detail-txt"><%= page['itemCont01'] %></p>               </div>             </li><!-- li -->             <li class="anm-up js-scroll">               <div class="detail-img"><img src="<%= baseUrl %>/img/item/<%= page['category'] %>/<%= page['id'] %>/scene02.jpg" alt=""></div>               <div class="detail-body">                 <h2 class="detail-ttl js-anm"><%= page['itemTtl02'] %></h2>                 <p class="detail-txt"><%= page['itemCont02'] %></p>               </div>             </li><!-- li -->           </ul>         </div><!-- item-detail -->       </div><!-- wrap-main -->     </div><!-- contents-body -->   </main>   <%- include('../_inc/_footer.ejs', { baseUrl: baseUrl }) %> </div><!-- /wrapper -->  | 
ポイント:情報の出し分け
1 2 3 4 5  | <% if( page['hasInfo']) { %>   <div class="product-info"><%- page['productInfo'] %></div> <% } %>  | 
先ほどのJSONファイルにて、”hasInfo” が true になっていた箇所では、< div class="product-info" > が表示されるようになります。このように、JSONやEJSを用いて記事ごとの情報の出し分けも可能になるので、「この記事にはキャンペーンページへのバナーを出す・出さない」といったことが選べるようになり、サイトの運営がしやすくなるかと思います。
_head.ejs
通常のhead内をこちらのインクルードファイルに記載しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25  | <!DOCTYPE html> <html lang="ja"> <head>   <meta charset="utf-8">   <meta name="format-detection" content="telephone=no">   <meta name="viewport" content="width=device-width">   <link rel="shortcut icon" href="/favicon.ico" >   <link rel="apple-touch-icon" href="/apple-touch-icon.png">   <title><%= page['title'] %> | gulp+ejs+json</title>   <meta property="og:locale" content="ja_JP" />   <meta property="og:type" content="article" />   <meta property="og:title" content="<%= page['title'] %> | gulp+ejs+json" />   <meta property="og:description" content="<%= page['description'] %>" />   <meta property="og:site_name" content="gulp+ejs+json" />   <meta property="og:image" content="<%= baseUrl %>/img/common/ogp.jpg">   <meta name="description" content="<%= page['description'] %>" />   <meta name="keywords" content="<%= page['keywords'] %>">   <link rel="stylesheet" type="text/css" href="<%= baseUrl %>/css/style.css"> </head> <body>  | 
タイトル箇所には<%= page['title'] %> 、ディスクリプションには<%= page['description'] %>、キーワードには<%= page['keywords'] %>といったうように、JSONから読み込むデータを指定しています。
item01.ejs
アイテムページでは_head.ejsと_contents.ejsを読み込んでいます。
 ページの冒頭で pageData = ‘item01’ と書いてあり、その下の _contents.ejs にて JSON から読み込むデータを page: json[pageData] で指定しています。var pageData = ‘item01’; 箇所を変えるだけで、JSONから読み込みたいデータを選ぶことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25  | <% var baseUrl = '../..'; var pageData = 'item01'; %> <%-   include('../../_inc/_head.ejs', {     baseUrl: baseUrl,     page: json[pageData]   }); %> <%-   include('../../_inc/_contents.ejs', {     baseUrl: baseUrl,     page: json[pageData]   }); %> <script src="<%= baseUrl %>/js/jquery-3.6.1.min.js"></script> <script src="<%= baseUrl %>/js/common.js"></script> </body> </html>  | 
7. gulp でビルドする
最後に、ターミナルで以下のコマンドを叩くとgulpが起動しビルドされ、HTMLファイルが dist フォルダ内に出力されます。
1 2 3  | $ gulp  | 

/item01/~/item03/それぞれのページが生成されれば成功です。もし、先ほどJSONに記載したデータが任意の場所に表示されていない場合は、変数が正しいものか、タイプミスがないかなどを確かめてみてください。
ソース一式のダウンロード
今回のソース一式をまとめたものは、以下URLよりダウンロードいただけます。node用のファイルは含まれていないので、npmでパッケージのインストールをした後にフォルダへ以下のファイルをコピーしてください。
EJSについて
EJSの書き方について、基本的にはHTMLと同じなのですが、EJSを使いたい箇所では<% %>というタグ内に記入する必要があります。ほかにも少しだけルールがあるので簡単にまとめました。
EJSの記入
1 2 3  | <% ここに書きます %>  | 
<% %>内にJavaScriptを記入できます。HTMLとしては出力されません。
コメントアウト
1 2 3 4 5 6 7 8 9 10 11 12  | <%-   # 挨拶文   var message = はじめまして   # var comment = 宜しくお願いします  %> <p><%- message %><p> <p><%- comment %></p> ↓ <p>はじめまして<p> <p></p>  | 
EJS内でコメントアウトするときには「#」を使います。
エスケープする
1 2 3  | var message = 'はじめまして<br>こんにちは'  | 
このように宣言した変数を出力してみます。
1 2 3 4 5 6  | <p><%- message %></p> ↓ あああ あああ  | 
<%- では改行が反映されます。htmlの要素として反映させたい場合は <%- にしましょう。
エスケープしない
1 2 3 4 5  | <%= message  %> ↓ あああ<br>あああ  | 
<%= にすると改行コードも文字として出力されます。
インクルードしたファイルを読み込む
1 2 3  | <%- include('../../_inc/_head.ejs');%>  | 
インクルードしたファイルを読み込むには、<%- include('ファイルへのパス');%>と記載してください。
インクルードを使い値を受け渡す
1 2 3 4 5 6  | <%-    var baseUrl = '../..';   include('../inc/_head.ejs', { baseUrl: baseUrl }) %>  | 
上記のように記載すると、ページをまたいで値を渡すことも可能です。
1 2 3  |   <link rel="stylesheet" type="text/css" href="<%= baseUrl %>/css/style.css">  | 
ここに先ほど宣言されていた var baseUrl = ‘../..’; が入り、CSSへの正しいパスが設定されます。
まとめ
基本的にはほぼHTMLで作成でき、EJS事態もJavaScriptの文法をベースにしており学習コストが少ないため、HTMLでのサイト制作に慣れ親しんだ方であれば、すぐにEJSを活用できるかと思います。HTMLを大量に生産したり、共通パーツのちょっとした修正などにEJSは便利ですので、この機会にぜひ導入してみてはいかがでしょうか。
また、Node.jsの開発者が新たにDenoという環境を開発していたので、次はdeno+EJSを試してみたいと考えています。
参考
 【Gulp】EJSを使ってHTMLを量産する
 Gulp + EJS + JSONを使用してhtmlファイルを量産する方法
 
 




