2
5

More than 3 years have passed since last update.

外部API(リモート)で取得した画像URLを GatsbyJS の gatsby-plugin-image(前 gatsby-image)で最適化する方法

Last updated at Posted at 2021-06-18

microCMSと連携したGatsbyJS製のサービスを開発&運営しています。

そこでmicroCMS側で設定したサムネイルの画像最適化を行いたいと思ったのですがせっかくGatsbyJSの強力な画像最適化プラグインである gatsby-plugin-image があるにも関わらずこれはローカルフォルダ( ここでは gatsby-source-filesystem によるallFileスキーマ )にしか適用できません。

ネットの情報もあまり多くなく少し苦労しました。

(CMS側が用意してくれている gatsby-source-microcms で引っ張ってきたGraphQLスキーマに childImageSharp が含まれていないため gatsby-plugin-image を使うことができない)

CreateNode機能 と gatsby-source-filesystemプラグイン を使ってAPIで取得したデータをallFileスキーマに設定させる

詰んだのでは?と思っていたのですが gatsby-node.js(Gatsby Node APIs) 上で CrearteNode機能 と gatsby-source-filesystemプラグイン を使ってGraphQL上の allFileスキーマ に外部画像を追加することができました。

CreateNode機能 として用意されている sourceNodes と onCreateNode の関数を使います。
また gatsby-source-filesystemプラグイン が用意している createRemoteFileNode も呼び出してallFile用のNodeを生成します。

これは、gatsby-plugin-image を使うためには ImageSharp のスキーマが与えられていないといけないのですが gatsby-source-filesystemプラグイン によって与えられた allFileスキーマ では ImageSharp のスキーマが与えられているためです。

成果物

Jun-18-2021 13-53-41.gif

gitHubのソースコード

1枚目のシヴァ画像はローカルファイルに事前にあった画像です。2枚目と3枚目以降はmicroCMSから引っ張ってきた画像です。
同じallFileスキーマから引っ張ってきた画像です。

(シヴァの画像を表示するまでの流れは前回の記事にあります)

どれも gatsby-plugin-image で画像最適化処理を行なっています。

環境

ディレクトリ

src
  images
    shiva.jpeg(任意の画像)
  pages
    index.js
gatsby-node.js
gatsby-config.js
残り省略

config設定

gatsby-config.js
require("dotenv").config({
  path: `.env.${process.env.NODE_ENV}`,
})

module.exports = {
  plugins: [
    `gatsby-plugin-image`,
    `gatsby-plugin-sharp`,
    `gatsby-transformer-sharp`,
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `images`,
        path: `${__dirname}/src/images/`,
      },
    },
  ],
}

gatsby-starter-hello-world を使ってからプロジェクトを始めています。
これも、前回の記事gatsby-image が非推奨になり gatsby-plugin-image に推奨されていたので使い方を解説してみたと同じ環境なので揃えたい人はそちらを参考にどうぞ。
(index.jsや使用している画像も同じコードを使用)

gatsby側の準備

gatsbyの各種プラグインインストール

yarn add gatsby-plugin-image gatsby-plugin-sharp gatsby-transformer-sharp
yarn add crypto

ソースコード

gatsby-node.js

const { createRemoteFileNode } = require('gatsby-source-filesystem');
let crypto = require("crypto");
const fetch = require("node-fetch");

//URLにオプション付与
const getUrlOption = (number, url) => {
  const UrlandOption = String(url + `?limit=${number}`)
  return String(UrlandOption);
}

const getMicroCMSdata = async() => {
  const fetchTarget = {
    url: `https://100g.microcms.io/api/v1/100g`,
    option: {
      method: 'GET',
      mode: 'cors',
      headers: {
        'Content-Type': 'application/json',
        'X-API-KEY' : process.env.MICROCMS_API_KEY
      }
    }
  }

  // microCMSのコンテンツを引っ張ろうとするとデフォルトでlimit=10のオプションがついており全てのコンテンツを引っ張ってこれない。totalCountでコンテンツ総数をチェック
  const {url, option} = fetchTarget,
    getTotalCountUrl = getUrlOption(0, url),
    totalCountUrlData = await fetch(getTotalCountUrl, option),
    { totalCount } = await totalCountUrlData.json();

  const getContentUrl = getUrlOption(totalCount, url),
    contentUrlData  = await fetch(getContentUrl, option),
    { contents } = await contentUrlData.json();

  return { contents, totalCount };
}

// トップレベルのNodeを作成
exports.sourceNodes = async ({ actions: {createNode}, createNodeId }) => {

  const turnImageObjectIntoGatsbyNode = (image, microContent) => {
    const content = {
      content: microContent.title,
      ['image___NODE']: createNodeId(`project-image-{${microContent.id}}`),
    };
    const nodeId = createNodeId(`image-{${image.id}}`);
    const nodeContent = JSON.stringify(image);
    const nodeContentDigest = crypto
          .createHash('md5')
          .update(nodeContent)
          .digest('hex');

    const nodeData = {
      ...image,
      ...content,
      id: nodeId,
      microContent,
      internal: {
        type: 'Image',
        content: nodeContent,
        contentDigest: nodeContentDigest,
      },
    };

    // nodeとして格納されonCreateNodeのnode変数からも取得できるようになる
    return nodeData;
  };

  // 特に変更する必要はないと思ったので参考記事からそのまま転用
  const createImageObjectFromURL = (url) => {
    const lastIndexOfSlash = url.lastIndexOf('/');
    const id = url.slice(lastIndexOfSlash + 1, url.lastIndexOf('.'));
    return { id, image: id, url };
  };

  const microContentData = await getMicroCMSdata();

  // テストなので画像データを持っているコンテンツだけにあえて絞る
  const targetMicroContents =
    microContentData.contents
      .filter(({thumbnail}) => thumbnail !== undefined);

  targetMicroContents.map(content => {
    const imgObj = createImageObjectFromURL(content.thumbnail.url);
    const nodeData = turnImageObjectIntoGatsbyNode(imgObj, content);
    createNode(nodeData);
  });
};

// onCreateNodeはsourceNodesの中でcreateNodeしたものを含め全ての生成されたNodeを順番に取得してくれる
exports.onCreateNode = async ({
  node, actions, store, getCache, createNodeId
}) => {
  // onCreateNodeで設定したtypeにあったものだけ処理をする
  if (node.internal.type !== 'Image') return;

  const { createNode, createNodeField } = actions;
  const fileNode = await createRemoteFileNode({
      url: node.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
      store, // Gatsby's redux store
      getCache, // get Gatsby's cache
      createNode, // helper function in gatsby-node to generate the node
      createNodeId, // helper function in gatsby-node to generate the node id
  });

  // Object配列化
  const microContentArr = Object.entries(node.microContent).map( ( [key, value] ) => [key, value]);
  microContentArr.map( async([key, value]) => {
    await createNodeField(
      {
        node: fileNode,
        name: key,
        value: value,
      });
  });

  if (fileNode) {
    node.image___NODE = fileNode.id;
  }
};

const getUrlOption = (number, url) => (略) はオプションURLを付与

const getMicroCMSdata = async() => (略)はいい感じにMicroCMSからデータを引っ張る

exports.sourceNodes = async({略}) => (略)はトップレベルのNode作成

exports.onCreateNode = async ({略}) => (略)は作成したNodeをもとにcreateRemoteFileNodeでallFileスキーマにNodeを与える

soureNodesについて

sourceNodesはトップレベルのNodeを生成できます。

gatsby-node.js
// 省略
exports.sourceNodes = async ({ actions: {createNode}, createNodeId }) => {

  const turnImageObjectIntoGatsbyNode = (image, microContent) => {
    const content = {
      content: microContent.title,
      ['image___NODE']: createNodeId(`project-image-{${microContent.id}}`),
    };
    const nodeId = createNodeId(`image-{${image.id}}`);
    const nodeContent = JSON.stringify(image);
    const nodeContentDigest = crypto
          .createHash('md5')
          .update(nodeContent)
          .digest('hex');

    const nodeData = {
      ...image,
      ...content,
      id: nodeId,
      microContent,
      internal: {
        type: 'Image',
        content: nodeContent,
        contentDigest: nodeContentDigest,
      },
    };

    // nodeとして格納されonCreateNodeのnode変数からも取得できるようになる
    return nodeData;
  };
// 省略

変数nodeDataにNode作成に必要なデータを与えて返すことでNode生成できます。
変数nodeContentDigestではNodeを暗号化する必要があったためcryptoライブラリを別途追加してエンコードしています。

onCreateNode上

生成したNodeを任意のスキーマに代入する処理ができます。

gatsby-node.js
// 省略
exports.onCreateNode = async ({
  node, actions, store, getCache, createNodeId
}) => {
  // onCreateNodeで設定したtypeにあったものだけ処理をする
  if (node.internal.type !== 'Image') return;

  const { createNode, createNodeField } = actions;
  const fileNode = await createRemoteFileNode({
      url: node.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
      store, // Gatsby's redux store
      getCache, // get Gatsby's cache
      createNode, // helper function in gatsby-node to generate the node
      createNodeId, // helper function in gatsby-node to generate the node id
  });

  // Object配列化
  const microContentArr = Object.entries(node.microContent).map( ( [key, value] ) => [key, value]);
  microContentArr.map( async([key, value]) => {
    await createNodeField(
      {
        node: fileNode,
        name: key,
        value: value,
      });
  });

  if (fileNode) {
    node.image___NODE = fileNode.id;
  }
};

// 省略

createRemoteFileNode()でNodeからallFile用Nodeを作成して引っ張って来れるようになります。

GraphiQLの中身

スクリーンショット 2021-06-18 13.35.57.png

画像のようにallFileスキーマに対して任意で作ったfieldsとchildImageSharpの中に画像が増えていることがわかります。

microCMSを持っていない人用のダミーデータ

gatsby-node.js
  // microCMSを持っていない人の確認用ダミー処理
  // 73行付近のconst microContentData = await getMicroCMSdata();と置き換える
  // const dammyProjects = {
  //  id: 'dammy',
  //  title: 'dammyTitle',
  //   images : [`https://1.bp.blogspot.com/-eZgH3AYPT0Y/X7zMHMTQO2I/AAAAAAABcYU/Fk3btazNl6oqIHrfcxgJBiUKKSE1tSAIwCNcBGAsYHQ/s400/food_bunka_fry.png`, `https://1.bp.blogspot.com/-uc1fVHdj2RQ/X9GYFTpvwxI/AAAAAAABcs4/Gez9aftyhdc_Hm2kXt5RJm_vK9SuShz8wCNcBGAsYHQ/s400/food_komochi_konnyaku.png`, `https://1.bp.blogspot.com/-g0tbS-Rf0pk/X3hF_S_ScZI/AAAAAAABbmQ/u0Pd0qVobbYOfFYhmls3iBXzIUiuta2-gCNcBGAsYHQ/s400/food_sobagaki.png`]
  // }
  // const projects = await dammyProjects;
  // projects.images.map((image) => {
  //   const imgObj = createImageObjectFromURL(image);
  //   const nodeData = turnImageObjectIntoGatsbyNode(imgObj, projects);
  //   createNode(nodeData);
  // });


  // microCMSを持っていない人の確認用ダミー処理
  // 106行付近の await createNodeField(省略)と置き換える
  // await createNodeField(
  //   {
  //     node: fileNode,
  //     name: 'Sample',
  //     value: 'true',
  //   });

  // await createNodeField(
  //   {
  //     node: fileNode,
  //     name: 'Test',
  //     value: 'hello test!',
  //   });

失敗した方法

propsを使ったやり方

「propsで取得データを受け渡した画像を GatsbyImageコンポーネント に渡して最適化すれば良いのでは?」

と思いpropsを渡したところ GatsbyImageコンポーネント(StaticImageも同様) のコンポーネントにはpropsを渡すことは動的な画像生成になってしまうとのことできませんでした。

gatsby-source-graphql のプラグインを使ったやり方

GatsbyJSの gatsby-source-graphql を使えば外部APIからデータを引っ張って来れるとしているのですが以下のように設定してもエラーが生じます。

gatsby-config.js
//省略
    {
      resolve: "gatsby-source-graphql",
      options: {
        typeName: `MicroCMS`,
        fieldName: `microcms`,
        url: `https://hoge.microcms.io/api/v1/hoge`,
        headers: {
          method: 'GET',
          mode: 'cors',
          "X-API-KEY" : process.env.MICROCMS_API_KEY,
        },
      },
    },
//省略

でdevelopすると

 ERROR #11321  PLUGIN

"gatsby-source-graphql" threw an error while running the sourceNodes lifecycle:

Source GraphQL API: HTTP error 400 Bad Request


  - fetch.js:11 exports.fetchWrapper
    [100g-Gatsby]/[gatsby-source-graphql]/fetch.js:11:11

  - task_queues.js:97 processTicksAndRejections
    internal/process/task_queues.js:97:5

  - From previous event:

  - api-runner-node.js:610 Promise.catch.decorateEvent.pluginName
    [100g-Gatsby]/[gatsby]/src/utils/api-runner-node.js:610:11

  - From previous event:

  - api-runner-node.js:609 
    [100g-Gatsby]/[gatsby]/src/utils/api-runner-node.js:609:16

  - timers.js:456 processImmediate
    internal/timers.js:456:21

  - From previous event:

  - api-runner-node.js:580 
    [100g-Gatsby]/[gatsby]/src/utils/api-runner-node.js:580:5

  - From previous event:

  - api-runner-node.js:477 module.exports
    [100g-Gatsby]/[gatsby]/src/utils/api-runner-node.js:477:3

  - source-nodes.ts:99 _default
    [100g-Gatsby]/[gatsby]/src/utils/source-nodes.ts:99:9

  - source-nodes.ts:25 sourceNodes
    [100g-Gatsby]/[gatsby]/src/services/source-nodes.ts:25:9

  - interpreter.js:724 Interpreter.exec
    [100g-Gatsby]/[xstate]/lib/interpreter.js:724:27

  - interpreter.js:206 Interpreter.execute
    [100g-Gatsby]/[xstate]/lib/interpreter.js:206:22

  - interpreter.js:226 Interpreter.update
    [100g-Gatsby]/[xstate]/lib/interpreter.js:226:18

  - interpreter.js:125 
    [100g-Gatsby]/[xstate]/lib/interpreter.js:125:23

  - scheduler.js:60 Scheduler.process
    [100g-Gatsby]/[xstate]/lib/scheduler.js:60:13

  - scheduler.js:44 Scheduler.schedule
    [100g-Gatsby]/[xstate]/lib/scheduler.js:44:14

  - interpreter.js:121 Interpreter.send
    [100g-Gatsby]/[xstate]/lib/interpreter.js:121:29

  - interpreter.js:842 actor.id
    [100g-Gatsby]/[xstate]/lib/interpreter.js:842:23

といったように怒られます。(gatsbyバージョン2系と3系の両方で同じように怒られた)

参考にした記事

全般参考:Load Gatsby ImageSharp from Image URL Source

createNodeField参考:Gatsbyにおける外部取得画像へのgatsby-image適用方法

2
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5