静的サイトジェネレータMetalSmith
今回は MetalSmith という超シンプル!超タイニー!なNodeで動作する静的サイトジェネレータのお話。
なんつーかその、メタルスミスとカタカナで書きたい衝動を煽られます。
そう、名前って大切ですよね。
スタティックサイトジェネレータはAssembleをはじめ、他にいくつもあって、 なかにはhexo なんていうものもあるんですが・・・うん・・・・。名前大事。
このメタルスミスというネーミング自体はマイナス34℃の環境下で作られたという、同じく静的サイトジェネレータのWinterSmithにインスパイアされたのかなーと思ったり。
というわけで公式ドキュメントはこちら
背景
さて、このメタルスミス、segment.ioという、Webデータ解析のインテグレーションツールを提供している会社によって作られていて、彼らのgithub organizationをのぞいて見ると、赤シャツのTJやJulian Gruberがいたりします。
で、作者はこのSegment.ioのco-founderである、Ian Storm Taylor氏。イアン!ストーム!!テイラー!!!必殺技みたいなお名前でございますな。
EVERYTHING IS A PLUGIN
エヴリシン イッザ プラァギン! でかでかとTシャツにプリントされていそうなこのスローガンがもう、メタルスミスそのものを体現していると行っても過言ではありません。
中身をのぞいて見るとわかるのですが、もう謳い文句通り、ちょうシンプル!ちょうタイニー!
これだけで良いのかと不安になるくらいの軽量っぷりでございます。
ほぼ全てはプラグインなので、やることと言えば単純にuse()をチェーンしてミドルウェアを登録し、最後にbuild()を呼ぶだけ。
例えば
Metalsmith(__dirname)
.use(markdown())
.use(templates('handlebars'))
.build();
簡単なサイトであれば上記のようにメタルスミス・ユーズ・ユーズ・ビルドみたいな感じでオッケー!
今までのスタティックサイトジェネレータはこの辺の、なにやってるかわからない感が酷かったのですが、メタルスミスを使えばコード内に過程をうまいこと可視化できるので組みやすいです。
処理を少し増やしてもまだ全然、怖くない感じですね。
Metalsmith(__dirname) .use(drafts()) .use(markdown()) .use(permalinks('posts/:title')) .use(templates('handlebars')) .build();
超シンプル!超タイニー!
exampleディレクトリそぞろ歩き。
では実際にメタルスミスを使ってみましょう。npmでインストールして実際に作り始めてしまうのも良いのですが、残念なことにnpmパッケージにはexampleが含まれていないので、まずはgithubからプロジェクトをcloneして、exampleを見て行きまっしょい。
$ git clone https://github.com/segmentio/metalsmith.git $ npm install
exampleディレクトリには今のところ6つのプロジェクトがあります。とりあえずstatic-siteディレクトリを見てみましょう。
package.jsonを開いてdependencyを確認します。
- metalsmith-template
- metalsmith-markdown
- metalsmith-permalinks
- metalsmith
- handlebars
このうち、metalsmith- から始まるものはメタルスミスのプラグインで、それら自体はパッケージに組み込まれていないので別途インストールが必要です。package.jsonにはscriptsエントリがありません。そこで Readme.md を読んでみると make build せよと。
Makefileを開いてみるとnpm installしてmetalsmithコマンドを実行するだけになっています。Windowsではmake入れるのが面倒だったりしますが、単純なのでコマンドを手入力しても良いし、バッチファイル書いてもよさそうです。
メタルスミスコマンドの利用
このメタルスミスコマンドはnodeのプログラムを書かなくても、metalsmith.jsonを作成して、実行内容を書いておくだけで処理を実行してくれるエラいやつ。
この方式だと自分でプログラムを書かなくても良いのです。
つまり、メタルスミスを利用する場合は、自分でモジュールを呼び出す方法か、JSONファイル を作成してメタルスミスコマンドを実行するかを選べるということですね。
では次にmetalsmith.jsonを確認してみます。
{ "metadata": { "title": "My Blog", "description": "My second, super-cool blog." }, "plugins": { "metalsmith-markdown": {}, "metalsmith-permalinks": { "pattern": ":title" }, "metalsmith-templates": { "engine": "handlebars" } } }
先ほどの package.json の中に記述してあった依存関係のあるプラグインが plugins に登場してますね。何やら引数を渡せる感じになっています。あとはメタデータのみとシンプル。
実際にmakeを走らせるとメタルスミスコマンドが実行され、buildディレクトリに生成されたファイルが格納されます。
と、これだけではアレなので、もうちょっと深堀りしていきましょう。
metalsmith.json のフォーマットですが、基本的なプロパティとしてsource, destination, metadata を含みます。で、source, destination は指定がない場合、それぞれsrc、buildディレクトリがデフォルトで利用されます。
pluginsのエントリですが、キーになっているプラグインモジュールがrequireされ、valueがそのオプションとして渡されます。つまり
"plugins": { "metalsmith-permalinks": { "pattern": ":title" } }
とあった場合、内部的には
var plugin = require("metalsmith-permalinks"); var opts = {"pattern": ":title"}; metalsmith.use(plugin(opts));
と変換されます。
こうして各プラグインをユーズ・ユーズ・ユーズで、ガーッと積み上げて、最後に`build()` を走らせるっていうのがメタルスミスコマンドのやっていることです。
なので、さきほどの
{ "metadata": { "title": "My Blog", "description": "My second, super-cool blog." }, "plugins": { "metalsmith-markdown": {}, "metalsmith-permalinks": { "pattern": ":title" }, "metalsmith-templates": { "engine": "handlebars" } } }
という例は
var metalsmith = new Metalsmith(process.cwd()); metalsmith.metadata({title: "My Blog", description: "My second, super-cool blog."}); metalsmith .use(markdown()) .use(permalinks({pattern:":title"})) .use(templates({engine:"handlebars"})) .build();
へと内部的に変換されて実行されます。
データの流れ
`src`ディレクトリにあるファイルを読み込み、それをプラグインに渡し、結果を`destination`ディレクトリへ出力するというのがメタルスミスの処理内容です。ちなみそれぞれのディレクトリのデフォルトは`src`、`build`となっていて、変更可能です。
ここではそのデータに着目して処理を追って行きましょう。
`example/static-site`の、処理対象となるファイルが格納されている`src`ディレクトリをのぞいて見ると、`index.md`を初め、`first-post.md`から`fourth-post.md`までフラットな階層に並んでいます。
それらの中身は
--- title: My First Post date: 2012-08-20 template: post.html --- An interesting post about how it's going to be different this time around. I'm going write a lot more nowadays and use this blog to improve my writing.
と、こんなだいたいこのような形式になっており、YAMLのFrontmatterにメタ情報を記述し、続いてMarkdown形式で書かれています。このへんはjekyllに影響を受けたんでしょうね。
Markdownファイルに限らず、全てのファイルは一旦このYAMLのFrontmatterの解析器にかけられ、attributeの抽出が行われます。必須というわけではないようですが、各プラグインで必要となりそうなページのメタ情報はFrontmatterにまとめておくのが良いですね。
ちなみにこのFrontmatterは`build()`で出力されるテキストからは省かれます。
さて、pluginが一つもない場合どうなるかというと、`src`ディレクトリに存在するファイルがYAML Frontmatterを除去された後に、全て`destination`ディレクトリにコピーされるという動きになります。
プラグイン
さて、このstatic-siteのexampleではmarkdown, permalinks, templatesの3つのプラグインが使われています。
それぞれその名の通り、markdownはmarkdownファイルの解釈、permalinksプラグインはindexからのpermalinkの生成を、templatesはテンプレート処理を行います。
各プラグインに渡すオプションは、それぞれのプラグインで定義されてるため、使いこなしにはプラグインのドキュメントも読んでおいたほうがよいですね。
ではそれぞれを詳しく見て行きましょう。
metalsmith-markdown
markdownプラグインはmarkedモジュールを利用しており、引数にmarkedへのオプションを取ります。
オプションは例えばこんな感じ。
{ smartypants: true, gfm: true, tables: true }
処理対象は `.md` 拡張子を持つファイルで、それらはMarkdownからHTML形式に変換され、拡張子も含めHTMLファイルとして出力されます。
metalsmith-permalinks
permalinksプラグインは入力ファイルに対し、指定されたパターンを適用したディレクトリを出力先に作成してその中に`index.html`を作成します。結果としてたとえば`about.html`が`about/index.html`として出力されたりします。
プラグインの引数にはパターンを指定するのですが、パターンに使われる要素も指定できます。例えば、
{ pattern: ':date/:title', date: 'YYYY'
と指定すると`年/タイトル`といった形式でディレクトリが作成され、その下にindex.htmlが作成されます。dateの変換に関してはこのプラグインの中でmomentモジュールを利用して変換されており、momentが理解できるフォーマットを指定します。
--- title: My First Post date: 2012-08-20 ---
このようなFrontmatterがあり、先ほどのパターンを適用すると`2012/my-first-post/index.html`として出力されます。
そのほか、`relative` オプションというものがデフォルトで`true`なのですが、これが`true`の場合は`src`ディレクトリにあるファイルが出力先ディレクトリに全てコピーされます。例えば
src/ css/ style.css post.html
このようなディレクトリ構成は
build/ post/ index.html css/ style.css css/ style.css
として出力されます。この挙動をオフにしたい場合は`relative`オプションを`false`に指定する必要があります。
metalsmith-templates
さて、最後のtemplatesプラグインですが、内部でConsolidate.jsを利用しており、引数にはそれで利用可能なものを指定できます。
exampleではテンプレートエンジンにhandlebarsを指定していますね。
プラグインの引数に指定できるのは
- engine
- テンプレートエンジンの指定
- directory
- テンプレートが置かれているディレクトリ指定、デフォルトは`templates`
- inPlace
- 本文中のタグをFrontmatterのattributeで置換するか否かのフラグ。デフォルトOFF。
- pattern
- テンプレート適用のマッチャーに使うパターンを記述します。記述はmultimatchを使っているのでpythonやphpのglob記法のような、`*.md`や`file??.txt`といった表現で書くことが出来ます。
となっており、
metalsmith.use(templates({ engine: 'handlebars', directory: 'templates_dir', pattern: 'publish*.md' }));
このように指定できます。
プラグインではFrontMatterの`template`がテンプレートファイル名を指定しており、このファイルの実体は`templates`ディレクトリに置かれることを想定しています。
中身は
<html> <head> <title>My Blog</title> </head> <body> <h1>{{ title }}</h1> <time>{{ date }}</time> {{{ contents }}} </body> </html>
こんな感じで、handlebarsによって title や date、contentsがそれぞれ置き換えられて出力されます。titleやdateはYAML frontmatterから、それ以外がcontentsと置き換えられます。