OctpressとかTinkererとか
StaticGenに載っているツールでやる作業をgulpでやってみようという話。
元ネタはFrom Jekyll to Gulp.jsです。
最初はmetalsmithを使おうと思っていた・・・
更新
- bootstrapは本記事では蛇足なの削りました。
- 「次へ」と「前へ」の実装を追記
markdownをhtmlに変換する
プロジェクトのディレクトリを作って・・・
> mkdir gulp_static_site
> cd gulp_static_site
> npm init -y
> npm install gulp-load-plugins gulp-marked gulp-debug -D
適当な記事を作ってみる。
Tinkererを運用していたときのディレクトリ構造をそのまま使うことにして、
posts/yyyy/mm/dd/title.md
というファイル名を使うことにする。titleはascii文字だけを使う。
# 記事タイトル
gulpで静的なサイトを生成するで
gulp設定を作る
var gulp=require('gulp');
var $ = require('gulp-load-plugins')();
//
// markdownをhtmlに変換
//
gulp.task('posts', function(){
gulp.src('posts/**/*.md')
.pipe($.debug({title: '*.md'}))
.pipe($.marked())
.pipe(gulp.dest('build/posts'))
;
});
gulp実行
> gulp posts
buildディレクトリにhtmlが出力される。問題ない。
目次を作る
##ローカルプラグインを作ってみる
とりあえず何もしないpluginの雛形。これは、gulpというよりはnode.jsのStreamApiのTransformの様式らしい。通常のTransformには、Buffer, StringやObjectが流れてくるそうだが、gulp.srcの場合vinylというファイルを抽象化したオブジェクトが流れてくる。
vinylのpathをconsoleに表示してみる。
> npm install through2 -D
var through = require('through2');
module.exports = function () {
return through.obj(function (file, enc, cb) {
console.log(file.path);
cb();
});
}
var makeToc=require('./plugins/make-toc');
gulp.task('posts', function () {
var dst=__dirname + '/build/posts';
gulp.src('posts/**/*.md')
.pipe($.debug({ title: '*.md' }))
.pipe($.frontMatter()) // frontmatter
.pipe($.marked()) // md to html
.pipe(gulp.dest(dst))
// 目次を作る
.pipe(makeToc())
;
> gulp
中身を実装する
gulpプラグインの基本構造(プラグイン開発者向け)
を参考に入ってきたファイルをリストして最後に目次のhtmlを出力するようにしてみた。
> npm install gulp-util -D
var through = require('through2');
var gutil = require('gulp-util');
var path = require('path');
module.exports = function (outputFileName, baseDir) {
var filelist = [];
function transform(file, encoding, callback) {
// ファイルがnullの場合
if (file.isNull()) {
// 次のプラグインに処理を渡すためにthis.push(file)しておく
this.push(file);
// callback()は必ず実行
return callback();
}
// ファイルがstreamの場合(このサンプルプラグインはstreamに対応しない)
if (file.isStream()) {
// emit('error')を使って、プラグイン呼び出し側に'error'イベントを発生させる
this.emit('error', new gutil.PluginError('gulp-diff', 'Streaming not supported'));
// callback()は必ず実行
return callback();
}
filelist.push(file);
// callback()は必ず実行
callback();
}
function flush(callback) {
if (filelist.length > 0) {
var file=filelist[0];
// 目次
var output = new gutil.File({
cwd: file.cwd,
base: file.base,
path: file.base + '/' + outputFileName,
});
html='';
for(var i in filelist){
var f=filelist[i];
var rel=f.path.substr(baseDir.length);
console.log(rel);
html+='<ul><a href="' + rel + '">'+ path.basename(rel, '.html') + '</a></ul>\n';
}
output.contents = new Buffer(html);
this.push(output);
}
// callback()は必ず実行
callback();
}
return through.obj(transform, flush);
};
// 目次を作る
.pipe(makeToc('index.html', __dirname + '/build/'))
.pipe($.debug({ title: 'toc' }))
.pipe(gulp.dest('build'))
baseDirの相対パスの取り回しにつらみがあるけれど自作なら耐えられる。
最低限のものができた。
ejsでtemplateをつかえるようにしてみる
元記事はSwigというのを使っているのだけど、こっちはejsでやる。
gulp-ejsというプラグインが使えるかと思ったが
これはパイプラインからテンプレートが流れて来て変数は固定が想定されている。
今回の用途は、パイプラインから変数(frontMatterやhtml化された記事)が流れてきてテンプレートは固定で逆なのでejs用のローカルプラグインを作ってみる。
> npm install ejs -D
var through = require('through2');
var gutil = require('gulp-util');
var fs = require('fs');
var ejs = require('ejs');
module.exports = function (options) {
options = options || {};
if(!options.contentKey){
options.contentKey='content';
}
if (!options.filename) {
throw new gutil.PluginError('gulp-ejs-json', '`filename` required');
}
return through.obj(function (file, enc, cb) {
if (file.isNull()) {
cb(null, file);
return;
}
if (file.isStream()) {
cb(new gutil.PluginError('gulp-ejs-json', 'Streaming not supported'));
return;
}
try {
//console.log(file.frontMatter);
var template = fs.readFileSync(options.filename, 'utf-8');
var data = {};
data[options.contentKey]=file.contents.toString();
file.contents = new Buffer(ejs.render(template, data, options));
file.path = gutil.replaceExtension(file.path, ".html");
this.push(file);
} catch (err) {
this.emit('error', new gutil.PluginError('gulp-ejs-json', err));
}
cb();
});
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<%- content %>
</body>
</html>
gulp.src('posts/**/*.md')
.pipe($.debug({ title: 'md' }))
.pipe($.frontMatter()) // frontmatter
.pipe($.marked()) // md to html
.pipe(ejsApplyer({
contentKey: 'content',
filename: './templates/page.ejs'
}))
.pipe(gulp.dest('build/posts'))
// 目次を作る
.pipe(makeToc('index.html', __dirname + '/build/'))
.pipe(ejsApplyer({
contentKey: 'content',
filename: './templates/page.ejs'
}))
.pipe($.debug({ title: 'toc' }))
.pipe(gulp.dest('build'))
;
各記事と目次をhtmlでラップした。
FrontMatterに対応する
記事の先頭にメタデータを記述するYAML Front-matter とかいうやつに対応してみる。
---
title: "記事タイトル"
tags: [gulp, node.js]
---
# 記事タイトル
gulpで静的なサイトを生成するで。ふろんとまたーを追加してみた。
> npm install gulp-front-matter -D
markedの前に差す。
.pipe($.frontMatter()) // frontmatter
.pipe($.marked()) // md to html
ejsプラグインをさらに改造してfrontMatterの値を使えるようにする
var data = JSON.parse(JSON.stringify(file.frontMatter || {}));
data[options.contentKey] = file.contents.toString();
for (var key in options.map) {
data[key] = options.map[key];
}
ejsのテンプレート。title変数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><%= title %></title>
</head>
<body>
<%- content %>
</body>
</html>
「次へ」と「前へ」を実装する
テンプレートに次のようにしてリンクを追加した。
<div><a href="<%= prev %>">前へ</a></div>
<div><a href="<%= next %>">次へ</a></div>
frontMatterにprevとnextへの相対パスを差したい。
- makeTocでprevとnext値を追加する
- listに入れたファイルを再びストリームに流すプラグインを作成する
でできた。
新しいgulpfile.jsは、
先にリストを作ってfrontMatterを加工する。
var gulp = require('gulp');
var $ = require('gulp-load-plugins')();
var makeToc = require('./plugins/make-toc');
var resplit = require('./plugins/resplit');
var ejsApplyer = require('./plugins/ejs-applyer');
//
// markdownをhtmlに変換
//
gulp.task('posts', function () {
gulp.src('posts/**/*.md')
.pipe($.debug({ title: 'md' }))
.pipe($.frontMatter()) // frontmatter
.pipe($.marked()) // md to html
// 目次を作ってfrontMatterに追加する
.pipe(makeToc('index.html', 'posts'))
.pipe(gulp.dest('build'))
// リストをばらす
.pipe(resplit())
.pipe(ejsApplyer({
contentKey: 'content',
filename: './templates/page.ejs',
map: {
root_path: '../../../../'
}
}))
.pipe(gulp.dest('build/posts'))
;
});
gulp.task('build', ['posts']);
gulp.task('default', ['build']);
javascriptなので
ストリームに流すオブジェクトのプロパティに何でもかんでも追加していい。
var through = require('through2');
var gutil = require('gulp-util');
var path = require('path');
function relativeFromTo(src, dst) {
return path.relative(path.dirname(src), dst).replace(/\\/g, '/');
}
module.exports = function (outputFileName, dest) {
var filelist = [];
function transform(file, encoding, callback) {
if (file.isNull()) {
this.push(file);
return callback();
}
if (file.isStream()) {
this.emit('error', new gutil.PluginError('gulp-diff', 'Streaming not supported'));
return callback();
}
filelist.push(file);
callback();
}
function flush(callback) {
if (filelist.length > 0) {
var file = filelist[0];
// 目次
var output = new gutil.File({
cwd: file.cwd,
base: file.base,
path: file.base + '/' + outputFileName,
});
var html = '<ul>\n';
//console.log(filelist.length);
for (var i = 0; i < filelist.length; ++i) {
var f = filelist[i];
var rel = dest + "/" + f.path.substr(f.base.length).replace(/\\/g, '/');
//console.log(rel);
html += '<li><a href="' + rel + '">' + f.frontMatter.title + '</a></li>\n';
// 各アイテムのfrontMatterにnextとprevを付ける
// 降順に並んでいる
if (i === 0) {
// 先頭
f.frontMatter.next = "";
f.frontMatter.prev = relativeFromTo(f.path, filelist[i + 1].path);
}
else if (i === filelist.length - 1) {
// 終端
f.frontMatter.next = relativeFromTo(f.path, filelist[i - 1].path);
f.frontMatter.prev = "";
}
else {
f.frontMatter.next = relativeFromTo(f.path, filelist[i - 1].path);
f.frontMatter.prev = relativeFromTo(f.path, filelist[i + 1].path);
}
}
html += '</ul>\n';
output.contents = new Buffer(html);
// filelistをoutputにくっつける
output.filelist = filelist;
this.push(output);
}
// callback()は必ず実行
callback();
}
return through.obj(transform, flush);
};
上流から流れてきたfileにfile.filelistがあると決め打ちしてそれを下流に一個ずつ流す
var through = require('through2');
var gutil = require('gulp-util');
var path = require('path');
module.exports = function () {
function transform(file, encoding, callback) {
// ファイルがnullの場合
if (file.isNull()) {
// 次のプラグインに処理を渡すためにthis.push(file)しておく
this.push(file);
// callback()は必ず実行
return callback();
}
// ファイルがstreamの場合(このサンプルプラグインはstreamに対応しない)
if (file.isStream()) {
// emit('error')を使って、プラグイン呼び出し側に'error'イベントを発生させる
this.emit('error', new gutil.PluginError('gulp-diff', 'Streaming not supported'));
// callback()は必ず実行
return callback();
}
// do something
for(var key in file.filelist){
var output=file.filelist[key];
this.push(output);
}
// callback()は必ず実行
callback();
}
return through.obj(transform);
};
#まとめ
github-pages向けのブログ的なサイト生成にgulpを使う方法について書いてみた。
このパターンだと、先にすべての記事をリストに集めてからカテゴリやタグ、前後のリンクや、目次、月別ページ等の生成をして、各記事にメタデータを付与してから、テンプレートを適用した個別ページ出力をするというパターンに落ち着きそう。当初やるつもりだったbootstrapを使って見た目をおされにするのは今回は見送りました。