yasuda

Pug(Jade)で効率的なマークアップ環境を作る

HTMLのコーディングをするとき、メタ情報やヘッダーのような共通部分を効率的に管理するためにPugというテンプレートエンジンをよく使っています。最初は「導入コストが高い」と考えていましたが、それ以上のメリットがあると感じるようになりました。
今回はPugのテンプレートを作ったので、その使い方を紹介します。

今回はPugの記法については細かく説明できませんので、詳しい仕様は公式サイトを確認してください。

Getting Started – Pug

テンプレートエンジンとPugについて

テンプレートエンジンというのは、特定の処理をおこなうテンプレートに表示させたいデータ(文字列やタグなど)を渡すことで、無駄なくHTMLを作成していく仕組みのことです。CSSにおけるSassのようなものと考えてもいいと思います。

今回使用するテンプレートエンジンのPugには以下のような特徴があります。

  • 閉じタグ(<>)がなく、インデントで階層を表現する
  • クラスやIDの指定がCSSの記法と同じ(.#
  • JavaScriptが書けるので変数やif文などが使える
  • 別ファイルをインクルード(ファイルの中身だけ取得)できる

テンプレートエンジンにはEJSのようなHTMLと書き方が変わらないものと、Pugのような独自の記法で簡潔に書けるものとがあります。
Pugは慣れないとコンパイルエラーを頻繁に起こしてしまったりして導入コストは多少あるのですが、閉じタグを書く必要がないので要素の追加や階層の変更がしやすく、保守性が高くなります。


PugはJadeというテンプレートエンジンがリネームされたものです(Jadeが登録商標だったので変更せざるをえなかったようです)。GitHubにも説明がありますが、Jadeから一部仕様の変更をしているようです。シンタックスや出力結果の変更、APIの削除などをしているので、Jadeの書き方をしていると意図しない結果になる可能性があります。

GulpとPugで開発環境を作る

いちから開発環境を作るのは大変なので、GitHubにリポジトリを作りました。フォークをするか、よく分からなければダウンロードでも大丈夫です。

gulp-pug-test | 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/

基本的な流れとしてはこのようになっています。

  1. index.pugから_layout.pugをインクルード
  2. _layout.pugで_meta.pugや_header.pugなどをインクルード
  3. 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のテンプレートをもとに共通化から始めてみてください。制作の効率が変わってくるはずです。
案件で使うベースになるテンプレート(個人的に作っているものです)も公開していますのでカスタマイズしていくときの参考にしてみてください。

新しいウェブ体験を作ろう TAMのPWA開発
お問い合わせはこちら