2026年 01月 14日

トップページの長さを若干アダプティブにした

この日記は「日記」なので、日付単位で出している。新しい日付順 (date DESC) かつ、同一日付内では上が古い時系列 (time ASC)。そして日付単位では全てのエントリを出す。

トップページにおいては10日分を一気に表示するようにしてたけど、これだとすごく活発に日記を書いてるときは長大なページになってしまう。

これを若干ややこしい処理にかえた。10日分のエントリをすべてひいてくるが、エントリ数が10以下になるまで日付を減らしていく、というものに。

来週健康診断で気が重い

「公開を遅延」する機能はもともとあったけど、予約投稿もどうせなら欲しいなと思って実装してみた。しかし思ったよりも大変だった。

考えかたとして「予約投稿」で指定した日付に新規投稿されるだけだから、そんなに難しくないのだが、再編集を考慮しはじめると一気に複雑性が増す。

# エントリのステータスとパス(URL)決定ロジック

本プロジェクトにおけるエントリのステータス管理は、「URL(パス)がいつ確定するか」という一点において区別されます。

## 1. コンセプト:URLの確定タイミング

| ステータス | 役割 | URLの確定タイミング |
| :--- | :--- | :--- |
| **`public`** | 即時公開 | **保存時**に確定する。 |
| **`scheduled`** | 公開を遅延 | **保存時**に確定する(公開まで非表示なだけ)。 |
| **`reserved`** | 予約投稿 | **公開当日**の最新番号で確定する(未来の新着として扱うため)。 |

*   **`scheduled`** は「URLを今日の日付で固定したまま、公開だけを未来(30日後など)にしたい」場合に使用します。`public` と同様の扱いです。
*   **`reserved`** は「公開されるその日の新着記事として予約したい」場合に使用します。公開されるまで URL は未確定です。

## 2. URLの不変原則

一度確定した URL (`YYYY/MM/DD/N`) は、**原則として変更されません**。
本文の修正、`public``scheduled` の切り替え、あるいは一旦 `draft`(下書き)に戻して再公開した場合でも、最初期に確定した URL が維持されます。

### 唯一の例外:`reserved` による「未来への移動」
公開済みの記事を `reserved` に変更して未来の日付を指定した場合のみ、URL が変更(再採番)されます。これは、その記事を未来の「新着記事」として扱い直すための特別な操作です。

## 3. 状態遷移の基本ルール

| 行先ステータス | パス (`path`) の扱い |
| :--- | :--- |
| **`draft`** | 既存パスがあれば維持。なければ空。 |
| **`public`** | 既存パスがあれば維持。なければ**保存時**に採番。 |
| **`scheduled`** | 既存パスがあれば維持。なければ**保存時**に採番。 |
| **`reserved`** | 既存パスがあれば維持。ただし無視し、**公開時**にその日の最新番号で採番。 |

## 4. `date` との関係

`path``YYYY/MM/DD/N` の場合、 `date``YYYY-MM-DD` となる

テストも全遷移を一応テストさせている。これに加えて不安なシナリオのテストもある…… golang だと fmt が自動でかかるので空白いれてテーブルテストを成形するのが難しい。しょうがないので文字数をあわせてるけど、LLMはこの手の操作が死ぬほど下手なのがつらい。もっと良い方法あるのだろうか……

	tests := []struct {
		ST        Transition
		SavedPath string
		PubAt     time.Time
		WantStat  string
		WantPath  string
		Name      string
	}{
		//    Transition        SavedPath  PubAt    WantStat  WantPath
		// --- New Entry ---
		{FromTo(NONE_, DRAFT), _____, ZERO_, DRAFT, EMPT_, "New -> Draft"},
		{FromTo(NONE_, PUBLI), _____, ZERO_, PUBLI, GENT_, "New -> Public"},
		{FromTo(NONE_, PUBLI), _____, FUTUR, PUBLI, GENT_, "New -> Public (Future Ignored)"},
		{FromTo(NONE_, SCHED), _____, FUTUR, SCHED, GENT_, "New -> Scheduled"},
		{FromTo(NONE_, RESER), _____, FUTUR, RESER, EMPT_, "New -> Reserved"},

		// --- From Draft ---
		{FromTo(DRAFT, DRAFT), _____, ZERO_, DRAFT, EMPT_, "Draft -> Draft"},
		{FromTo(DRAFT, PUBLI), _____, ZERO_, PUBLI, GENT_, "Draft -> Public"},
		{FromTo(DRAFT, SCHED), _____, FUTUR, SCHED, GENT_, "Draft -> Scheduled"},
		{FromTo(DRAFT, SCHED), _____, FUTUR, SCHED, TODAY + "/1", "Draft -> Scheduled (Path is Today, not Future)"},
		{FromTo(DRAFT, RESER), _____, FUTUR, RESER, EMPT_, "Draft -> Reserved"},

		// --- From Public (URL Immutability) ---
		{FromTo(PUBLI, DRAFT), P2025, ZERO_, DRAFT, KEEP_, "Public -> Draft (Keep URL)"},
		{FromTo(PUBLI, PUBLI), P2025, ZERO_, PUBLI, KEEP_, "Public -> Public (Keep URL)"},
		{FromTo(PUBLI, SCHED), P2025, FUTUR, SCHED, KEEP_, "Public -> Scheduled (Keep URL)"},
		{FromTo(PUBLI, RESER), P2025, FUTUR, RESER, KEEP_, "Public -> Reserved (Keep URL until publish)"},

		// --- From Scheduled (URL Immutability) ---
		{FromTo(SCHED, DRAFT), P2025, ZERO_, DRAFT, KEEP_, "Scheduled -> Draft (Keep URL)"},
		{FromTo(SCHED, PUBLI), P2025, ZERO_, PUBLI, KEEP_, "Scheduled -> Public (Keep URL)"},
		{FromTo(SCHED, SCHED), P2025, FUTUR, SCHED, KEEP_, "Scheduled -> Scheduled (Keep URL)"},
		{FromTo(SCHED, RESER), P2025, FUTUR, RESER, KEEP_, "Scheduled -> Reserved (Keep URL until publish)"},

		// --- From Reserved ---
		{FromTo(RESER, DRAFT), _____, ZERO_, DRAFT, EMPT_, "Reserved -> Draft"},
		{FromTo(RESER, PUBLI), _____, ZERO_, PUBLI, GENT_, "Reserved -> Public"},
		{FromTo(RESER, SCHED), _____, FUTUR, SCHED, GENT_, "Reserved -> Scheduled"},
		{FromTo(RESER, RESER), _____, FUTUR, RESER, EMPT_, "Reserved -> Reserved"},

		// --- From Draft with Path (Previously Published) ---
		{FromTo(DRAFT, DRAFT), P2025, ZERO_, DRAFT, KEEP_, "Draft(P) -> Draft (Keep URL)"},
		{FromTo(DRAFT, PUBLI), P2025, ZERO_, PUBLI, KEEP_, "Draft(P) -> Public (Keep URL)"},
		{FromTo(DRAFT, SCHED), P2025, FUTUR, SCHED, KEEP_, "Draft(P) -> Scheduled (Keep URL)"},
		{FromTo(DRAFT, RESER), P2025, FUTUR, RESER, KEEP_, "Draft(P) -> Reserved (Keep URL until publish)"},
	}

日記の編集画面に過去エントリのリンクを一瞬で貼れる機能をつけた

よく昔の日記を検索してはりつけるので、さっさと作ればよかった。

Ctrl-L で起動して、Ctrl-N Ctrl-P で選択し、よくわからんかったら一度 Shift-Enter で別窓で開き、確定なら Enter で挿入できる。すごい便利

スクリーンレコードしたやつを video 要素でそのまま貼る。

↑試しにスクリーンレコード(OBS)したものを以下のように AV1 + webm にして、video 要素ではりつけてみた。107KB

こういうことするとき anigif を貼り付けてたけど、もうそんなことしないでもよい時代になっているか?

$ ffmpeg -ss 1 -i "/Users/cho45/Movies/2026-01-14 23-01-13.mkv" \
	-t "5" \
	-vf "scale=1280:-2" \
	-an \
	-c:v libsvtav1 \
	-crf 45 \
	-preset 4 \
	-svtav1-params tune=0:enable-overlays=1 \
	-pix_fmt yuv420p \
	"./foo.webm"

Gemini はレートリミットはなんかちょっと小賢しい。細かい単位(数十秒程度)でリミットをかけて「AIの思考時間」を装っている感じ。

2026年 01月 13日


2011年1月2日撮影 / この猫は去年この世を去りました

Claude Code Pro の Weekly Limit が普通にキツくて、あんまり使えてない。Claude は計画の部分とかツールの使いかたとかがどんどん良くなってて、計画だけ決まればあとは自動運転でも割と良いことが多い (おかしいことしてるときもあるけど)。

そしてここしばらくは Google AI Pro を契約して gemini-3-flash-preview を Gemini CLI でかなり使っている。基本的に gemini-3-flash-preview ならほぼずっと使い続けられる。gemini-3-pro-preview は遅いしリミットくるし、それほど頭よく感じないしでほぼ使ってない。

Gemini CLI と Claude Code はツールとしての完成度がぜんぜん違うので、使いかたも違う。

Gemili CLI はとにかく編集するという機能が弱くて、だいぶつらい。

  • 巨大な replace をしようとして失敗する
  • replace でコメントを消しちゃう
  • replace で大量の改行を入れこむ
  • うまくいかなくて here doc でやろうとしてまた失敗する
  • write_file で全部書き戻そうとしてぶっこわす
  • 小さく変更しようとすると前後の文脈の重要な部分を省略して消しちゃう

とにかく編集という基本機能に問題があるので基本的に一切 accept editing してないで変更は全チェックしている。ここだけ我慢できれば普通に使えてる。


あと特定ケースでピンポイントにQwen3-Coder-480B-A35B-Instructを Kilo 経由で使ってみてる。Kilo よくできてるなあという気持ち

このサイトのドメインにDKIMも設定した。あと無駄に spf についてたアドレスを外して google のリレーだけ許可するようにかえた。

自分しか見ないけど管理画面を充実させつつある。今気付いたけどオレンジの枠なのは Cloudflare カラーなのか?

あとプレビュー機能もつけた。


Cloudflare のキャッシュも明示的な設定を入れた。R2の配信ドメインはクエリパラメータを無視するとか、年キャッシュをデフォルトにするとか

短文には関連エントリー出さないみたいな最適化を入れたい。1エントリが仰々しいと、短かい日記が書きにくいみたいなところある。どうしたもんか

短文エントリ (タグない・画像ない・140文字未満) にはリストで関連エントリとシェアボタンを出さなくした。

2026年 01月 12日

golang で AVIF 対応 image.Decode

標準ないし準標準 (golang.org/x/image/webp みたいな) にはないので面倒っぽそうだなあとなんとなく思ってたけど意外にも簡単に対応できた。

CGo を許容するかで今のところ2択っぽい

  1. _ "github.com/vegidio/avif-go" CGo
  2. _ "github.com/gen2brain/avif" libavif を WASM にコンパイルし、wazero で実行

どっちもブランクインポートするだけでつかえる。ベンチ的には CGo のほうが10倍早い

こういう検証とベンチをむちゃくちゃサクっとやれるのすごいいいよなあ。Agentic Coding。脳の負荷が位置が変わっているのを感じる。

検証

vegidio/avif-go

# register_vegidio_test.go
package main

import (
	"bytes"
	"image"
	"os"
	"testing"

	_ "github.com/vegidio/avif-go"
)

func TestRegisterVegidio(t *testing.T) {
	data, err := os.ReadFile("../../static/fixtures/sample.avif")
	if err != nil {
		t.Fatal(err)
	}

	img, format, err := image.Decode(bytes.NewReader(data))
	if err != nil {
		t.Fatalf("Vegidio registration failed: %v", err)
	}

	t.Logf("Successfully decoded %dx%d %s image using vegidio", img.Bounds().Dx(), img.Bounds().Dy(), format)
	if format != "avif" {
		t.Errorf("Expected format avif, got %s", format)
	}
}
go test -v register_vegidio_test.go   
=== RUN   TestRegisterVegidio
    register_vegidio_test.go:23: Successfully decoded 2886x2164 avif image using vegidio
--- PASS: TestRegisterVegidio (0.08s)
PASS
ok      command-line-arguments  0.313s

github.com/gen2brain/avif

# register_gen2brain_test.go
package main

import (
	"bytes"
	"image"
	"os"
	"testing"

	_ "github.com/gen2brain/avif"
)

func TestRegisterGen2brain(t *testing.T) {
	data, err := os.ReadFile("../../static/fixtures/sample.avif")
	if err != nil {
		t.Fatal(err)
	}

	img, format, err := image.Decode(bytes.NewReader(data))
	if err != nil {
		t.Fatalf("Gen2brain registration failed: %v", err)
	}

	t.Logf("Successfully decoded %dx%d %s image using gen2brain", img.Bounds().Dx(), img.Bounds().Dy(), format)
	if format != "avif" {
		t.Errorf("Expected format avif, got %s", format)
	}
}
go test -v register_gen2brain_test.go 
=== RUN   TestRegisterGen2brain
    register_gen2brain_test.go:23: Successfully decoded 2886x2164 avif image using gen2brain
--- PASS: TestRegisterGen2brain (0.73s)
PASS
ok      command-line-arguments  1.119s

Cloudflare R2を使ってみる

容量的には 10GB までは無料で、そのあと従量課金になってもそれほど高額ではなさそう。

ただクラスB操作(参照系)がコントロールしにくいのでちょっと怖い。CDN経由のキャッシュミスだけが問題なのでファイル数が十分に少なければ恐るることはなさそう。現状ではこのサイトの全GETリクエスト(ほとんどクローラーだけど)がR2に飛んだとしても無料枠に収まる。

まず新規アップロードを R2 にするようにしてある。様子を見つつ過去分もアップロードするつもりではあるけど、その前に JPEG ファイルを全ファイル AVIF にするということをしたいのでまだやれてない。

もともと Cloudflare Registrar 契約してるので使いはじめに抵抗がない感じ。

CDNエッジでジオシティーズ的なやつを作ってみる

半年以上前に Cloudflare Workers上で Honoフレームワーク使いつつ懐しい構成のサイトを作ってみたやつ、日記に書いてなかった。

中身がシーディーエンヌのエッジのワーカーで動いてるけど見てくれがジオシティーズだったら面白いなと思って、当時は Github Copilot 使いながら作った記憶。

いわゆる6hotサイトみたいのは現状の Worker の無料枠で全然問題なかろうという気がするし、そういう意味では現代のジオシティーズなんじゃと思ったのだった。

Durable Objects まわり仕様が変化していくのでエーアイに書かせて困った思い出がある。作りっぱなし

AVIFすごすぎるな

過去のをちまちま変換してるけど、1/10 になってかつ見た目がまったく変わらないのも多い。JPEGはやめよう!

リンク切れした画像の一部を復活させた

過去画像の AVIF 化にともなって、Picasa → Google Photos → セルフホストと画像移行する過程で、うまくファイル名一致で移行できずにリンク切れになっていたファイルを、Lightroom Classic から再現像することである程度は復元した。

エントリの中にはあるがファイルシステムにないものを抽出して、

2026/01/12 17:38:39 AVIF変換を検証中...
2026/01/12 17:38:39 [欠落] エントリID:19390 (2012/04/20/1) 画像: IMG_0191-2048.jpg (AVIF:なし)
2026/01/12 17:38:39 [欠落] エントリID:19392 (2012/04/21/1) 画像: IMG_0123-2048.jpg (AVIF:なし)
2026/01/12 17:38:39 [欠落] エントリID:19393 (2012/04/22/1) 画像: IMG_0170-2048.jpg (AVIF:なし)
...
|<

この情報をGeminiに投げつけながらワンライナーでファイル名の一部を抽出してもらい、Lightroom の検索窓に投げんだ。あきらかに無関係なファイル以外をクイックコレクションに追加してすべて書き出た。

ただ、カメラのファイル名はあんまり一意ではない(買い替えのタイミングとかでリセットされている)。なので exiftool で撮影日時を取得し、↑ のログの日付と、因果律が崩れない範囲でファイル名をマッチングさせるコードを書いてもらった (なぜか Ruby で書いてくれた)

そのうえで何度か漏れたファイルを出力した。1件だけどうしても見つからないファイルがあったけどもういいかなという感じ。

過去画像を R2 に移行した

AVIF変換おわらせたあとガッと移行させた。

これで写真の表示が早くなるといいな。なんだかんだ Google Photos から移行してきてからは表示の遅さゆえに写真アップロードするモチベが落ちていたなと思う。

事前処理

もともとのファイル郡と拡張子ごとのサイズ

$ find . -type f -printf "%s %f\n" | awk '{ext=$NF; if(ext ~ /\./) {sub(/.*\./,"",ext); ext=tolower(ext)} else {ext="no_ext"}; sum[ext]+=$1} END {for(e in sum) print sum[e], e}' | sort -nr | numfmt --to=iec --field=1
5.5G jpg
298M png
60M webp
56M gif
1.5M avif
1.1M jpeg
219K ds_store

oxipng と avif への変換後

$ find . -type f -printf "%s %f\n" | awk '{ext=$NF; if(ext ~ /\./) {sub(/.*\./,"",ext); ext=tolower(ext)} else {ext="no_ext"}; sum[ext]+=$1} END {for(e in sum) print sum[e], e}' | sort -nr | numfmt --to=iec --field=1
3.3G avif
450M jpg
240M png
60M webp
56M gif

この450MBの jpg については参照されてないものっぽいのでGCすることに。