December 31, 2018
はてなブログで書き続けていた個人ブログを Gatsby に移行して、Google Domains で取得した独自ドメインを使って Firebase Hosting 上で配信している。移行そのものは 11 月には完了していたが、肝心のブログ更新が止まってしまった。せっかくなので、ブログを移行するに至ったモチベーションや、移行に際して考えたことや実行したもろもろをまとめてみようと思う。
はてなブログをそのまま使い続けるという選択肢もあった。はてな記法という便利フォーマットがあるし、デザインのテンプレートも多いし。ではなぜはてなブログを捨てたかというと、ちょっとしたマンネリ感とか、個人的なアウトプットのネタとして…みたいな些細なものだ。syntax highlight がイマイチなところとか、サイトの表示がなんとなく重いとか、イマイチだった点を挙げようと思えば出てくるけれど、本筋ではないのでここでは省略する。
他社のブログサービスを使わず、わざわざ自分でサイトを作って運用していく以上、配信するサーバのライブラリアップデートとかスケール対応とかセキュリティ対策や HTTPS 対応とか、余計なところで消耗したくなかった。また日々自分がさわっていくプロダクトであるので、使用する技術スタックに愛着を持てるかという点もモチベーション的な意味で前者と同じかそれ以上に重要と考えた。
冒頭の通り Gatsby でサイトを生成し、Firebase Hosting 上にデプロイしてサイトを配信しているのだが、これとは別に Netlify を利用して本番公開前のサイトを事前にチェックしている。ここからは、これらの技術選定に至った背景や調べたことをまとめてみる。
gatsbyjs/gatsby: Build blazing fast, modern apps and websites with React
いわゆる静的サイトジェネレーターのひとつ。 GraphQL を利用して Markdown で書かれたプレーンテキストや WordPress や Drupal など外部の CMS といったデータソースにアクセスし、取得したデータをもとにサイト全体をひとつの React アプリケーションとして生成する。 PRPL パターンを踏襲した設計思想を採用しており、特別なことをしなくても爆速なサイトが容易に作れるのが大きな特徴。
Gatsby のほかにも同じ静的サイトジェネレーターの Hugo や Hexo も候補に入れていたが採用を見送った。所感は以下の通り。
{{ hogehoge }}
みたいなテンプレートの記法がなじめず導入をやめたFirebase Hosting | 高速で安全なウェブ ホスティング | Firebase
Google が提供する mBaaS である Firebase で利用できるフルマネージドホスティングサービス。 HTTPS 対応や CDN 経由による静的アセットの配信がデフォルトで対応済。もちろん独自ドメインの利用も OK だし、任意のデプロイ時点へのロールバックだってできる。しかもアセットの配信量が著しく増えないかぎり基本無料なのもうれしい。
Firebase Hosting と同じくフルマネージドホスティングサービスの一種。GitHub リポジトリへの push などをトリガーにした Netlify 上でのビルドおよびデプロイ、ホスティングがほぼ全自動で行えるのが特徴。特筆すべきはブランチごとにサイトをデプロイしてくれる機能が用意されていて、たとえば「Pull Request を作成したらレビュー環境が立ち上がって、よしなにブラウザで確認できる」という一連の体験がシュッとできあがるのが最高にすばらしい。残念ながら Firebase Hosting にはこうした便利機能が用意されていないため、「ブランチ切って作成した記事を Netlify にデプロイされたプレビュー環境上で確認して、問題なければ master にマージして Firebase Hosting へデプロイする」という運用フローを採用している。
実際にブランチ切って作業した内容はもう少しあった気がするけれど、だいたいこんなことをやってたらブログサイトができた。
ブログ用のスターターキットも用意されていたけど、まずはミニマムな構成ではじめたかったので、gatsby new
を叩いて gatsby-starter-default で生成される素朴なテンプレートを使用した。先述の通りテンプレートエンジンに React を使用しているのだけど吐き出されたファイルが JavaScript(ES2015) だったので「React 使うのに TypeScript で書かない理由があるだろうか…」と思い、サクッと TypeScript 化。Gatsby 公式で型定義ファイルを用意してくれていて非常に助かる。
デザインは CSS フレームワークの Bulma および Bloomer を採用した。Bulma を使えばなにもしなくてもレスポンシブ対応が完了するのと、Bulma をもとに作成された Bloomer は Atomic Design でいう Atom や Molecule といった粒度で使えるコンポーネントが用意されているので、これらを自分たちで組み合わせていくとそれっぽいデザインが手間なく完成する。実際に Bloomer で組み合わせたコンポーネントは Organism くらいの粒度で管理している(たとえばヘッダーやフッター、あとは後述する OGP 部分とか)。
わりと頭を悩ませたのがここ。まず、はてなブログ標準で提供されているのエクスポート機能では出力時のフォーマットが Movable Type 形式のテキストファイルに固定される。自分がほしいのは Markdown 形式のテキストファイルなので困った。なにか手段はあるはずだと探してみると、はてなブログは開発者向けに AtomPub API が提供されているのだが、いまいち使い勝手がよいとは言い難かった。これは万策尽きたか…と思いかけたが、motemen/blogsync というはてなブログ用の CLI ツールですべてが解決した。ブログ記事の本文が Markdown 形式でエクスポートできるのはもちろん、記事のタイトルや投稿日時、記事に付与したタグやカテゴリといったメタデータもまとめて取得できる。とにかく便利なので、気になったら作者の記事を参照してもらうといいはず。
先述の通り本番への公開前は GitHub と連携した Netlify に自動デプロイされたプレビュー環境上でサイトの見た目を確認できるので説明は割愛して、ここでは CircleCI による CI 環境の構築および Firebase Hosting へのデプロイについて触れていく。
…と書いてみたものの、だんだんと説明が面倒くさくなったので、実際に CircleCI で使用している config.yml
をそのまま貼り付けてみる。
ポイントは以下の通り。
persist_to_workspace
および attach_workspace
を利用して、build
ステップで生成した public/
以下のファイルを deploy
ステップに横流しして、そのままデプロイできるようにしているversion: 2
default_settings: &default_settings
docker:
- image: circleci/node:10.15.0-stretch
working_directory: ~/repo
jobs:
build:
<<: *default_settings
steps:
- checkout
- restore_cache:
keys:
- yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
- yarn-packages-v1-{{ .Branch }}-
- yarn-packages-v1-
- run: yarn install
- save_cache:
key: yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
- run: yarn lint
- run: yarn typecheck
- run: yarn build
- persist_to_workspace:
root: ~/repo
paths:
- public/
- node_modules/
- package.json
- firebase.json
deploy:
<<: *default_settings
steps:
- attach_workspace:
at: ~/repo
- run:
name: deploy to firebase
command: $(yarn bin)/firebase deploy --only hosting --token $FIREBASE_DEPLOY_TOKEN --project $FIREBASE_PROJECT
workflows:
version: 2
build_and_deploy:
jobs:
- build
- deploy:
requires:
- build
filters:
branches:
only: master
ここまでやればブログサイトの公開に必要な準備はひととおり済んだことになるが、公開した記事を SNS でシェアする際には OGP 対応をしておくと見た目が少しだけよくなるので、あわせてやってみた。とはいえ React Helmet を使ってメタタグを生成するだけなのだが、強いて言うなら個々のブログ記事ページと一覧ページや 404 ページとで微妙に内容を出し分ける工夫を加えたのがポイントで、先述の Ogp
コンポーネントを用意して、個々のページで読み込ませている。
やはり説明が面倒くさくなってきたのでコードを雑に貼り付ける。
// src/components/organisms/ogp.tsx
import * as React from 'react';
import Helmet from 'react-helmet';
import { siteMetadata } from '../../../gatsby-config';
const defaultProps = {
isRoot: false,
title: '',
path: '',
description: '',
};
type Props = Partial<typeof defaultProps>;
export const Ogp: React.SFC<Props> = ({ isRoot, title, path, description }) => {
const { title: siteTitle, siteUrl } = siteMetadata;
return (
<Helmet
title={title}
meta={[
{
property: 'description',
content: description || 'something awesome',
},
{
property: 'og:title',
content: title ? `${title} - ${siteTitle}` : siteTitle,
},
{ property: 'og:type', content: isRoot ? 'website' : 'article' },
{
property: 'og:url',
content: `${path ? `${siteUrl.concat(path)}` : siteUrl}`,
},
{
property: 'og:image',
content:
'https://www.gravatar.com/avatar/544edf5a0f3541a800f0b2911a3176df.jpg?size=400',
},
{
property: 'og:description',
content: description || 'something awesome',
},
{ property: 'twitter:card', content: 'summary' },
{ property: 'twitter:site', content: '@cheezenaan' },
]}
/>
);
};
// src/pages/index.tsx
import { Box, Container, Content, Heading, Section, Subtitle } from 'bloomer';
import { graphql, Link } from 'gatsby';
import * as React from 'react';
import { Ogp } from '../components/organisms/ogp';
import { Layout } from '../components/templates/layout';
// (snip)
const IndexPage: React.SFC<Props> = ({ data }) => {
const { posts } = data.allMarkdownRemark;
const filteredPosts = posts.filter(
({ post }) => post.frontmatter.title.length > 0
);
return (
<Layout isRoot>
<Ogp isRoot />
<Section>{/* snip */}</Section>
</Layout>
);
};
export default IndexPage;
// (snip)
// src/components/pages/blog-post.tsx
import { Container, Content, Heading, Section, Title } from 'bloomer';
import { graphql, Link } from 'gatsby';
import * as React from 'react';
import { Ogp } from '../organisms/ogp';
import { Layout } from '../templates/layout';
// (snip)
export const BlogPost: React.SFC<Props> = ({ data, pageContext }) => {
const { markdownRemark: post } = data;
const { prev, next } = pageContext;
return (
<Layout>
<Ogp
title={post.frontmatter.title}
path={post.frontmatter.path}
description={post.excerpt}
/>
<Section>{/* snip */}</Section>
</Layout>
);
};
export default BlogPost;
// (snip)
日常業務の合間を縫いながらかれこれ 1 ヶ月弱でブログ移行を進めてきたけど、おおむね満足できるものができたんじゃないかと思っている。
Gatsby は React や Webpack など Node.js ベースのライブラリを多く採用しているので比較的キャッチアップしやすく、プラグインも豊富に用意されていたのでやりたいことはだいたいすべてラクに実現できた。サイト配信に使用した Firebase Hosting や Netlify もこうした静的サイトジェネレーターとの相性がだいぶよく、こちらで何かしら設定をすることなくシュッと環境が立ち上がるのでとても体験がよかった。
また先に触れた OGP 対応のほかにも、前後記事へのナビゲーション追加などサイトへの細かいカスタマイズも入れているが、記事の検索やタグ・カテゴリの追加、あとは通称 About me ページの追加など手付かずな部分が多く残っている。まぁこの先に向けてのお楽しみとして残しておくのも悪くない。
まだまだ書き足りないこともありそうだけど、いったんこのあたりで区切ることにしよう。