Gatsby で記事ごとの著者情報とアバター画像を表示する(その2 アバター画像の表示)

Gatsby で記事ごとの著者情報とアバター画像を表示する(その2 アバター画像の表示)

Gatsbyjs で記事ごとの著者のアバター画像を表示する方法について、二本立てでお送りしております。

  1. 各記事に著者情報を表示する
  2. 著者情報にアバター画像を表示する 👈いまここ

概要

前回のおさらいです。

Gatsby の gatsby-starter-blog スターターでは著者が Kyle Mathews 固定になっています。

gatsbyjs author bio and avatar for each post 2 1

これを記事ごとの著者 (author) に合わせた著者情報とアバター画像が表示されるようにします。

gatsbyjs author bio and avatar for each post 2 2

前回はこんな感じで著者情報をアバター画像なしで表示するところまでをやりました👇

gatsbyjs author bio and avatar for each post 2 3

前提条件

こちらも再掲です。

環境

  • Node.js 14
  • Gatsby 3.11.1

仕様

  • ベースは gatsby-starter-blog スターターを使用
  • 各記事ページに著者情報を表示する
  • 著者は全員 GitHub のアカウントを持っている
  • 各記事の著者は frontmatter の author で GitHub の ID を指定する
  • アバター画像は GitHub のアバターを利用する

今回の想定は取り扱う著者が全員 IT 技術者のため、上記のような仕様にしています。

GitHub のアバターでない場合は、適宜読み替え、応用していただければ幸いです。

アバター画像を読み込む

さていよいよ本題です。アバター画像を GitHub から読み込んでキャッシュし、さきほどの Bio コンポーネントに表示していきます。

基本的には Gatsby Node - Preprocessing External Images | Gatsby の方法を参考にします。

画像ファイルのフィールドを追加

まず gatsby-node.js を編集して frontmatter に画像”ファイル”ノードを追加します。ここでは名前は avatarImage とします。

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions

  createTypes(`
    type MarkdownRemark implements Node {
      frontmatter: Frontmatter
      fields: Fields
    }

    type Frontmatter {
      title: String
      description: String
      date: Date @dateformat
      avatarImage: File @link(from: "avatarImage__NODE")
    }

    type Fields {
      slug: String
    }
  `)
}

avatarImage: File @link(from: "avatarImage__NODE") が追加部分です。

この avatarImage__NODE がリンク元となるフィールドになります。フィールド名は任意ですが、他のフィールドとかぶる可能性のないものにします。

なお Frontmatter.authormapping でリンクしているので、この GraphQL の Type 宣言には登場していないことに留意してください。

次に同じく gatsby-node.jsonCreateNodeavatarImage__NODE の中身を設定します。

// 冒頭で createRemoteFileNode の require が必要です
const { createRemoteFileNode } = require(`gatsby-source-filesystem`) 

exports.onCreateNode = async ({
  node,
  actions,
  getNode,
  store,
  cache,
  createNodeId,
}) => {
  const { createNode, createNodeField } = actions

  if (node.internal.type === `MarkdownRemark`) {
    const value = createFilePath({ node, getNode })

    createNodeField({
      name: `slug`,
      node,
      value,
    })

    if (node.frontmatter.author) {
      const url = `https://avatars.githubusercontent.com/${node.frontmatter.author}`;
      const fileNode = await createRemoteFileNode({
        url, // string that points to the URL of the image
        parentNodeId: node.id, // id of the parent node of the fileNode you are going to create
        createNode, // helper function in gatsby-node to generate the node
        createNodeId, // helper function in gatsby-node to generate the node id
        cache, // Gatsby's cache
        store, // Gatsby's Redux store
      })
      if (fileNode) {
        node.frontmatter.avatarImage__NODE = fileNode.id
      }
    }
  }
}

slug の部分はスターターテンプレートでもともと記述されている記事のスラグを指定する部分ですね。

それ以外なかなかややこしいですが、基本的には createRemoteFileNode でファイルノードを作成し、先ほどの avatarImage__NODE というフィールドに代入しているだけです。

createRemoteFileNode のパラメーターが並びますが、ほとんどは onCreateNode のパラメーターをそのまま渡せば大丈夫です。

ここで urlhttps://avatars.githubusercontent.com/node.frontmatter.author をくっつけて生成しています。 たとえば私の GitHub ID kenzauros なら下記のようになります。

https://avatars.githubusercontent.com/kenzauros

この時点では node.frontmatter.author は author.yaml の情報に解決されていないことに注意が必要です。 よってこの node.frontmatter.author には記事の frontmatter で指定した author がそのまま入っています

GraphQL の宣言で avatarImagenode.frontmatter.author の下層でなく node.frontmatter の下層に定義しているのもこれが理由です。

GraphQL クエリーでアバター画像を取得

いよいよアバター画像を復活させましょう。

まず src/templates/blog-post.jspageQueryavatarImage を追加します。

    markdownRemark(id: { eq: $id }) {
      id
      excerpt(pruneLength: 160)
      html
      frontmatter {
        title
        date(formatString: "MMMM DD, YYYY")
        description
        author {
          id
          name
          bio
        }
        avatarImage {
          childImageSharp {
            gatsbyImageData(width: 50, height: 50, layout: FIXED)
          }
        }
      }
    }

ここで avatarImage はファイルオブジェクトです。これに childImageSharp, gatsbyImageData を指定して最適化された画像に変換します。

次にこのクエリーで取得した画像を Bio コンポーネントに渡します。

<footer>
  { author
    ? <Bio
        github={author.id}
        name={author.name}
        bio={author.bio}
        avatarImage={post.frontmatter.avatarImage}
        />
    : null }
</footer>

Bio コンポーネントの修正

次に Bio コンポーネントで props から渡された画像を表示します。

元々はローカルの静的ファイルだったため StaticImage コンポーネントが使われていましたが、動的ファイルを表示させるため GatsbyImage コンポーネントを使います。

import * as React from "react"
import { GatsbyImage, getImage } from "gatsby-plugin-image"

const Bio = (props) => {
  const bio = props.bio || null;
  const github = props.github;
  const name = props.name || '名無しの権兵衛';
  const githubUrl = github ? `https://github.com/${github}` : null;
  const avatarImage = getImage(props.avatarImage)

  return (
    <div className="bio">
      <GatsbyImage
        image={avatarImage}
        className="bio-avatar"
        width={50}
        height={50}
        alt={github}
        />
      <div>
        <p>
          <strong>{name}</strong>
          { github ? <> (<a href="{githubUrl}" target="_blank" rel="noopener">{github}</a>)</> : null }
        </p>
        <p>{bio}</p>
      </div>
    </div>
  )
}

export default Bio

GatsbyImage の image には IGatsbyImageData 型を渡す必要がありますので、 avatarImage から getImage()file.childImageSharp.gatsbyImageData を取得しています。

このあたりは gatsby-plugin-image プラグインのページを参照してください。

さて、これで下記のように表示されれば OK です😃

gatsbyjs author bio and avatar for each post 2 2

お疲れ様でした😅

kenzauros