このブログは meta
タグに埋め込まれている通り、Hugo を使って作られている。
The world’s fastest framework for building websites |...

Hugo はかなりメジャーな静的サイトジェネレーターの1つであるので、たいていのことは調べれば情報が出てくるだろう。このブログもほとんどは公式ドキュメントにある基本的な機能で構成されている。この記事では Hugo を Firebase Hosting で運用するいくつかの Tips と個人的な課題を残す。
目次
Hugo
設定ファイルの切り替え
開発環境 (development)、開発環境 (staging)、本番環境 (production) でそれぞれ設定ファイルを切り替えることができる。例えば、Google Analytics のタグコードを本番環境のみに埋め込むなどの状況で役立つ。
Configure Hugo | Hugo

ちなみに、Google Analytics については、これから導入するのであれば Google Analytics 4 一択だが1、Hugo 的には Universal Analytics2 にも対応している。
Embedded templates | Hugo

aタグ
通常のマークダウンリンクをビルドすると、a
タグに target="_blank"
を付けることができないが3、Render Hooks という機能を使うことで、マークダウンのレンダリング機能を上書きすることができる。
Render hooks | Hugo

target="_blank"
を付ける例は上記ドキュメントに例として紹介されている。
1<a href="{{ .Destination | safeURL }}"{{ with .Title}} title="{{ . }}"{{ end }}{{ if strings.HasPrefix .Destination "http" }} target="_blank" rel="noopener"{{ end }}>{{ .Text | safeHTML }}</a>
しかし、この例では、リンクが http で始まらない場合、期待したように動かない。例えば、ref
や reflink
といった shortcode と組み合わせた場合などにも機能させるには修正が必要である。
現状、そこまでこのブログでも対応しきれていないので、追々対応したい課題である。Markdown のパースと shortcode の展開順序を考えるに、一筋縄ではいかなさそう・・・。
こういうときに、Go が書けると良かったになぁと思わないこともないが、同時に、このためだけにわざわざ Go に手を出すのもなぁと思う当たり、歳を取ってしまったと実感する・・・
コメント機能
現状、各記事にコメント機能を実装していない。このブログを開発中の段階では、しばらくコメントは Twitter で受け付ければ問題なかろうと考えていたが、数ヶ月の間にイーロン・マスク体制の Twitter に様変わりし、大きく状況が変わってしまった。
コメント機能を静的サイトに付けるには Disqus が有名だろうか。Hugo ではこれ以外にもいくつかのサポートがなされているらしい。
Comments | Hugo

コメントでのやりとりも含めて1つのコンテンツと捉えると、Disqus のようなサービスを利用することはこのブログの方針に反する。
大事なものはきちんと手の届くところに置いておきたい。
https://tsukasa.oomo.to/2022/10/31/open-a-new-blog/
そうなると、セルフホストで同様なものを探すか作るかしないといけないが、せっかくほぼ Firebase Hosting で運用が完結しているのに勿体ないなぁという気持ちが芽生える。コメント機能は様子を見つつ、もう少し考える事とする4。
全文検索機能
全文検索機能もあれば便利だなと思うけども、未対応である。
Search tools | Hugo

幸い、既に Google のインデックスには含まれているみたいなので、優先度は低い。どうしても必要な時はサイト検索を使えば良いだろう・・・。
Firebase Hosting
Firebase Hosting
Hugo でのビルド成果物を Firebase Hosting にデプロイし、運用している。公式ドキュメントにも(これだけ見てもどうしようもできない程度の)言及がある。
Host on Firebase | Hugo

基本的に無料で Firebase Hosting の利用は出来るが、Cloud Functions for Firebase を利用するためには有料プランに登録する必要がある。詳細は料金プランにて比較できる。
Firebase Pricing

従量課金制プランになると、最悪ケース、悪質な攻撃の対象となるとクラウド破産の恐れがある。最低でも、月のアクセス推定量から予算とアラートを厳しめに設定しておく必要はあるだろう。ただし、予算とアラートを設定したところで、リソースや API の使用量は制限されないの気休め程度にしかならない5。
Cloud Functions
元々の Cloud Functions には Python ランタイムが存在するが、残念ながら Cloud Functions for Firebase は Node.js ランタイムにしか対応していない6。私は JavaScript には不慣れで、何か作ろうとする度に Google で(恐らく)基本的な文法を検索している。
これでは少し複雑な機能を開発するには効率が悪く厳しい・・・。ということで、GCP の Cloud Functions も利用している。
ブログカードの表示
具体例として、The Open Graph protocol (OGP) タグの取得が挙げられる。Hugo は静的サイトジェネレーターという性質のため、下のようなブログカードを表示するために、OGP タグをビルド時なのか、デプロイ時なのか、表示時なのか、どこかで取得してくる必要がある。
株式会社AlphaImpact

ブログカードに限らず、様々なメディアカードを表示するサービスとして Embedly というのを見つけたが、無料プランだと Embedly のブランド表示が付与されてしまうため、若干見た目がださい。これを消すためには、有料プランを検討する必要があるが、ただブログカードを表示するためだけに、毎月 $9 も支払うのは正直無駄のように感じる。
Cards are a clean, responsive, and shareable way to...

そこで、OGP タグを取得するだけのサーバーレスアプリをぱぱっと作成して、Cloud Functions にデプロイした。実装のメイン部は下記のようになった。
1ogp = {}
2for meta in soup.select(r"head > meta[property^=og\:]"):
3 _content = meta.get('content', '').rstrip()
4 # ex.) 'og:title' -> (og, title) -> 'title'
5 # ex.) 'og:image:height' -> (og, image, height) -> 'image:height'
6 _property = ':'.join(meta.get('property', '').split(':')[1:])
7 ogp[_property] = _content
8else:
9 # OGPが設定されていないとき、その他のタグから同様の情報を取得する
10 if (title := soup.select_one(r"head > title")):
11 title = title.string.strip()
12 ogp['title'] = title
13 ogp['site_name'] = title
14 if (description := soup.select_one(r"head > meta[name=description]")):
15 description = description.get('content', '').strip()
16 ogp['description'] = description
17 ogp['url'] = url
対象ページを取得したら、Beautiful Soup 4で meta
タグを解析し、OGP タグを抽出している。
Cloud Armor
Cloud Functions (for Firebase) は関数をデプロイすると、エンドポイントさえ知っていれば、デフォルトで誰でも関数を叩けてしまう。アクセス制限を導入するために Cloud Armor を導入した。
Cloud Armor のネットワーク セキュリティ | Google Cloud

固定IPを持つサーバーを保有しているため、そのIPからのみアクセス可能なルールを現状運用しているが、他にも日本国内のみアクセス可能といったルール (origin.region_code == 'JP'
) の設定も可能である。
他にもよりクオリティの高い関連記事の表示などにも手を付けられると思うが、まだそこまで手が回っていない。必要最低限の機能は揃っているので、今は、そもそものコンテンツ充実が最優先であった。