GatsbyでmicroCMSの画像を扱う

背景

microCMSを使用する際に、 無料プランだとデータ転送量が気になるので、 画像データをビルド結果に含めたい。

また、ダウンロードした画像はGraphQLから、 Sharp を使った画像編集や gatsbyImageData の取得も行いたい。

問題点

microCMSの公式プラグインは、 画像データがSharpに対応していない。 また、2022年8月29日以降は更新されていない。 おそらく、Sharpに対応するには、API(モデル)のスキーマが必要になるが、 microCMSのWeb APIからスキーマを取得することができないのが原因だと考えられる(このドキュメントに沿って管理画面からエクスポートすることは可能)。 ちなみに、ContentfullやStrapiはWeb APIからスキーマを取得することができるためSharpに対応している。

https://www.contentful.com/developers/docs/concepts/data-model/ https://docs.strapi.io/dev-docs/backend-customization/models

成果(gatsby-node.js)

microCMSの画像ファイルを自動でSharpで加工可能な形式にする。 たとえば sample-posts というAPIに、 sampleImage という画像のフィールドが設定されている場合

{
  "sampleImage": {
    "url": "https://images.microcms-assets.io/assets/xxxx/yyy/zzz.jpg",
    "width": 640,
    "height": 480,
  }
}

以下のようなGraphQLにより、 gatsbyImageData で使用するデータを取得することができる

{
  allMicrocmsSamplePosts(sort: [{createdAt: DESC}]) {
      localSampleImage {
        childImageSharp {
          gatsbyImageData(width: 600, layout: FIXED)
        }
      }
    }
  }
}

そのための gatsby-node.js がこちら。

const config = require('./gatsby-config')
const { createRemoteFileNode } = require('gatsby-source-filesystem')
const axios = require('axios')
const camelCase = require('camelcase');
const {
    apiKey: microcmsApiKey,
    serviceId: microcmsServiceId,
    apis: microcmsApis
} = config.plugins
    .filter(plugin => plugin.resolve === 'gatsby-source-microcms')[0]
    .options
const microcmsPrefix = 'Microcms'
const localPrefix = 'local'
const ignoreFields = []

/**
 * フィールドがmicroCMSの画像型か判定する
 * @param {*} field 
 * @returns 
 */
const isMicroCmsImageField = field => {
    return (
        field &&
        typeof field === 'object' &&
        field.url &&
        field.width &&
        field.height
    )
}
/**
 * フィールドがmicroCMSの画像配列型か判定する
 * @param {*} field 
 * @returns 
 */
const isMicroCmsImageListField = field => {
    if (!Array.isArray(field)) {
        return false;
    }
    
    if (field.length === 0) {
        return false
    }

    for (const el of field) {
        if (!isMicroCmsImageField(el)) {
            return false
        }
    }

    return true
}


exports.createSchemaCustomization = async ({ actions }) => {
    const { createTypes } = actions
    /**
     * microCMSにはスキーマを取得するAPIがないため
     * 全てのコンテンツを取得して画像型のフィールドを検索する必要がある
     */
    for (const api of microcmsApis) {
        const endpoint = api.endpoint
        const url = `https://${microcmsServiceId}.microcms.io/api/v1/${endpoint}`

        let offset = 0
        const limit = 10
        const imageFields = []
        const imageListFields = []
        while (true) {
            const res = await axios.get(url, {
                params: { offset, limit },
                headers: {
                    'X-MICROCMS-API-KEY': microcmsApiKey,
                }
            })
            for (const content of res.data.contents) {
                for (const field in content) {
                    if (ignoreFields.includes(field)) {
                        continue
                    }
                    if (imageFields.includes(field)) {
                        continue
                    }
                    if (imageListFields.includes(field)) {
                        continue
                    }
                    if (isMicroCmsImageField(content[field])) {
                        imageFields.push(field)
                        continue
                    }
                    if (isMicroCmsImageListField(content[field])) {
                        imageListFields.push(field)
                        continue
                    }
                }
            }

            offset += limit
            if (offset >= res.data.totalCount) {
                break
            }
        }
        
        for (const field of imageFields) {
            const typeName = camelCase(
                [microcmsPrefix, endpoint],
                { pascalCase: true }
            )
            const fieldName = camelCase(
                [localPrefix, field],
                { pascalCase: false }
            )
            createTypes(`
                type ${typeName} implements Node {
                    ${fieldName}: File @link(from: "fields.${fieldName}.id")
                }
            `)
        }
        
        for (const field of imageListFields) {
            const typeName = camelCase(
                [microcmsPrefix, endpoint],
                { pascalCase: true }
            )
            const fieldName = camelCase(
                [localPrefix, field],
                { pascalCase: false }
            )
            createTypes(`
                type ${typeName} implements Node {
                    ${fieldName}: [File] @link(from: "fields.${fieldName}.id")
                }
            `)
        }
    }
}

exports.onCreateNode = async ({ node, actions, createNodeId, cache }) => {
    const { createNode, createNodeField } = actions
    if (node.internal.owner === 'gatsby-source-microcms') {
        await Promise.all(Object.keys(node)
            .map(async field => {
                if (ignoreFields.includes(field)) {
                    return
                }
                // *** フィールドが画像型の場合 ***
                if (isMicroCmsImageField(node[field])) {
                    console.log(`start: ${node[field].url}`);
                    return createRemoteFileNode({
                        url: node[field].url,
                        parentNodeId: node.id,
                        cache,
                        createNode,
                        createNodeId,
                    }).then(fileNode => {
                        console.log(`end: ${node[field].url}`);
                        const name = camelCase(
                            [localPrefix, field],
                            { pascalCase: false }
                        )
                        return createNodeField({
                            node,
                            name,
                            value: fileNode,
                        })
                    })
                }
                // *** フィールドが画像型リストの場合 ***
                if (isMicroCmsImageListField(node[field])) {
                    return Promise.all(node[field].map(img => {
                        console.log(`start: ${img.url}`);
                        return createRemoteFileNode({
                            url: img.url,
                            parentNodeId: node.id,
                            cache,
                            createNode,
                            createNodeId,
                        }).then(() => console.log(`end: ${img.url}`))
                    })).then(fileNodes => {
                        const name = camelCase(
                            [localPrefix, field],
                            { pascalCase: false }
                        )
                        return createNodeField({
                            node,
                            name,
                            value: fileNodes,
                        })
                    })
                }
            }))
    }
}

GatsbyでmicroCMSの画像を扱う

By Katsuya Endoh, 2024-05-23