アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

ブログを WordPress + さくらのVPS から GatsbyJS + Netlify へ移行

January 11, 2019開発GatsbyJS, Netlify

ブログを WordPress + さくらのVPS から GatsbyJS + Netlify へ移行した。その作業メモをまとめる。

移行補助ツール

WordPress から GatsbyJS へ移行する際に使用した補助ツールについて。

wpxml2md

WordPress の記事を GatsbyJS 用に Markdown 化するためのツール。

これは WordPress の管理画面から出力した XML を解析して、記事単位の Markdown ファイルに変換するというもの。当初は変換ぐらいしか実装していなかったが、移行にともない機能不足を感じたので以下を追加した。

  • メタデータ埋め込み

    • 記事 XML からタイトルや日付を解析
    • 記事ファイル冒頭へ gatsby-transformer-remark 用の形式で出力
  • 記事から参照されている画像の自動ダウンロードと相対リンク変換

    • 画像は記事ファイルと同じフォルダーに出力
    • リンクもそこに対するパスとなる
  • 記事ファイル末尾へのコメント埋め込み

    • 記事 XML からコメントやトラックバックを解析
    • 記事の末尾にリストとして埋め込む
  • リンクのプレフィックス変換

    • 記事間のリンクを相対にするための機能
    • 例えば http://akabeko.me/blog//blog/ にするなど

これらを経ても Markdown 変換のバグとか GatsbyJS の知識不足ゆえ、かなり手動で直したファイルも多い。しかしそれでも全て手動だったら数ヶ月は掛かっていただろう変換作業が一週間ぐらいで済んだからツールを作ってよかった。

icon-gen

このツールは SVG または PNG から各種アイコンを生成するもの。

機能のうち GatsbyJS でも利用する Favicon 生成は所定の画像サイズ構成としていた。しかしサイトのデスクトップ化で使用する gatsby-plugin-manifest の求めるサイズが不足していたため、任意の構成で PNG 出力する機能を追加。

この目的で手を付けたのだけど CLI 引数の設計が失敗してて新モード追加が辛い。かつ状態依存がほとんどないのにクラス中心だったので大幅に手を入れ v2.0.0 とした。結果、よくはなったものの GatsbyJS 移行はかなり遅れることに。元の計画では 2018 秋にはそうしたかったのだけど 2019 年明けになってしまった。

GatsbyJS によるサイト構築

当初は Starter Library からブログ向けで高機能なものをベースにカスタマイズする予定だった。しかし GatsbyJS に入門した時期が v1.0 前夜で破壊的な変更により動作しないものがあるなどして、ならば Starter は参考に留めて積み上げ式にフルスクラッチしたほうがよいと判断した。

本ブログのプロジェクトは以下に公開している。以降の内容はこれと過去 issue などを併読すると理解しやすいはず。

プロジェクト構成

基本的なプロジェクト構成をまとめる。GatsbyJS v2 系を想定。

.
├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-node.js
├── gatsby-ssr.js
├── package.json
├── public/
└── src/
    ├── components/
    ├── pages/
    ├── scss/
    ├── static/
    └── templates/

各種フォルダーとファイルの内訳。

名前 内容
gatsby-browser.js サイトが Web ブラウザーで実行された時の動作を定義するファイル。詳細は Browser APIs を参照のこと。
gatsby-config.js サイトのメタデータや GatsbyJS プラグインの設定を定義するファイル。詳細は Gatsby Config を参照のこと。
gatsby-node.js ビルド (サイト構築) 時の処理を定義するファイル。詳細は Node APIs を参照のこと。
gatsby-ssr.js SSR (Server Side Rendering) 処理を定義するファイル。詳細は SSR APIs を参照のこと。
package.json プロジェクト構成ファイル。依存モジュールや開発用のタスク処理などを定義。
public/ サイトとして公開されるフォルダー。GatsbyJS でビルドを実行すると生成される。
src/ 開発用リソースを格納するフォルダー。
src/components/ ページを構成するパーツとなるコンポーネント置き場。
src/pages/ サイトとして表示するページのリソース置き場。Markdown や JavaScript ファイルで構成される。
src/scss/ CSS の元となる SCSS ファイル置き場。
src/static/ 静的リソース置き場。アイコン フォントや Favicon など固定的なものが格納される。
src/templates/ ページのレイアウトを定義するテンプレート置き場。components/ に定義したものはテンプレート内で組み合わせられてページとなる。

ページ定義 - JavaScript

GatsbyJS のページ定義は src/pages 内でおこない、方法は index.js と Markdown に大別される。index.js は配置された階層がそのままサイトの URL に対応する。定義は React コンポーネントと GraphQL を利用。以下は 404 ページの例。

import React from 'react'
import PropTypes from 'prop-types'
import { graphql } from 'gatsby'
import Header from '../components/header.js'

const NotFoundPage = ({ data }) => (
  <div className="page">
    <div className="container">
      <Header
        siteTitle={data.site.siteMetadata.title}
        siteSubTitle={data.site.siteMetadata.subtitle}
      />
      <div className="content">
        <h1>NOT FOUND</h1>
        <p>You just hit a route that doesn&#39;t exist... the sadness.</p>
      </div>
    </div>
  </div>
)

NotFoundPage.propTypes = {
  data: PropTypes.object
}

export default NotFoundPage

export const query = graphql`
  query NotFoundPageQuery {
    site {
      siteMetadata {
        title
        subtitle
      }
    }
  }
`

query として export された GraphQL を GatsbyJS が解析、要求に応じたデータを React コンポーネントの props として渡す流れになる。GraphQL の大本は gatsby-config.js の定義と gatsby-node.js による処理で決定。

コンポーネントはクラスにしてもよいが、静的サイト ジェネレーターなら静的ゆえに SFC (Stateless Functional Component) としておいたほうが混乱しにくいだろう。更に単純な式として書けば見た目もほぼ HTML となる。ページの描画に必要なデータは props のみで受け取てそのままテンプレートへ流し込むだけ、というのがわかりやすくて好ましい。

ページ定義 - Markdown

Markdown は gatsby-transformer-remark でページに変換する。関連プラグインも含めた設定は以下。

module.exports = {
  plugins: [{
   resolve: 'gatsby-transformer-remark',
   options: {
     plugins: [
       'gatsby-remark-embed-video',
       'gatsby-remark-responsive-iframe',
       'gatsby-remark-prismjs',
       'gatsby-remark-copy-images',
       {
         resolve: 'gatsby-remark-external-links',
         options: {
           rel: 'noopener',
           target: '_blank'
         }
       },
       {
         resolve: 'gatsby-remark-custom-blocks',
         options: {
           blocks: {
             note: { classes: 'note' },
             important: { classes: 'note-important' },
             tip: { classes: 'note-tip' },
             warning: { classes: 'note-warning' },
             help: { classes: 'note-help' }
           }
         }
       }
     ]
   }
 }]
}

Markdown について何か処理したければ gatsby-remark-XXXX を探すとよい。これらを gatsby-transformer-remarkoptions: plugins: [] へ指定すれば Markdown 処理へ反映。私の利用しているものをまとめる。

npm 役割
gatsby-remark-embed-video YouTube や Vimeo などの動画をページに埋め込める。
gatsby-remark-responsive-iframe ↑の動画のように外部コンテンツを <iframe> としてページ埋め込みした時のサイズをページ幅へあわせて適切に調整してくれる。
gatsby-remark-prismjs コード ブロックを Prism で構文強調してくれる。私としては highlight.js にしてほしかったのだが、#1077 によれば、そういうプラグインを誰かが作ればいい (2019/1 時点では存在しないようだ) とのこと。
gatsby-remark-copy-images Markdown から参照している画像をビルド時に public/ へコピーしてくれる。
gatsby-remark-external-links サイト外へのリンクに対して rel="noopener"target="_blank" などを設定してくれる。
gatsby-remark-custom-blocks 特別な記法で書かれた部分を任意の class がついた <div> に変換してくれる。Markdown にないマークアップ、例えば注釈や警告を独自の外観で表示したい、といった用途によい。

Markdown によるページのメタデータはファイルの先頭へ定義する。書式は YAML。

---
path: "/blog/2019/01/gatsbyjs-netlify/"
date: "2019-01-10"
title: "ブログを WordPress + さくらのVPS から GatsbyJS + Netlify へ移行"
categories: ["開発"]
tags: ["GatsbyJS", "Netlify"]
excerpt: "ブログを WordPress + さくらのVPS から GatsbyJS + Netlify へ移行したので、そのメモ。"
---

これは gatsby-node.js から graphql 関数で取得可能。定義名はそのまま JavaScript オブジェクトのプロパティー名に対応する。これは任意の定義を追加することも可能。私の利用しているものをまとめる。

内容
path String ページの URL。ドメインからの相対となる。
date String ページ作成日時。UTC になる。時刻まで含めるなら 2009-10-03T13:06:09Z のように定義。
title String ページのタイトル。
categories String[] ページの属するカテゴリー配列。WordPress のそれと対応。カテゴリーの記事リスト ページ生成に利用。
tags String[] ページ内容を示すタグ配列。WordPress のそれと対応。タグ単位の記事リスト ページ生成に利用。
excerpt String ページ内容の要約。記事一覧に表示するための短文用。
single Boolean ページがブログ記事か単体ページかを識別するためのフラグ。WordPress のそれと対応。ページに前後記事のナビゲーションを設定するか?などの判定に利用。

WordPress ではメタデータを DB で管理していたが、GatsbyJS + Markdown はファイル単体でメタデータと本文を一元管理する。この方式は Hugo などでも採用されているそうなので、定義しておけば静的サイト ジェネレーター間の移行にも役立つだろう。

CSS

GatsbyJS では CSS in JS や CSS Modules も利用可能だが、今回は SCSS ファイルからビルドする方式を採用。gatsby-plugin-sass と関連 npm をインストールして gatsby-config.js から有効にすれば SCSS (Sass) を利用可能となる。

module.exports = {
  plugins: [
   'gatsby-plugin-sass'
 ]
}

機能ごとにファイル分割してエントリー ポイント index.scss でまとめて読み込む。normalize.css も含めた定義をまとめる。

@import "../../node_modules/normalize.css/normalize.css";
@import "./base.scss";
@import "./header.scss";
@import "./footer.scss";
@import "./content.scss";
@import "./location.scss";
@import "./top-menu.scss";
@import "./categories.scss";
@import "./tags.scss";
@import "./custom-block.scss";
@import "./plugin.scss";

IcoMoon で生成したアイコン フォントと CSS は src/static へ配置。これも SCSS に定義する予定だったが、アイコン フォント ファイルへのパス参照などで問題があってやめた。src/static は最終的なサイトのルート ディレクトリーに対応するため、SCSS に Bundle できない (または難しい) ものを管理するのに適している。

GatsbyJS で Markdown のコードブロックを構文強調する標準プラグインとして gatsby-remark-prismjs が提供されている。これのテーマ (外観) を変更するためには対応する CSS を読み込む必要がある。

これら全てを含めた gatsby-browser.js の定義は以下。

require('prismjs/themes/prism-okaidia.css')
require('./src/scss/index.scss')
require('./src/static/icon.css')

GatsbyJS における CSS 適用のルールがよくわからなくて、はじめはテンプレートから読み込んでいた。この方法だとテンプレートが対象となるため、ページを index.js で構築されている場合は CSS が適用されない。

ただし index.js からリンクしている Markdown ページのテンプレートに CSS が適用されていると、Web ブラウザーからそれをマウス オーバーした際に GatsbyJS のプリ ロードのためか CSS が遅延適用される。

この問題 (仕様) は #5100 で気づいた。Creating global styles にもグローバルな CSS は gatsby-browser.js で読むとあるため、そのようにした。この方法でも SCSS ファイルは適切に処理されるし gatsby develop 時の自動ビルドと HMR も効くので問題もない。

編集スタイル

Node + npm がインストールされている環境を想定して以下の npm-scripts を組んだ。

{
  "scripts": {
    "start": "npm run develop",
    "release": "npm run build",
    "build": "gatsby build",
    "develop": "gatsby develop --open",
    "serve": "gatsby serve --open",
    "format": "prettier --write 'src/**/*.js'"
  }
}

編集手順をまとめる。

  1. src/pages/ 以下の Markdown ファイルをエディターで開く
  2. ターミナル (Terminal や PowerShell) でプロジェクトのフォルダーに移動
  3. npm start で自動ビルドと Web ブラウザーによるリアルタイム プレビュー開始
  4. 停止は Ctrl + C

エディターは Markdown に対応しているものならお好みで。私は開発には vscode を利用しているのだけど #45629 の問題が致命的すぎるため Markdown だけは Atom で書いている。GatsbyJS のプレビューは HMR (Hot Module Replacement) が効くため非常に快適。

本番サイトに公開する前の動作確認は以下のようにする。

  1. ターミナルでプロジェクトのフォルダーに移動
  2. npm run build でサイトをビルド、エラーや警告が出たら修正すること
  3. npm run serve で本番サイトを Web ブラウザーでプレビュー開始
  4. 停止は Ctrl + C

これで特に問題なければ公開する。

Netlify

GatsbyJS で構築したサイトを配信する手段としては Netlify や GitHub Pages がよく利用されているようだ。以前は独自ドメインで HTTPS 運用するために Netlify を選ぶ感じだったが 2018/2 に GitHub Pages も Let's Encrypt 対応したので、こちらもよさそう。しかし今回は GatsbyJS 連携の知見が蓄積されていそうな Netlify にしておく。

GatsbyJS と Netlify 連携は実に簡単。ただ公開するだけなら

  1. Netlify に GitHub アカウントで Sing Up
  2. Netlify プロジェクトとして GatsbyJS サイトの GitHub リポジトリーを指定
  3. サイト名やビルド コマンドを指定してデプロイ

で済む。ビルド コマンドは GatsbyJS であることを自動判定しているのか、初期状態では gatsby build が設定されていた。これでも動作するけど npm-scripts に定義した npm run build へ変更。こうしておけば複数コマンドを実行したくなったら npm-run-all を噛ませるなど、処理を拡張しやすくなる。

Netlify による配信は netlify.com のサブ ドメインを利用できて、例えば akabeko.netlify.com のようになる。独自ドメインで HTTPS にするのも簡単。私は以下の記事を参考に設定した。

通常はこれで作業完了。あとは GatsbyJS プロジェクトを編集して GitHub の master ブランチへ push された都度、自動的にデプロイが走る。...となるだろうけど、いくつか追加設定やトラブルに遭遇したのでそれらについて記録しておく。

常時 HTTPS 化

サイトに HTTP でアクセスされても HTTPS へリダイレクトする設定。Netlify は _redirects というファイルをサイトのルートに配置することでリダイレクトを実行してくれる。

GatsbyJS なら src/static にファイルを配置しておけばビルド時に public/、つまりサイトのルートへコピーしてくれるので、そうしておく。私のサイトでは _redirects を以下のように定義。

http://akabeko.netlify.com/*  https://akabeko.me/:splat  301!
http://akabeko.me/*  https://akabeko.me/:splat  301!

Netlify のサブ ドメインとして割り当てられたものと HTTP へのアクセスを HTTPS 側へリダイレクトしている。301 なので恒久的な転送となる。Google もこれを拾ってくれるので、過去に HTTP 側で検索結果に表示されていたものも私のサイトだと 2 時間ぐらいで HTTPS 側に切り替わっていた。

URL 禁則文字

はじめてのデプロイで以下のエラーが発生した。

1:09:36 PM: Failing build: Failed to deploy site
1:09:36 PM: failed during stage 'deploying site': Invalid filename 'blog/tag/C#/index.html'. Deployed filenames cannot contain # or ? characters

私のサイトでは記事のタグ単位で一覧ページを出力している。それらのうち C## が問題らしい。GatsbyJS としてはメタデータを URL へ使用する際に Percent Encoding するようだが # は対象外のようだ。推測になるけどエンコードに encodeURI を使用しているのかもしれない。

このタグがなくても意味の通じる記事群だったので短絡的だがタグを消すことで対応した。なお GatsbyJS 的にはエラーとならないようで、タグ ページも生成されるのだがリンクを解決できない (# がそのまま Hash になる) ため 404 になる。

混在コンテンツ警告

無事にサイトを公開できたが、デプロイのログを見ると以下のような警告が記録されていた。

2:03:28 PM: Mixed content detected in: /blog/2013/06/develop-wordpress-theme/index.html
2:03:28 PM: --> insecure img urls:
2:03:28 PM:   - http://ax.phobos.apple.com.edgesuite.net/ja_jp/images/web/linkmaker/badge_macappstore-sm.gif
2:03:28 PM:   - http://ax.phobos.apple.com.edgesuite.net/ja_jp/images/web/linkmaker/badge_appstore-sm.gif

これらは過去に Mac App Store のアフィリエイト用に表示していた Apple 公式のストア リンク画像である。画像自体は 2019/1 現在も存在しているが

  • Mac App Store のアフィリエイトは既に終了している
  • 画像の HTTPS は提供されていなさそう

なことからリンクを削除することで対応。

まとめ

WordPress に比べてブログ編集が大幅に快適となった。プレーン テキスト ファイル形式で管理する強みとしてエディターによる強力な grep や置換を利用できるのがよい。wpxml2md のバグで変換が漏れたり崩れた部分とか、パスなんかの修正も正規表現で簡単に修正できた。せっかく Markdown 化したのだから、そのうち textlint/textlint による校正も試したい。

WordPress からの移行で損なわれるものとしてはコメント機能が大きい。私のブログはプログラミングを扱うことが多いこともあり、技術的な質問や指摘コメントで有益なものも結構ある。静的サイト ジェネレーターなサイトでは Disqus を設置しているのを見かけるけど、国内のサイトだとコメントされることが滅多にない感じなので見送り。意図的にやめたものとしては SNS ボタンがある。かつては Twitter、はてブ、Facebook の言及数を気にしたものだが現在はどうでもよくなった。

環境構築こそ手こずったけどブログ以外のサイトでも有用そうでよい経験だった。職場のホーム ページとかに採用するかも。いちど環境構築してしまえば Markdown の知識程度でサイトを簡単に更新してゆけるため開発者でない人にも案内しやすいだろう。