Express による SSR から Eleventy(SSG) による静的サイトジェネレートに変更した話
はじめに
Enjoy SFV More のフレームのページを Express
による サーバーサイドレンダリング(SSR
) から Eleventy
という静的サイトジェネレーター(SSG
)による静的サイトジェネレートに変更しました。
※静的サイトジェネレートとは言わない・・・?
下記のような各キャラクターのフレームのページが Eleventy
を利用して生成しているページです。
今回は主に Eleventy
を解説していきたいと思います。
Enjoy SFV More については下記記事で技術解説をしているのでこちらもどうぞ。
Express による SSR
先に変更前の構成を解説していきます。
変更前は Firebase Functions
で Express
を動かし、EJS
でレンダリングを行っていました。
基本的に Enjoy SFV More はクライアントでのレンダリングのページがほとんどです。
実装当初はフレームのページもクライアントでのレンダリングをしていたのですが、AMP
対応で <link rel="amphtml" href="">
を実装する必要がありました。
今まで通りクライアントサイドで <link rel="amphtml" href="">
をレンダリングしていたのですが、どうやら Google
が認識してくれなかったので、SSR
で実装をしました。
Express
Express
は Node.js
で動く Web アプリケーションフレームワークです。
EJS
EJS
は Node.js
などで動くテンプレートエンジンです。
Embedded JavaScript
略して EJS
っぽいです(今知りました)
コード
Enjoy SFV More では下記のように SSR
を行っていました。
index.js
app.get("/frames/:characterEnName", (req, res) => { const character = characters.getCharacterByEnName(req.params.characterEnName); if (character) { res.status(200).render("frame", { characterEnName: character.enName, characterName: character.name }); } else { res.status(404); } });
まず Express
では URL からキャラクター名を元に、キャラクターのデータを取得して、テンプレートにデータを渡します。
frame.ejs
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title><%= characterName %> - フレーム表&対策メモ【ストリートファイター V (スト5, ストV)】 - Enjoy SFV More</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content="<%= characterName %>のフレーム表です。技の種類で検索ができたり、フレームの有利不利で並び替えができたり、フレームを見ながらリュウの対策のメモを取ったりすることができます。Enjoy SFV More はストリートファイター V (スト5, ストV) のラウンジを募集したり、キャラやLPやルールでラウンジを探したり、フレーム表や確定反撃(確反)を見ながら対策のメモができたりする、上達のためのサイトです。"> <link rel="amphtml" href="https://enjoy-sfv-more.com/amp/frames/<%= characterEnName %>"> </head> <body> <enjoy-sfv-more-body header-back="/frames"> <enjoy-sfv-more-frame></enjoy-sfv-more-frame> </enjoy-sfv-more-body> <script src="/src/js/index.js" async></script> </body> </html>
EJS
では受け取ったデータを元に title
や description
や amphtml
をレンダリングします。
Eleventy による静的サイトジェネレート
SSR
でも問題はないのですが表示速度と、https://web.dev
が Eleventy
を利用しているのを知りリファクタリングすることにしました。
下記はweb.dev
の Eleventy
の解説記事です。
Eleventy
Eleventy
は静的サイトジェネレーターで、特徴は
- シンプル
- 好きなクライアントサイドの
JavaScript
が使える - データによる生成
といったところです。
使用方法はインストールして実行すれば、コンフィグ無しで HTML
がジェネレートされます。
$ npm install --save-dev @11ty/eleventy $ echo '# Page header' > README.md $ npx @11ty/eleventy
サーバーを起動させたり
$ npx @11ty/eleventy --serve
ファイルの変更を監視したりできます
$ npx @11ty/eleventy --watch
Nunjucks
Nunjucks
は Mozilla
が作っているテンプレートエンジンです。
Webpack との連携
Enjoy SFV More では webpack
を利用しています。
webpack
で /src/js/index.js?hash=1fc568a7c0940a33ee7c
といったようにハッシュをつけてバンドルしています。
Eleventy
でこのハッシュ付きのバンドルされたスクリプトを読み込むためには一工夫必要です。
まず webpack-manifest-plugin
を利用してハッシュとのマッピングが記述された manifest.json
を出力します。
出力される manifest.json
は下記のようになっています。
manifest.json
{ "main.js": "/src/js/index.js?hash=1fc568a7c0940a33ee7c" }
次に、Eleventy
に用意されている Shortcodes
という機能を利用して、 manifest.json
を読み込んでハッシュ付きのスクリプトを返す処理を実装します。
.eleventy.js
const path = require("path"); const fs = require("fs"); const manifestPath = path.resolve(__dirname, "public/src/js/manifest.json"); const manifest = JSON.parse(fs.readFileSync(manifestPath, { encoding: "utf8" })); module.exports = eleventyConfig => { eleventyConfig.addShortcode("webpackAsset", name => { return manifest[name]; }); };
上記コードでは webpackAsset
という名前で Shortcodes
を定義しています。
最後に定義した Shortcodes
をテンプレートエンジンで利用しスクリプトを読み込みます。
default.njk
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>{{ character.title or title }}</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content="{{ character.description or description }}"> <body> <enjoy-sfv-more-body> {{ content | safe }} </enjoy-sfv-more-body> <script src="{% webpackAsset 'main.js' %}" async></script> </body> </html>
Nunjucks
を利用しているので {% webpackAsset 'main.js' %}
といった書き方で Shortcodes
を呼び出しています。
データを元に HTML を生成する
Eleventy
ではデータを元に HTML
を出力できます。
_data/possums.json
[ { "name":"Fluffy", "age":2 }, { "name":"Snugglepants", "age":5 }, { "name":"Lord Featherbottom", "age":4 }, { "name":"Pennywise", "age":9 } ]
possum-pages.njk
--- pagination: data: possums size: 1 alias: possum permalink: "possums/{{ possum.name | slug }}.html/" --- {{ possum.name }} is {{ possum.age }} years old
pagination.data
に指定されたデータを元にループ処理が行われて HTML
を出力します。
この例では pagination.data
に possums.json
のファイル名を指定しています。
※これは後述する Global Data Files
という仕組みを利用しているのでファイル名の指定になります
pagination.size
は指定された数値でデータをチャンクします。
pagination.alias
はループする際のデータの変数名です。
この例では possum = {
"name":"Fluffy",
"age":2
}
, possum = {
"name":"Snugglepants",
"age":5
}
... となります。
permalink
は生成される HTML
名です。
slug
フィルターは URL
で利用可能な文字列に変換されます。
{{ "My Title" | slug }} -> "my-title"
Enjoy SFV More での生成
Eleventy
では6つの方法でデータを利用できます。
- Computed Data
- Front Matter Data in a Template
- Front Matter Data in Layouts
- Template Data Files
- Directory Data Files (and ascending Parent Directories)
- Global Data Files
とても簡単に言うとテンプレートファイルに書かれたデータか、ディレクトリに置かれたデータか、そしてどのディレクトリまで有効になるかという6つになります。
最初6つ目の Global Data Files
という、どこでもデータを使える方法を利用しようとしました。
_data
ディレクトリ(デフォルトの設定)に置かれた *.json
や .js
のデータが利用できるのですが、なぜか Enjoy SFV More では利用できませんでした(ディレクトリの構成が悪いのか原因不明です)
そのため5つ目の Directory Data Files
という一部のディレクトリでデータが利用できる方法を採用しました。
Enjoy SFV More ではテンプレートファイルを views
ディレクトリに置いてます。
.eleventy.js
module.exports = () => { return { dir: { input: "views", output: "public", } }; };
そのためキャラクターのデータを views
ディレクトリに views.11tydata.js
という名前で置いてます。
views.11tydata.js
const characters = require("street-fighter-v-data/dist/data/character"); const characterMetadata = []; characters.characters.characterForEach(character => { characterMetadata.push({ title: `${character.name} - フレーム表 & 対策メモ【ストリートファイターV(ストV,スト5,SFV,SF5)】 - Enjoy SFV More`, description: `${character.name}のフレーム表です。技の種類で検索ができたり、フレームの有利不利で並び替えができたり、フレームを見ながら対策のメモを取ったりすることができます!【ストリートファイターV(ストV,スト5,SFV,SF5)】 - Enjoy SFV More`, enName: character.enName }); }); module.exports = { characterMetadata: characterMetadata };
views.11tydata.js
ではキャラクターのデータを取得して、 characterMetadata
という配列を生成します。
frame.njk
--- layout: ja/default.njk pagination: data: characterMetadata size: 1 alias: character permalink: "ja/frame/{{ character.enName | slug }}.html" --- <enjoy-sfv-more-frame></enjoy-sfv-more-frame>
フレームのテンプレートファイルでは pagination.data
に characterMetadata
を指定しています。
Directory Data Files
ではファイル名ではなく module.exports
で定義された key
の名前になります。
layout
を利用して、HTML
の雛形を継承しています。
ja/default.njk
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>{{ character.title or title }}</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content="{{ character.description or description }}"> {% if character %} <link rel="amphtml" href="https://enjoy-sfv-more.com/amp/frames/{{ character.enName }}"> {% endif %} </head> <body> <enjoy-sfv-more-body> {{ content | safe }} </enjoy-sfv-more-body> <script src="{% webpackAsset 'main.js' %}" async></script> </body> </html>
<title>{{ character.title or title }}</title>
では、 character.title
があれば表示してなければ title
を表示するようにしています。
{{ content | safe }}
に <enjoy-sfv-more-frame></enjoy-sfv-more-frame>
が代入される仕組みになっています。
これで eleventy
を実行すればキャラクター数分 HTML
が生成されます。
この ja/default.njk
は他のテンプレートでも利用をしています。
index.njk
--- layout: ja/default.njk title: Enjoy SFV More - ストリートファイターV(ストV,スト5,SFV,SF5)をより楽しむためのサイト description: Enjoy SFV More はストリートファイターV(ストV,スト5,SFV,SF5)をより楽しむため、ラウンジの募集や検索、コンボの作成や検索、配信の検索などができるサイトです! --- <enjoy-sfv-more-top></enjoy-sfv-more-top>
例えばトップページではテンプレートファイル内で、title
や description
を定義しています。
Firebase Hosting との連携
連携というほど何かしているわけではないのですが、Firebase Hosting
の rewrite
では動的に source
と destination
をマッピングができません。
どういうことかというと、https://enjoy-sfv-more.com/frames/dhalsim という URL
で public/frame/dhalsim.html
を表示するのを変数など使って簡単に書くことができません。
firebase.json
"rewrites": [ { "source": "/frames/:name", "destination": "/ja/frame/:name.html" } ]
上記のようなパスを変数名に定義して、動的に HTML
を表示することができません😭
そのため Enjoy SFV More では40体全ての URL
を下記のように書いています。
firebase.json
"rewrites": [ { "source": "/frames/ryu", "destination": "/ja/frame/ryu.html" }, { "source": "/frames/chunli", "destination": "/ja/frame/chunli.html" }, { "source": "/frames/nash", "destination": "/ja/frame/nash.html" } ]
まとめ
Global Data Files
が何故か動かなかった問題さえ除けば、公式サイトを見ながらスムーズに実装ができました!
もっとゴリゴリレンダリングできるのですが、body
内は Web Componets
のレンダリングに任せようと思います(昨今のテンプレートエンジンのつらみが伺えるので)