Pug(Jade)で効率的なマークアップ環境を作る
HTMLのコーディングをするとき、メタ情報やヘッダーのような共通部分を効率的に管理するためにPugというテンプレートエンジンをよく使っています。最初は「導入コストが高い」と考えていましたが、それ以上のメリットがあると感じるようになりました。
今回はPugのテンプレートを作ったので、その使い方を紹介します。
今回はPugの記法については細かく説明できませんので、詳しい仕様は公式サイトを確認してください。
テンプレートエンジンとPugについて
テンプレートエンジンというのは、特定の処理をおこなうテンプレートに表示させたいデータ(文字列やタグなど)を渡すことで、無駄なくHTMLを作成していく仕組みのことです。CSSにおけるSassのようなものと考えてもいいと思います。
今回使用するテンプレートエンジンのPugには以下のような特徴があります。
- 閉じタグ(
<>
)がなく、インデントで階層を表現する - クラスやIDの指定がCSSの記法と同じ(
.
や#
) - JavaScriptが書けるので変数やif文などが使える
- 別ファイルをインクルード(ファイルの中身だけ取得)できる
テンプレートエンジンにはEJSのようなHTMLと書き方が変わらないものと、Pugのような独自の記法で簡潔に書けるものとがあります。
Pugは慣れないとコンパイルエラーを頻繁に起こしてしまったりして導入コストは多少あるのですが、閉じタグを書く必要がないので要素の追加や階層の変更がしやすく、保守性が高くなります。
PugはJadeというテンプレートエンジンがリネームされたものです(Jadeが登録商標だったので変更せざるをえなかったようです)。GitHubにも説明がありますが、Jadeから一部仕様の変更をしているようです。シンタックスや出力結果の変更、APIの削除などをしているので、Jadeの書き方をしていると意図しない結果になる可能性があります。
GulpとPugで開発環境を作る
いちから開発環境を作るのは大変なので、GitHubにリポジトリを作りました。フォークをするか、よく分からなければダウンロードでも大丈夫です。
以下のような構成になっています。
root
├── README.md
├── gulpfile.js
├── package.json
└── src/
├── _data/
│ └── site.json
├── _includes/
│ ├── _footer.pug
│ ├── _header.pug
│ ├── _layout.pug
│ ├── _meta.pug
│ └── _script.pug
├── assets/
│ ├── css/
│ │ └── common.css
│ └── js/
│ └── common.js
└── index.pug
整形ツール「EditorConfig」
Pugはスペースとタブが混在するとエラーになってしまうのでEditorConfigを導入して解決しています。
EditorConfigはテキストエディタによって設定やパッケージがあるので準備しておいてください。詳しくは以下の記事を参考にしてください。
EditorConfig でチームみんなのエディタの設定を揃える
インストール
ローカルに落とせたらターミナルなどでnpm install
を実行してNode.jsのパッケージをインストールします。
GulpでPugを動かすためには以下のパッケージが最低限必要です。
以下のパッケージはPugの機能の拡張やエラー通知、リアルタイムプレビューのために使用しています。
ビルド
npm start
タスクを実行することで必要なGulpのタスクが実行されます。このタスクにはPugをHTMLにコンパイルするgulp html
タスクやリアルタイムプレビューをするためのgulp browser-sync
タスクなどが含まれています。
タスクを実行すると/dest/index.htmlが以下のように出力されます。
<!DOCTYPE html>
<html lang="ja">
<head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# website: http://ogp.me/ns/website#">
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="format-detection" content="telephone=no">
<title>サイトの名前</title>
<meta name="description" content="サイトの概要">
<meta name="keywords" content="サイトのキーワード1, サイトのキーワード2">
<link rel="stylesheet" href="/assets/css/common.css">
<meta property="og:title" content="サイトの名前">
<meta property="og:type" content="website">
<meta property="og:image" content="http://example.com/images/og-image.jpg">
<meta property="og:url" content="http://example.com/index.html">
<meta property="og:description" content="サイトの概要">
<meta property="og:site_name" content="サイトの名前">
<meta property="og:locale" content="ja">
<meta property="fb:admins" content="">
<meta property="fb:app_id" content="">
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@">
</head>
<body>
<!-- header-->
<p>header</p>
<!-- /header-->
<!-- contents-->
<p>Contents</p>
<!-- /contents-->
<!-- footer-->
<p>footer</p>
<!-- /footer-->
<script src="/assets/js/common.js"></script>
</body>
</html>
開発環境の説明
開発環境で確認するファイルとディレクトリは以下になります。
- /gulpfile.js
- /src/_data/site.json
- /src/index.pug
- /src/_includes/
基本的な流れとしてはこのようになっています。
- index.pugから_layout.pugをインクルード
- _layout.pugで_meta.pugや_header.pugなどをインクルード
- index.pugで変数を上書きして各インクルードファイルに反映させる
Gulpの設定
gulpfile.jsには以下のようなPug用のタスクが指定されています。
locals
オプションでJSONを読み込ませるbasedir
オプションでルート相対パスでインクルードできるようにルートディレクトリを指定- gulp-dataとpathで各ファイルのルート相対パスを
relativePath
に格納
var gulp = require('gulp');
var pug = require('gulp-pug');
var fs = require('fs');
var data = require('gulp-data');
var path = require('path');
var plumber = require('gulp-plumber');
var notify = require("gulp-notify");
var browserSync = require('browser-sync');
/**
* 開発用のディレクトリを指定します。
*/
var src = {
// 出力対象は`_`で始まっていない`.pug`ファイル。
'html': ['src/**/*.pug', '!' + 'src/**/_*.pug'],
// JSONファイルのディレクトリを変数化。
'json': 'src/_data/',
'css': 'src/**/*.css',
'js': 'src/**/*.js',
};
/**
* 出力するディレクトリを指定します。
*/
var dest = {
'root': 'dest/',
'html': 'dest/'
};
/**
* `.pug`をコンパイルしてから、destディレクトリに出力します。
* JSONの読み込み、ルート相対パス、Pugの整形に対応しています。
*/
gulp.task('html', function() {
// JSONファイルの読み込み。
var locals = {
'site': JSON.parse(fs.readFileSync(src.json + 'site.json'))
}
return gulp.src(src.html)
// コンパイルエラーを通知します。
.pipe(plumber({errorHandler: notify.onError("Error: <%= error.message %>")}))
// 各ページごとの`/`を除いたルート相対パスを取得します。
.pipe(data(function(file) {
locals.relativePath = path.relative(file.base, file.path.replace(/.pug$/, '.html'));
return locals;
}))
.pipe(pug({
// JSONファイルとルート相対パスの情報を渡します。
locals: locals,
// Pugファイルのルートディレクトリを指定します。
// `/_includes/_layout`のようにルート相対パスで指定することができます。
basedir: 'src',
// Pugファイルの整形。
pretty: true
}))
.pipe(gulp.dest(dest.html))
.pipe(browserSync.reload({stream: true}));
});
JSONでサイト情報を管理する
サイトの名前やURLなどのサイト固有の情報はJSONで一括管理します。/src/_data/site.jsonに以下のように指定してあります。
{
"name": "サイトの名前",
"description": "サイトの概要",
"keywords": "サイトのキーワード1, サイトのキーワード2",
"rootUrl": "http://example.com/",
"ogpImage": "http://example.com/images/og-image.jpg"
}
gulpfile.jsからsite.jsonを読み込んでいるので、どのPugファイルからでもsite.name
のようにして値を取得することができます(gulpfile.jsでsite
に格納するように指定をしています)。
index.pug
トップページとして/src/index.pugを用意しています。extendによって/src/_includes/_layout.pugをインクルードしています。
extend /_includes/_layout
append variables
//- Required
- var pageTitle= "";
- var pageDescription= site.description;
- var pageKeywords= site.keywords;
//- Optional
- var pageOgpTitle= "";
- var pageOgpImage= site.ogpImage
- var pageLang= "ja";
- var pageOgpType= "website";
//- Not modified
- var pageUrl= relativePath;
block content
// contents
p Contents
// /contents
append variables
以下にページごとに変更できる変数を定義しています。
//- Required
とコメントしているところは変更をするしないに関わらず確認してください。
//- Optional
とコメントしているところは任意です。pageOgpType
はトップページのときはwebsite
、それ以外はarticle
に変更します。
その他に、例えばpageTitle
にはそのページの<title>
タグの値を指定します。""
のように空にするとサイトのタイトルだけになり、"ページタイトル"
のように値を渡すと、ページタイトル | サイトタイトル
のようになります。
index.pugだけである程度の変更ができるようにしています。
もし、そのページ固有のCSSファイルを読み込みたいという場合は、
append css
link(rel="stylesheet" href="/about/css/about.css")
block content
のようにすると、そのページにだけCSSファイル(上記の場合はabout.css)を追加で読み込むことができます。
_includes
/src/_includesディレクトリには共通で使用するファイルを用意しています。
- _meta.pug - メタ情報
- _script.pug - JavaScript
- _header.pug - グローバルヘッダー
- _footer.pug - グローバルフッター
- _layout.pug - 必要な共通ファイルをインクルード
_meta.pug
_meta.pugには<head>
タグ内のメタ情報を定義しています。
page
で始まる変数はindex.pugで変更ができます。site
で始まる変数はsite.jsonで定義しているサイト固有のものになります。
meta(charset="UTF-8")
meta(http-equiv="X-UA-Compatible" content="IE=edge")
meta(name="viewport" content="width=device-width, initial-scale=1.0")
meta(name="format-detection" content="telephone=no")
if pageTitle
title #{pageTitle} | #{site.name}
else
title #{site.name}
meta(name="description" content=pageDescription)
meta(name="keywords" content=pageKeywords)
block css
link(rel="stylesheet" href="/assets/css/common.css")
//- OGP
if pageOgpTitle
meta(property="og:title" content=pageOgpTitle + ' | ' + site.name)
else
meta(property="og:title" content=site.name)
meta(property="og:type" content=pageOgpType)
meta(property="og:image" content=pageOgpImage)
meta(property="og:url" content=site.rootUrl + pageUrl)
meta(property="og:description" content=pageDescription)
meta(property="og:site_name" content=site.name)
meta(property="og:locale" content=pageLang)
//- OGP Facebook insights
meta(property="fb:admins" content="")
meta(property="fb:app_id" content="")
//- /OGP Facebook insights
//- OGP Twitter Cards
meta(name="twitter:card" content="summary")
meta(name="twitter:site" content="@")
//- /OGP Twitter Cards
//- /OGP
if文を使って<title>
タグの値を条件分岐で出し分けています。これはindex.pugの説明でも例に出しましたね。
if pageTitle
title #{pageTitle} | #{site.name}
else
title #{site.name}
OGPまわりなど、不要なものは削除してかまいません。
_script.pug
JavaScriptの読み込みをまとめています。例えばここにJQueryを追加してもいいでしょう。
block js
script(src="/assets/js/common.js")
ポイントはblock js
としているところです。index.pugでappend
することでコードを追加(挿入)することができるようになります。
例えば、以下のようにappend
すると、
append js
script(src="/assets/js/common2.js")
block content
このように出力されます。
<script src="/assets/js/common.js"></script>
<script src="/assets/js/common2.js"></script>
_layout.pug
_layout.pugは共通化したファイルを文書構造に合わせてインクルードしています。このファイルでまとめておくことで、index.pugを簡潔にしておくことができます。
block variables
doctype html
html(lang=pageLang)
head(prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# " + pageOgpType + ": http://ogp.me/ns/" + pageOgpType + "#")
include /_includes/_meta
body
include /_includes/_header
block content
include /_includes/_footer
include /_includes/_script
ポイントはblock variables
としているところです。index.pugで
extend /_includes/_layout
append variables
とすることで変数の上書きをすることができます。
他言語対応などで共通ファイルを追加したい場合は、_layout_en.pugや_header_en.pugのように別のファイルとして分けます。
index.pugはなるべくプレーンな状態を保って、_layout.pugを用途によって使い分けた方が変更に強くなると思います。
まとめ
最初はPugの記法に抵抗を感じるかもしれませんが、サイトの規模が大きかったり、ボリュームのあるコンテンツを作成・変更するときにPugのメリットを感じることができます。あらかじめ必要十分な機能が揃っているのも魅力です。
複雑にしたくないので個人的にはあまり使いませんが、eachなどのIterationを使ったり、Mixinsでスニペット化したり、FiltersでMarkdownを使えるようすることもできます。
まだ普通のHTMLしか使ったことがないという方は、まずはこのPugのテンプレートをもとに共通化から始めてみてください。制作の効率が変わってくるはずです。
案件で使うベースになるテンプレート(個人的に作っているものです)も公開していますのでカスタマイズしていくときの参考にしてみてください。