身軽な静的サイトジェネレーター Charge の紹介&採用例


この記事は Speee Advent Calendar 2019 4日目の記事です。
GatsbyJS の重装備を外して、Charge で身軽になるまでの経緯をご紹介します。

前段

あんた誰?

(昨年から引き続き)Markdown によるスライド作成ツール Marp の次世代版を開発している人です。

プロジェクト公式サイト

今年の 6 月に、プロジェクトの公式サイトを開設しました。

このサムネイルは開設当初のもので、GatsbyJS を使って立ち上げたものの、プロジェクトのランディングページとしては思ったより重装備だったことが災いし、ほぼ蛻の殻のままの状態が数ヶ月続いてしまい、コンテンツを拡充できませんでした。

この記事は、そんな状況を打破するために移行先として選んだ、新進気鋭の静的サイトジェネレーター Charge の紹介です。

Charge

Charge"an opinionated, zero-config static site generator" と銘打っている通り、設定より規約 を重視した JavaScript / React による静的サイトジェネレーターです。

  • ゼロ設定
  • JSX と MDX によるテンプレート
  • サーバーサイド React
  • Babel / PostCSS のビルトインサポート
  • ライブリロード対応の開発サーバー
  • ファイルのリビルドが最小限
  • 充実したドキュメント

Charge は GatsbyJS や Next.js と比べても、現時点ではユーザーも少ないマイノリティなツール(Marp は一応その中では一番の大口顧客)ですが、シンプルかつ必要十分な機能が揃っており、設定なしですぐにサイトを構築し始めることができます。

作者によれば、 『本当は新しい静的サイトジェネレーターなんて作りたくなかったけど、シンプルで、ドキュメントが充実していて、機能が揃っていて、メンテナンスされていて、望むように動くようデザインされているものがなかった』 とのことで作り始めたとのことです。

Yeah, I know, another static site generator. Let me be clear, I really did not want to make a static site generator. It’s really the very last thing I wanted to do.

I went on StaticGen and looked at every JavaScript-based one. I could not find a single one that I thought was simple, well-documented, had the features I needed, was actively maintained, and was designed and worked the way I wanted. So here I am, making a static site generator.

https://github.com/brandonweiss/charge#why

GatsbyJS との比較

リポジトリおよびドキュメントで、GatsbyJS との違いについて 述べられています。大きな違いは以下の2点です。

GatsbyJS は複雑な Web アプリケーションを作れるが、設定のために複雑な知識を必要とする

https://qiita.com/uehaj/items/1b7f0a86596353587466#20181228%E8%BF%BD%E8%A8%98 でも説明されていますが、GatsbyJS は React を使った Web アプリケーションに応用することができ、サイトの要件に合わせて柔軟な設定が可能です。

一方、設定に際しては、関連する技術 (Webpack, GraphQL, ルーターなど) の習得が必要になるため、要求される知識量は必然的に多くなります。

Charge は、サーバーサイドでの静的サイト生成にフォーカスを絞っているため、設定可能な箇所はほとんどなく、動的なアプリケーションや、複雑なサイトを作成するのには不向きです。

逆に言えば、LP やドキュメント、ポートフォリオなど、シンプルなサイトではこのような機能は必要無いはずです。目的がマッチしていれば、最小限の知識でサイトを作成することができます。

GatsbyJS はクライアント側に提供された React を使ってページを表示するが、Charge は 真の静的ページ を作る

Charge は GatsbyJS と同じく React を採用しているものの、大きく異なる点として 『サーバーサイドレンダリングのため "だけ" に React を使う』 という点が挙げられます。

GatsbyJS が出力するサイトには、クライアント側の HTML 向けの React / JavaScript も少なからず注入されます。事実、GatsbyJS 製のサイトで PWA や高速な画面遷移を実現しているのはそのおかげでもあるのですが、オプトアウトする手立てはありません。

Charge には、そのような暗黙的な注入はなく、HTML のみで動く "真の静的ページ" を作成することができます。クライアント側の React はそもそも注入されていないので、Charge の React は SSR を気にするだけで OK です。

実例: GatsbyJS に感じたモヤモヤ

実際のサイト構築で GatsbyJS を使ってみて、モヤモヤを感じたのが以下でした。

  • 単なる「LPを作る」という目的に対し、手段にオーバーテクノロジー感があり、着手から公開まで半年ほどかかってしまった
  • @reach/router のコンテナ <div> が CSS やスクロールなどの各種挙動を邪魔してしまう

時間がかかったのは、単に私の知識とリソースの不足に依るところが大いにありますが、それを抜きにしても、今回の目的に対して要求される技術の釣り合いが合わない印象があり、オーバーテクノロジーだなぁと感じた面は否めませんでした。

また、GatsbyJS で採用されているルーター @reach/router のコンテナに注入される <div> 1 によって、CSS(グローバルスタイル)が壊れたり、ルーター側のフォーカス管理による スクロール関連のトラブル が散見されるなど、地味にハマりどころが多かったのも尾を引きました。

結果的に、サイトを公開しても、メンテナンスを継続することが難しくなってしまったため、よりシンプルな Charge に白羽の矢が立ったというわけです。

使い方

インストール

npm i --save-dev @static/charge

CLI コマンド

Charge の CLI コマンド は迷いようが無いほど本当にシンプルで、開発用の serve とビルド用の build の2種類しかありません。

# 開発用サーバー起動
npx charge serve <source-dir>

# ビルド
npx charge build <source-dir> <dist-dir>

コンセプト

Charge の基本は、 source-dir に配置された静的ファイルは、ビルド時に dist-dir にコピーされる』 という設計です。これにより、ソースとサーバーのディレクトリ構成はほぼ一致します。

JSX や CSS など、一部の拡張子を持つファイルのみ、『ビルド時にそれぞれのファイルタイプに合わせて変換される』という設計になっています。

JSX

前述の仕様のため、そのまま HTML を置くことも勿論できますが、通常は、JSX でページを書き進めていくことになることでしょう。

命名規則として、index.html.jsx というファイルを置くと JSX がパースされ、ビルド結果が index.html というファイルに出力されます。

index.html.jsx
import Layout from './layout.html.jsx'

export default () => (
  <Layout title="My first Charge">
    <h1>Hello, world!</h1>
    <p>こんにちは!</p>
  </Layout>
)

中身は React なので、以下のようにレイアウトを分けたり、コンポーネントを分割したりできます。

layout.html.jsx
export default ({ children, title }) => (
  <html>
    <head>
      <title>{title}</title>
    </head>
    <body>
      {children}
    </body>
  </html>
)

MDX

Charge は MDX をサポートしているため、Markdown + JSX でページを書くこともできます。

index.html.mdx
import Layout from './layout.html.jsx'

export const layout = ({ children }) => (
  <Layout title="My first Charge">
    {children}
  </Layout>
)

# Hello, world!

こんにちは!

ドキュメントや、ブログの記事のようなページに最適です。

JavaScript

クライアント側で JavaScript が必要な場合、JS ファイルを配置できます。変換時には Babel が適用されます。

index.js
import message from './message.js'

console.log(message)

import による依存関係がある場合は、呼び出し元のファイルにマージされます。

message.js
// このファイルはビルド時に出力されない
export default 'Hello, world!'

拡張子を省略した import (import message from './message') だと、ビルド時に依存関係がうまく解決されないことがあるので、拡張子まで指定しておくのが無難です。

生成された JavaScript をブラウザで使用するには、<script> タグを明示します。Webpack などに慣れていると、つい import を使って呼び出したくなりますが、グッと堪えましょう。

layout.html.jsx
export default ({ children, title }) => (
  <html>
    <head>
      <title>{title}</title>
      <script src="/index.js" async></script>
    </head>
    <body>
      {children}
    </body>
  </html>
)

CSS

スタイルシートは PostCSS が自動的に通され、いくつかの最新の CSS 機能(Stage 2 以上 + α)を使用できます。また、JavaScript と同様に @import ルールに基づいて依存関係が解釈・マージされます。

CSS-in-JS

React に慣れている方であれば CSS-in-JS を使いたくなると思いますが、 "Charge の React は SSR 専門" なので、スタイルシートの注入に CSR が必要なライブラリ (styled-components など) は使用できないようです。

ドキュメント では利用可能な CSS-in-JS ライブラリの1つとして emotion が挙げられています。

その他の機能

全体を通して見ても、そこまで凝った機能はなく、『必要最低限のことを、必要最小限の労力で実現できる』という感じのツールになっています。

デプロイ

デプロイに関するドキュメント では、同作者による Amazon S3 へのデプロイのためのコンパニオンツール Discharge が紹介されています(このツール自体は Charge 以外でも使用可能)。

Next.js を開発する ZEIT が提供する静的サイトホスティングサービス ZEIT Now による Charge サイトのデプロイガイド もあります。

採用してみて

先月 11 月に、ユーザー導線の改善を目的に、Charge + emotion を使用して Marp の公式サイトを再構築 しました。2

ページ自体はほぼ HTML + CSS で構成されたページですが、主要な要素(ボタンなど)は React コンポーネントとして分割してあり、GatsbyJS の時に作ったコンポーネントなどは Charge でも引き続き使えるので、使用感にはそれほど違和感はありませんでした。

Charge 特有の要素は無い訳ではありませんが、大したものではなく、React についても「SSR しか考えなくて良い」と割り切れるため、実装はかなりスピーディーに進めることができました。暗黙的なコンテンツの注入に邪魔されず、コンテンツの内容を完全にコントロールできるのも安心できるポイントです。

中央に見える Marp スライドのサンプルは、画像ではなく、実際に Marp Core を使った変換結果を Charge 向けに作成した React コンポーネント を使ってレンダリングしています。

難点

Charge は『1ファイルにつき1ページ』の原則があるため、例えばページネーションを扱うために 1.html 2.html ... のような連番ページを作成する、なんてことはできません。もしこのような仕組みを実現するのであれば、自分でクライアント向けの JS を書いて実装する必要があります。

Marp プロジェクトのブログページ は、現状ページネーションが必要なほど記事が爆発的に増えることは想定していませんが、今後プロジェクトが拡大して記事が増えていくと、この点は考える必要が出てくるかもしれません。

Charge における動的ページ (Dynamic pages) の対応については "Coming soon" となっているため、今後に期待しましょう。

おわりに

静的サイトジェネレーター Charge の採用例のご紹介でした。

Charge の機能は本当にシンプルで、用途によってハッキリとした向き不向きこそありますが、GatsbyJS の重装備についていけなかった人(私)にとってはピッタリフィットする身軽なツールになると思いますので、同じ悩みを抱えている人には是非お勧めします。

Charge — an opinionated, zero-config static site generator
https://charge.js.org/


  1. このコンテナは無効にすることができず、該当 Issue でもハマった人達による多くの報告がなされています。 

  2. ソースコードは https://github.com/marp-team/marp/tree/master/packages/website で閲覧できます。コンテンツの拡充にも関わらず、GatsbyJS 採用時と比べると、コード量は およそ半減 しました。