Gatsbyで動的に画像を生成する

なぜ自動生成したか

ブログを書くにあたり、サムネイル・OGを探したり作ったりする必要がありました。 Qiitaのようにただ記事を書きたいのに、画像探しなど他のところに時間を使うのが嫌でした。

なので自動化しよう!とおもいました。

仕組み

仕組みは簡単で、Contentfulからページデータを取得したときに外部のAPIに記事タイトルを渡し画像化します。

外部のAPIはnodejsで実装してVercelでデプロイしました。 Vercelにog-imageを作成できるパッケージがあるので、これをベースにしました。

やり方

APIの構築はほぼforkで使えるのでReadmeを読んでください。 PuppeteerでスクショしてるのとTS使って書かれてるぐらいなので特に難しくはなかったです。

Gatsbyでの実装

gatsby-node.jscreatePages内でリクエストを飛ばします。

gatsby-node.js
const { createRemoteFileNode } = require(`gatsby-source-filesystem`);
const { fluid } = require(`gatsby-plugin-sharp`);

exports.createPages = async ({ graphql, actions, getCache, createNodeId, cache, reporter }) => {
  const { createPage, createNode } = actions;

  const result = await graphql(
    `
      {
        allContentfulBlogPost(sort: { fields: [publishDate], order: DESC })         {
          edges {
            node {
              id
              slug
              title
            }
          }
        }
      }
    `
  )

  if (result.errors) {
    throw result.errors
  }

  // Create blog posts pages.
  const posts = result.data.allContentfulBlogPost.edges

  // ここで外部リクエストして画像を受けとっている
  const externalFluidImages = await generateImageFromPageTitle(posts, getCache, createNode, createNodeId, reporter, cache);

  const blogPostTemplate = path.resolve(`./src/templates/post.js`)

  posts.forEach(post => {
    createPage({
      path: `post/${post.node.slug}`,
      component: blogPostTemplate,
      context: {
        id: post.node.id,
        slug: post.node.slug,
        externalFluidImage: externalFluidImages.get(post.node.slug),
      },
    })
  })
}

gatsby-source-filesystemはローカルの画像をGatsbyで扱いやすくするのに使われますが、外部の画像を取り込むこともできます。 gatsby-plugin-sharpは画像Webpにしてくれます。 なので、requireしています。

あとは、クエリからブログを作ってます。

gatsby-node.js
const externalFluidImages = await generateImageFromPageTitle(posts, getCache, createNode, createNodeId, reporter, cache);

ここは長くなったので別メソッドに分けてます。 内容は下記になります。

gatsby-node.js
async function (pages, getCache, createNode, createNodeId, reporter, cache) => {
    const featureImages = new Map();

    for (page of pages) {
        const { node } = page;
        let url = `${process.env.OPEN_GRAPH_GENERATE_API}${encodeURIComponent(node.title)}.png?md=1&fontSize=100px&background&fontColor=#777`

        if (await cache.get(node.title) || featureImages.has(node.slug)) {
            continue;
        }

        const fileNode = await createRemoteFileNode({
            url: url,
            parentNodeId: node.id,
            getCache,
            createNode,
            createNodeId,
            name: node.title
        });

        const generatedImage = await fluid({
            file: fileNode,
            reporter,
            cache,
        });

        await cache.set(node.title, node.id);
        featureImages.set(node.slug, generatedImage);
        console.info('generate image from api', decodeURI(url));
    }
    return featureImages;
};

取得したページデータを引数にとり、ループしてます。 gatsby-source-filesystemcreateRemoteFileNodeでリモートファイルをダウンロードし、サイトのGraphQLに追加してます。 メソッドの詳細はこちら

取得した画像をWebp化してMapに突っ込んで返却しています。 一度作成したものはキャッシュにいれてもう生成しないようにします

あとは、ページでpageContextから受け取って使用します。 画像全部のMap自体をそのまま渡そうとしたらうまく行かなかったので、その場合はallImageSharpから取れるので、クエリ使って取得してください。

posts.js
const { allImageSharp } = useStaticQuery(graphql`
    query {
      allImageSharp {
        nodes {
          fluid(maxWidth: 1600) {
            originalName
            ...GatsbyImageSharpFluid_withWebp
          }
        }
      }
    }
  `)
About

現役フリーランスエンジニアの勉強・備忘録。
バックエンドがメイン。フロントからインフラまで色々やってます。