自分が欲しいブログシステムを作った。 はてなブログやQiita、Mediumみたいに技術系の文章をまとめるためのブログサービスは世の中に色々あるけど、自分の要件にマッチするサービスを見つけられなかったのと技術習得欲に駆られたので車輪の再発明をした。
欲しかった要件としては、
- コンテンツに集中できるデザイン
- 高速かつ綺麗な数式のレンダリング
- Markdown形式での記事の編集
- サーバーサイドレンダリング
- (記事のデータをすべて手元で管理できる)
あたりがあった。 要件1のデザインの観点ではMediumが有力だった(有り体に言えばかっこよかった)んだけど、数式の記述に難点があったからやめた。 他に取り立てて魅力を感じるサービスがなかったので、 どうせならNode.js/npmでの開発の流れを知りたかったということもあり自分で作ることにした。
Next.js
ブログ作成にあたって、フレームワークにNext.jsを採用した。
Next.jsはZeit, Incが中心となって開発しているOSSで、公式サイトによれば
a small framework for server-rendered universal JavaScript webapps, built on top of React, Webpack and Babel
とのこと。 少なくとも自分のケースにおいては、Reactを使ったシステムをフルスクラッチで作るのは辛いという要求に対して、ルーティングだったりAPI部分を受け持ってくれるフレームワークと理解している。 加えて、同じくZeit, Incが開発しているstyled-jsxというライブラリをベースにしてHTML, JS, CSSを1つのファイルにまとめて記述することができる。例えば
only this paragraph will get the style :)
というDOM要素が以下のコードから得られる。
export default () => (
<div>
<p>only this paragraph will get the style :)</p>
<style jsx>{`
p {
color: red;
}
`}</style>
</div>
)
jsx形式のファイル内にテンプレートリテラルでCSSを書いていくという方式は好みが別れるかもしれない。 個人的にはVue.jsのsingle-file componentに似ていてそんなに違和感は感じなかった。
Next.jsの最小構成としては、 npm init
したディレクトリで
$ npm install next --save
$ mkdir pages
を実行する。 pages/index.js
を
import React from 'react'
export default () => <div>Hello world!</div>
のように作成し、 package.json
に
{
"scripts": {
"dev": "next"
}
}
を追加すれば、 npm run dev
で実行できる。
Next.jsでは、ファイルシステムがそのままAPIとして使用される。 具体的には pages/
以下のファイル名 xxx.js
が自動的に /xxx
にマッピングされる。 なおこれは server.js
を用意して自らルーティングをカスタマイズすることも可能。
今回は要件3のMarkdownでの記事の編集と、要件4のサーバーサイドレンダリングから、custom routingを設定してURI /articles/xxx-yyy-zzz
に対して /articles/{:03d}-xxx-yyy-zzz.md
のMarkdownファイルがレンダリングされる設定にした。 このあたりの書き方がパフォーマンスにクリティカルに影響しそうな気はしたが、とりあえず終わらせたいときの魔法の言葉に従うことにした。
Markdownのサーバーサイドレンダリング
はじめに要件で書いたとおり、モチベーションとしてウェブ上でのWYSIWYG編集みたいなことは全く想定してない。 むしろ記事データ含めてすべてGitリポジトリで管理したかったため、今回は /static/articles
以下に直接Markdown形式のファイルを保存する形を取った。
Markdownのレンダリングにはmarkdown-itを使用した。 選定理由は、数式のレンダリングやコードのシンタックスハイライトなど、プラグインを使用したカスタマイズが容易だったことが大きい。 なお、数式のレンダリングにはKaTeX、コードのシンタックスハイライトにはhighlight.jsを使用した。 例えば、上記プラグインを使用したMarkdownのレンダリングは以下のコードで実現できる。
const fs = require('fs')
const md = require('markdown-it')()
.use(require('markdown-it-highlightjs'))
.use(require('markdown-it-katex'))
md.render(fs.readFileSync('/path/to/md', 'utf8'))
実際にはMarkdownファイルの非同期読み込みやfrontMatterのパースをやったりしているので実際のコードとは少し異なるが、本質的には上記コードで実現できる。 ぶっちゃけ今回のブログ構築をやった理由の半分くらいはMarkdownのサーバーサイドレンダリングが技術的にどのくらいの難易度でできるのか知りたかっただけみたいなところがある。 この部分の機能が完成した瞬間にすごい満足した。
なぜ静的サイトジェネレーターを使わないのか?
前述の要件をすべて満たそうと思うと、真っ先に静的サイトジェネレーターが候補に挙がる。 有名どころだとJekyllやMiddlemanあたりが候補に挙がるし、node製の制約をおいてもgatsbyというフレームワークを見つけた。
静的サイトジェネレーター、かなり好きで実際以前プロフィールサイトを作ったときはMiddlemanをベースにした。 単純なサイトを作る分には不満がなかったんだけど、複雑なことをやりだそうとした時にDOMを自分で操作する設計に限界を感じた。
そういう意味ではgatsbyはカタログスペックはよかったんだけど、なんか直感的にこなれてなさを感じて避けた。 gatsby new
で生成されたディレクトリの雑然さから、学習コストの割に得るもの少なそうな気配を感じ取った。 一方でNext.jsは初見の洗練されてる感がすごくて心を持っていかれた。
おきもち
npmを中心に据えた開発フローすごく快適なんだけど、未だにnpm scriptsに何を書けば適切にコードが変換されるのか? とか今動いてるコードは本当に変換後のコードなのか? とかよくわかってない。 あんまりサーベイに時間かけずにとにかく動くものを作る方針で突っ走ってしまったので正しさがよくわからない。