😗

Hugo × Vercel × tcardgen で動的な OGP 画像を設定

2022/02/20に公開

ざっくり

Hugo で作成している静的サイトに OGP 画像を動的に設定したくなったので, 色々と試行錯誤しました.
思いのほか骨が折れたので, 結果をまとめることにしました.

変更前
smile_meta_image.png

変更後
dynamic ogp image

OGP 画像を作成するツール

tcardgen を使いました.
tcardgen はテンプレートと設定を用意すれば, 元になるマークダウンファイルのフロントマターから情報を抜き出して OGP 画像を作成してくれるツールです.
OGP 画像を生成するには サイトのOGP画像を自動生成する で紹介されるように他にも色々あるようです.
そんな中で tcardgen を選んだ理由は, テンプレートとなる画像と設定だけ用意すれば OGP 画像を作ってくれるため簡単そうだったからです.
しかし, 後述するように, Vercel との相性を考えると, 上のサイトで紹介されるような node-canvas を使った方法の方が楽ちんだったかもしれません.

tcardgen をカスタマイズ

使い方は簡単な tcardgen ですが, 私の用途からすると困ったところもありました.
そこで, 自分用に少しカスタマイズを行いました.

なぜカスタマイズしたか

tcardgen の困ったところは, フロントマターに必須の項目があることです.
例えば, authorcategories が必須であり, この2つが自分にとっては厄介でした.

まず, author については, 自分しか記事を書かないので, author: qawatake で固定したいです.
しかし, tcardgen では author のデフォルト値を指定することはできません.
つまり, すべての記事に author: qawatake を記載する必要があります.
これはちょっと冗長です.
さらに, Obsidian → Hugo の運用方法 で説明したように, 自分は Obsidian から Hugo 用に記事を自動生成しているため, あらかじめ Obsidian 内のファイルで author:qawatake を記載しなければいけなくなります.
まぁ大した手間ではないので記載してもいいのかもしれないですが, 個人的には無駄な情報をノートに記載したくないので, できればファイルで明示的に指定せずとも, author: qawatake として処理してもらいたかったです.

次は, categories です.
私はノートの分類に tags を使用しています.
そして, tcardgen も実際 tags に対応しています.
個人的には, この2つを設定する必要性が感じられません.
ノートのカテゴリー分類には categoriestags のどちらかで十分でしょう.
ということで, categories を指定しなかった場合, 関連する処理をスキップして欲しくなりました.

カスタマイズ内容

↑で説明したような困ったことに対処するため, 次のように仕様を変更しました.

  1. -a フラグでデフォルトの author を指定できるようにする
  2. categories が指定されなかった場合, エラーを発生させず, その部分の処理をスキップする.

ありがたいことにそれほど変更せずとも, ↑の仕様を実現できました.
カスタマイズしたソースコードは GitHub - qawatake/tcardgen: Generate a TwitterCard(OGP) image for your Hugo posts. にあります.

Hugo 側の設定

これまではどの投稿も同じ OGP 画像を参照していましたが, これからはそれぞれ異なる画像を参照させなければいけません.
私の場合 ↓ のようなものを設定しました. (使用するテーマによって指定箇所は違います.)

...
{{- $tcardRelativeImagePath := path.Join "tcard" (print .File.BaseFileName ".png") -}}
{{ if .Params.meta_image }}
  {{ with .Params.meta_image }}
    <meta property="og:image" content="{{ . | absURL }}">
    <meta property="og:image:url" content="{{ . | absURL }}">
    <meta name="twitter:card" content="summary_large_image">
    <meta name="twitter:app:name:iphone" content="{{ $.Site.Title }}">
    <meta property="twitter:title" content="{{ $.Param "title" }}">
    <meta property="twitter:description" content="{{ $.Param "description" }}">
  {{ end }}
{{ else if os.FileExists (path.Join "static" $tcardRelativeImagePath) }}
  {{ with $tcardRelativeImagePath }}
    <meta property="og:image" content="{{ . | absURL }}">
    <meta property="og:image:url" content="{{ . | absURL }}">
    <meta name="twitter:card" content="summary_large_image">
    <meta name="twitter:app:name:iphone" content="{{ $.Site.Title }}">
    <meta property="twitter:title" content="{{ $.Param "title" }}">
    <meta property="twitter:description" content="{{ $.Param "description" }}">
  {{ end }}
...

テンプレート画像の作成

Figma を使って作成してみました.
Figma は初めてでしたが, 今回のような画像なら簡単に作れました.

template

デプロイ

これまでは, ただ GitHub に push すれば, Vercel が勝手に連携してデプロイが済んでいたわけですが, 動的に OGP 画像を生成するとなるとこれではうまくいかなさそうです.
というのも hugo コマンドでビルドする前に, tcardgen をインストールしてそれで OGP 画像を生成する, という処理が必要になるためです.
仕方ないので GitHub Actions を使って, OGP 画像の生成 & ビルド & Vercel へデプロイをすることにしました.

もし OGP 画像の生成に node-canvas を使っていれば, GitHub Actions などを使う必要はなさそうでした.
Vercel では package.json で指定したビルドコマンドを実行できるので, わざわざ他所で OGP 画像を生成しなくても Vercel 上で生成までやってしまえるようです.
GitHub Actions や Vercel CLI の設定でそれなりに骨が折れたので, そのあたりをすっとばせるのはいいなぁと今では思っています.

Vercel CLI を使ってみる

いきなり GitHub Actions を使うと, エラー, push, エラー, push, ... と面倒なことになりそうだったので, まずは手元の vercel cli でデプロイをしてみることにしました.

いきなりエラー

ところがいきなりエラーが発生してビビりました.
Image from Gyazo

検索しても同じ問題が発生している方が見つからず, 困りましたが原因は ↓ のようです.

  1. Hugo の設定で enableGitInfo=true にしている.
    • 投稿の最終更新日を Git のコミットログから取得するために必要.
  2. Vercel に CLI でデプロイする際には .git はアップロードされない.
  3. Vercel 上で hugo ビルドしようとすると .git が無いのでエラー.

ということで, 解決策としては,

  • enableGitInfo=false にする
    • 投稿の最終更新日を設定できなくなるので却下.
  • ローカルであらかじめビルドしておく

後者を採用する場合には, Vercel 上でビルドしないように, build command を空欄で上書きする必要があります.
Image from Gyazo

GitHub Actions の設定

ひとまず, CLI でデプロイできるようになったのであとは GitHub Actions で自動化するだけです.
GitHub Actions の書き方は毎回忘れてしまい, 今回もそれなりに苦労したのですが, 最終的には ↓ のようになりました.

name: Deployment

on:
  push:
    branches:
      - main
      - staging
  pull_request:
    branches: [ main ]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true  # Fetch Hugo themes (true OR recursive)
          fetch-depth: 0    # Fetch all history for .GitInfo and .Lastmod

      - name: Setup Go environment
        uses: actions/setup-go@v2.2.0
        with:
            go-version: ^1.17.0

      - name: Install tcardgen
        run: go install github.com/qawatake/tcardgen@latest

      - name: Generate ogp images
        run: tcardgen -a qawatake  -f ogp/font -o static/tcard -c ogp/config.yaml obsidian/public/notes/*.md

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: '0.91.2'
          extended: true

      - name: Build (Preview)
        run: hugo --baseURL "https://${{ secrets.PREVIEW_DOMAIN }}"
        if: ${{ github.ref != 'refs/heads/main' }}

      - name: Build (Production)
        run: hugo --baseURL "https://${{ secrets.PRODUCTION_DOMAIN }}"
        if: ${{ github.ref == 'refs/heads/main' }}

      - name: Deploy to Vercel (Preview)
        uses: amondnet/vercel-action@v20
        if: ${{ github.ref != 'refs/heads/main' }}
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }} # Required
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID}}  #Required
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID}} #Required
          alias-domains: ${{ secrets.PREVIEW_DOMAIN }} #Optional

      - name: Deploy to Vercel (Production)
        uses: amondnet/vercel-action@v20
        if: ${{ github.ref == 'refs/heads/main' }}
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }} # Required
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID}}  #Required
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID}} #Required
          vercel-args: '--prod' #Optional

本当は, tcardgen の処理などは, 別途カスタムアクションを定義したほうがよさそうですが, 疲れたので当面はこれでやっていこうと思います.

Discussion