Help us understand the problem. What is going on with this article?

GatsbyでGoogle Lighthouseで満点を取るブログを一から作る

概要

image.png

Gatsbyは、React.jsを使ったStatic Site Generator(SSG、静的サイトジェネレーター)で、とても早いウェブサイトを作れる技術です。静的なサイトだけでなく、CSRやSSRサイトも作れますが、この記事では静的サイトに集中しておきたいと思います。Gatsbyが使用するGraphQLについても扱います。

Google Lighthouse Score

image.png
学生時代には取れなかったオール100点

完成品はこちら
コードはこちら

対象読者

  • Gatsbyがどう動くか知りたい方
  • Reactの基本的知識がある方
  • ブログをScratchで作りたい方

目標

  • Gatsbyの構造を知ることによって、色んなライブラリーを応用できるようになる

注意事項

  • 完成品の基本的構造にふれるだけなので、上記の例と全く同じものが作られる記事ではありません。
  • Gatsbyのことがよくわかってて、開発時間を下げたい方はGatsby Starterを使用することをおすすめします。
npm i -g gatsby-cli
gatsby new [project-name] [starter]

目次

  1. インストールと環境設定
  2. レイアウトを作る
  3. GraphQLでQueryを使う
  4. Markdownファイルを扱う
  5. Postページを生成する
  6. タグを生成する
  7. Paginationを行う
  8. イメージを使う
  9. スタイリング
  10. SEO、PWA設定
  11. デプロイする
  12. 最後に

インストールと環境設定

GatsbyをScratchから作るので、空のfolderからスタートします。
Gatsbyには大きく3種類のpluginが存在します。

  • Transformer Plugin
    • JSONやMarkdownといった、そのままでは使えないデータを、GraphQLでqueryできる形に変換してくれます。ここでは、Markdownをhtmlに変換してくれるgatsby-transformer-remarkと、MDXをhtmlに変換してくれるgatsby-plugin-mdxを使います。
  • Functional Plugin
    • TypeScriptサポートやPWA設定など、機能的な要素を追加してくれます。ここではリンクをリロードなしに開いてくれることでSPAの感覚を与えてくれるgatsby-plugin-catch-links等を使います。
  • Source Plugin
    • Gatsbyのデータシステムの核心であるnodesを作ってくれます。例えばgatsby-source-filesystemはディスクからファイルをロードして、Transformer pluginがデータを変換できるようにします。ここでは使用しませんが、wordpressやcontentfulなどのCMSからnodesを作成することもできます(gatsby-source-wordpress, gatsby-source-contentful)

まず、npm(またはyarn)を使ってdependenciesをインストールしていきます。

mkdir [project-name]
cd [project-name]
npm init -y
npm i react react-dom gatsby gatsby-source-filesystem gatsby-transformer-remark gatsby-plugin-catch-links

そうしたら、gatsby-config.jsを作成し、
自分が作りたいサイトのmeta情報を書き込みましょう。

gatsby-config.js
module.exports = {
  siteMetadata: {
    title:'My Wonderful Website',
    description: 'Welcome to your brilliant website.',
    author: 'Super Cool Developer'
  },
}

レイアウトを作る

レイアウトは私達が作るblogの土台になる部分です。
例えばすべてのページに共通して現れるHeader, Footer, Sidebar等が該当します。

src/layouts/index.jsx
import React from 'react'

export default ({ children }) => (
  <div>
    <h3>My Wonderful Website Layout</h3>
    {children}
  </div>
)

そしてホームの画面とAboutの画面を作っていきます。
Gatsbyのsrc/pagesの中のファイルは、各々一つのページとして作成されます。
例えば、src/pages/about.jsxというファイルがあると、/aboutが作られます。

src/pages/index.jsx
import React from 'react'
import Layout from '../layouts'

export default () => (
  <Layout>
    <h1>My Wonderful Website Home Page</h1>
    <p>This is the home page</p>
  </Layout>
)
src/pages/about.jsx
import React from 'react'
import Layout from '../layouts'

export default () => (
  <Layout>
    <h1>My Wonderful Website About</h1>
    <p>This is the about page.</p>
  </Layout>
)

ここで、gatsby developコマンドを実行してみましょう。
http://localhost:8000/
これだと寂しいので、先程作ったLayoutにリンクを足してみましょう。

src/layouts/index.jsx
import React from 'react'
import { Link } from 'gatsby'

export default ({ children }) => (
  <div>
    <Link to="/">
      <h3>My Layout</h3>
    </Link>
    <Link to="/about">
      About
    </Link>
    {children}
  </div>
)

Aboutページにも移動できました。
試しにhttp://localhost:8000/not-foundなど、存在しないページに移動すると、
image.png

このようにdevelopモードでは存在するページの一覧を表示してくれます。

GraphQLでqueryを使う

Gatsbyはデータをfetchする際にGraphQLを使用します。
GraphQLはデータにアクセスするためのQuery言語です。他のQuery言語の例としてはSQLがあります。
GraphQLは自分がほしいデータのみをもらうことができます。

GraphiQLを使う

まずはGraphiQLというツールを使ってqueryを実行してみましょう。

http://localhost:8000/__graphqlに移動してみてください。

image.png

GraphiQLのplaygroundが出てきます。
Explorerをクリックしたら、fetchできるqueryの情報が出てきます。
Docsをクリックしたら、queryに必要なInputの情報や返ってくるデータの情報が見れます。

ここで私達が先程設定したサイトのデータをfetchしてみましょう。

{
  site {
    siteMetadata {
      title
      description
      author
    }
  }
}

そうしたら、下の結果が見れるはずです。

{
  "data": {
    "site": {
      "siteMetadata": {
        "title": "My Wonderful Website",
        "description": "Welcome to your brilliant website.",
        "author": "Super Cool Developer"
      }
    }
  }
}

コンポーネント内でGraphQLを使う

現在homeとaboutには各々サイトのタイトル、My Wonderful Websiteが入ってますが、
この書き方だとタイトルを修正したい時、毎回両方のページを修正する必要があります。
なので、gatsby-config.jsに書かれている情報をそのまま持ってきて使えるようにしましょう。

Page Query

src/pages内のコンポーネントでは、以下のようにqueryを実行します。

graphql`
{クエリ}
`

違和感を感じるかもしれませんが、ちゃんとしたJavaScriptです。

src/pages/about.jsx
import React from 'react'
import { graphql } from 'gatsby'
import Layout from '../layouts'

export default ({ data }) => (
  <Layout>
    <h1>{data.site.siteMetadata.title} About</h1>
    <p>This is the about page.</p>
  </Layout>
)

export const query = graphql`
  query AboutQuery {
    site {
      siteMetadata {
        title
      }
    }
  }
`

/aboutページに行くと、titleがしっかり入ってるのがわかります。

StaticQuery

ページではないコンポーネントでは、StaticQueryを使用します。
先程作成したLayoutで使ってみましょう。
まずgraphqlStaticQuerygatsbyからインポートします。

StaticQueryqueryrenderプロパティを受け取ります。
renderはpropとしてqueryを実行した結果のdataを返します。

src/layouts/index.jsx
import React from 'react'
import { Link, graphql, StaticQuery } from 'gatsby'

export default ({ children }) => (
  <StaticQuery
    query={graphql`
      query {
        site {
          siteMetadata {
            title
          }
        }
      }
    `}
    render={data => (
      <div>
        <Link to="/">
          <h3>{data.site.siteMetadata.title} Layout</h3>
        </Link>
        <Link to="/about">
          About
        </Link>
        {children}
      </div>
    )}
  />
)

Markdownファイルを扱う

形はできてきたので、コンテンツを作ってみましょう。
ルートフォルダーにpostsフォルダーを作成し、その中に[ポスト名]のフォルダーを作成してください。
その中にindex.mdファイルを作ります。
私はポスト名をpost-oneにしました。

posts/post-one/index.md
---
path: '/post-one'
date: '2019-08-18'
title: 'My First Post'
tags: ['gatsby', 'qiita']
---

This is my beautiful first post.

ファイルを読み込む(pluginの設定)

マークダウンファイルをGatsbyが読み込むためにはプラグインが必要です。
一番最初にインストールしたプラグインをgatsby-config.jsに書き込み、設定をしていきます。

gatsby-config.js
module.exports = {
  siteMetadata: {
    title: 'My Wonderful Website',
    description: 'Welcome to your brilliant website.',
    author: 'Cool Developer'
  },
  plugins: [
    'gatsby-plugin-catch-links',
    'gatsby-transformer-remark',
      {
        resolve: 'gatsby-source-filesystem',
        options: {
          name: 'posts',
          path: `${__dirname}/posts`
        }
      }
    ]
}

pathは自分の*.mdファイルが存在するところである必要があります。
準備はできたのでGraphiQLに戻ります。

GraphiQLで確認する

GraphiQL-Explorerから、allMarkdownRemarkを選び、すべてのフィールドを確認してみてください。
edgesはファイルパスを意味し、その中にあるnodeは先程作成したmdファイルを意味します。

以下のqueryを実行してみましょう。

{
  allMarkdownRemark {
    edges {
      node {
        frontmatter
      }
    }
  }
}

実行結果

{
  "data": {
    "allMarkdownRemark": {
      "edges": [
        {
          "node": {
            "frontmatter": {
              "title": "My First Post",
              "path": "/post-one",
              "date": "2019-08-18"
            }
          }
        }
      ]
    }
  }
}

frontmatterの中に先程作成したmdファイルの中身が入ってくるのがわかります。
gatsby-transformer-remarkプラグインが働いてくれました。
このデータを自由に使ってポストページを作っていきます。

ポストリストをHomeに表示する

余裕のある方は、このセクションは自分の力でやってみてください!
page-queryを使用してhomeにデータが渡ってきたら、それをReact方式に表示するだけです。

nodeの中にexcerptは、コンテンツの内容を前の部分だけ切り取って表示してくれます。
私はpruneLengthを使って、先頭100文字だけを表示するようにします。(defaultは140)

src/pages/index.jsx
import React from 'react'
import { graphql } from 'gatsby'
import Layout from '../layouts'

export default ({ data }) => {
  const { edges } = data.allMarkdownRemark
  return (
  <Layout>
    <h1>Gatsby Tutorial Home Page</h1>
      {edges.map(({ node }) => (
        <div key={node.id}>
          <h3>{node.frontmatter.title}</h3>
          <p>{node.frontmatter.date}</p>
          <p>{node.excerpt}</p>
        </div>
    ))}
  </Layout>
)}

export const query = graphql`
  query {
    allMarkdownRemark {
      edges {
        node {
          id
          excerpt(pruneLength: 100)
          frontmatter {
            title
            date
          }
        }
      }
    }
  }
`

image.png

ポストがちゃんと表示されました!

ソート

allMarkdownRemarksortfilterskiplimitの4つのプロパティを受け取れます。
skiplimitはPaginationの時に有用な項目ですが、ここではsortfilterを扱います。

sortではorderfieldsを設定できます。
新しいポストが上に来てほしいので、orderDESCにし、
fieldsはformatmatter___dateに設定します。

また、dateも日本語表記にしたいので、formatStringYYYY年MM月DD日に指定しましょう。

{
  allMarkdownRemark(sort: { order: DESC, fields: [frontmatter___date] }) {
    edges {
      node {
        id
        excerpt
        frontmatter {
          title
          date(formatString: "YYYY年MM月DD日")
        }
      }
    }
  }
}

フィルタリング

ブログを書くと下書き状態のポストもあるかと思いますが、
今のままではすべてのポストが公開されてしまいます。

なので作成したポストにstatusを追加しましょう。

posts/post-one/index.md
---
path: '/post-one'
date: '2019-08-18'
title: 'My First Post'
tags: ['gatsby', 'qiita']
status: 'published'
---

This is my beautiful first post.
posts/post-two/index.md
---
path: '/post-two'
date: '2019-08-19'
title: 'My Second Post'
tags: ['gatsby', 'qiita']
status: 'draft'
---

This is my magnificent second post.

では、statuspublishedのポストのみをfetchするqueryを書きましょう。

{
  allMarkdownRemark(
    filter: { 
      frontmatter: { 
        status: { eq: "published" } 
      } 
    }) {
    edges {
      node {
        id
        excerpt
        frontmatter {
          title
          date
        }
      }
    }
  }
}

これでpublished状態のポストしか見えないようになりました。

Postページを生成する

ポストのリストが表示されたので、中身が見れるように各々のページを生成していきます。
まず、入り口を作るために、Linkを設定しましょう。

src/pages/index.jsx
import { graphql, Link } from 'gatsby'

...
    <Link to={node.frontmatter.path}>
      <h3>{node.frontmatter.title}</h3>
    </Link>
...

queryにもpathを追加します。

...
    frontmatter {
      title
      date(formatString: "YYYY年MM月DD日")
      path
    }
...

ポストページを生成する処理を書くgatsby-node.jsをルートディレクトリに作成します。
また、ポストの詳細を表示するテンプレート、src/templates/post.jsxを作成します。

src/templates/post.jsx
import React from 'react'

const Post = (props) => {
  console.log('後で消す', props)
  return (
    <div>
      This is a post
    </div>
  )
}

export default Post

次にgatsby-node.jsではGatsbyのAPIの一つであるcreatePagesを使用します。
まずは、Pageを作成する関数を書いていきましょう。

path.resolveを使い、先程作成したpostのテンプレートを取得、
queryを実行してGatsbyが用意してくれるaction関数のcreatePageを実行していきます。

gatsby-node.js
const path = require('path')

exports.createPages = (({graphql, actions}) => {
  const { createPage } = actions
  return new Promise((resolve, reject) => {
    const postTemplate = path.resolve('src/templates/post.jsx')

    resolve(
      graphql(
        `
          query {
            allMarkdownRemark {
              edges {
                node {
                  frontmatter {
                    path
                    title
                  }
                }
              }
            }
          }
        `
      ).then(result => {
        if (result.errors) {
          return Promise.reject(result.errors)
        }

        const posts = result.data.allMarkdownRemark.edges;

        posts.forEach(({node}) => {
          const path = node.frontmatter.path

          createPage({
            path,
            component: postTemplate,
            context: {
              pathSlug: path
            }
          })
        })
      })
    )
  })
})

長い関数ですね。お疲れさまです!
では、サーバーを再起動して、postページに行ってみてください。
template通りのページが作成されたと思います。

デベロッパーコンソールを開いて、console.logを確認してみてください。
pageContextが渡ってきてるのがわかります。
ここにはcreatePageのcontextに設定したデータが入ってきます。
従って、pathSlugが入っているはずです。

このデータを活用してページを作成するために、template側を修正する必要があります。

src/templates/post.jsx
import React from 'react'
import { graphql } from 'gatsby'
import Layout from '../layouts'

const Post = ({ data }) => {
  const post = data.markdownRemark
  const title = post.frontmatter.title
  const date = post.frontmatter.date
  const html = post.html

  return (
    <Layout>
        <h1>{title}</h1>
        <p>{date}</p>
        <div dangerouslySetInnerHTML={{ __html: html }} />
    </Layout>
  )
}
export const query = graphql`
  query($pathSlug: String!) {
    markdownRemark(frontmatter: { path: {eq: $pathSlug} }) {
      html
      frontmatter {
        date
        title
      }
    }
  }
`
export default Post

markdownRemarkqueryを使用し、そこに先程gatsby-node.jsで渡したpathSlugを入れてます。
pageContextに設定したプロパティをそのままpage-queryに入れてます。
これでPostの詳細ページの完成です!

タグを生成する

次に、タグの一覧を集めたページと、
タグをクリックした時に同じタグのポストのみ表示させるページを作りたいと思います。

src/pages/tags.jsx
import React from 'react'

const Tags = props => {
  return (
    <div>
      Tags
    </div>
  )
}

export default Tags

次に、前のセクションと同様、タグのテンプレートを作成します。

src/templates/tag.jsx
import React from 'react'

const Tag = props => {
  return (
    <div>
      Tag
    </div>
  )
}

export default Tag

gatsby-node.jsに戻り、タグ別にページを作る処理を書きましょう。
すべてのポストのタグを集め、タグ別にポストを分類し、
createPageのcontextにタグの情報を渡して上げればOKです。

gatsby-node.js
const path = require('path')

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

  return new Promise((resolve, reject) => {
    const postTemplate = path.resolve('src/templates/post.jsx')
    const tagPage = path.resolve('src/pages/tags.jsx');
    const tagPosts = path.resolve('src/templates/tag.jsx');

    resolve(
      graphql(
        `
          query {
            allMarkdownRemark {
              edges {
                node {
                  frontmatter {
                    path
                    title
                    tags
                  }
                }
              }
            }
          }
        `
      ).then(result => {
        if (result.errors) {
          return Promise.reject(result.errors)
        }

        const posts = result.data.allMarkdownRemark.edges;

        const postsByTag = {};

        posts.forEach(({ node }) => {
          if (node.frontmatter.tags) {
            node.frontmatter.tags.forEach(tag => {
              if (!postsByTag[tag]) {
                postsByTag[tag] = [];
              }
              postsByTag[tag].push(node);
            });
          }
        });

        const tags = Object.keys(postsByTag);

        tags.forEach(tagName => {
          const posts = postsByTag[tagName]
          createPage({
            path: `/tags/${tagName}`,
            component: tagPosts,
            context: {
              posts,
              tagName
            }
          })
        })

        createPage({
          path: '/tags',
          component: tagPage,
          context: {
            tags: tags.sort(),
          },
        });

        posts.forEach(({node}) => {
          const path = node.frontmatter.path

          createPage({
            path,
            component: postTemplate,
            context: {
              pathSlug: path
            }
          })
        })
      })
    )
  })
})

gatsby-node.jsの修正が終わったので、サーバーを再起動して、
contextを受け取るコンポーネント側も修正しましょう。

src/pages/tags.jsx
import React from 'react'
import { Link } from 'gatsby'

const Tags = ({pageContext}) => {
  const { tags } = pageContext
  return (
    <div>
      <ul>
        {tags.map((tagName, index) => {
          return (
            <li key={index}>
              <Link to={`/tags/${tagName}`}>
                {tagName}
              </Link>
            </li>
          )
        })}
      </ul>
    </div>
  )
}

export default Tags
src/templates/tag.jsx
import React from 'react'
import { Link } from 'gatsby'

const Tag = ({ pageContext }) => {
  const { posts, tagName } = pageContext
  return (
    <div>
      <div>
        Posts about {`${tagName}`}
      </div>
      <div>
        <ul>
          {posts.map((post, index) => {
            return (
              <li key={index}>
                <Link to={post.frontmatter.path}>
                  {post.frontmatter.title}
                </Link>
              </li>
            )
          })}
        </ul>
      </div>
    </div>
  )
}

export default Tag

せっかくだしポストのページにもタグを追加しましょう。
タグリストのコンポーネントを作成します。

src/components/TagList.jsx
import React from 'react'
import { Link } from 'gatsby'

const TagList = ({ tags }) => {
  return (
    <div>
      {tags.map(tag =>
        <Link key={tag} to={`/tags/${tag}`}>
          {tag}
        </Link>
      )}
    </div>
  )
}

export default TagList

src/pages/post.jsx
...
    <TagList tags={post.frontmatter.tags || []} />
...

これでタグができました!

前の記事、次の記事を実装する

前の記事、次の記事のリンクをポストのページにおきたいです。
そのために、gastby-node.jsでpostページ生成のロジックをいじってみましょう。

gatsby-node.js
...
  posts.forEach(({ node }, index) => {
    const path = node.frontmatter.path
    const prev = index === 0 ? null : posts[index - 1].node;
    const next = index === posts.length - 1 ? null : posts[index + 1].node;

    createPage({
      path,
      component: postTemplate,
      context: {
        pathSlug: path,
        prev,
        next,
      }
    });
  });
...

インデックスを使って前、次を判断してるので、まずはデータをソートする必要がありそうです。
前のセクションでやった内容の復習ですね。しかし今回は降順ではなく昇順でソートします。

gatsby-node.js
...
          query {
            allMarkdownRemark (
              sort: {order: ASC, fields: [frontmatter___date]}
            ) {
              edges {
                node {
                  frontmatter {
                    path
                    title
                    date
                  }
                }
              }
            }
          }
...

これでprevnextの情報がpageContextで渡ってきますので、src/pages/post.jsxを修正しましょう。

src/pages/post.jsx
import React from 'react'
import { graphql, Link } from 'gatsby'
import Layout from '../layouts'
import TagList from '../components/TagList';

const Post = ({ data, pageContext }) => {
  const post = data.markdownRemark
  const title = post.frontmatter.title
  const date = post.frontmatter.date
  const html = post.html
  const { prev, next } = pageContext
  return (
    <Layout>
        <h1>{title}</h1>
        <p>{date}</p>
      <div dangerouslySetInnerHTML={{ __html: html }} />
      <TagList tags={post.frontmatter.tags || []} />
      {next &&
        <Link to={next.frontmatter.path}>
          Next
        </Link>
      }
      {prev &&
        <Link to={prev.frontmatter.path}>
          Previous
        </Link>
      }
    </Layout>
  )
}

これでシンプルなページ設定は終わりです。

イメージを使う

今回はイメージを読み込めるようにします。
いろんな方法がありますが、ここでは普通のJSインポートと、
gatsbyのpluginを使った2つの方法を紹介します。

JSインポート

まずは適当なロゴファイルをプロジェクトフォルダーに入れましょう。
私はsrc/images/logo.pngという名前にしました。

src/layouts/index.jsx
...
import logo from '../images/logo.png'
...
    <img src={logo} alt="logo" />
...

import文を使ってインポートしたイメージをそのままimgタグのsrcに渡せばいいです。
Gatsbyのすごいところは、パフォーマンスを重視した設定を自動でやってくれることです。
10KB以下のファイルはuriで渡してくれますが、
10KBを超えるファイルはstaticフォルダーにバンドルしてくれます。

Pluginを使う

とても高いパフォーマンスでイメージをさばいてくれるpluginを使ってみましょう。
まずは、プラグインとgatsby-imageをインストールしましょう。

npm i gatsby-transformer-sharp gatsby-plugin-sharp gatsby-remark-images gatsby-image

gatsby-config.jsでプラグインを設定します。
今回はマークダウンの中のイメージをプロセスしてくれるgatsby-remark-imagesを、
gatsby-transformer-remarkのoptionsに設定します。

更に、maxWidthqualitylinkImagesToOriginalを書いていきます。
他の詳しいoptionは、
(https://www.gatsbyjs.org/packages/gatsby-remark-images/?=remark)
を参考にしてください。

gatsby-config.js
plugins: [
    'gatsby-plugin-catch-links',
    'gatsby-transformer-sharp',
    'gatsby-plugin-sharp',
      {
        resolve: 'gatsby-source-filesystem',
        options: {
          name: 'posts',
          path: `${__dirname}/posts`
        }
    },
    {
      resolve: 'gatsby-transformer-remark',
      options: {
        plugins: [
          {
            resolve: 'gatsby-remark-images',
            options: {
              maxWidth: 690,
              quality: 90,
              linkImagesToOriginal: true,
            }
          }
        ]
      }
    }]

次はポストのカバーイメージを設定しましょう。
ポストの同じフォルダーにイメージを準備しておき、coverイメージを追加します。

posts/post-one/index.md
...
    date: '2019-08-18',
    title: 'My First Post',
    cover: './image.jpg'
...

GraphiQLのページですべてのマークダウン内のイメージを読み込んでみましょう。

{
    allMarkdownRemark {
      edges {
        node {
          id
          excerpt(pruneLength: 100)
          frontmatter {
            title
            date(formatString: "YYYY年MM月DD日")
            path
            cover {
              childImageSharp {
                fluid(maxWidth: 1000, quality: 90) {
                  src
                }
                fixed(width: 650) {
                  src
                }
              }
            }
          }
        }
      }
    }
  }

childImageSharpの中のfixedfluidを見てください。
fixedは指定されたwidthheightでイメージを返してくれ、
fluidはresponsiveにイメージを返してくれます。
Gatsbyはユーザーの使っている端末に合わせて最適なサイズのイメージを自動的に使ってくれます。
lazy-loadingなどもやってくれるので、設定無しですごくパフォーマンスの良いサイトを作れます。

残念ながらGraphiQLのエラーでfragmentを使えません。
fragmentは、GraphQLのqueryを使い回すために、予め設定しておいたものです。
gatsby-imageには、予め定められたfragmentがたくさんあるので、そこから選んで使いましょう。
https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-image#fragments

では実際のコンポーネントで使ってみます。

src/pages/index.jsx
import React from 'react'
import { graphql, Link } from 'gatsby'
import Img from 'gatsby-image'
import Layout from '../layouts'

export default ({ data }) => {
  const { edges } = data.allMarkdownRemark
  return (
  <Layout>
    <h1>Gatsby Tutorial Home Page</h1>
      {edges.map(({ node }) => (
        <div key={node.id}>
          <Img 
            fluid={node.frontmatter.cover.childImageSharp.fluid} 
            alt={node.frontmatter.title}
          />
          <Link to={node.frontmatter.path}>
            <h3>{node.frontmatter.title}</h3>
          </Link>
          <p>{node.frontmatter.date}</p>
          <p>{node.excerpt}</p>
        </div>
    ))}
  </Layout>
)}

export const query = graphql`
  query {
    allMarkdownRemark {
      edges {
        node {
          id
          excerpt(pruneLength: 100)
          frontmatter {
            title
            date(formatString: "YYYY年MM月DD日")
            path
            cover {
              childImageSharp {
                fluid(maxWidth: 1000, quality: 90) {
                  ...GatsbyImageSharpFluid_withWebp_tracedSVG
                }
              }
            }
          }
        }
      }
    }
  }
`

homeのページで、すべてのマークダウンのカバーイメージを表示するようにしました。
gatsby-imageImgとしてインポートして、その中のfixedfluidプロパティに、イメージのデータを渡してあげます。

src/templates/post.jsxでも、同じくカバーイメージを追加しましょう。

src/templates/post.jsx
import React from 'react'
import { graphql, Link } from 'gatsby'
import Img from 'gatsby-image'
import Layout from '../layouts'
import TagList from '../components/TagList';

const Post = ({ data, pageContext }) => {
  const post = data.markdownRemark
  const title = post.frontmatter.title
  const date = post.frontmatter.date
  const html = post.html
  const { prev, next } = pageContext
  return (
    <Layout>
        <Img 
          fluid={post.frontmatter.cover.childImageSharp.fluid} 
            alt={node.frontmatter.title}
        />
        <h1>{title}</h1>
        <p>{date}</p>
      <div dangerouslySetInnerHTML={{ __html: html }} />
      <TagList tags={post.frontmatter.tags || []} />
      {next &&
        <Link to={next.frontmatter.path}>
          Next
        </Link>
      }
      {prev &&
        <Link to={prev.frontmatter.path}>
          Previous
        </Link>
      }
    </Layout>
  )
}
export const query = graphql`
  query($pathSlug: String!) {
    markdownRemark(frontmatter: { path: {eq: $pathSlug} }) {
      html
      frontmatter {
        date
        title
        tags
        cover {
          childImageSharp {
            fluid(maxWidth: 1920, quality: 90) {
              ...GatsbyImageSharpFluid_withWebp
            }
          }
        }
      }
    }
  }
`

これで、イメージを表示できるようになりました!

スタイリング

ここまでお疲れさまです!
ブログがちゃんと動くようになりましたね。
しかし、見栄えが全然よくないので、スタイリングを追加していきます。

Reactでのスタイリングはいろんな方法があります。
ここではreact-emotionというライブラリーを使っていきたいと思います。
styled-componentsと似てるので、入れ替えても問題ないかと思います。

まず、関連ライブラリーをインストールしましょう。

npm i emotion @emotion/core @emotion/styled emotion-theming gatsby-plugin-emotion

gatsby-config.jsgatsby-plugin-emotionを追加しておきましょう。

Global、ThemeProvider

srcフォルダーの外にconfig/theme.jsを作成します。
theme.jsには、使いまわしたいcssを変数として保存しておきます。
それをThemeProviderに渡すことで、いろんなコンポーネントで使い回すことができます。

config/theme.js
const colors = {
  black: {
    base: '#333438',
    light: '#4b4e57',
    lighter: '#696d77',
    blue: '#2e3246',
  }
}
const transition = {
  easeInOutCubic: 'cubic-bezier(0.645, 0.045, 0.355, 1)',
  easeOutBack: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
  duration: '0.4s',
}
const theme = {
  colors,
  transition,
}

export default theme

ThemeProviderはReactのContext APIを使ってますが、DarkModeなどの設定を書くのも便利です。
詳しい方法は下の記事に書いてあったので、気になる方は試してみてください。
https://morimo7.hatenablog.com/entry/2019/07/13/155418

theme.jsを書いたので、実際にThemeProviderを使ってみます。
すべてのページで共有するlayoutに変更を加えましょう。
中身は、src/components/NavBar.jsxに切り出します。

src/layouts/index.jsx
import React from 'react'
import { ThemeProvider } from 'emotion-theming'
import { Global, css, } from '@emotion/core'
import theme from '../../config/theme'

const reset = css`
  *, *:before, *:after {
    box-sizing: inherit;
  }
  html, body {
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
  }
`

export default ({ children }) => (
  <ThemeProvider theme={theme}>
    <>
    <Global styles={reset}/>
      {children}  
    </>
  </ThemeProvider>
)

Globalでは全体に適用したいスタイルを書きました。
ここではcssのリセットですね。

src/components/NavBar.jsx
import React from 'react'
import { Link } from 'gatsby'
import styled from '@emotion/styled'
import logo from '../images/logo.png'

const StyledLink = styled(Link)`
  display: flex;
  font-weight: 700;
  align-items: center;
`

const Nav = styled.nav`
  display: flex;
  justify-content: flex-end;
  font-weight: 500;
  font-size: 1.25rem;
  align-items: center;

  a {
    color: ${props => props.theme.colors.black.base};
    margin-left: 2rem;
    transition: all ${props => props.theme.transitions};
    &:hover {
      color: ${props => props.theme.colors.black.lighter};
    }
  }
`

const NavBar = () => (
  <>
    <StyledLink to="/">
      <img src={logo} alt='Gatsby Logo' />
    </StyledLink>
    <Nav>
      <Link to='/'>Home</Link>
      <Link to='/about'>About</Link>
    </Nav>
  </>
)

export default NavBar

styledを使ってスタイリングしていきます。
emotionThemeProviderを使えば、propsからthemeにアクセスすることができます。

一例としてemotionを使う方法を紹介しましたが、
他に自分の気に入ってる方法があればご自由に使用してください。
この記事はCSSの記事ではないので、スタイリングに関しては以上になります。

SEO、PWA設定

ブログは、コンテンツをより多くの人に露出するために、SEO対策がとても大事です。
このセクションでは、Google LighthouseのSEOスコアを100にしていきます。

manifest

まずはPWAサポートのためにmanifestを追加します。

npm i react-helmet gatsby-plugin-react-helmet gatsby-plugin-manifest gatsby-plugin-sitemap gatsby-plugin-offline

今まではgatsby-config.jsにサイトの情報を書いておきましたが、config/site.jsに切り出します。

config/site.js
module.exports = {
  pathPrefix: '/',
  title: 'My Wonderful Website', // タイトル
  titleAlt: 'My Wonderful Website', // JSONLDのためのタイトル
  description: 'Welcome to my brilliant website.',
  url: 'https://[].netlify.com', // スラッシュなしのサイトURL
  siteURL: 'https://[].netlify.com/', // スラッシュありのサイトURL
  siteLanguage: 'ja', // HTMLの言語(ここでは日本語)
  logo: 'src/images/logo.png',
  banner: 'src/images/banner.png',
  favicon: 'src/images/favicon.png', // ファビコン
  shortName: 'CoolSite', // サイトの略称、12文字以下
  author: 'so99ynoodles', // schemaORGJSONLDの作成者
  themeColor: '#3e7bf2',
  backgroundColor: '#d3e0ff',
  twitter: '@so996ynoodles', // TwitterのID
};
gatsby-config.js
const config = require('./config/site');

module.exports = {
  siteMetadata: {
 ...config
  },
...

また、インストールしたプラグインの設定も書きましょう。

gatsby-config.js
...
'gatsby-plugin-sitemap',
    {
      resolve: 'gatsby-plugin-manifest',
      options: {
        name: config.title,
        short_name: config.shortName,
        description: config.description,
        start_url: config.pathPrefix,
        background_color: config.backgroundColor,
        theme_color: config.themeColor,
        display: 'standalone',
        icon: config.favicon,
      },
    },
 'gatsby-plugin-offline'
]
...

gatsby-plugin-sitemapを書いたあとに、gatsby-plugin-manifestを書きます。
gatsby-plugin-offlineをその後に書くことで、manifestファイルがcacheされます。

SEO

src/components/SEO.jsxを作成します。
まずは先程作成したサイトの情報をとってきて、react-helmetを使って記述していきます。
react-helmetはdocumentのheadをいじれるライブラリーです。
Helmetの情報は自動的にstaticなHTMLとして変換されます。
JavaScriptのコンテンツは一般的にSEOに向かないので助かりますね。

src/components/SEO.jsx
// src/components/SEO.jsx
import React, { Component } from 'react'
import Helmet from 'react-helmet'
import PropTypes from 'prop-types'
import { StaticQuery, graphql } from 'gatsby'

const SEO = ({ title, desc, banner, pathname, article }) => (
  <StaticQuery
    query={query}
    render={({
      site: {
        buildTime,
        siteMetadata: {
          defaultTitle,
          titleAlt,
          shortName,
          author,
          siteLanguage,
          logo,
          siteUrl,
          pathPrefix,
          defaultDescription,
          defaultBanner,
          twitter,
        },
      },
    }) => {
      const seo = {
        title: title || defaultTitle,
        description: defaultDescription || desc,
        image: `${siteUrl}${banner || defaultBanner}`,
        url: `${siteUrl}${pathname || '/'}`,
      };
    }}
  />
);

export default SEO

SEO.propTypes = {
  title: PropTypes.string,
  desc: PropTypes.string,
  banner: PropTypes.string,
  pathname: PropTypes.string,
  article: PropTypes.bool,
};

SEO.defaultProps = {
  title: null,
  desc: null,
  banner: null,
  pathname: null,
  article: false,
};

const query = graphql`
  query SEO {
    site {
      buildTime(formatString: "YYYY年MM月DD日")
      siteMetadata {
        defaultTitle: title
        titleAlt
        shortName
        author
        siteLanguage
        logo
        siteUrl: url
        pathPrefix
        defaultDescription: description
        defaultBanner: banner
        twitter
      }
    }
  }
`;

次にschema.orgを使ってJSON-LDを書いていきます。
JSON-LDはGoogle推奨、リッチスニペットを表示させる構造的マークアップです。
詳しい内容はQiitaにいい記事があったので、参考にしてください。
https://qiita.com/narumana/items/b66969b80cce848b2ddf

src/components/SEO.jsx
...
     const seo = {
        title: title || defaultTitle,
        description: defaultDescription || desc,
        image: `${siteUrl}${banner || defaultBanner}`,
        url: `${siteUrl}${pathname || '/'}`,
      };
      const realPrefix = pathPrefix === '/' ? '' : pathPrefix;
      let schemaOrgJSONLD = [
        {
          '@context': 'http://schema.org',
          '@type': 'WebSite',
          '@id': siteUrl,
          url: siteUrl,
          name: defaultTitle,
          alternateName: titleAlt || '',
        },
      ];
      if (article) {
        schemaOrgJSONLD = [
          {
            '@context': 'http://schema.org',
            '@type': 'BlogPosting',
            '@id': seo.url,
            url: seo.url,
            name: title,
            alternateName: titleAlt || '',
            headline: title,
            image: {
              '@type': 'ImageObject',
              url: seo.image,
            },
            description: seo.description,
            datePublished: buildTime,
            dateModified: buildTime,
            author: {
              '@type': 'Person',
              name: author,
            },
            publisher: {
              '@type': 'Organization',
              name: author,
              logo: {
                '@type': 'ImageObject',
                url: siteUrl + realPrefix + logo,
              },
            },
            isPartOf: siteUrl,
            mainEntityOfPage: {
              '@type': 'WebSite',
              '@id': siteUrl,
            },
          },
        ];
      }
    }}
  />
);
...

最後に、書いた情報をHelmetに埋め込みます。
FacebookのためのOpenGraphブロックと、twitterブロックも用意しました。

src/components/SEO.jsx
...
              '@id': siteUrl,
            },
          },
        ];
      }
      return (
        <>
          <Helmet title={seo.title}>
            <html lang={siteLanguage} />
            <meta name="description" content={seo.description} />
            <meta name="image" content={seo.image} />
            <meta name="apple-mobile-web-app-title" content={shortName} />
            <meta name="application-name" content={shortName} />
            <script type="application/ld+json">{JSON.stringify(schemaOrgJSONLD)}</script>

            {/* OpenGraph  */}
            <meta property="og:url" content={seo.url} />
            <meta property="og:type" content={article ? 'article' : null} />
            <meta property="og:title" content={seo.title} />
            <meta property="og:description" content={seo.description} />
            <meta property="og:image" content={seo.image} />

            {/* Twitter Card */}
            <meta name="twitter:card" content="summary_large_image" />
            <meta name="twitter:creator" content={twitter} />
            <meta name="twitter:title" content={seo.title} />
            <meta name="twitter:description" content={seo.description} />
            <meta name="twitter:image" content={seo.image} />
            </Helmet>
        </>
      );
    }}
  />
);
...

完成です!
SEO.jsxlayoutに置くこともできますし、
postsみたいなテンプレートに置くことで、デフォルトな情報ではなく、そのページに特化した情報を渡すこともできます。

src/templates/post.jsx
...
  return (
    <Layout>
      <SEO
        title={title}
        description={post.frontmatter.description || post.excerpt || ' '}
        image={image}
        pathname={post.frontmatter.path}
        article
      />
...

デプロイする

お疲れさまです!
デプロイにはいろんな方法があります。
githubページとか、Netlify, Surge, Herokuなど。
個人的にはNetlifyとgithubを連携して、ブログを更新するために自動deployする方法が好きです。
詳しい内容は下をご参考ください。
https://www.gatsbyjs.org/docs/hosting-on-netlify/

最後に

いかがだったでしょうか。
長文お読みいただきありがとうございます。
説明が下手な部分や、省略してしまった部分もあり、読みにくい文章になってたらごめんなさい。
この記事で少しでもGatsbyへの理解が深まったなら幸いです。

完成品はこちら
コードはこちら

so99ynoodles
ダイエットのために、ラーメンは一日一食だけにしてます。 フロントエンドメインのUI / UX好きエンジニア。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした