📝
Hugoだけでやりきるブログカード生成
こんにちは。リケイのオコジョです。
Web開発やSaaS関連の記事を書いています。
はじめに
Hugoのみで完結するブログカード生成を行います。
従来はサーバ環境を準備し必要なタグをopen-graph-scraperでshortcodeに埋め込んでいました。
ちなみに 「ブログカード」 とは以下のリンクの表示です。
Hugoでサイト構築しているので是非見に来てください!
Hugoでブログカードを生成する
zennではURLを貼り付けるだけでカードを生成してくれます。
Hugoにはshortcodeという仕組みが準備されているのでこれを使いましょう。
今回は以下のような書き心地を目指します。
markdown
{{< webcard "https://mounmou.com/" >}}
ブログカードを生成するshortcode
この記事を参考にGetRemoteを用いたshortcodeを書いてきます。
また、Tailwind CSSを使っています。
HugoからTailwind CSSを使う場合は以下を参考にしてください。
実際のコードはこちら↓
shortcodeはこちら
webcard.html
<!--
引数で与えられたURLをパースする。パース後の構造はドキュメントを参照。
ドキュメント: https://gohugo.io/functions/urls.parse/
-->
{{- $url := (.Get 0) -}}
{{- $u := urls.Parse $url -}}
<!-- 引数のURL先をごっそりとGetする -->
{{- with $result := resources.GetRemote $url -}}
<!-- Status Code が 4XX/5XX などGetに失敗する場合 -->
{{- with $result.Err -}}
<div class="web-card hover:cursor-not-allowed h-[120px] bg-white border shadow rounded-lg my-4 sm:my-6 px-0 overflow-hidden">
<div class="flex">
<div class="flex h-[120px] w-[120px] max-w-[230px] bg-gray-300 justify-center items-center text-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8">
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
</div>
<div class="flex flex-1 flex-col justify-between bg-gray-50 py-2 px-4 sm:py-3 sm:px-5 ">
<div class="flex flex-1 flex-col gap-y-1">
<div class="text-sm sm:text-base font-bold text-gray-800 leading-normal break-words line-clamp-2">
{{ $u.Host | truncate 30 }} にアクセスできません
</div>
</div>
<div class="flex flex-1 flex-col justify-end">
<div class="flex flex-row items-center">
<img src="https://www.google.com/s2/favicons?sz=14&domain_url={{ $u.Scheme }}://{{ $u.Host }}" class="fav-icon mr-1" alt="{{ $u.Host }} favicon image">
<span class="text-xs sm:text-sm text-gray-600">{{ $u.Host | truncate 30 }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Status Code が 2XX などGetに成功した場合 -->
{{- else -}}
{{- $title := "" -}}
{{- $description := "" -}}
{{- $image := "" -}}
{{- $find := "" -}}
<!-- Youtubeはbody内にmetaタグがあるので場合分け(m.youtube.comはhead内にあるのに...) -->
{{- if eq $u.Hostname "www.youtube.com" -}}
{{- $find = index (findRE "<body.*?>(.|\n)*?</body>" $result.Content) 0 -}}
<!-- head内のタグを正規表現で取得 -->
{{- else -}}
{{- $find = index (findRE "<head.*?>(.|\n)*?</head>" $result.Content) 0 -}}
{{- end -}}
<!-- metaタグ内のパラメータを変数にマッピング -->
{{- range $meta := findRE "<meta.*?>" $find -}}
{{- $name := replaceRE "<.*name=\"(.*?)\".*>" "$1" $meta -}}
{{- $property := replaceRE "<.*property=\"(.*?)\".*>" "$1" $meta -}}
{{- $content := replaceRE "<.*content=\"(.*?)\".*>" "$1" $meta -}}
{{- if eq $property "og:title" -}}
{{- $title = $content -}}
{{- else if eq $property "og:description" -}}
{{- $description = $content -}}
{{- else if eq $property "og:image" -}}
{{- $image = $content -}}
{{- end -}}
{{- if and (eq $description "") (eq $name "description") -}}
{{- $description = $content -}}
{{- end -}}
{{- end -}}
<!-- 変数をshortcodeのHTMLに埋め込み -->
<div class="web-card group h-[120px] bg-white border shadow rounded-lg my-4 sm:my-6 px-0 overflow-hidden">
<a href="{{ $url }}" class="flex group-hover:no-underline">
<div class="h-[120px] w-[120px] sm:w-auto max-w-[230px]">
<!-- og:image が取得できた場合 -->
{{ with $image }}
<img src="{{ htmlUnescape $image }}" alt="{{ $description }}" class="m-0 h-full w-full object-cover shrink-0">
<!-- og:image が取得できなかった場合 -->
{{ else }}
<div class="flex h-[120px] w-[120px] max-w-[230px] bg-gray-300 justify-center items-center text-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-8 w-8">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
</svg>
</div>
{{ end }}
</div>
<div class="flex flex-1 flex-col justify-between py-2 px-4 sm:py-3 sm:px-5 transition ease-in-out duration-100 group-hover:bg-primary-color-50">
<div class="flex flex-1 flex-col gap-y-1">
<div class="text-sm sm:text-base font-bold text-gray-800 leading-normal break-words line-clamp-2">
<!-- og:title が取得できた場合 -->
{{ with $title }}
{{ $title }}
<!-- og:title が取得できなかった場合 -->
{{ else }}
{{ $u.Host | truncate 30 }}
{{ end }}
</div>
<div class="text-xs sm:text-sm text-gray-600 line-clamp-2">
{{ $description | truncate 100 }}
</div>
</div>
<div class="flex flex-1 flex-col justify-end">
<div class="flex flex-row items-center">
<img src="https://www.google.com/s2/favicons?sz=14&domain_url={{ $u.Scheme }}://{{ $u.Host }}" class="fav-icon mr-1" alt="{{ $u.Host }} favicon image">
<span class="text-xs sm:text-sm text-gray-600">{{ $u.Host | truncate 30 }}</span>
</div>
</div>
</div>
</a>
</div>
{{- end -}}
{{- end -}}
生成されるブログカード
まずは全てのパラメータがうまく返却された場合です。
次にog:image
、og:title
、og:description
が返ってこない場合の表示です。
ドメインをそのままタイトルに当てています。
最後に無効なドメインの場合です。
hover:cursor-not-allowed
を指定し、クリックできないことを表現しています。
ページの性質に応じて、そもそも表示させないといった選択も必要です。
まとめ
Hugoのみで完結するブログカード生成する方法をまとめました。
次回はHugoのみで完結する動的OGPについてまとめます。
参考記事
Discussion