ブログをはてなブログからHugo+Cloudflare Pages(+R2)に移行しました

Page content

ブログをHugo+Cloudflare Pagesに移行した。とかいいつつ、これを書いてる時点ではまだローカルで hugo server -Dとかやって遊んでるレベルなのだが、今後移行に際して遭遇する様々な問題は都度書き残しておかないとどうせ忘れてしまうので、取り急ぎ筆をとった次第である。移行完了がいつになるのかはわからないけどとりあえず振り返り用の記事として用意する。

移行に至った経緯

そもそもなんで移行しようとしたのか

主に以下2点。1.による部分は結構大きい。

  1. はてなIDを変更できないのが気に食わなかった(変更したかった)。はてなIDを変更するためにはまた別にアカウントつくって任意のはてなIDを取得する必要があるということで、そうなるとはてなブログ間の移行が発生するわけだが、どうせ移行が発生するならもういっそ完全に別物にしちゃってもいいんじゃねーのという気持ちがあった。逆に言うとはてなIDが自由に変更できるならはてなブログで特に文句はなかったのだ。ここはとても惜しいところである。はてなさんに頑張ってもらいたいところ。
  2. はてなブログProを契約してたのだが、諸事情あってこれを解約したくなった。(無料に戻そうと思った)ただ、はてなブログの無料プランでは、自作のスタイルシートを使えなくなるので、無料プランに戻すと、それに頼ってる既存記事が色々影響を受ける。今更既存記事のスタイルシートを1から手入れするのも面倒だし、仮に移行するとしたらその辺の考慮も一括で実施するんだろうから、いっそ移行しちゃった方がむしろわかりやすいのでは、と思うに至った。

移行先の選定 - Hugo

実は、はてなブログにしてからの記事執筆の運用は、ローカルのテキストエディタで99%書いて、HTML/Markdownをコピーし、はてなブログの記事作成画面に貼りつける、という形で記事を作成していた。要するに、個人的なブログの更新運用は、究極的にはHTML/mdファイルをFTPでサーバーにアップロードして、Webサーバーがそれを静的コンテンツとして配信する、というのとイメージが近かった。(大分古いw)ただまあ実際には、アクセス解析、タグ・カテゴリでの分類、検索機能などの機能は必要になるので、そのイメージと完全に一致していたわけではなかったんだが。。

ともあれ、このイメージに近い運用ができるブログサービス/機能を探していた。どうせだから記事はGitで管理して、Githubにpushしたらそのまま更新される流れが望ましい。Github Pagesを使った運用の例なんかも検索で見かけたのだが、その関連でHugoというのをちょいちょい目にした。最初は少し腰が重かったんだが-というのも、「ビルド」という工程が入るのが個人的にイマイチ「ブログ」の管理と一致していなくて、これは上に書いたところからも明らかなのだが、新しく書いた記事をGithubにpushしたらそれで記事公開という流れで個人的には十分なのに、新しい記事をpushする度に過去の記事を含めて「ビルド」するという流れがどうにも肌にあわなかった。(これは正直今でもそうではある)ただ、Hugoにおける「ビルド」というのは、作成したコンテンツデータを読み込んでタグやカテゴリを起動時に整理することを「ビルド」と呼んでるにすぎないようだとわかった。next buildみたいな大仰な奴を想像していたんだが、そもそも「ビルド」専用の工程がHugoにはない(hugo serverの実行時にビルドが行われるだけ)。個々の記事の実態はただのMarkdownファイルだという点がわかると、途端に親近感がわいて、重い腰が一気に軽くなった。中身が分かるとスッキリするものだ。そんなわけでHugoにかけてみることにしたのだった。実際、後述するが、過去約10年分のブログ記事全部ひっくるめても、hugo serverの起動は約2秒前後で終わった。気にするほどではなかった。

移行先の選定 - Cloudflare Pages、Cloudflare R2

Hugoでブログ作る、といった場合の事例として、ホスティング先として検索するとCloudflare Pagesがたくさん出てくる。確かに今までCloudflare Pagesを自分の個人サービスとして本格的に利用したことはなかったので、この辺で使ってみてもいいかもしれない、と思い、使ってみることにした。ついでにCloudflare R2も、今まで使ったことがなかったので使ってみたくなった。関連情報が探せばたくさん出てくるというのもあるが、正直ここは技術的好奇心によるところが大きいw。エンジニア的な趣味というか。

移行関連

MTファイル->mdファイル

はてなブログからMTファイルがエクスポートできるので、それをHugoに使える形に整形するツールを組んだ。MTファイルは全記事が1ファイルにまとまってる形式だが、Hugoは各記事を個別の.mdファイルにわけて管理するため、MTファイルを上から順に読んでいって、情報を補完し、一記事分が終了したと見なしたら.mdファイルで出力して、、、というの感じのツールである。かなりガリガリしてるのと、カテゴリの変換や画像の抽出等は自分用の要素を多分に含むため、ここでは共有しないが、ツール実装のポイントは以下の辺りだった。同じことしようとしてる方の参考になれば。

  • AUTHOR:TITLE:といった記事のメタ情報は、基本的にはHugoのFront Matterのメタ情報部にそのまま使える(というか実際自分はほぼ流用した)。ただ、例えば記事に複数のカテゴリが設定されている場合、CATEGORY: ほにゃららみたいな文字列が2行に渡って出力されているので、実際のところはFront Matter用に少し加工が必要になるとは思う。自分の場合は、ここは内部的に配列にして、あとでJSON.stringify()でFront Matterのcategoriesに出力する、ということをした。まあその辺は良しなに。
  • 自分の場合、Front Matterのtitle:属性をダブルクォートで囲って、そこにテンプレートリテラルでtitle: "${title}"みたいにして出力する形でmdファイル作ってたので、当てはめてる変数titleにダブルクォートが入ってると、hugo serverが落ちる。このため、ダブルクォートは\"とかしてエスケープしないといけなかった。これは自分の実装の場合の考慮不足というだけの話のような気がするが、まぁ記録として残しておくことする。
  • 開発作業はRemote Containerで実施しており、Hugoでのブログ執筆も同様なのだが、実行環境のタイムゾーンを特に変更していないため、hugo newで作成する記事の日時はUTCになる。一方、はてなブログからエクスポートしたMYファイルのDATE:はJSTになっており、両者で齟齬が出る。ただいちいちタイムゾーンの調整とかするのが面倒だったので、そのまま移行した。なので当時はてなブログで投稿した日時(JST)が9時間戻った形で設定されていることになる、のだと思われる。まぁ、個人的には些細な事なので、最早気にしない。見なかったことにする。
  • はてなブログの「続きを読む」はMTファイル上ではEXTENDED BODY:という文字列で出力される。これはHugoだと<!--more-->という文字列に変換することで同じ挙動にできる。というわけでここは単純置換した。
  • はてなブログが最近(2022年だか2023年くらいに始めたという認識である)始めたブログランキング参加リンクが一部の記事に貼り付けてあったので、取っ払った。見た感じaタグに"embed-group-link js-embed-group-link"というclassがついてるようだったので、この文字列でひっかけて存在していたら取り込まない、という単純処理。
  • 一つの記事は"--------"という文字列で終わる。この文字列は記事の「終わり」にしか含まれていない。ここを1つの記事の.mdファイルの出力の条件にした。ただ、逆に言うと、当然だが記事内に"--------"という文字列があると意図せずそこを「記事の終わり」だと勘違いしてしまう可能性があるので注意が必要だ(実際、自分の過去記事に偶然この文字列がまぎれていて、デバッグに結構手間取った)。
    • なお、記事にコメントをくれた方がいた場合、自分が書いた記事の終わりにはこの文字列は来ず、コメントの終わりにこの文字列が来る。いちいち省くのが面倒だったのでそのまま記事の本文に乗せる形で移行してしまったが、まあ気になる方がいたら参考にして省いてください。
    • 似たようなので"-----"というのもそこかしこに出てくるが(記事の終わりを示す"--------"とはハイフンの個数が違う)、これはAUTHOR:TITLE:といったメタ情報部や、BODY:部やコメント部など、1つの記事内における「セクション」の終わりを示す。昔の記事には「続きを読む」が設定してなかったり(つまりEXTENDED BODY:の手前にこの文字列がない)、コメントをもらったかどうか等、1つの記事内でも色々な条件やパターンでこの文字列の登場回数が変わるので、個人的には移行ツールでこのセクション文字列を使った処理は組んでいない。基本的に全部無視した。
  • はてなブログ上で記事をMarkdownで書いていても、エクスポートしたMTファイル上では全てHTMLタグに変換された状態になる。(少なくとも自分のブログの場合はそうだった)このため、MTファイルの内容をそのまま移行する場合、後述するmarkup.goldmark.renderer.unsafe=trueの設定が必須になる。自分の場合は、それ以外の理由(今後Hugoで新たに記事を書く場合)でもHTMLで直接書きたいケースが出てくることが明確だったので、移行とは別の理由で設定したが、はてなブログをMarkdownで書いてて、かつ今後もMarkdownでしか書くつもりありませんという人にとっては、もしかしたら抵抗あるかもしれない。
  • はてなブログ上だと、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B6%DB%B5%DE%C3%E5%CE%A6">緊急着陸</a>みたいに、一部のワードが勝手に「キーワードリンク」にされるのだが、鬱陶しいことにMTファイルにはこれも一緒に出力されてしまう。面倒くさいのでこれそのままでもいいかなーと思ってたんだが、正規表現を駆使して取り除けたので一応共有しておく。こんなかんじ:
    // 「line」はMTファイルの1行分が入ってる文字列
    line = line.replace( /<a class="keyword" href="[^"]+">([^<]+)<\/a>/g , function (match, capturedText) {
                      return capturedText;
    });
    
    どうでもいいけどこれは正直機能的にどうなんだろうと思う。この機能自体は、はてなブログが自分の都合で勝手に付けているものなので、要するに執筆者(俺)が書いていないコードが勝手に追加させられている。他ブログにMTファイル食わせて機械的に移行する場合、移行先にも(それがはてなブログじゃなくても)勝手にこのリンクが付くことになって、なんだこれ?って感じになりそうな気がする。どうも設定で解除できるらしいんだが、昔このリンクが付いた記事に関してはそのままらしく、結局この対処は必要になる模様。はてなブログのマーケティング的な都合か?余計な事しないでほしかったなあ、というのが正直な感想である。。
  • 記事内に自ブログの別記事へのリンクを貼ってるケースが結構あったので、それも今回の移行に際して変換する必要があった。ただ、「自ブログへのリンク」って、後から更新しない限り通常は「過去の記事へのリンク」になってるはずなので、MTファイルを上から順に読んでいくと、そのリンクを見つけた段階では、そのリンク先が移行後にどういうパスになるかまでわからないはずだ。既存のパス(BASENAME: という部分に記述されている)を活かしててそのまま移行するなら機械的に変換できるんだろうが、自分の場合はここを少し加工して移行したので、そういうわけにはいかなかった。このため、諸々の置換や加工が終わったあと、出力した.mdファイル群を再度再帰読み込みして、リンクの変換を行うということを実施した。加えて、これは自分が悪いんだが、自ブログ内へのリンクを相対パスじゃなくて絶対パスで書いてる箇所もあって、しかもデフォルトドメインとカスタムドメインが混在していたりしており、それら旧はてなブログのドメインを書いてるところも一括で消去する必要があった。そういうのもあって、移行ツールの中では、なんだかんだでこの処理を組むのが一番手間かかったかもしれない。。相対パスで書かれている旧ブログへのリンクを新ブログのリンクに書き換える処理はこんなかんじ:
    const regex = new RegExp('<a\\s+[^>]*\\bhref\\s*=\\s*["\']\\' + oldLink + '["\'][^>]*>', 'gi');
    filedata = filedata.replace(regex, '<a href="' + newLink + '">');
    
    実際には、oldLinknewLinkの部分は、配列から取り出した変数になっていて、一度全ての.mdファイルを出力する際に、旧リンクと新リンクのマッピングを生成しており、それをもとにループして置換する処理を組んでいる。つまりこの処理は、.mdファイル×新旧リンクマッピングの二重ループ。自分でもイケてないなーとは思うが仕方ない。

画像の移行

はてなブログの画像はhttps://cdn-ak.f.st-hatena.com/というドメイン配下に配置されているようだ。実際にはもっと長くてhttps://cdn-ak.f.st-hatena.com/images/fotolife/r/xxx/20221118/20221118173715.png みたいな感じなのだが、すべての人が同じ同じパス配下にいるのかどうかはわからない。とりあえず私はこれでしたという話。

で、このURLには、単にアクセスするだけで普通に画像がかえってくるので(直リンで画像が見える)、fetchでGETリクエストして画像データ取得の後、それをCloudflare R2にUploadする形で移行した。Cloudflare R2へのアップロードは普通に@aws-sdk/client-s3で実施。具体的には、fetchresponse.arrayBuffer()を取り出しBuffer.fromでBufferにして、PutObjectCommandBodyパラメータにセットして送信、ってかんじ。

あと、アップロードの完了後、記事中の画像のURLも変換する必要がある。これはimgタグのsrc値を置換すればいいだけなのでそんなに難しくない。が、それに合わせてだが、私の場合だけか知らないが、MTファイルで出力した内容には、はてなブログの独自のattribute(classaltitempropとか)がいくつかついていて鬱陶しかったので、それも一緒に取っ払った。抜粋すると以下のような感じ。

<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/xxx/20200915/20200915001256.jpg" alt="f:id:xxx:20200915001256j:plain" title="f:id:xxx:20200915001256j:plain" class="hatena-fotolife" itemprop="image" /></p>
// 余計なattributeの除去
line = line.replaceAll('class="hatena-fotolife"', '').replaceAll('itemprop="image"', '').replace(/ alt="[^"]*"/, '').replace(/ title="[^"]*"/,'');
// src値の書き換え
line = line.replaceAll(`src="${oldUrl}"` , `src="${newUrl}"`);

移行の実績

  • 正確な数値は測ってないが、移行ツール自体の実行時間は約7分ほどだった(このツールの実行に、mdファイルの変換、画像のダウンロード・アップロード全てを含んでいる)。記事数は501(=出力ファイル数)、MTファイルが7.4MB程度なので出力ファイル数の合計もほぼ同じ(/posts配下をdu -shしたら7.3MBだった)画像数は1174、画像の合計は約220MB(意外に少ないな、という感想であった)移行ツールをブン回すだけの目的なら、高スペックのEC2を一時的にたててそのうえで実行して、実行結果をscpとかで持ってくる、とかでも良いかもしれない。この程度のボリュームならローカルで十分だとは思うが。
  • 移行ツールの作成~テスト~Cloudflare側の環境準備等等含め、移行作業に費やしたのは合計で約4~5日といったところだった。
  • ちなみに、どんなに頑張っても最終的にツールでは取り除けない部分がいくつか残存した。非常に細かいところまでツールのロジックに考慮を入れるとそれはそれでまた別のデグレを生み出しそうで、恐らくきりがなくなる。残存した部分に関しては数もそれほど多くなさそうなので、それらは後ほど手動で手直しをかける予定である。

Hugo関連

Hugo のテーマ選び

Mainroadにした。理由は主に以下。1.が一番大きいかな。

  1. その時点で使用していたはてなブログのレイアウトに大分近い印象
  2. ドキュメントがしっかりしている
  3. そこそこちゃんと更新されてる
  4. 日本語の記事も探せばそこそこ見つかった

config.toml

  • baseurlを設定した。これExampleサイトのGithubリポジトリではbaseurl = "/"って書かれていたので、これが正解だと思い込んでいたんだが、ちゃんと設定しておかないと少なくとも検索が動作しない。
  • [[Params.widgets.social.custom]] セクションにあるicon = "youtube.svg"等のアイコンの設定、コメントにもある通り layouts/partials 配下に画像置けということだと思って最初配置したんだが、どうもこれはpng画像だとだめで、hugo server起動時にエラーになる。ちなみに以下のようなエラーである:
    Error: add site dependencies: load resources: loading templates: "/workspaces/.../blog/layouts/partials/sns-icon.png:64:1": parse failed: template: partials/sns-icon.png:64: bad character U+FFFD '�'
    
    かつ、重要なことに、icon = "youtube.svg"という設定を コメントアウト していても同じエラーになる。つまりlayouts/partials 配下にそういう画像ファイルがあるだけで落ちる。よって、設定ごとばっさり削除・かつ画像ファイル自体も何も置かないという形で対応した。
  • 旧ブログを生のHTMLで書いてることが多かったので、.mdファイルにそれを埋め込む必要があった。これはこちらの記事に書かれている内容で対応できた。以下の設定をconfig.tomlに追加しただけ。簡単!
    [markup]
      [markup.goldmark]
        [markup.goldmark.renderer]
          unsafe = true
    

「続きを読む」の設定方法

これがないとトップページの一覧画面で記事の内容が全部出ちゃうので、なんとか設定したかった。これはMainroadテーマっていうかHugo自体の仕様なんじゃないかと思うんだが、特にここに関して記述が見当たらなかったので、MainroadのExample Siteのコードを覗いたところ、あっさり見つかった。要するに「続きを読む」をいれたい箇所に<!--more-->を入れればそれでいいらしい。はてなブログと同じだね。以上。

タグの値が大文字になる

こんなかんじ↓
2024/20240123_0001.png

なんでかなーと思って色々調べてたらMainroadのCSSにtext-transform: uppercase;がついてるからだった。なんでこんなことしてるんだろう。。ここ以外にも同様の設定をしている箇所がいくつかあるが、特別、目立ったのがここと.widget-taglist__linkクラスのところだったので、ここを修正した。といっても/themes/mainroad/assets/cssを直接直すんじゃなくて、/assets/css/style.cssにコピーしてきて、そっちを直した。HugoはテーマのCSSをそのまま利用もできるし、/assets/css/style.cssにスタイルシートを置けばそれをカスタマイズすることも可能だからなのだ。原因が分かってしまえば手軽なもんだった。はい。

ビルド時間

懸念していた「Hugoのビルド時間」だが、記事数501でのhugo serverの起動までの時間は、ローカルですら約3秒前後といったところで、気にしていたのが馬鹿らしくなるレベルだった。Cloudflare Pagesに至ってはpushしてからダッシュボード見に行ったらもうビルド後の成果物のアップロードが終わってたという、まさにポルナレフのレベル。「最速」を謳ってるだけあって確かに早い。

Cloudflare関連

画像置き場にR2を、ホスティングにCloudflare Pagesを使っているが、どちらも非常にスムーズに導入出来て、そのDX(Developer Experience)にはとても感動している。今のところ100点。完璧すぎてまじで何も言うことがない。むしろスムーズに導入できすぎてしまって怖いくらいだ。CDNやDNSもCloudflareと統合されてるので設定もすごい楽だったし(というかほとんど何もしていない)、現時点で機能上も何も問題なく動作している。素晴らしい。

まぁ強いて言うなら、使い方がいたってシンプルなので特に何も不満らしい不満が出なかったというところはあるかもしれない。例えばR2をとっても、AWSのS3と比べて管理画面が物凄くシンプルだったのだが、逆に言うとS3ほどの複雑な設定や機能はおそらく実現できないということ。現時点で求めてもいないので個人的には全然問題ないのだが、そういう複雑な要件が発生すると恐らくこれらでは実現できないのだろう。これからその辺の開発が進むと、機能が複雑化し、少し不満も出てくる、のかもしれない。ただ現時点では全く何の不満もない。

はてなブログとの運用の差

実現できていること、できなくなったこと

以下は実現できている。

  • Markdown、htmlでの記事の執筆
  • 固定ページ、作者ページなど、記事以外の固有ページの作成。当然だが、できる。まだ作ってないけど(作るつもりもあんまり…)
  • 記事のカテゴライズ、タグ付け
  • 記事へのコメント機能。Disqusで実現。ただ、はてなブログのように統一されたプラットフォーム上で一括管理できるというわけにはいかなくなった。
  • アクセス解析。Cloudflare Web Analyticsにより可能。はてなブログ時よりむしろ機能上はパワーアップしている
  • デザインの変更。当然だがはてなブログより自由度が高い、っていうかむしろ全部自分でやれという話なので、ガッツリやろうとするとはてなブログ時代より運用負荷は上がりそうな気はする。今のところそこまで凝るつもりはないが。
  • カスタムドメイン。ちなみにCloudflareにしたことでCDNも適用された。まぁ逆に言うとCDNの管理もしなきゃいけなくなったわけだが…

逆に以下はできなくなった。

  • 予約投稿。はてなブログ時代は実は(ほぼ)すべての記事をこれで投稿していたのだが、これは記事のURLに統一的なパターンを持たせたいという意図によるものであり、この機能が絶対に必要というわけではない。ちなみにHugoは未来日時の記事を書くことはできるがビルド時に無視される。逆に言うと毎日(あるいは毎時間?)再起動する仕組み作っておけば、この機能がなんとなく実現できることにはなるなぁとはボンヤリ考えていたりはする。そこまでしてやりたいとも思えないが…
  • 「読者になる」「言及」など、はてなブログ独自の機能。これは当然。しかし別にそこまで使っていたわけでもないので、まぁ今までお世話になりました、という感じ。なお、「言及」に関しては、言及元のブログの方に移行した旨をお伝えしようとは思っている。(そんなに数も多くないし)
  • スマホアプリからの管理・操作。記事の執筆等はPC上で作業することを前提にしているため、スマホからどうこう、っていうのはできなくなった。これも、別にほとんど使ってなかったし、アクセス解析とかはスマホからもブラウザで見れるし。別にそこまで困ることでもなさそう。ちなみに今ふと思ったんだが記事更新する場合ってGithubのリポジトリ直接直せばできたりすんのかな。。そんなことしたくはないが。

総じて、ブログを運営するうえで個人的に必要だと思っていた機能は基本的に全て実現できており、今のところは不満はない。まぁ移行直後なので、これから長く続けていくとまた別の不満が出てくる可能性はある。年末あたりまでにまた見直してみたい。

画像の運用

はてなブログ時代は、はてなブログにアップロードすればそれでOKだったが、今後はその辺の管理も自分でやらなければいけなくなった。画像はGithubに管理させたくなかったので、画像の管理ディレクトリは.gitignoreにしておいて、前回更新分からの差分を抽出してR2にアップロードするような仕組み(ツール)を作った。これは自作漫画サイトRESIGN THREATの開発中に作った、画像をS3にアップロードする仕組みと似ている。ローカルでの編集段階からR2へ画像をアップロードしておき、先にURLを取得しておいて、MDファイル側にそのURLを書き込むという編集スタイルにした。

なお、過去の分の画像はこの仕組みの範囲外であり、移行ツールの実行結果として全部R2に全部乗せたことで、個人的にはもう役目はもう終えている。一応.tar.gzにしてローカルには持っているものの、これらの画像について手を出す気は今のところなく、必要であればそのときになってから考えればいいという感じだ。今後も全ての画像はR2に集約し、万が一この後移行する場合もR2から引っ張り出すという考えでやっていく。

残課題とか他にやりたいこと

  • iPhoneのChromeで開いたときだけ、「このサイトの証明書がE1発行のものであると確認されました。」というSSL証明書の警告が出る。iPhoneのSafariとかWindows/Mac OSのChromeとかは平気なんだが、なぜかiPhoneのChromeだけだめ。どうもLet’s EncryptのE1という中間CAが認められてないらしいんだが…。調査中だが、イマイチ原因が分からない。Chromeでだけ起きる以上、Chromeと他のブラウザで使ってる証明書の種類が違うんだろうけど、使ってるのが端末固有のものかブラウザ固有のものか、の調べがついていない。一応動作に問題はないので今は優先度を下げてるのだが、気になるのでそのうち何かしら対応したい。
  • はてなブログにあった機能のうち、「Twitterへの投稿」ができなくなった。これが地味に痛いので、何かやりたいなと思っている。Github Actions使って何かできないかと検討中。あるいは、RSSフィードをIFTTTでポーリングしてもらってどっかのWebhookに回してそっちで投稿、ってのもできそうではある。(そもそも、はてなブログもpingの更新通知機能がなく、IFTTT使って各サービスに通知してたので、この仕組みは流用できそうではある)
  • 年月ごとの記事数集計をつくりたい。また、カテゴリ・タグに記事数のカウントをつけたい。はてなブログだと実装されていた機能なので、ここは単純にHugoの方が劣っていることになる(別のそんな重要な機能だとも思ってないが…個人的に気になっているだけ)。これは他のHugoのブログテーマだと実現できてるのもあるので、多分なんかやれば出来る気がする。実装方法を調べてレイアウトに組み込む。そのうち。
  • Pagefindを試してみたい。今の検索機能はただのGoogleサイト検索なので、はてなブログのときみたいに自サイト内検索を実現したい。それと 単純に技術的に興味がある。w そのうちやりたい。