Full Reactな静的サイトジェネレーターのGatsbyを触ってみた

前から少し気にしていた Gatsby が バージョン1 になっていたのでさわってみました。

ぎゃっつびー

なぜ?

まずReactベースというのに惹かれました。

次にこのサイトでも使っているhugoなど他の静的サイトジェネレーターは確かに静的サイトなので 表示は速いんですが、それでも画面遷移時の切り替わりが無駄なのではないかと気になって いました。
ReactなどによるSPAであれば最初に全部読み込んでしまえばあとはサクサクですが、 1ファイルに全記事詰め込むのは論外ですし、mdファイルを別に用意しておいて それを読ませるという方法もありだとは思いますが、それでもsitemap.xmlやら のメンテナンスを考えないとダメになるので運用が煩雑になるだろうなというのが 懸念点でした。

他のアプローチで、 Server Side Renderingというものもありますが、これはこれで 少し疑問があって、

  • サーバー側でレンダリングする負荷ってどんなもん?
  • レンダリング済みのオブジェクトをS3とかに配置しておいたらいいんじゃないの?

と思っていたところ、それを見事に応えてくれそうなソリューションだったのが大きいです。

やってみる

Gatsby公式のブログにちょうど Creating a Blog with Gatsby | GatsbyJS というのがあったのでそれにならってやってみました。

結構準備にかかるので、実利用時には上記ブログの最後の方で紹介されている starter を使うなりした方が良さそうです。

以下は公式ブログの手順をはしょっていますのでご了承ください。

インストール

npm install -g gatsby-cli

ブログ作成

gatsyby new myblog
cd myblog

1〜2分時間がかかりました。

基本的なプラグインのインストール

yarn add gatsby-plugin-catch-links gatsby-plugin-react-helmet gatsby-source-filesystem gatsby-transformer-remark
  • gatsyby-plugin-catch-links

    • ブログ内の別ページにリロードなしで遷移する為に必要らしい(react-routerと違うの?)
  • gatsyby-plugin-react-helmet

    • react-helmetですね、titleタグとかmetaタグとかを制御するのに便利なやつです
  • gatsyby-source-filesystem

    • どのディレクトリのファイルを原稿ファイル(mdなど)とするかを指定したりするのに必要なプラグインみたいです
  • gatsby-tarnsformer-remark

    • remarkはMarkdownをHTMLに変換してくれる便利ライブラリですね

gatsyby-config.js にプラグインと設定を書き込む

gatsby new myblog 実行時にできた gatsby-config.js を編集します。

module.exports = {
  siteMetadata: {
    title: `Gatsby Default Starter`,
  },
  plugins: [`gatsby-plugin-react-helmet`],
}

となっているのを、

module.exports = {
  siteMetadata: {
    title: `Gatsby Default Starter`,
  },
  plugins: [
    'gatsby-plugin-catch-links',
    'gatsby-plugin-react-helmet',
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        path: `${__dirname}/src/pages`,
        name: 'pages',
      },
    },
    {
      resolve: 'gatsby-transformer-remark',
      options: {
        plugins: [],
      }
    },
  ],
}

と、 plugins セクションに各プラグインに必要な設定を書き込みます。 この記述で、 src/pages 以下の Markdown を変換してくれるようです。

記事を書く

公式ブログにしたがって、 src/pages/06-09-2017-getting-started/index.md という ファイルに以下の内容を書き込みました。

---
path: "/hello-world"
date: ""2017-07-12T17:12:33.962Z""
title: "My First Gatsby Post"
---

Oooooh-weeee, my first blog post!

これで起動したあと http://localhost:8000/hello-world にアクセスすると表示できるはず。

起動

yarn develop

で、ブラウザで http://localhost:8000/hello-world にアクセスすると404... oh...
早とちりでした

(in a later step!)

テンプレートを作る

src/templates/blog-post.js を作ります。もうReactの世界ですね。とりあえずコピペしました。

import React from 'react';
import Helmet from 'react-helmet';

// import '../css/blog-post.css'; // make it pretty!

export default function Template({
  data // this prop will be injected by the GraphQL query we'll write in a bit
}) {
  const { markdownRemark: post } = data; // data.markdownRemark holds our post data
  return (
    <div className="blog-post-container">
      <Helmet title={`Your Blog Name - ${post.frontmatter.title}`} />
      <div className="blog-post">
        <h1>{post.frontmatter.title}</h1>
        <div className="blog-post-content" dangerouslySetInnerHTML={{ __html: post.html }} />
      </div>
    </div>
  );
}

GraphQL qeuryを書く

GraphQL... REST APIの代わりに今後流行るかもしれない奴という程度の認識です。ここで出てくるとは...
GatsbyとGraphQLの関わりは以下の記事がわかりやすかったです。

React, Webpack, GraphQLを用いる静的サイトジェネレーター “Gatsby” で遊ぼう | Blue Napoleon (unofficial)

src/templates/blog-posts.js に追記して以下のようにします。

import React from 'react'
import Helmet from 'react-helmet'

// import '../css/blog-post.css'; // make it pretty!

export default function Template({
  data // this prop will be injected by the GraphQL query we'll write in a bit
}) {
  const { markdownRemark: post } = data // data.markdownRemark holds our post data
  return (
    <div className="blog-post-container">
      <Helmet title={`Your Blog Name - ${post.frontmatter.title}`} />
      <div className="blog-post">
        <h1>{post.frontmatter.title}</h1>
        <div className="blog-post-content" dangerouslySetInnerHTML={{ __html: post.html }} />
      </div>
    </div>
  )
}

export const pageQuery = graphql`
  query BlogPostByPath($path: String!) {
    markdownRemark(frontmatter: { path: { eq: $path } }) {
      html
      frontmatter {
        date(formatString: "MMMM DD, YYYY")
        path
        title
      }
    }
  }
`

Node APIをかく

gatsyby-node.js というファイルをプロジェクトルートに作成します。これはGatsby起動時に 自動でパースされるそうです。

const path = require('path');

exports.createPages = ({ boundActionCreators, graphql }) => {
  const { createPage } = boundActionCreators;

  const blogPostTemplate = path.resolve(`src/templates/blog-post.js`);
    return graphql(`{
    allMarkdownRemark(
      sort: { order: DESC, fields: [frontmatter___date] }
      limit: 1000
    ) {
      edges {
        node {
          excerpt(pruneLength: 250)
          html
          id
          frontmatter {
            date
            path
            title
          }
        }
      }
    }
  }`
)
    .then(result => {
      if (result.errors) {
        return Promise.reject(result.errors);
      }
            result.data.allMarkdownRemark.edges
        .forEach(({ node }) => {
          createPage({
            path: node.frontmatter.path,
            component: blogPostTemplate,
            context: {} // additional data can be passed via context
          });
        });
    });
}

起動する

yarn develop

これで http://localhost:8000/hello-world にアクセスできるはず...!できました!!

ビルド結果の中身を見てみる

ここまでは公式ブログのまんまなのでよくって、ビルド結果の中身を見ていきます。

yarn build

publicディレクトリにビルド結果が出力されるので確認

tree public
public
├── 404
│   └── index.html
├── 404.html
├── app-5d23e5fe7018e4291a7d.js
├── app-5d23e5fe7018e4291a7d.js.map
├── build-js-styles.css
├── build-js-styles.css.map
├── chunk-manifest.json
├── commons-07cc4bd56523cc910c39.js
├── commons-07cc4bd56523cc910c39.js.map
├── component---src-layouts-index-js-9f9943b0798776268443.js
├── component---src-layouts-index-js-9f9943b0798776268443.js.map
├── component---src-pages-404-js-4503918ea3a16cfcdb75.js
├── component---src-pages-404-js-4503918ea3a16cfcdb75.js.map
├── component---src-pages-index-js-4de036b7fc4e26c86a08.js
├── component---src-pages-index-js-4de036b7fc4e26c86a08.js.map
├── component---src-pages-page-2-js-814c3250cda4a015aef7.js
├── component---src-pages-page-2-js-814c3250cda4a015aef7.js.map
├── component---src-templates-blog-post-js-6e63ce0a4c581d31d0c9.js
├── component---src-templates-blog-post-js-6e63ce0a4c581d31d0c9.js.map
├── hello-world
│   └── index.html
├── index.html
├── page-2
│   └── index.html
├── path----557518bd178906f8d58a.js
├── path----557518bd178906f8d58a.js.map
├── path---404-a0e39f21c11f6a62c5ab.js
├── path---404-a0e39f21c11f6a62c5ab.js.map
├── path---404-html-a0e39f21c11f6a62c5ab.js
├── path---404-html-a0e39f21c11f6a62c5ab.js.map
├── path---hello-world-8e4fe07e2170bccf715c.js
├── path---hello-world-8e4fe07e2170bccf715c.js.map
├── path---index-a0e39f21c11f6a62c5ab.js
├── path---index-a0e39f21c11f6a62c5ab.js.map
├── path---page-2-a0e39f21c11f6a62c5ab.js
├── path---page-2-a0e39f21c11f6a62c5ab.js.map
├── render-page.js.map
├── static
├── stats.json
└── styles.css

4 directories, 37 files

この中から記事として追加した、 hello-world/index.html を見て見ます。 minifyされているので適当に整形しているのと、スクリプトとスタイルははしょっています。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="x-ua-compatible" content="ie=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
    <link rel="preload" href="/component---src-layouts-index-js-9f9943b0798776268443.js" as="script"/>
    <link rel="preload" href="/component---src-templates-blog-post-js-6e63ce0a4c581d31d0c9.js" as="script"/>
    <link rel="preload" href="/path---hello-world-8e4fe07e2170bccf715c.js" as="script"/>
    <link rel="preload" href="/app-5d23e5fe7018e4291a7d.js" as="script"/>
    <link rel="preload" href="/commons-07cc4bd56523cc910c39.js" as="script"/>
    <script id="webpack-manifest">()</script>
    <title data-react-helmet="true">Your Blog Name - My First Gatsby Post</title>
    <meta data-react-helmet="true" name="description" content="Sample"/>
    <meta data-react-helmet="true" name="keywords" content="sample, something"/>
    <script>()</script>
    <style id="gatsby-inlined-css">
      ()
    </style>
  </head>
  <body>
    <div id="___gatsby">
      <div data-reactroot="" data-reactid="1" data-react-checksum="-1274218021">
        <!-- react-empty: 2 -->
        <div style="background:rebeccapurple;margin-bottom:1.45rem;" data-reactid="3">
          <div style="margin:0 auto;max-width:960px;padding:1.45rem 1.0875rem;" data-reactid="4">
            <h1 style="margin:0;" data-reactid="5"><a style="color:white;text-decoration:none;" href="/" data-reactid="6">Gatsby</a></h1>
          </div>
        </div>
        <div style="margin:0 auto;max-width:960px;padding:0px 1.0875rem 1.45rem;padding-top:0;" data-reactid="7">
          <div class="blog-post-container" data-reactid="8">
            <!-- react-empty: 9 -->
            <div class="blog-post" data-reactid="10">
              <h1 data-reactid="11">My First Gatsby Post</h1>
              <div class="blog-post-content" data-reactid="12">
                <p>Oooooh-weeee, my first blog post!</p>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </body>
</html>

data-reactid="1" やら、 <!-- react-empty: 2 --> やらReactを使っていると 見慣れたワードが散見されます。クライアント側での描画を省略する サーバーサイドレンダリングのレンダリング結果をあらかじめ静的ファイルとして 用意しておくという、まさに冒頭の

  • サーバー側でレンダリングする負荷ってどんなもん?
  • レンダリング済みのオブジェクトをS3とかに配置しておいたらいいんじゃないの?

という疑問を解消してくれていると思います。

なお、公式のブログの方ではこの後、 「これだけじゃブログとして機能するにはほど遠いので、タグ一覧ページを作っていきましょう」 コーナーが残っています。

Gatsby、やりよる。。。

もうちょっと触ってみて、期待通りの動きであればこのブログも移行していこうと思います。

参考リンク