0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HugoのGetJSON, GetCSVがdeprecatedになった件について

Last updated at Posted at 2024-10-14

はじめに

k8s.ioのサイト構築などにも使われているHugoを使っていくつかの静的サイトを構築しています。

静的とはいっても時系列によって変化するデータをD3.jsを使ってプロットしたり、項目立てに外部のREST APIを参照するなどHugoのData Source機能をよく利用しています。

v0.123から従来の機能が廃止予定(Deprecated)扱いになってから、Minorバージョンが6進んだv0.129.0からWarningとなり、12進んだ最新版のv0.135.0からはErrorになって処理が停止します。

最新版を利用したいので移行の顛末をまとめておくことにしました。

従来の機能 (GetCSV, GetJSON)

名前のとおりCSV形式のデータソース、 JSON形式のデータソースをHugoの内部データ構造に変更してくれるHugo Data Functionの機能です。

サンプルに記載されている利用例は以下のようになっていてネットワーク経由以外に、静的コンテンツとしてローカルに配置するファイルを読み込むことも出来るようになっています。

Hugo公式サイトの利用例(旧)
{{ $opts := dict "Authorization" "Bearer abcd" }}
{{ $data := getCSV "," "https://example.org/pets.csv" $opts }}

実際の利用ではcontent/ディレクトリに配置する各ファイルのFront-matterにデータソースとなるRESET- APIのURLを記載しています。

そしてlayouts/ディレクトリに置いたsingle.htmlからGetJSONにURLを渡しています。

layouts/data/single.html
...
  {{ $feed_url := .Params.rss_url }}
  {{ $feed_data := getJSON $feed_url }}
  {{ $feed_items := index $feed_data "items" }}
...

こんな感じのコードを新しい記法に変更していきます。

新しいデータ参照方法

GetCSVの説明に追加されているドキュメントをみると、resources.Gettransform.Unmarshalを利用する利用例が紹介されています。

Hugo公式サイトの利用例(新)
{{ $data := dict }}
{{ $p := "pets.csv" }}
{{ with .Resources.Get $p }}
  {{ $opts := dict "delimiter" "," }}
  {{ $data = . | transform.Unmarshal $opts }}
{{ else }}
  {{ errorf "Unable to get resource %q" $p }}
{{ end }}

この利用例はPageオブジェクトの配下から.Resources.Getを利用していてpets.csvを獲得しています。

ここでResource functionsを眺めてみると、URLを指定したい場合にはresources.GetRemoteが紹介されています。

おそらく柔軟にリクエストヘッダーを設定できるようにしたいニーズに対応するための対応だと思いますが、いくらか面倒になりつつもエラー処理は楽になっていると思います。

data.GetCSVの説明には、transform.Unmarshalglobal, page, remote リソースなどと一緒に使うように記載されています。

データ参照方法の変更

GetJSON等はcontent/以下に配置されたファイルも読み込んでくれましたが、resources.Getを利用することで少し面倒になりました。

content/ディレクトリにあるファイルはおよそ自動的にpublic/ディレクトリにコピーされますが、全てがリソースとして認識されているわけではありません。

Hugoのトップディレクトリからみて、data/やassets/以下にファイルを配置すればファイルパスを指示してJSON/CSVファイルにアクセスすることができます。

content/以下に配置した場合は明示的にPageオブジェクトの配下に置けば参照できますが、任意のpathから読み込めたGetJSON/GetCSVとは挙動が少し異なります。

このため複数のページから参照して外部にも公開するCSVファイル群は、assets/の配下とcontent/配下の2箇所に配置するよう変更しました。

ディレクトリ構造の変更
.
├── archetypes
│   ├── default.adoc
├── assets
│   ├── js
│   │   └── default.js
│   ├── json
│   │   ├── data.20240101.json
|  ...
├── content
│   ├── _index.adoc
│   ├── summary
│   │   ├── _index.adoc
|   |   ├── data.20240101.json
|   |   ├── ...
│   │   ├── summary.adoc

_index.{en,ja}.adocに対応するlayoutから直下のJSONファイルにアクセスできますが、summary.{en,ja}.adocからはアクセスできません。summary/の中に配置すれば参照可能です。

データソースとして複数のコンテンツファイルから参照する用途のためにassets/json/ディレクトリを準備して同じ内容を配置しています。

多言語化している場合にも同様の問題は発生していて、外部に公開するファイルをページの配下に配置すると日本語ページにはコピーされないといった現象が発生します。Pageのリソースの一部にしないため、あえて_index.adocなどと同じディレクトリに配置していました。

transform.Unmarshal (a.k.a. unmarshal)

このtransform.Unmarshalは上の利用例でも使用されていて、CSVファイルから取得した生のデータを内部構造に落し込んでいます。

globalpageGlobal functionspage, siteを指していると思われます。remoteというカテゴリはないので、Resource functionsを指しているのだと思われます。

対応しているフォーマットは CSV, JSON, TOML, YAML, XML となっていて、CSVの場合だけデリミタなどを指定する利用例が紹介されています。

公式サイトで紹介されている利用例
{{ $csv := "a;b;c" | transform.Unmarshal (dict "delimiter" ";") }}

ネットワーク経由でのデータソースの取得

Hugoでは主にJSONとCSV形式のデータをネットワーク経由で取得しています。

これまでのコードはローカルファイルかURLかの違いしかなかったのですが、withを使った書き換えは少し煩わしく感じます。

ただ変更によってErrorで処理を停止したり、Warningで処理を継続したりといった制御についてコントロールできるようになった点はあるべき姿になったのかなという印象でした。

変更前のコード
{{- $url := printf "%s%s" $baseurl $scopus_id }}
{{- $csv_data := getCSV "," $url }}
{{- $csv_data_first := index $csv_data 0 }}
書き換え後のコード
{{- $url := printf "%s%s" $baseurl $scopus_id }}
{{- $csv_data := dict }}
{{- with resources.GetRemote $url }}
  {{- $csv_opt := dict "delimiter" "," "comment" "#" "lazyQuotes" true }}
  {{- $csv_data = . | transform.Unmarshal $csv_opt }}
{{- else }}
  {{- errorf "Failed to get %s" $url }}
{{- end }}

データソースに対する変更点

resources.GetRemoteGetJSONと違い、MIMEヘッダーを要求します。

以前作成した古いRESET APIはMIMEヘッダーを指定していなかったのでデフォルトの"text/html"を返すようになっていたことが原因でresources.GetRemoteがエラーを返すようになりました。

ERROR render of "section" failed: "...layouts/newsfeed/list.html:11:28": execute of template failed: template: newsfeed/list.html:11:28: executing "main" at <transform.Unmarshal>: error calling Unmarshal: MIME "text/html" not supported  

参照していた自前のREST APIはruby3.2ベースだったので、この機会にruby-3.3ベースに変更するついでにcontent-typeを正しく設定するようにアプリケーションを再度デプロイすることで解決することができました。

content-typeはapplication/jsonを利用することで問題は解決しました。なおCSVの場合はtext/csvであれば問題なく動作しています。

閑話休題: Hugoで久しぶりにはまった点

.Site.LangaugeCodeを取得しようとして、rangeの内部では参照できない状況に陥りました。

ループの外で別の変数に代入しておけば問題なく利用できることが分かり解決したのですが、データを個別表示しようとデータソースをrangeで取得したら期待どおりに画面が表示されず困りました。

本番環境にデプロイした後に発覚した問題

テスト系ではここまでで問題なく、hugo serverでもちゃんと画面が表示できている状態でリポジトリに反映しました。

本番環境はk8s上で動作するCronJobオブジェクトにコンテナが登録されていて、定期的にhugoコマンドを実行し、生成した静的コンテンツを本番サーバーにpushするようにしています。

unmarshal が MIME "text/plain" でエラーになる

似たようなメッセージはコードを変更する過程でも遭遇していましたが、"text/plain" はREST APIサーバー側では設定しない値だったのでコード側の問題だろうという想定で原因を調べてみました。

本番環境でのみ出現するエラーメッセージ
ERROR render of "page" failed: ".../layouts/data/single.html:201:34": execute of template failed: template: data/single.html:201:34: executing "main" at <transform.Unmarshal>: error calling Unmarshal: MIME "text/plain" not supported
Total in 4369 ms       

本番環境ではalpineでコンパイルしたバイナリを利用していて、試しにバイナリをコピーしてlibc6-compatなどを加えて実行させていましたが、同様の現象が発生しています。

fmt.Printlnを埋め込んで実行してみる

hugoのコードを確認すると、FromContent()の判定結果が環境によって違うようです。

hugo/media/mediaType.go
func FromContent(types Types, extensionHints []string, content []byte) Type {
	t := strings.Split(http.DetectContentType(content), ";")[0]

正常な環境ではこのtが一旦text/plainになってから最終的に正しい値が格納されます。

alpineコンテナではContent-Typeがtext/csv;charset=utf-8の場合のみ、FromContent関数によって080822080845といった謎の数値になります。

application/jsonの場合には正しくjsonが識別されています。

コンテナで実行した時にmimeパッケージの関数の挙動が異なることが分かりました。

Alpineコンテナ上で検証用コードを実行する

この処理を行っているのは標準のmimeパッケージなので簡単なコードで挙動を確認します。

alpineコンテナで挙動を確認する
$ podman run -it --rm docker.io/library/golang:1.23-alpine sh
/go # 

この状態で簡単なコードを作成します。

/go # cat > main.go
                                
package main
                                
import (
  "fmt"
  "mime"
)
                                
func main() {
  fmt.Println("test")
                                
  exts, _ := mime.ExtensionsByType("text/csv;charset=utf-8")
  fmt.Println("exts of text/csv;charset=utf-8: ", exts)
}

このmain.goをビルドして実行します。

/go # go build main.go
/go # ./main
test
exts of text/csv;charset=utf-8:  []

Go公式ドキュメントのmime#TypeByExtensionを確認すると、/etc/mime.typesの他にapache2などのMIME設定をロードするようになっています。

/etc/mime.typesが存在しないことが原因だったので、mailcapパッケージを追加することで無事に解決しました。

最初に環境変数の可能性も少し考えたのですが、手元で$ env -i hugoを実行しても挙動に変化がなかったので、この可能性は早々になくなりました。

最終的な解決策

Gnome系のglobs2ファイルはサイズが大きくなるであろうことは想像できたので、mailcapパッケージを加えることで解決しました。

Alpineはminimumな環境だとは理解していましたが、mime.typesは標準ファイルだと思っていたので意外な発見でした。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?