ContentfulからMarkdown内に使用できるGatsbyの独自コンポーネント内の画像も最適化する

# はじめに
こんにちは。東京五輪のチケットは入場不可に伴い、自動払い戻しになった模様です、筆者です :innocent: 

さて、前回以下記事で**Gatsby**の独自コンポーネントを作成したのですが、画像に対して最適化をするべく、**gatsby-plugin-image**を挟むことができませんでした。

https://qiita.com/akitkat/items/58332b972aac6145de80

今回はそれを対応していこうと思います！
結構これ系の記事が少なかったので、今後作成する方に参考になれば幸いです :smile:

もし当記事が役に立ったら、**LGTM**ください！ :pray:

# 方針
1. プラグインのインストール.
2. gatsby-config.js編集.
3. 記事内から独自コンポーネント箇所を抽出.
4. 2から画像URLを取得.
5. 3のファイルノード生成.
6. 2-4の処理を`gatsby-node.js`に取り込む.
7. 独自コンポーネント作成.
8. 独自コンポーネント内で4を取得.
9. 5を基に`gatsby-plugin-image`を用いて独自コンポーネント内に展開する.
10. 独自コンポーネントの読み込みを有効化.
11. 10でrenderAstに渡しているhtmlAstをGraphQLの取得項目に追加.
12. ビルドする.

# 前提

## フォルダ構造
全体像が理解しやすいように、フォルダ構造を記載します。

```sh
$ tree

.
├── gatsby-config.js
├── gatsby-node.js
├── package-lock.json
├── package.json
└── src
    ├── components
    │   └── ArticleItem.js
    ├── gatsby
    │   └── node
    │       ├── createPages.js
    │       ├── createSchemaCustomization.js
    │       └── onCreateNode.js
    ├── images
    └── templates
        └── post.js
```

## Contentfulで入力するマークダウン

```md
<article-item title="おすすめランチ紹介" url="https://example.com" image="https://via.placeholder.com/350x150"></article-item>
```


# ContentfulからMarkdown内に使用できるGatsbyの独自コンポーネント内の画像も最適化する

## 1. プラグインのインストール.

```sh
$ npm install gatsby-plugin-image gatsby-remark-component gatsby-transformer-sharp rehype-react
```

## 2. gatsby-config.js編集.
1でインストールしたプラグインを以下のように追加します。

```js:./gatsby-config.js
module.exports = {
  plugins: [
    `gatsby-plugin-image`,
    `gatsby-transformer-sharp`,
    {
      resolve: `gatsby-transformer-remark`,
      options: {
        plugins: [
          {
            resolve: `gatsby-remark-component`,
            options: {
              components: ['article-item']
            }
          },
        ],
      },
    },
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `images`,
        path: './src/images/',
      },
    }
  ],
}
```

## 3. マークダウン内にから独自コンポーネントの箇所を抽出.
`Gatsby Node APIs` の`onCreateNode`に処理を含めます。

https://www.gatsbyjs.com/docs/reference/config-files/gatsby-node/#onCreateNode

## 4. 3から画像URLを取得.
こちらも`Gatsby Node APIs` の`onCreateNode`に処理を含めます。
## 5. 4のファイルノード生成.
こちらも`Gatsby Node APIs` の`onCreateNode`に処理を含めます。

ポイントは、`createRemoteFileNode`時の名前を設定し、`fileNode.internal.content`に画像urlを入れます。

これは、**後続5の処理**で指定画像の情報を取得する際に使用します。

## 上記3-5を反映したものが以下です.

```js:./src/gatsby/node/onCreateNode.js
const { createRemoteFileNode } = require('gatsby-source-filesystem')

module.exports = async ({ actions: {
    createNode,
    createNodeField
  },
  node,
  createNodeId,
  cache,
  store,
  getNodesByType
}) => {
  if (node.internal.type !== `MarkdownRemark`) return

  // ②
  const regex = /^<article-item.+(image|title)="(.[^"]+)"><\/article-item>$/gm
  const tags = getNodesByType('MarkdownRemark')
    .map(e => e.rawMarkdownBody.match(regex))
    .filter(e => !!e)
    .flat()

  if (! tags) return

  // ③
  const images = tags.map(e => e.match(/image="([^"]*)"/)[1])
  if (! images) return

  // ④
  await Promise.all(
    images.map(async url => {
      const fileNode = await createRemoteFileNode({
        url: url,
        cache,
        store,
        createNode,
        createNodeId,
        name: 'articleItem'
      })
      fileNode.internal.content = url

      if (fileNode) {
        node.localFile___NODE = fileNode.id
      }

      return fileNode
    })
  )
}
```

## 6. 3-5の処理を`gatsby-node.js`に取り込む.

```diff:./gatsby-node.js
  exports.createSchemaCustomization = require('./src/gatsby/node/createSchemaCustomization')
  exports.createPages = require('./src/gatsby/node/createPages')
+ exports.onCreateNode = require('./src/gatsby/node/onCreateNode')
```

## 7. 独自コンポーネント作成.

```js:./src/components/ArticleItem.js
import React from 'react'
import styled from '@emotion/styled'

const Wrapper = styled.div`
`

const ThumbnailWrapper = styled.div`
`

const TitleWrapper = styled.div`
`

const Title = styled.h3`
`

const ArticleItem = props => {
  return (
    <Wrapper>
      <ThumbnailWrapper>
        <a href={props.url} rel="noopener noreferrer nofollow" target="_blank">
          {/* <img /> */}
        </a>
      </ThumbnailWrapper>
      <TitleWrapper>
        <a href={props.url} rel="noopener noreferrer nofollow" target="_blank">
          <Title>{props.title}</Title>
        </a>
      </TitleWrapper>
    </Wrapper>
  )
}

export default ArticleItem
```

## 8. 独自コンポーネント内で4を取得.

## 9. 4を基に**gatsby-plugin-image**を用いて独自コンポーネント内に展開する.

## 上記8-9を反映したものが以下です.

```diff:./src/components/ArticleItem.js
+ import { GatsbyImage, getImage } from 'gatsby-plugin-image'
+ import { graphql, useStaticQuery } from 'gatsby'

  import React from 'react'
  import styled from '@emotion/styled'

  ...
  ...
  ...

  const ArticleItem = props => {
+   const data = useStaticQuery(graphql`
+     query {
+       allFile(filter: {name: {eq: "articleItem"}}) {
+         edges {
+           node {
+             childImageSharp {
+               gatsbyImageData
+             }
+             internal {
+               content
+             }
+           }
+         }
+       }
+     }
+   `)
+   const image = getImage(data.allFile.edges.find(e => e.node.internal.content === props.image).node)

    return (
      <Wrapper>
        <ThumbnailWrapper>
          <a href={props.url} rel="noopener noreferrer nofollow" target="_blank">
-           {/* <img /> */}
+           <GatsbyImage image={image} alt={props.title} />

          </a>
        </ThumbnailWrapper>
        <TitleWrapper>
          <a href={props.url} rel="noopener noreferrer nofollow" target="_blank">
            <Title>{props.title}</Title>
          </a>
        </TitleWrapper>
      </Wrapper>
    )
  }

  export default ArticleItem
```

## 10. 独自コンポーネントの読み込みを有効化.

```diff:src/components/PageBody.js
  import React from 'react'
+ import ArticleItem from "../components/ArticleItem"
+ import RehypeReact from "rehype-react"

  ...
  ...
  ...

  const PageBody = props => {
+   const renderAst = new RehypeReact({
+     createElement: React.createElement,
+     components: { "article-item": ArticleItem }
+   }).Compiler

    return (
-    <Body
-      dangerouslySetInnerHTML={{ __html: props.body.childMarkdownRemark.html }}
-    />
+    <Body>
+      {renderAst(props.body.childMarkdownRemark.htmlAst)}
+    </Body>
    )
  }

  export default PageBody
```
## 11. 10で`renderAst`に渡している`htmlAst`をGraphQLの取得項目に追加

```diff:src/templates/post.js
  export const query = graphql`
    query($slug: String!) {
      contentfulPost(slug: { eq: $slug }) {
        title
        slug
        metaDescription {
          internal {
            content
          }
        }
        publishDate(formatString: "MMMM DD, YYYY")
        publishDateISO: publishDate(formatString: "YYYY-MM-DD")
        tags {
          title
          id
          slug
        }
        heroImage {
          title
          fluid(maxWidth: 1800) {
            ...GatsbyContentfulFluid_withWebp_noBase64
          }
          ogimg: resize(width: 1800) {
            src
          }
        }
        body {
          childMarkdownRemark {
            timeToRead
            html
+           htmlAst
            excerpt(pruneLength: 320)
          }
        }
      }
    }
  `
```

## 12.  ビルドする

ビルドしましょう！

# おわりに
ポイントは、`Gatsby Node APIs` の`onCreateNode`の処理で、`gatsby-plugin-image`が使用できるようにリモート画像URLからnodeを作成し、`GraphQL`から取得できるようにすることです :ok_hand:

また、上記に合わせて、GraphQLから取得する際にkeyとなる値をいれておくことも重要ポイントです :+1:

これでContentfulから画像URLをpropsしていい感じに表示できるまでできました！
それでは！