きゃべログ

Gatsby.js製ブログで記事毎にOGP画像を自動生成する

JavaScript

概要

Kentaro Matsushitaさんが開発されたOGP画像生成ライブラリ「catchy-image」を使用して、Gatsby.js製ブログにOGPを自動生成・設定する実装をしました。お手軽に記事タイトルに応じたOGP生成ができる素晴らしいライブラリです。gatsby-starter-blogをベースに構築したブログを想定し、導入までの作業をメモしておきます。

完成したOGP画像の例

記事のタイトルとサイト名、アイコンが記載されたシンプルなOGP画像が完成しました。

完成したOGP画像

執筆時構成

  • node
    • v12.15.0
  • gatsby
    • 2.27.5
  • catchy-image
    • 0.1.6

最新の情報はcatchy-imageリポジトリのREADMEをご参照ください。

手順

catchy-imageをインストール

catchy-image パッケージをインストールします。

npm install --save catchy-image

Gatsby.jsでビルド時にOGP画像を生成するプラグインを作成する

catchy-image は与えられたメタデータ (Title, Author, Iconなど) の情報を元にOGP画像を自動生成するライブラリです。 そのため、Gatsbyの記事に応じてTitleなどを設定するため、Gatsbyプラグインを自作する必要があります。

でもプラグインを作成するといっても難しいことはしません。 数行の index.jspackage.json を作成し、配置するだけです。 今回は公式に倣って自作プラグインの名前を gatsby-remark-og-image として設定します。

詳しいプラグインの作り方はGatsby公式のドキュメントを参照するとよいでしょう。

Creating a Remark Transformer Plugin

ディレクトリ構成

リポジトリルートに plugins というディレクトリを作成して、その中にプラグイン名のディレクトリを作成します。 そしてさらにその中に index.jspackage.json を配置します。

gatsby-config.js
...
plugins
└ gatsby-remark-og-image
  ├ index.js
  └ package.json

index.jsの中身

catchy-image 作者の方のブログ記事に書いてあるものをそのまま持ってくれば動きます。 私は画像生成先のディレクトリを1階層深くしたかったので少しだけいじっています。 gatsby-config.js に設定を書きますが、ここで参照していないものは無効なので要注意です。 具体的には、options.output.directorygatsby-config.js 内で変更しても、ライブラリの実行部分(index.js)で参照されていないと何も変わりません。

const catchy = require('catchy-image')

module.exports = async ({ markdownNode }, pluginOptions) => {
  
  // gatsby-config.jsの設定情報とマークダウンのメタデータを画像生成ライブラリの引数に渡す
  const result = await catchy.generate({
    ...pluginOptions,
    output: {
      ...pluginOptions.output,
      directory: `./public/blog/${markdownNode.fields.slug}`,      fileName: pluginOptions.output.fileName,
    },
    meta: {
      ...pluginOptions.meta,
      title: markdownNode.frontmatter.title
    }
  })

  console.info(`gatsby-remark-og-image: Successful generated: ${result}`)
}

package.jsonの中身

外部公開の予定はなく、またエントリーポイントや名前が定義されていれば動くと思うので、最低限の情報だけ定義しました。

{
    "name": "gatsby-remark-og-image",
    "description": "Generate OGP image from article title.",
    "version": "1.0.0",
    "main": "index.js"
}

使用するフォントをリポジトリに追加する

私は Noto Sans JP の Bold を使いたいので Google FontsのNoto Sans JPのページからフォントファミリーをダウンロードしてきます。 保存したZIPファイルを解凍し、「NotoSansJP-Bold.otf」を取り出します。そして src/assets/fonts/ ディレクトリを作成し、そこにフォントファイルを設置します。

gatsby-config.jsを設定する

gatsby-config.jsgatsby-transformer-remark の plugin として、先ほど作った gatsby-remark-og-image を設定します。 ここで設定した値は index.js から pluginOptions を介して呼び出します。 フォント、背景画像、アイコンなどは必要に応じて変更します。

記事タイトルの長さによってはタイトル文字列が入りきらない旨のエラーが発生します。 その場合は長いタイトルを短くするか、25行目のfontSizeを小さくしましょう。具体的に何文字以内まで大丈夫かは計測していませんが、fontSize=64でエラーがでないくらいの長さが読みやすさ的にもちょうどよいと思います。

module.exports = {
  plugins: [
    {
      resolve: `gatsby-transformer-remark`,
      options: {
        plugins: [
          {
            resolve: `gatsby-remark-og-image`,
            options: {
              output: {
                directory: '',
                fileName: 'thumbnail.png'
              },
              image: {
                width: 1200,
                height: 630,
                backgroundImage: './src/assets/images/og-background.jpg'                //backgroundColor: '#e2eadd',
              },
              style: {
                title: {
                  fontFamily: 'Noto Sans JP',                  fontColor: '#333333',
                  fontWeight: 'Bold',
                  fontSize: 64,
                  paddingTop: 100,
                  paddingBottom: 200,
                  paddingLeft: 150,
                  paddingRight: 150,
                },
                author: {
                  fontFamily: 'Noto Sans JP',                  fontColor: '#333333',
                  fontWeight: 'Bold',
                  fontSize: 42,
                }
              },
              meta: {
                title: '',
                author: 'kyabe.net'              },
              fontFile: [
                {
                  path: require.resolve('./src/assets/fonts/NotoSansJP-Bold.otf'),                  family: 'Noto Sans JP',                  weight: 'bold',
                },
              ],
              iconFile: require.resolve('./content/assets/profile-pic.png'),              timeout: 10000,
            },
          },
        ],
      },
    },
  ],
}

og:imageメタタグを設定する

次のようなメタタグを設定します。

<meta property="og:image" content="https://kyabe.net/blog/gatsby-auto-generate-ogp-image/thumbnail.png" data-react-helmet="true">

ヘッダタグのコンポーネントの設定

React Helmetを使っている場合であれば、SEO用の共通コンポーネントで次のような image 引数をとれるようにしておくと柔軟かと思います。 下記はSEOコンポーネントの例です。image 指定がなければデフォルトの画像をOGP画像として設定するようにしています。

import React from "react"
import PropTypes from "prop-types"
import { Helmet } from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"
import ogp_image from "../../content/assets/default_og_image.jpg"

const SEO = ({ description, lang, meta, title, image }) => {  const { site } = useStaticQuery(
    graphql`
      query {
        site {
          siteMetadata {
            title
            description
            siteUrl
          }
        }
      }
    `
  )

  const titleText = `${title ? title + " | " : ""}${site.siteMetadata.title}`
  const siteUrl = site.siteMetadata.siteUrl
  const defaultImage = `${siteUrl}${ogp_image}`

  return (
    <Helmet
      htmlAttributes={{
        lang,
      }}
      title={titleText}
      meta={[
        // 中略
        {
          property: "og:image",
          content: image || defaultImage,        },
      ].concat(meta)}
    />
  )
}

SEO.defaultProps = {
  lang: `en`,
  meta: [],
  description: ``,
  image: null,
  title: ``
}

SEO.propTypes = {
  description: PropTypes.string,
  lang: PropTypes.string,
  meta: PropTypes.arrayOf(PropTypes.object),
  title: PropTypes.string.isRequired,
  image: PropTypes.string,
}

export default SEO

記事テンプレートの設定

上記SEOコンポーネントを blog-post.js から呼び出します。 imageの実引数にはOGP画像への絶対パスを指定する必要があります。 相対パスだとうまく表示されないので気を付けましょう。

import React from "react"

// Components
import Layout from "../components/layout"
import SEO from "../components/seo"

const BlogPostTemplate = ({ data, pageContext, location }) => {
  const post = data.markdownRemark
  const siteTitle = data.site.siteMetadata.blogTitle
  const { previous, next } = pageContext

  return (
    <Layout location={location} title={siteTitle}>
      <SEO
        title={post.frontmatter.title}
        description={post.excerpt}
        lang="ja"
        image={`${data.site.siteMetadata.siteUrl}/blog${post.fields.slug}thumbnail.png`}      />
    // 中略
    </Layout>
  )
}

export default BlogPostTemplate

export const pageQuery = graphql`
  query BlogPostBySlug($slug: String!) {
    site {
      siteMetadata {
        blogTitle
        siteUrl
      }
    }
    markdownRemark(fields: { slug: { eq: $slug } }) {
        // 中略
    }
  }
`

参考URL


きゃべ / Masaya Kurahashi
きゃべ / Masaya Kurahashi
Software Engineer, Product Manager