npm scriptsでPugをコンパイルする(JSONの参照、Markuplintの導入)
要件
- srcディレクトリにあるPugファイルを対象にコンパイルしてhtdocsディレクトリに出力する
- PugからJSONを参照できるようにする(サイト共通データなどを管理)
- Pugファイルの品質はMarkuplintで担保する
- ページ作成用のPugファイルでサイト共通の値や関数を使えるようにしつつ、ページに合わせて上書き可能にする
パッケージをインストールする
Pugに必要なパッケージをインストールします。
npm i -D pug github:pugjs/pug-cli#master
スタイルガイドにFractalを使用する場合はpug
ではなくpug@2.0.0-rc.4
でインストールしてください。
@rsm/fractal-pug-adapter
をインストールしたときにPugの3系が使えず、2系に落とすようにコマンドの指示がありました。
また、pug-cli
はgithub:pugjs/pug-cli#master
をインストールしてください。
こちらの記事にある通り、pug-cli 1.0.0-alpha6
には_
から始まるPugファイルもHTMLとして出力されてしまうバグがあります。
2023年9月14日時点でも1.0.0-alpha6が最新(2016年6月2日から更新なし)なので、本体が修正されるのは難しいかもしれません。
Markuplintも使いたいのでインストールします。
npm i -D markuplint @markuplint/pug-parser
クロスプラットフォームに対応して簡潔に記述できるnpm-run-allとファイルの監視機能があるonchangeもインストールします。
npm i -D npm-run-all onchange
ディレクトリ構造
以下のディレクトリ構造で構成しています。
.
├── _data
│ ├── data.json
│ └── json
│ ├── fooList.json
│ └── site.json
├── _mixin
│ ├── _Icon.pug
│ ├── _LinkText.pug
│ ├── _Picture.pug
│ ├── _SrOnly.pug
│ └── _all.pug
├── _partial
│ └── _meta.pug
├── _template
│ └── _default.pug
└── index.pug
-
_data
:サイトの共通データや一覧などのコンテンツデータです -
_mixin
:mixinを一括管理します -
_partial
:メタタグやグローバルヘッダーなどを管理します -
_template
:上記をすべて_default.pug
として集約します -
index.pug
:_default.pug
を継承してページを作成します
サイトの共通データを1枚のJSONファイルに結合する
--obj
オプションにJSONファイルを渡すと、すべてのPugファイルから参照できます。
ただ、複数のファイルを渡すことは出来なさそうだったので、スクリプトを事前に実行して1つのファイルにまとめています。
const fs = require('fs');
const path = require('path');
/**
* 指定されたディレクトリ内のJSONファイルをマージします。
* @param {string} inputDir - JSONファイルが格納されているディレクトリのパス
* @returns {Object} マージされたJSONデータ
*/
const getMergedData = (inputDir) => {
return fs.readdirSync(inputDir).reduce((acc, file) => {
if (path.extname(file) === '.json') {
const key = path.basename(file, '.json');
const data = require(path.join(inputDir, file));
acc[key] = data;
}
return acc;
}, {});
};
// 入力ディレクトリと出力ディレクトリの設定
const inputDir = path.join(__dirname, '../src/_data/json');
const outputDir = path.join(__dirname, '../src/_data');
const outputFile = path.join(outputDir, 'data.json');
// マージされたデータを取得
const mergedData = getMergedData(inputDir);
// マージされたデータをファイルに出力
fs.writeFileSync(outputFile, JSON.stringify(mergedData, null, 2));
_data/json/
以下にJSONファイルを保存、スクリプトを実行すると_data/data.json
として出力されます。
.
├── _data
│ ├── data.json
│ └── json
│ ├── fooList.json
│ └── site.json
site.json
とfooList.json
が次の内容だったとすると、
{
"url": "https://example.com",
"name": "サイト名",
"title": "サイトのタイトル",
"description": "サイトの説明文",
"keywords": "キーワード1,キーワード2,キーワード3",
"ogpImage": "/ogp.png",
"fbAppId": "iiiiiiiiiiiiiii",
"fbAdmins": "aaaaaaaaaaaaaaa",
"twitterCard": "summary_large_image",
"twitterSite": "@account_name",
"gtm": "GTM-XXXXXX"
}
{
"data": [
{ "id": "dummy1", "text": "テキスト1", "url": "/dummy1/", "external": true },
{ "id": "dummy2", "text": "テキスト2", "url": "/dummy2/", "external": false }
]
}
次のように、ファイル名をキーにして結合されます。
{
"fooList": {
"data": [
{
"id": "dummy1",
"text": "テキスト1",
"url": "/dummy1/",
"external": true
},
{
"id": "dummy2",
"text": "テキスト2",
"url": "/dummy2/",
"external": false
}
]
},
"site": {
"url": "https://example.com",
"name": "サイト名",
"title": "サイトのタイトル",
"description": "サイトの説明文",
"keywords": "キーワード1,キーワード2,キーワード3",
"ogpImage": "/ogp.png",
"fbAppId": "iiiiiiiiiiiiiii",
"fbAdmins": "aaaaaaaaaaaaaaa",
"twitterCard": "summary_large_image",
"twitterSite": "@account_name",
"gtm": "GTM-XXXXXX"
}
}
Markuplintを設定する
.markuplintrc
を作成します。
touch .markuplintrc
.markuplintrc
には次のように設定しています。
{
"extends": [
"markuplint:recommended"
],
"parser":{
".pug$": "@markuplint/pug-parser"
},
"nodeRules": [
{
"selector": "img",
"rules": {
"required-attr": [
"src",
"alt"
]
}
}
]
}
- プリセットは「推奨プリセット」である
markuplint:recommended
を設定、今回はPugファイルを直接リントするので追加プリセットは設定していません -
parser
には@markuplint/pug-parser
を設定しています -
nodeRules
でimgタグの必須属性をsrc
とalt
だけに上書きしています(width
とheight
、decoding
の必須設定をリセットしています)
Pugファイルを設定する
index.pug
extend /_template/_default
append variables
- const pageAbsolutePath = "/"; // ページのルート相対パス(必須)
- const pageTitle= `${site.title}`; // ページタイトル(必須)
- const pageDescription= site.description; // ページのメタディスクリプション(必須)
- const pageKeywords= site.keywords; // ページのメタキーワード(任意)
- const pageLocale= "ja"; // ページの言語(必須)
- const pageOgpType= "website"; // ページのOGPタイプ(必須)
- const pageOgpTitle= pageTitle; // ページのOGPタイトル(必須)
- const pageOgpImage= site.ogpImage; // ページのOGP画像(必須)
block content
article
ul
each item in fooList.data
li
a.common-LinkText(href=item.url target=item.external && "_blank")
span !{item.text}
-
block content
内にコンテンツを追加します -
append variables
内に変数を定義、既存の変数は_partial/_meta.pug
で使用しています -
target=item.external && "_blank"
の箇所は、external
がtrue
だったらtarget="_blank"
が出力されます -
!{変数名}
のようにするとHTMLとして解釈されるので、brタグを入れることができるようになります
ページ固有のCSSとJSを読み込みたい場合は次のように追加できます。
//- 個別CSSファイルを追加で読み込む
block append css
- link(rel="stylesheet" href="/assets/css/xxx.css")
//- 個別JSファイルを追加で読み込む
block append js
- script(src="/assets/js/xxx.js")
block content
_template/_default.pug
block variables
include /_mixin/_all
doctype html
html(lang=pageLocale)
head
include /_partial/_meta
body
// Google Tag Manager (noscript)
noscript
iframe(src=`https://www.googletagmanager.com/ns.html?id=${site.gtm}` height="0" width="0" style="display:none;visibility:hidden")
// End Google Tag Manager (noscript)
//- include /_partial/header
block content
//- include /_partial/footer
-
include /_mixin/_all
でmixinを一括で参照しています -
/_partial/_meta
の他、/_partial/header
などを追加していきます
_mixin/_all.pug
include /_mixin/_Icon
include /_mixin/_SrOnly
include /_mixin/_Picture
include /_mixin/_LinkText
-
src/_mixin/
内にあるmixin用のPugファイルをすべてインクルードします
_Icon.pug
であれば次のように定義しています。
//- @param {Object} params
//- @param {String} params.name [""] SVGスプライトのid名
//-
//- @examples Input
//- +Icon({ name: "menu" })
//- @examples Output
//- <svg role="img">
//- <use href="/assets/svg/sprite.svg#menu"></use>
//- </svg>
mixin Icon(params={})
-
const props = Object.assign({
name: "",
}, params)
svg(role="img")&attributes(attributes)
use(href=`/assets/svg/sprite.svg#${props.name}`)
_partial/_meta.pug
meta(charset="UTF-8")
meta(name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no")
// Google Tag Manager
script.
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer',`#{site.gtm}`);
// End Google Tag Manager
title #{pageTitle}
meta(name="description" content=pageDescription)
if pageKeywords
meta(name="keywords" content=pageKeywords)
block font
.
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@300;400;500;700&display=swap" rel="stylesheet">
block css
link(rel="stylesheet" href="/assets/css/site.css")
link(rel="canonical" href=site.url + pageAbsolutePath)
meta(name="format-detection" content="telephone=no")
meta(property="og:title" content=pageOgpTitle)
meta(property="og:description" content=pageDescription)
meta(property="og:type" content=pageOgpType)
meta(property="og:image" content=site.url + pageOgpImage)
meta(property="og:url" content=site.url + pageAbsolutePath)
meta(property="og:site_name" content=site.name)
meta(property="og:locale" content=pageLocale)
if site.fbAppId
meta(property="fb:app_id" content=site.fbAppId)
else if site.fbAdmins
meta(property="fb:admins" content=site.fbAdmins)
if site.twitterCard
meta(name="twitter:card" content=site.twitterCard)
if site.twitterSite
meta(name="twitter:site" content=site.twitterSite)
block js
script(src="/assets/js/site.js" defer)
-
site
から始まる変数は_data/json/site.json
から参照しています -
page
から始まる変数はindex.pug
で定義していますが、index.pug
でもsite
から始まる変数を使うこともできます
npm scriptsを設定する
Pugのコンパイルに関する処理だけ抜き出しています。
"scripts": {
"start": "run-s -c dev watch",
"build": "npm-run-all -s clean -p build:*",
"watch": "run-p watch:*",
"watch:html": "onchange \"src/**/*.pug\" \"src/_data/json/**/*.json\" -- npm run dev:html",
"dev": "run-p dev:*",
"dev:html": "NODE_ENV=development npm-run-all -c -s html:{json,pug} -s html:markuplint",
"build:html": "NODE_ENV=production run-s -c html:*",
"html:json": "node scripts/mergeJson.js",
"html:markuplint": "markuplint \"src/**/*.pug\"",
"html:pug": "pug src --out htdocs --basedir src --obj src/_data/data.json --pretty",
},
-
dev
は開発用のビルド、build
は本番用のビルドとしています -
run-s
は順番に実行、-c
はエラーが起きても最後の処理まで止まらずに実行するオプションです -
node scripts/mergeJson.js
を実行してPugに渡すJSONを作成します -
run-s -c html:*
のようにすると、scripts
にあるhtml:
から始まるスクリプトを上から順番に実行します -
html:pug
では、コンパイル元・出力先・ルート相対パスのルート指定・JSONファイルの参照先・整形された状態でHTML出力(設定なしだと1行で出力)を設定しています -
npm-run-all -c -s html:{json,pug} -s html:markuplint
の部分で一連の処理を開始、JSON作成をしてからPugをコンパイル、その後にMarkuplintを実行します -
onchange \"src/**/*.pug\" \"src/_data/json/**/*.json\" -- npm run dev:html
の部分でPugファイルと関連するJSONファイルの保存を監視、ファイルが上書きされたら一連の処理を開始します
Discussion