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 のスキーマが与えられているためです。
成果物
1枚目のシヴァ画像はローカルファイルに事前にあった画像です。2枚目と3枚目以降はmicroCMSから引っ張ってきた画像です。
同じallFileスキーマから引っ張ってきた画像です。
(シヴァの画像を表示するまでの流れは前回の記事にあります)
どれも gatsby-plugin-image で画像最適化処理を行なっています。
環境
ディレクトリ
src
images
shiva.jpeg(任意の画像)
pages
index.js
gatsby-node.js
gatsby-config.js
残り省略
config設定
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
ソースコード
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を生成できます。
// 省略
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を任意のスキーマに代入する処理ができます。
// 省略
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の中身
画像のようにallFileスキーマに対して任意で作ったfieldsとchildImageSharpの中に画像が増えていることがわかります。
microCMSを持っていない人用のダミーデータ
// 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からデータを引っ張って来れるとしているのですが以下のように設定してもエラーが生じます。
//省略
{
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適用方法