こんにちは。
HugoでサムネイルとOGP画像(SNSでシェアしたときや Google Discover などで表示される画像)を自動生成する方法について紹介します。
↓こういうやつです

OGP画像とは? #
OGP画像は、SNSでシェアされたときに出てくる画像のことです。ZennとかQiitaにもこういうものがありますよね。
また、Google Discoverでもこの画像が使用されます。
画像があると目立ちますし、パッと情報が入ってきます。クリック数を増やすためには、画像はあったほうがいいと思います。
OGP画像の標準的なサイズは 1200x630px(縦横比 約1:91:1)とのことです。
なんでこんな比率なんでしょうか……?調べたところ、スマートフォンで見たとき表示スペースを埋めない程度に画像を置きたかったっぽいです。とにかく、16:9よりもさらに若干横長です。
背景の画像を作る #
さて、ここからOGP画像を作っていきますが、まずは背景になる画像を作っていきましょう。
1200x630pxで作れば問題ないです。
私はCanvaで作りました。

周りに色をつけて、真ん中は文字を置くスペースを空けています。
作り方としては、1200x630pxでキャンバスを作って、グラデーションの青い四角形の上に角丸をつけた白い長方形を乗せているだけです。
色のついた四角形にはグラデーションを付けるとおしゃれになる気がするのでおすすめです。
必要な素材を追加する #
コードを書く前に、必要な素材を追加していきます。
OGP画像にはタイトルとサイトアイコン画像を付けますので、それらをassetsに追加しましょう。なお、外部サイトからフォントをダウンロードしてくることも可能です。
assets/img/に、ogp-bg.pngという名前で先ほどの背景画像を追加し、favicon.pngという名前でこのサイトのアイコン画像を追加しました。
また、assets/fonts/にNotoSansJP-Medium.ttfを追加しました。フォントはGoogle Fontsなどからダウンロードするのがいいと思います。今回、バリアブルフォントでは太さが指定できなかったので、スタティックフォントを使いました。
Hugoの設定をする #
ではこれに文字や画像を乗せていきましょう。
layouts/partials/にogp-image.htmlを追加して、処理を書いていきます。
コードを載せますが、いろいろ工夫したので詳しく解説していきます。
{{/* OGP画像を生成し RelPermalink を返す。生成不可の場合は空文字列。 */}}
{{ $ogpImage := .Resources.Get "ogp.png" }}
{{ if not $ogpImage }}
{{ $fontTitle := resources.Get "fonts/NotoSansJP-Medium.ttf" }}
{{ $fontSub := resources.Get "fonts/NotoSansJP-Medium.ttf" }}
{{ $baseImage := resources.Get "img/ogp-bg.png" }}
{{ if and $fontTitle $fontSub $baseImage }}
{{ if .IsPage }}
{{/* タイトルの折り返し処理(全角=2幅、半角=1幅、英単語の途中で折り返さない) */}}
{{ $maxVisual := 32 }}
{{ $titleText := "" }}
{{ $lineWidth := 0 }}
{{ $lineStart := 0 }}
{{ $totalChars := strings.RuneCount .Title }}
{{ $wordStart := 0 }}
{{ $widthAtWordStart := 0 }}
{{ $inAsciiWord := false }}
{{ if gt $totalChars 0 }}
{{ range $i := seq 0 (sub $totalChars 1) }}
{{ $char := substr $.Title $i 1 }}
{{ $isAscii := not (findRE "[^\x00-\x7F]" $char) }}
{{ $isSpace := eq $char " " }}
{{ $charWidth := 1 }}
{{ if not $isAscii }}
{{ $charWidth = 2 }}
{{ end }}
{{/* 英単語の開始を追跡(スペース・非ASCII→ASCIIの遷移で新単語) */}}
{{ if and $isAscii (not $isSpace) }}
{{ if not $inAsciiWord }}
{{ $wordStart = $i }}
{{ $widthAtWordStart = $lineWidth }}
{{ end }}
{{ $inAsciiWord = true }}
{{ else }}
{{ $inAsciiWord = false }}
{{ end }}
{{ $newLineWidth := add $lineWidth $charWidth }}
{{ if gt $newLineWidth $maxVisual }}
{{ if and $inAsciiWord (gt $wordStart $lineStart) }}
{{/* 英単語の前で改行(単語を次の行へ) */}}
{{ $line := substr $.Title $lineStart (sub $wordStart $lineStart) }}
{{ $titleText = printf "%s%s\n" $titleText $line }}
{{ $lineStart = $wordStart }}
{{ $lineWidth = add (sub $lineWidth $widthAtWordStart) $charWidth }}
{{ $widthAtWordStart = 0 }}
{{ else }}
{{/* 現在位置で改行(日本語・スペース・行頭からの長い単語) */}}
{{ $line := substr $.Title $lineStart (sub $i $lineStart) }}
{{ $titleText = printf "%s%s\n" $titleText $line }}
{{ $lineStart = $i }}
{{ $lineWidth = $charWidth }}
{{ if $inAsciiWord }}
{{ $wordStart = $i }}
{{ $widthAtWordStart = 0 }}
{{ end }}
{{ end }}
{{ else }}
{{ $lineWidth = $newLineWidth }}
{{ end }}
{{ end }}
{{ $lastLine := substr $.Title $lineStart (sub $totalChars $lineStart) }}
{{ $titleText = printf "%s%s\n" $titleText $lastLine }}
{{ end }}
{{/* 中央にタイトル */}}
{{ $articleTitle := images.Text $titleText (dict "font" $fontTitle "color" "#333333" "size" 60 "linespacing" 10 "x" 600 "y" 315 "alignx" "center" "aligny" "center") }}
{{/* 右下にタグ(1つ目のみ) */}}
{{ $tagText := "" }}
{{ if .Params.tags }}
{{ $tagText = printf "#%s" (index .Params.tags 0) }}
{{ end }}
{{ $tagArea := images.Text $tagText (dict "font" $fontSub "color" "#666666" "size" 30 "x" 1000 "y" 430 "alignx" "right") }}
{{/* 左下にファビコンと著者名 */}}
{{ $faviconFilter := "" }}
{{ $faviconRaw := resources.Get "img/favicon.png" }}
{{ if $faviconRaw }}
{{ $favicon := $faviconRaw.Resize "96x96" }}
{{ $faviconFilter = images.Overlay $favicon 120 445 }}
{{ end }}
{{ $authorName := site.Params.author.name | default "tamate" }}
{{ $authorArea := images.Text $authorName (dict "font" $fontSub "color" "#666666" "size" 40 "x" 250 "y" 480) }}
{{/* 右下にブログ名 */}}
{{ $siteTitleArea := images.Text site.Title (dict "font" $fontSub "color" "#666666" "size" 40 "x" 1050 "y" 480 "alignx" "right") }}
{{/* 画像を合成 */}}
{{ if $faviconFilter }}
{{ $ogpImage = ($baseImage | images.Filter $articleTitle $tagArea $authorArea $siteTitleArea $faviconFilter) }}
{{ else }}
{{ $ogpImage = ($baseImage | images.Filter $articleTitle $tagArea $authorArea $siteTitleArea) }}
{{ end }}
{{ else }}
{{/* トップページ等はサイト名のみを中央に表示 */}}
{{ $siteTitleLarge := images.Text site.Title (dict "font" $fontTitle "color" "#333333" "size" 80 "x" 600 "y" 315 "alignx" "center" "aligny" "center") }}
{{ $ogpImage = ($baseImage | images.Filter $siteTitleLarge) }}
{{ end }}
{{ end }}
{{ end }}
{{ with $ogpImage }}{{ .RelPermalink }}{{ end -}}全角・半角文字を考慮した文字の折り返し #
文字数だけをもとに折り返すと、全角文字と半角文字で幅が違うことによって見た目が悪くなってしまうので、全角半角を判断して、文字幅によって折り返すかどうかを判断しています。
半角文字かどうかを判定している部分、キーコードが0x00~0x7Fかどうかで判断しているので、äとかéとかは全角の幅として処理されてそう……まあいいか
英語の単語の禁則処理 #
英語は単語の途中で改行されると気持ち悪いので、スペースの位置を確認して改行するようにしています。
全角半角と英単語の禁則処理によってコードが複雑になってしまってます……
文字とアイコンを表示 #
文字とアイコンを座標を指定して表示しています。記事のタイトルや設定された著者名を自動で取得して表示するようになっていますが、フォールバックの文字列がtamateなどとなっている場所がありますので、表示する文字列は適宜変更してください。
文字を表示する座標や大きさ、文字色なども変えられますので、カスタマイズしてみてください!
Webフォントを使用する場合は #
Webフォントを使用する場合は、
{{ $fontTitle := resources.Get "fonts/NotoSansJP-Medium.ttf" }}の部分を
{{ $fontTitle := resources.GetRemote "https://example.com/hogefont.ttf" }}などとしてください。
まとめ #
ということで今回は、OGP画像を自動で作成する方法を紹介しました。
一度設定しておけばずっと自動でOGP画像が作られ、SNSで共有されたときの見た目が豪華になるのでおすすめです。