anz blog

Metalsmith でブログ構築した話

2019-01-06 #misc #Metalsmith

このブログは Metalsmith でもって構築されているわけなので、
せっかくですしいろいろと書いておきます。

心構え

Metalsmith は1つ1つの機能をプラグイン追加することで実現していくタイプのジェネレータなので、
いきなり完璧な完成形よりかは、絶対に実現しなきゃいけない機能だけをまずは目指してやっていくほうが良いと思います。

各プラグイン同士で連携が必要なのだけれど、どちらかが古くなっていてできなかったり、
自分が目指す理想の機能にはプラグインの機能が少し足りないとか...そういうのが割と起きてくると思うので、
これだけは譲れない!みたいのをとりあえずは目指すっていう...一気にすべてを満たそうとはしないのが大事(だと、いまは思います。やり始めたときの自分に伝えたい(笑)

僕の場合

  • 記事はマークダウンでかける
  • レイアウトは記事(コンテンツ)と別で書く
    • 今回は handlebars を採用(事例が多い気がした

っていう2つだけを最初は目指すことにして、
それから少しずつ機能を追加できるものは追加していく感じになるかと。。

  1. マークダウンで書いて html にできるか
  2. マークダウン内で handlebars を利用できるか
    記事内でも変数の展開ぐらいはしたい
  3. 記事とレイアウトのがっちゃんこができるか
  4. 記事リストを表示できるか
  5. ページングができるか
  6. タグを設定できるか
    タグで絞った記事リストができるか
  7. RSS/Sitemap ができるか

こんな感じで少しずつステップを踏みながらやっていくのがよいのではないかとおもいます。
大体この1ステップごとに1つ以上のプラグインを追加することになります。
(最悪記事リストまでできればブログとしての一応の体裁は整う気がします)

実装話

ここからは実際の実装の話を。

先にも書いた通り、僕の場合 markdown + handlebars というので構築してますんで、
ここが違うひと参考になるかどうか🤔

index.js

ビルドを担っている js です。
Metalsmith は js で構築手順を指定していくこともできるし、json で指定することもできるけど、
僕は js でやってます。

とりあえずコア部分を全部のっけておきます。
各プラグインの説明とかはあとで。
siteMetaData っていうのは諸々の設定が書かれてる dict です)

const Metalsmith = require('metalsmith'),
      assets = require('metalsmith-assets'),
      browserSync = require('metalsmith-browser-sync'),
      collections = require('metalsmith-collections'),
      dateformat = require('dateformat'),
      dateFormatter = require('metalsmith-date-formatter'),
      feed = require('metalsmith-feed'),
      inplace = require('metalsmith-in-place'),
      layouts = require('metalsmith-layouts'),
      pagination = require('metalsmith-pagination'),
      partials = require('metalsmith-discover-partials'),
      permalinks = require('metalsmith-permalinks'),
      sass = require('metalsmith-sass'),
      sitemap = require('metalsmith-sitemap'),
      tags = require('metalsmith-tags'),
      watch = require('metalsmith-watch')

const builder = Metalsmith(__dirname)
  .metadata(siteMetaData)
  .source('./src')
  .destination('./build')
  .clean(true)
  .use(dateFormatter({
    dates: [
      {
        key: 'publishDate',
        format: 'YYYY/MM/DD'
      }
    ]
  }))
  .use(collections({
    articles: {
      pattern: './src/articles/*.hbs',
      sortBy: 'publishDate',
      reverse: true
    }
  }))
  .use(pagination({
    'collections.articles': {
      perPage: 20,
      path: './page/:num/index.html',
      layout: 'layout.hbs',
      first: 'index.html'
    }
  }))
  .use(tags({
    handle: 'tags',
    path: './tag/:slug/page/1/index.html',
    pathPage: './tag/:slug/page/:num/index.html',
    perPage: 20,
    layout: 'layout.hbs',
    sortBy: 'publishDate',
    reverse: true
  }))
  .use(partials({
    directory: 'partials',
    pattern: /\.hbs$/
  }))
  .use(inplace({
    pattern: '**/*.md',
    engineOptions: {
      html: true
    }
  }))
  .use(feed({
    collection: 'articles',
    preprocess: (file) => {
      file.title = file.articleTitle
      file.url = siteMetaData.baseUrl + 'articles/' + dateformat(file.publishDate, 'yyyymmdd') + '/' + file.fileName + '/'
      return file
    }
  }))
  .use(permalinks({
    relative: false,
    pattern: ':collection/:publishDate/:fileName'
  }))
  .use(layouts({
    default: 'layout.hbs',
    pattern: '**/*.html'
  }))
  .use(sass({
    outputStyle: 'expanded',
    outputDir: 'assets/stylesheet'
  }))
  .use(assets({
    source: './assets',
    destination: './assets'
  }))
  .use(sitemap({
    hostname: siteMetaData.baseUrl,
    omitIndex: true,
    lastmod: new Date()
  }))

if (debuggabble) {
  builder.use(browserSync({
    port: 8081,
    files  : [
      'src/**/*', 
      'layouts/**/*', 
      'partials/**/*',
      'assets/**/*'
    ]
  }))
  .use(watch({
    paths: {
      'src/**/*': '**/*',
      'layouts/**/*': '**/*',
      'partials/**/*': '**/*'
    },
    livereload: true
  }))
}

builder.build(function(err, files) {
  if (err) { throw err }
})

プラグインを適用していく順番が結構重要なので、
プラグインのページでサンプルとかあればその順番を踏襲してくのがおすすめです。
なんでできないんだろう?みたいなハマり方をしたら、適用順をかえると動作したりするので、
ハマったときは、そのプラグインが何をどうしているのかっていうの考えながら順番を変えていくといけるかもです。
(パズルみたいで楽しいね!🤤)

ちなみに、フォルダ構成はこういう感じです。

Metalsmithフォルダ構成

これらを踏まえて各プラグインの説明(概要?)みたいのを書いてきます
(大事なものから)
細かい実装とかオプションとかは気になったものをググれば Github のリポジトリがあるので、
そこの README とかみれば大体は。
doc系が古かったりなかったりする場合は、コードをちょろっとみればわかるはず。
1機能1プラグインみたいな感じで作られてるので、そこまで複雑なコードはないと思うので。

metalsmith-in-place

最もコアなプラグインになるかとおもいます。
マークダウンをhtmlに書き出すのもこれですし、
マークダウン内の handlebars の展開もこれが担っています。

マークダウン内でなぜに handlebars ...?と思うかもですが、
記事内に yaml で date とか title とかを書くことになる(記事リストとかで後々必要になる)のですが、
yaml で記事タイトルを書いて、マークダウン内でまたタイトル書くのって馬鹿らしいので、
yaml で書いたものがマークダウン内でも利用したくなる...というわけですが、これを実現してくれるのがこのプラグイン。

---
title: 記事タイトル
---

## \{{title}}

こういうこと。

実は in-place だけでは展開・変換をできなくって、それにプラスで専用のモジュールが別途必要になります。
今回でいうと、マークダウン内の handlebar の展開とマークダウンのHTML変換の2つ。

  • jstransformer-handlebars
  • jstransformer-markdown-it

in-place を install したら併せてこの2つも install します。

in-place がどの transformer を使うかっていう設定は
js 側ではなくて、ファイル名(拡張子)で指定する感じになります。
僕の場合でいうと記事マークダウン内の handlebars を展開してから、html に変換するっていう手順になるので、

xxxxxx.hbs.md

こういうファイル名にするというわけです。

jstransformer-markdown-it よりも jstransformer-markdown のほうがサンプルなんかでは利用されていることが多いと思いますが、
it の方を選択している理由は、マークダウン内で html を直接書きたい(例えばツイートの埋込など)ときに it だと実現できるけど、そうじゃないっほうができないというのがあるからです。
(it のほうが柔軟性高そうなのでこっちのが良い気がしています)

.use(inplace({
  pattern: '**/*.md',
  engineOptions: {
    html: true
  }
}))

html: true というのがそういう指定になります。

ちなみに、「metalsmith markdown」とぐぐると
こちらが出てきますが、
おそらく in-place を使うのであれば transformer を使うので不要かと
(実際僕は使っていないです)

in-place をマスターしたら記事の書き出しまではokという具合

metalsmith-layouts

in-place で記事(コンテンツ)をつくって、この layouts でレイアウトとガッチャンコするっていう感じなります。
in-place と layouts でコンテンツが html として完成するイメージ。
レイアウトファイルないの handlebars 展開なんかはこいつが担当している(はず)です。

昔は metalsmith-template というものが in-place と layouts の両方の機能を担っていたみたいですが、
今は細分化されて別プラグインとしてあるという話らしいです。

ほかの cms とか触っているとレイアウトは1つで、
中でいろいろ条件とかみて、if とかで分岐させて構築していくといのが普通かと思いますが、
リスト系のページとコンテンツ系のページとで適用させていくプラグインが異なったりするので、
いっその事役割が違うページごとにレイアウトファイルを作っておいたほうが良いかも...と感じています
僕は今の所、リスト系と記事単体系で分けてる感じですかね。
レイアウトファイルは分けておいて、細かく partial として分けて書いておくことで、
各レイアウトファイルでは共通部分の partial を呼び出すことにしておけば、分けてレイアウトファイルを作っておいてもそこまで手間じゃないですし。

metalsmith-discover-partials

レイアウト部分の共通化したものを適用していくやつです。
僕の場合フォルダ構成の partials にそれらがいます。
metaタグをずらずら書いてるやととか、サイトヘッダーとかサイトフッターとかそういう感じのもの。
レイアウトファイル内で下記のような感じで呼び出せます
(ファイル名をそのまま指定するとok)

<head>
\{{> meta}}
</head>

metalsmith-collections

一つ一つの記事をリストで扱えるようにしてくれるやつ。

僕の指定の仕方だと

.use(collection({
  articles: { ... }
}))

という感じでやっているので、レイアウト側では articles で取得ができるようになります。
handlebars だと以下のようにすると記事リストの画面が出来るようになります。

<ul>
\{{#each articles}}
<li>\{{ title }}</li>
\{{/each}}
</ul>

ちなみに。。
metalsmith collections とぐぐるとこちらが筆頭で出てくると思いますが、
ぼくはこれではなくてこっちの方を使っています。
理由は metalsmith-watchmetalsmith-browser-sync で変更を監視しつつリロードとかしてるのだけれど、
その時に記事が重複して表示されるという問題が発生しまして、
このissueで議論されてて、対応したものつくったよ!っていうコメがあったので遠慮なく使わせてもらってるというわけです。
べつに実際に記事が増えているわけではなくて、表示上の問題なので要らない人は元のものをつかっても全然ok

ここまでくると、リストと記事コンテンツができて一応のブログとしての体裁が整ってテンションあがります。
同時にあれもこれもやりたくなります😭

metalsmith-pagination

記事をリストで表示できるようになったら当然のようにページングしたくなるわけで...
それを実現してくれるのがこのプラグイン。
\{{#each articles}} と記事リストを取得していたものが、
このプラグインを適用すると以下のような感じに変わります。

\{{#each pagintion.files }}
\{{/each}}

肝心のページングは、最初/最後、前/次 へのパスが取得できるようになっているので、それで表現できるようになるかと。

\{{#if pagination.previous }}
<a href="\{{ pagination.previous }}"></a>
\{{/if}}
\{{#if pagination.next }}
<a href="\{{ pagination.next }}"></a>
\{{/if}}

パーマリンクを設定してくれるもの。
これは人によっては要らないかもです?🤔

example.md という記事をかいたら build してできあがるものは、articles/example.html みたいになるかと思いますが、
これが articles/example/index.html というふうに出力フォーマットが変更されます。
一般的には index は指定しないので、実際にしていするパスは articles/example/ となると。
これをいちいち手動で変換するのめんどいんのだけれど、このプラグインを適用してくと、
レイアウトで \{{path}} と取得すると全部置き換わっているので手動変換の心配は不要です。

metalsmith-tags

タグを設定できるようになしてくれるプラグイン。
ここまでくるとだいぶ装飾系のプラグインになってきます。
記事のマークダウンで yaml で tags を指定するだけです。

tags: Swift, Xdode

このプラグインだけでページングまでやってくれるので、タグを踏んだときのリスト表示も通常の記事リストも同じように表現できます。
ただ、

  • path と表示するがわで別で指定できない

という仕様でちょっとこまったので、僕は fork してちょちょっと手を入れました。
ここらがサクッと出来るのもいいですね。

metalsmith-assets

src 配下のものは build 配下に書き出されますけど、それ以外はそのまま残ってしまうので
src 以外でもこれを使って build 内に持っていってくれるプラグインです。
画像とかサードパーティ製のなにかとか...


雑にですが大体主要なものはこういう感じでしょうか。
あとは sitemap とか feed とか sass とかもつかってますけど、
そこまで迷うことはないかとおもうので割愛。
(書くのに疲れた...)

参考

[雑談]ブログを引っ越しを画策(すでに移行を始めてるなう)