Help us understand the problem. What is going on with this article?

gulpでスタティックなサイトを生成する(github-pagesにブログを作る想定)

More than 3 years have passed since last update.

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文字だけを使う。

posts/2015/11/15/hello.md
# 記事タイトル
gulpで静的なサイトを生成するで

gulp設定を作る

gulpfile.json
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
plugins/make-toc.js
var through = require('through2');

module.exports = function () {
    return through.obj(function (file, enc, cb) {
        console.log(file.path);
        cb();
    });
}
gulpfile.js
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
plugins/make-toc.js
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);
};
gulpfile.js抜粋
        // 目次を作る
        .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
plugins/ejs-applyer.js
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();
    });
}
templates/page.ejs
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
</head>
<body>
<%- content %>
</body>
</html>
gulpfile.js
    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 とかいうやつに対応してみる。

posts/2015/11/15/hello.md
---
title: "記事タイトル"
tags: [gulp, node.js]
---

# 記事タイトル
gulpで静的なサイトを生成するで。ふろんとまたーを追加してみた。
> npm install gulp-front-matter -D

markedの前に差す。

gulpfile.json
        .pipe($.frontMatter()) // frontmatter
        .pipe($.marked()) // md to html

ejsプラグインをさらに改造してfrontMatterの値を使えるようにする

plugins/ejs-applyer.js
            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変数

templates/page.ejs
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title><%= title %></title>
</head>
<body>
<%- content %>
</body>
</html>

「次へ」と「前へ」を実装する

テンプレートに次のようにしてリンクを追加した。

templates/page.ejs
<div><a href="<%= prev %>">前へ</a></div>
<div><a href="<%= next %>">次へ</a></div>

frontMatterにprevとnextへの相対パスを差したい。

  • makeTocでprevとnext値を追加する
  • listに入れたファイルを再びストリームに流すプラグインを作成する

でできた。

新しいgulpfile.jsは、
先にリストを作ってfrontMatterを加工する。

gulpfile.js
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なので
ストリームに流すオブジェクトのプロパティに何でもかんでも追加していい。

make-toc.js
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があると決め打ちしてそれを下流に一個ずつ流す

resplit.js
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を使って見た目をおされにするのは今回は見送りました。

ousttrue
virtualcast
VRシステム(バーチャルキャスト)の開発、運営、企画
https://virtualcast.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした