PRODUCT NOTE / PLATFORM PRD
個人開発者 nzw のポートフォリオ兼 AI プラットフォーム。
この文書は、このサイトそのものをなぜ・誰のために・どんな思想で作っているかから記録した開発ノートです。
| ステータス | Active(継続運用・開発中) |
|---|---|
| ドメイン | nzw.jp(姉妹サイト zerohalf.jp と別ドメインで共存) |
| 運営 | nzw(Hidekazu Nozawa) — PM × 作家 × バイブコーダー |
| 載せているもの | 技術書 / モバイルアプリ / Web ゲーム / AI ツール / 開発ノート / グッズ |
| 最終更新 | 2026-10-11 |
| 技術構成 | FastAPI ・ Tailwind ・ Canvas ・ Flutter Web ・ Nginx ・ EC2 / Docker |
| 関連リンク | トップ ・ About(運営者) ・ グッズ ・ 開発ノート一覧 |
nzw.jp は、1 人の開発者がつくるアプリ・ゲーム・AI ツール・技術書・開発ノートを一元公開する場です。
単なる作品展示場ではなく、「AI に任せる」ではなく「AI を使うことで、あなた自身が強くなる」という設計思想(zerohalf ミッション)を体現するプラットフォームを目指しています。
土台にあるのは zerohalf の考えで、AI に何でも任せられる時代に、自分の手には何が残るのかを確かめたい、というところから始まっている。
そこで、書いた本と、作ったものと、その開発ノートを同じ場所に置くことにした。それが nzw.jp になっている。
PM と作家とバイブコーダーを 1 人でやっている。汎用の AI に無いのは、対象への偏りと、そこまで積み上げてきた文脈のほうだと思っている。
「思想とプロダクトが結びついて届く」状態かを見る。指標は仮置き
アプリ一覧・開発ノート・デザインシステムのページには、ブラックホールが 1 匹だけ現れて画面上をゆっくり横切る。Synapse Link の敵役をそのまま連れてきたもので、タップするとゲームが開く。
最初はトップページに置いていたが、後述の理由でこちらへ移した。絵の中身(6 層の重ね方・配色・脈動)は Synapse Link の開発ノートに書いたので、ここではサイトに載せるためにやったことを残す。
原典は Flutter の BlackHoleRenderer.draw()。層の順番・係数・色は一切変えず、API だけ差し替えた。数値を触らないと決めたので、見比べても違いが分からない状態を保てている。
| Flutter | Canvas 2D | 注意した点 |
|---|---|---|
| ui.Gradient.radial | ctx.createRadialGradient() | 停止位置の配列をそのまま addColorStop に写す |
| MaskFilter.blur | ctx.filter = 'blur(7px)' | 未対応環境があるので起動時に一度だけ判定し、駄目なら素の輪郭で描く |
| BlendMode.plus | globalCompositeOperation = 'lighter' | 降着円盤だけに掛け、save/restore で確実に戻す |
| HSLColor.fromAHSL | hsla(...) 文字列 | 色相は 0〜360 に丸めてから渡す |
| Color.lerp | 自前の lerpColor() | 危険度による紫 → 赤 → 金の補間で使う |
| 1 フレーム = 60fps 前提 | step = dt ÷ (1/60) | 原典の += 0.02 にこの係数を掛け、高リフレッシュレートでも速くならないようにした |
ゲーム内では重力計算で動くが、サイトには引き合う相手がいない。そこで移動だけを別に書いた。まっすぐ飛ばすと単なるスクリーンセーバーになるので、気まぐれさを足している。
トップページに置いていた頃は、時間帯ごとの抽選(朝・昼・夕は 3 回に 1 回、夜は 2 回に 1 回)で「たまに出会うもの」にしていた。
いまはアプリ一覧・開発ノート・デザインシステムに常駐させ、抽選はやめている。経緯は下の意思決定ログに書いた。
大きさは blackHoleSizes の tiny / small / medium から等確率で引くので、出会う大きさは毎回違う。
速度は原典の表どおり大きいほど遅く、tiny は 36px/秒、medium は 12px/秒。
開発ノートでだけ声量を落としている。このページ群は「色は情報として使わない」で組んであるので、原典の配色のままだと紫がページで唯一の彩度になる。
notes.css の末尾で濃さを 0.34 / 0.22、彩度を saturate(0.3) まで下げ、6 層目の VOID タグは切ってある(本文の上に数字が重なるのが、読むときにいちばん邪魔だった)。
アプリ一覧は黒地の製図台なので素のまま。
ずっと手前にいると邪魔になり、ずっと奥だとほぼ見えない。そこで z-index を往復させている。
負の z-index は body の背景より上・コンテンツより下に入る。だから奥に回しても消えず、半透明のカード越しにぼんやり透ける。
z-index は補間できないので、0.45 秒かけて不透明度 0.08 まで落とし、いちばん薄いところで層を差し替えてから戻す。層が飛ぶ瞬間が見えない。pointer-events: none にし、本体に追従する丸いボタンだけを transform: translate3d() で毎フレーム動かす。全面を当たり判定にするとページ全体が操作不能になる。setTransform(dpr, 0, 0, dpr, 0, 0) で高精細に描きつつ、3 倍以上の端末で無駄に重くしない。visibilitychange でループを止め、戻ったときは前回時刻をリセットする。これを忘れると復帰の 1 フレームで大きく瞬間移動する(dt にも 0.05 秒の上限を置いた)。setTransform も当て直す(経緯は意思決定ログ)。MutationObserver で監視するので、AUTO・手動・ランダムのどれでテーマが変わっても追従する。prefers-reduced-motion のときはキャンバスを非表示にし、スクリプト自体を起動しない。踏んだ罠 classList.remove() は消す対象が無くても class 属性を書き直す。
後片付けを毎回無条件に呼んだ結果、MutationObserver → 後片付け → 属性の書き換え → MutationObserver …… とマイクロタスクが循環し、ページが完全に固まった。
動作中かどうかのフラグを見て、必要なときだけ片付けるようにして解決。
About は、季節と時間帯で見た目が変わる。春夏秋冬の山の色と、朝・昼・夕・夜の空を重ねた上に、鳥・龍・恐竜・うさぎとかめ・花火・流れ星などをときどき通す。2026-10-08 から 10-10 の 3 日間で、1 枚の HTML の中に足していった。
足すたびに「この組み合わせはおかしい」と指摘が入り、その分だけ作り直している。実装の手順と層の順序は、リポジトリの docs/about-scene.md に置いた。ここには、直した理由を残す。
演出ごとの script が、それぞれ「冬は蝶を出さない」「夜は恐竜だけ」と自前で分岐していた。出す条件の正本を NZW_RULES に集め、各 script はそこを引くだけにした。
| 出るもの | 条件 |
|---|---|
| 蝶 | 春・夏の朝と昼だけ |
| コウモリ | 夜の春・夏・秋。冬は冬眠なので出さない |
| うさぎとかめの競走 | 春・夏・秋の朝・昼・夕。冬はかめが冬眠、夜は寝ている |
| 花火 | 夏の夜(12〜28 秒おき・1〜3 発)、夏の夕、冬の夜だけ。春・秋・昼は出さない |
| 流れ星・天の川・星座 | 夜。流れ星は夏と冬が多く、星座は季節ごとに入れ替わる |
| 雲海 | 朝に出やすく(0.6)、夜は出にくい(0.15)。秋はさらに 1.5 倍 |
山は不透明度 0.55〜0.96 の半透明で描いている。後ろに置いた太陽や恐竜が山の色越しに透け、山の手前にも後ろにもあるように見えた。山の色は他の面と揃えているので変えず、後ろの層を稜線より上だけに切った。
稜線の高さは、SVG パスから 3,000 点を拾い、x ごとの最小の y を表にして引く。同じ切り方が、太陽・雲・花火・夜空・恐竜の 5 つにそのまま使えた。山の上を歩く小さな動物は、遠景まで含めると主峰の上で隠れてしまうので、主峰だけで切っている。
最初の蝶と龍は、CSS の keyframes と、胴を流れる波で作った。蝶は羽を一定の周期で潰すだけで点滅に見え、龍は波が頭から尾へ流れるので蛇のように捻れた。蝶は羽ばたき・上下動・向きを同じ時計で計算し、龍は地面に固定した 1 本の曲線を頭も胴もなぞる形に作り直した。
iPhone の Chrome で、放置するとリロードされるという報告を受けた。ページ側にリロードの処理は無い。動きを減らす設定で生き物を止めると、放置中のメインスレッドは 1 秒あたり 0.003 秒で、メモリは 105 秒間 3.8〜4.2MB のまま増えなかった。ブラウザがタブを破棄している可能性が高いが、実機では確かめていない。スマホ幅では背面ぼかしを外し、雲海を 4 層から 2 層に、花火の canvas を等倍にした。
恐竜と、山羊・うさぎ・かめを 1 本の時計で順番に出していた。恐竜は 1 分近く歩くので、そのあいだ小動物が出ず、「恐竜ばかり」に見えた。層が別(奥の山の後ろと、二つの山のあいだ)なので、時計を 2 本にして同時に歩かせた。85 秒の計測で、見えていた時間は恐竜 44 秒、うさぎとかめの競走 52 秒、山羊 23 秒だった。
同じ動物は、消えてから 1 分は出さない。鳥と群れは同じ「鳥」、競走は「かめ」と「うさぎ」の 2 匹ぶんを数える。110 秒の観察で、同じ動物の早い再登場は無かった。
恐竜の 3〜5 倍の上半身を、奥の山の後ろに出した。姿は 3 回変わった。最初は牙と赤い目で「怖い」、次にやさしい顔にして「怪獣らしくない」、最後に角ばった顔・青白い目・鋭い背びれの鋼色になった。目は赤にしない。
消え方も 2 回直した。沈めると不自然で、遠ざけて縮めて薄れさせると「なぜ縮むのか」と指摘された。いまは縮まず、歩きながら入って(画面の端から、または途中で坂を上りながら)、歩きながら出る(反対の端まで、または坂を下りながら)。4 通りの組み合わせから毎回選ぶ。
天気は晴・曇・雨・雷・雪の 5 種で、それぞれ弱・中・強の 3 段階にした。値は NZW_WX の表 1 つで、空のおおいの濃さ(cover)、雨雪の粒数・速さ・傾き、稲光の間隔がそこに入っている。花火は cover が 0.35 を超えると上げず、流れ星は 0.2 から止める。雨と雷の日は、鳥・蝶と、猫・うさぎ・かめが出ない。
| 種類 | 弱 / 中 / 強 | 効くもの |
|---|---|---|
| 晴 | 快晴 / 晴れ / 雲多め | 雲の数(快晴は 1 個、雲多めは 2 個増える) |
| 曇 | 薄曇り / 曇り / どんより | cover 0.26 / 0.44 / 0.62。昼の雲は 9 / 13 / 17 個 |
| 雨 | 小雨 / 雨 / 大雨 | 粒 110 / 300 / 680(1440×900 換算)。雲海を出しっぱなしにする |
| 雷 | 遠雷 / 雷雨 / 激しい雷雨 | 稲光の間隔 16〜34 / 6〜16 / 3〜8 秒。雨は雨より少し多い |
| 雪 | ちらつく雪 / 雪 / 吹雪 | 粒 70 / 190 / 480。恐竜と怪獣の頭と背に積もる |
おおいは雲の下、太陽・月・星の上に敷く。雲の上へ敷いた最初の版では、雨の日でも太陽の輪郭が透けた。降っているあいだは不透明度を 0.97 にして、太陽・月・星を隠す。薄曇りだけは 0.5 前後にして、太陽がぼんやり透ける。
風は無風・そよ風・強風・突風の 4 つで、天気とは独立して選ぶ。自動のときは天気の強さに合わせて抽選し、嵐では強風と突風が出やすい。値は 80 ミリ秒ごとに更新する 1 つの数(0〜1.4)で、雨雪の傾きと横流れ、雲と雲海の流れる速さ、山頂の旗の傾きが、同じ値を見る。
突風は、ふだんの弱い風に、4〜11 秒おきの山を足している。立ち上がりが 0.5 秒、保持が 1.2 秒、収まりが 2 秒。旗の傾きを 12 秒間 0.5 秒ごとに読むと、2〜14 度のあいだで 2 回持ち上がって収まった。
範囲を「雲海の下」にすると、山より上は晴れのまま、雲海の上端より下だけに雨・雷・雪が降る。実装は、ルール表が「いまの天気」を晴れ・中に読み替えるだけで、生き物・花火・星の script には分岐を足していない。雲海は出しっぱなしにして、雨のときは雨雲の色に暗くする。稲妻は雲海の中から下へ落とす。
雨・雷・雪の日は、山の小動物が歩かない。2〜4 組が同時に出て、数十秒のあいだじっとしている。雨と雷は稜線に大きな葉を立てて、その下に入る。雪は鎌倉を 1〜2 つ作り、入り口の中に 1〜2 匹ずつ入る。動くのは息づかいと、首・尾のゆっくりした揺れだけ。恐竜と怪獣は残し、降る日は立ち止まる時間を長くした。雲海の下だけに降る回は、山の上が晴れなので、動物はふだんどおり歩く。
降る日の雲は、空のおおいと別の色にした。雨と雷は暗い雨雲、雪は白っぽい雲にして、灰色のおおいに溶けないようにしている。最初の版は雲を雲の色のまま暗くしたので、おおいと同じ灰色になり、数を増やしても「雲が少ない」と言われた。花火は、降っているあいだは範囲に関わらず上げない(雲海の下だけの回でも)。
細長い気嚢の下に木の船体を吊り、尾に回るプロペラと 2 枚の尾翼を付けた。飛ぶ頻度は城(0.07)と龍(0.3)のあいだの 0.45 にして、昼夜と季節を問わず出す。降る日(雨・雷・雪)は出さない。夜は船室の窓に明かりが灯る。プロペラは横から見た回転を、縦の刃を伸び縮みさせて見せている。風が強いほど速度を上げる(0.8〜1.6 倍)。
雨・雪の粒は、画面に出ている雲それぞれの底(絵の高さの 78%)の横幅の内側から出て、下へ落ちる。雲海の下だけに降る回は、雲海の底から出る。描画の canvas は雲の層より奥(空の層の中、雲の手前でなく後ろ)に置いたので、雲の絵が粒の上端を隠す。稲妻も、雲の底から落とす。最初の版は、画面の上端から粒を出し、雲や山、本文の手前に描いていたので、「雨の発生位置が誤っている」と言われた。雲の奥に回したら、昼の灰色のおおいの上では筋が溶けたため、昼は濃い青灰、夜は明るい青に塗り分けた。
範囲が「雲海の下」のとき、雲海を上端そのままに、下へ 116px 伸ばして厚くする。山頂は雲の上に残る。大きい丸の層は 26% 下げ、上端には小さい丸の層を足して、頂を波打たせた。粒は雲海の上端で切るので、雲が薄いと空から降っているように見える。
大型怪獣が姿を見せたときだけ、出会った地上の生き物(山羊・うさぎ・かめ・猫・登山夫婦・雨宿り中の動物)が、頭の上に «!» を出して 0.8 秒固まり、近い画面端へ駆け寄って、怪獣のほうを向いて見守る。怪獣が去って 1.6 秒たったら、端から歩いて出ていく。以前は、ふつうの恐竜にも猫が驚いて逃げていた。恐竜では動かさず、怪獣だけにした。出現の重みは 0.015 で、城より少ない。
晴れの日、登山夫婦は山頂の旗の下で立ち止まったあと、6 割でテントを張る。テントは山頂の平らな台地(x 598〜682)の真ん中に、台地からはみ出さない幅で立てる。最初の版は旗の少し先に立てたので、屋根が斜面にはみ出して見えた。張ったら全員(夫婦と猫 2 匹)でテントの中へ入り、窓に灯りをともして、ご飯の小さい煙を出す。6〜11 秒ごとに外へ出て、人間と猫で台地を走る・猫だけで走る・夫婦で珈琲を飲む(湯気)のどれかを 6〜9 秒して、また中へ戻る。40〜70 秒過ごしたら、たたんで歩き出す。雨・雷・雪の雨宿りでは、4 割の回で 1 組がこのテントの組になり、同じ台地で同じように過ごす。
山の形は viewBox 1440 に対する比で描いてあるので、スマホ幅(390px)だと台地(元は幅 84 単位)が約 23px しかなく、テントも夫婦も乗らなかった。台地が約 34px になるまで、山頂を左右へ広げて描き直す(半幅 42〜70 単位)。左の肩は、谷から台地の端までを 1 本のなだらかな曲線で上らせる。最初の版は肩を近くから急に立ち上げたため、スマホで崖のように見えた。旗の位置は変えない。広げたあとは稜線の高さ表を作り直す。テントの幅も台地の幅から決める。テントの色はサンド(カーキ)の山岳テントにそろえた。
テントの前では、夫婦が鍋でご飯を作る、椅子に座って顔を上げ、星や空を眺める、が外での過ごし方に加わった。大型怪獣が生き物のエリア(体の幅 + 30px)まで入ってきたら、端で見守っていた生き物は画面の外へ逃げ出す。風で揺れるもの(旗・花畑・テントの旗・葉の傘)は、雲と同じ向きで傾く。
登山夫婦は 1 枚の絵に 2 人を描いてあるので、必ず一緒に出る。猫は茶と黒の 2 匹で組にし、1 匹で歩く回は無くした。1 匹が先に歩き出し、5〜12 秒おくれてもう 1 匹が 1.8 倍の速さで追いかける。先頭は山頂の旗のあたりで待ち(最長 60 秒)、追いついたら一緒に山を越える。雨宿りの場面でも、猫は必ず 2 匹で入る。
猫の遊びは、入り方 2 通りと去り方 2 通りのあいだに、つなぎを 2〜3 個はさむ。つなぎは 8 種で、蝶を 2 匹で追う・1 匹だけ追う・毛づくろい・忍び寄り・しっぽ追い・昼寝・散歩・鬼ごっこ。春の昼寝は、花畑(花 9〜12 本)の中で寝る。大型怪獣が出たときだけ、びっくりして画面端へ寄る(ふつうの恐竜には反応しない)。茶キジは足が速くてよく跳び、黒はゆっくり付いていく。
鬼ごっこは、鬼が逃げる側を追い、触れたら交代する。新しい鬼は 0.7 秒その場で数える。茶は足が速いので鬼でもすぐ追いつき、黒が鬼のときは茶がからかうように逃げる。
山羊くらいの大きさで、手をつないだ 2 人が山頂の旗を目指して歩く。旗の下で 4.5 秒立ち止まって跳ね、また歩き出す。冬だけ冬の服(ダウン・ニット帽・マフラー)になる。6.5 割で猫を連れ、猫は先を歩いて、旗の下では一緒に止まる。昼間だけ出て、雨と雷の日は出ない。
雪の日は、積雪量(0〜1)が約 28 秒で上がり、降りやむと約 14 秒で下がる。恐竜と怪獣の頭と背の雪が、その量に合わせて厚くなる。雲海の下だけに降る回は、山の上の恐竜には積もらない。
offsetWidth は無い。太陽は <svg> なので、幅を getComputedStyle で読まないと clip-path が不正になり、何も言わずに無視される。transitionend)、ページの高さが変わったときに切り直す。--cloud-1 / --cloud-2)にして、両方が引くようにした。color-mix)で再現した。サイトが落ちるときは、たいてい作り込みではなく足回りで落ちる。同じ穴に二度落ちないよう、実際に踏んだものだけを残す。
2026-08-30、zerohalf.jp がブラウザで NET::ERR_CERT_DATE_INVALID になった。
証明書は 2026-08-25 に失効していて、同じサーバーの nzw.jp は正常に更新されていた。同居している 3 枚のうち 1 枚だけが更新に失敗し続けていたという壊れ方をしていた。
原因は認証方式の食い違い。zerohalf.jp の renewal/*.conf だけ authenticator = standalone になっていた。
standalone は certbot 自身が 80 番ポートを listen して ACME チャレンジに応える方式なので、nginx が常時 80 番を握っているこのサーバーでは構造的に成功しない。
ログを遡ると、更新開始ライン(失効の 30 日前)にあたる 2026-07-26 の初回試行から失効まで、1 日に数回のペースで同じ失敗を繰り返していた。
約 1 か月ぶんの再試行が、すべて同じ理由で落ち続けていたことになる。
| 証明書 | authenticator | 結果 |
|---|---|---|
| nzw.jp | nginx | 自動更新され続けていた |
| zerohalf.jp | standalone | Could not bind TCP port 80 で毎回失敗 → 失効 |
復旧は certbot certonly --nginx --cert-name zerohalf.jp -d zerohalf.jp -d www.zerohalf.jp の 1 発。
これで renewal 設定が authenticator = nginx / installer = nginx に書き換わり、nzw.jp と同じ形に揃った。
認証方式はサーバー内で 1 つに統一するのが結論で、standalone が使えるのは nginx を止められる新規構築時だけ。
教訓 証明書は「取れた」で終わりにせず、2 回目の自動更新が通るところまでが構築。certbot renew --dry-run は全証明書をまとめて検証するので、1 枚足したら必ず流す。
そして certbot は更新に失敗しても何も通知しない。今回の 5 日間の空白は、失効を知る手段がブラウザで見ることしか無かったせいで生まれた。
/usr/local/bin)・apt 版(/usr/bin)・snap 版 5.7.0(/snap/bin)。secure_path の並び順で実際に動くのは pip 版。certbot.timer と snap.certbot.renew.timer の 2 本が並走し、どちらも同じ /etc/letsencrypt を触る。sudo which certbot で実体を確かめる。nzw.jp と nzw.jp-0001 が同一の 3 ドメイン(nzw.jp / www.nzw.jp / api.nzw.jp)で発行済み。-0001 は同じ構成で certbot を 2 回走らせると自動的に作られるので、ドメインを足すときは --cert-name を明示して既存の 1 枚を差し替える。公開は ./deploy.sh(html/ を EC2 の /var/www/html/ へ rsync)だけだが、オプションの選び方がそのまま運用上の制約になっている。
| オプション | 意図 | 結果として起きること |
|---|---|---|
| --delete なし | 取り返しのつかない削除を防ぐ | ローカルで消したファイルはサーバーに残る。古いページが URL 直打ちで生き続けるので、消すときは手で消す |
| --exclude=*.apk | 数百 MB の転送を毎回やらない | APK は rsync に乗らない。リリースのたびに別途アップロードが要る(現在 10 本・約 1.1 GB) |
| --exclude=.well-known | ACME のチャレンジ置き場を守る | rsync で .well-known を消して証明書更新を壊す事故が起きない |
| --checksum | 中身で差分を判定する | タイムスタンプだけ変わったビルド成果物を取りこぼさない代わりに、転送前の走査が重い |
/static/ 以下だけ(try_files で無ければ FastAPI へ)。/book/、/about/ を含むそれ以外はすべて location / 経由で FastAPI に流しているので、nzwjp-api.service が落ちるとサイト全体が 502 になる。mime.types に .wasm が無く、既定では application/octet-stream で返る。WebAssembly.compileStreaming は application/wasm でないと拒否するため、Flutter Web の CanvasKit と drift の sqlite3.wasm が揃って読めなくなる(Certly の Web 版で発覚)。Content-Disposition: attachment を付ける — 付けないとブラウザが開こうとして落ちる。MIME も application/vnd.android.package-archive を明示する。main.dart.js 4.4 MB・canvaskit.wasm 7.2 MB・問題バンク 5 MB)あり、無圧縮だと回線しだいで起動画面から進まない。/zerohalf/*。html/zerohalf/ 内から nzw.jp 側の画像やページを参照するときは、必ず https://nzw.jp/... の絶対 URL で書く。今回いちばん効いたのは更新の失敗そのものではなく、失敗が誰にも届かなかったことだった。
そこで /usr/local/bin/cert-expiry-check.sh を置き、cron で毎日 JST 09:10(certbot のタイマーが動いたあと)に回している。
設計で決めたのは次の 6 点。
nginx -T から ssl_certificate を抜き出して対象を決める。未使用の nzw.jp-0001 で誤報を出さずに済み、監視対象が nginx 設定と自動で同期する。ok であることまで検証する — Webhook が失効していても curl 自体は成功してしまう。通知したつもりで届いていないのが監視としては最悪の壊れ方なので、HTTP ステータスと応答本文まで確かめて、駄目ならログに残す。踏んだ罠 .env の SLACK_WEBHOOK_URL は .env.sample のプレースホルダ(YOUR/WEBHOOK/URL)が残っていただけで、送信しても no_team の 404 が返るだけだった。
設定されていることと、届くことは別。プレースホルダのままなら送信をスキップして理由をログに書く作りにし、URL を差し替えた時点から通知が始まるようにしてある。
2026-08-30、Slack の Incoming Webhook を作成して差し替え、実際に投稿が届くところまで確認した。
以後は毎日 JST 09:10 に自動で走り、判定結果は /var/log/cert-expiry-check.log にも残る。
Slack は無料プランのままで問題ない(2022 年 9 月に「連携アプリ 10 個まで」の上限が撤廃されているため)。
ただし無料プランはメッセージ履歴が 90 日で古いアラートを遡れないので、ログ側を一次資料として残している。
監視の通知先を入れたことで、/home/nzw/api/.env に秘密情報がもう 1 つ増えた。このファイルは現在 644 で、サーバー上の他ユーザーからも読める。
.env の 1 行を書き換えるだけで済む。600 に絞れていない理由 — nzwjp-api.service は nzw ユーザーで動いているので問題ないが、同居している uvicorn.service(Bom2)が別ユーザーでこのファイルを読んでいる可能性がある。教訓 秘密情報を新しく足すときは、置き場所の権限が足す前から足りているかを先に見る。動かすことを優先して .env に追記していくと、緩い権限のまま中身だけが重くなっていく。
サイトを支える判断を、決定・仮説・結果の 3 点で残す。
NZW_WX の表 1 つで、空のおおい・雨雪の粒・雲の数・生き物と花火の出し分けが、同じ値を引く。雲海の下だけに降る範囲も選べる。そのときは山より上を「晴れ」として扱い、ルール表の側で種類を読み替える(描画の側には分岐を足さない)。/news/(種類で絞り込める)も同じ html/static/news.json を読む。リリースや変更のたびに、この 1 ファイルへ 1 件足す。足したあとは scripts/check_news.py が、重複 id・日付の逆順・不正な種類を落とす。site-nav.js が全ページへ描くので、ベルをそこへ足せば 60 余りのページに一度で出る。未読は localStorage に最後に見た id を 1 つだけ持つ。件数でなく id の位置で数えるのは、件が増えても減っても「それより新しいもの」が正しく出るため。site-nav.js を変えたので、60 余りのページの読み込み指定(?v=)を一括で上げた。作業ツリーに別件の変更があるファイルは、版の文字列だけを変えた内容でコミットした。NZW_RULES)で決める形にした。合わない組み合わせは確率を下げるのではなく、出さない。季節や時間帯が変わった瞬間に、飛行中・歩行中で合わなくなったものは 0.8 秒で消える。<svg> で offsetWidth が取れず、clip-path が不正になっていたためで、幅の読み方を直して解決した。scripts/fetch_repos.py が treeless の mirror で取る。deploy.sh が握り潰していた警告は build/deploy-warnings.txt に溜め、scripts/notify_slack.py が Slack と Actions の step summary へ出す。「止まっていること自体」を scripts/check_stats_fresh.py が検査する。画面のベタ書きの数字は gen_home_stats.py --fix-html が印へ書き戻し、人は打ち替えない。1147 コミット / 11 リポジトリ / 9/20 まで で 2 週間以上止まっていた。原因は 2 つあり、どちらも「人が手元で回して気づく」前提だった。(1) 数える材料が CI に無い。コミット数は nzw.jp の外にある 12 リポジトリを git log --all で数えるが、Actions はこのリポジトリだけをクリーンに checkout するので兄弟が 1 本も無く、生成器が「見つかったのは自分だけ」で更新をやめる。そのうえ gen_home_stats.py がコミット数を null で書き、本番の良い値を毎回潰していた(JSON が null だと home-stats.js も直せない)。(2) 警告を誰も読まない。deploy.sh は生成器の失敗を握り潰して配信を続ける——統計 1 つが作れないだけで本番への配信を止めないためで、判断としては正しい。だがワークフローは毎回 successなので、ログを開かない限り気づけなかった。見張りの判定に生成時刻を使わないのは、数えられないとき前の値を引き継ぐぶん生成時刻だけが新しくなって嘘をつくため。週ごとに進むはずの window_end を見る。app_status_registry.json の食い違い 8 箇所(Certly が v1.5.0・Bearise が v1.1.1 のまま。タグと pubspec はどちらも先に進んでいた)も出したので、裏を取って直した(39 箇所一致)。残るのは Secrets 2 本の登録で、どちらも無くてもデプロイは通る(統計が前の値のまま配信され、警告が step summary に出るだけ)。PAT が期限切れになっても、window_end が進まなくなるので通知が教えてくれる。開発ノート 23 ページ(一覧を含む)で読んでいた bh-wanderer.js と bh-ambient.js をやめた。
残すのはアプリ一覧(/apps/)とデザインシステムの 2 ページ。
notes.css にあったトーン調整のうち、アプリ一覧が使い続けている saturate(0.3) は apps.css へ移し、開発ノート用の濃さ(0.34 / 0.22)は消した。
「開発ノートにブラックホールの徘徊は要らない」という本人の判断。2026-09-04 にトップから移したときから、モノクロの紙面に紫の降着円盤が浮く懸念を残し、濃さを下げたうえで「実物を見てから」としていた。
その結論にあたる。
開発ノートの各ページで読む JS が 2 本(計 66KB、gzip で約 21KB)減った。アプリ一覧の見た目(濃さ 0.5 / 0.32・saturate(0.3))は変えていない。
デザインシステムと Synapse Link のノートにあった「開発ノートでも徘徊している」という説明も直した。
トップの幕 2 と開発ノート一覧で共有しているコミット数の図(activity-chart.js)を、週(月曜〜日曜)の合計で出すことにした。
窓は 90 日から 52 週へ延ばす。1 点が 1 日だったときに引いていた 7 日移動中央値と、中央値の 3 倍に跳ねた日のリングはやめた。
週で足した時点で均されているので、線は点を結ぶだけにする。今日を含む途中の週は図に入れず、最後に終わった日曜までで切る。
幕 1 と quiet 画面の実数も同じ窓(直近 52 週)に揃えた。
日ごとの点を並べると、書いている本人が見張られているように感じる、というのが理由。読む側に「何曜日に休んだか」まで見せる必要は無く、伝えたいのは止めていないことだけで、それは週で足しても残る。
途中の週を入れないのは、必ず低い点が右端に付いて「落ちた」ように見えるのと、「今週ここまで」という見え方がまさに日ごとの図と同じ息苦しさになるため。
窓を延ばしたのは、90 日を週にすると 13 点しか無く線にならないから。
2025-09-15 〜 2026-09-13 の 52 週で 1,054 コミット、手を動かした週は 33 週、最多は 1 週 179 コミット。
JSON は 90 点から 52 点に減った。日ごとの数字はどこにも出なくなり、点にかざして出るのも週の範囲と合計だけ。
全期間の月別は 開発の実測 にそのまま残している。
発売日と「発売予定/発売」の唯一の源を /book/books.json にした。
HTML 側は日付を含む既存の要素に data-book の印を付けるだけで、値は scripts/sync_books.py が書き戻す(deploy.sh が毎回 --fix-html を回す)。
JS で埋める案と、日付だけを <span> で包む案はやめた。
2026-09-16、Vol.4 の発売日を 2026-09-19 から 2026-09-22 へ 3 日ずらした。
手で 10 ファイル 24 箇所を直したのに、6 箇所が古いまま残った(窓辺の札の「9/19 発売」、カードの hero の「9/19」、ハブのバッジ「2026.09.19」、開発ノート 3 行)。
表記が「2026年9月22日/2026.09.22/9/22/2026-09-22」の 4 種に割れていて、grep でも拾いきれない。
3 画面のトップがアプリの版を独立に持つ問題と同じ形で、直す場所が増えるほど必ずどこかが残る。
JS で埋めると JS 無し・JSON 404・値が後から入る CLS が出る(幕 1 の実数と同じ理由でベタ書きを残す)。
日付だけを <span> で包むと、チップやバッジが inline-flex + gap なので flex item が割れて空白が gap に化ける。
だから既存の要素に属性を足し、要素の中の「日付の形をした字」だけを置き換える。
印は 11 ファイル 63 箇所(本文 55・属性 8。zerohalf.jp と開発ノートを含む)。books.json の発売日を 1 日ずらして状態を released にすると 51 箇所が一度に変わり、戻すと元に戻る。
一致していれば書き込まない(mtime 不変)。取りこぼしの 6 箇所は印を付けた時点で機械が直した。
2026-09-22 の発売時は status を released にするだけで「発売予定」が「発売」になる。ハブのバッジのクラス替えと購入ボタン化は手で。
bh-wanderer.js の「前フレームに描いた範囲だけ消す」は残し、1 秒に 1 回は canvas 全面を消して setTransform を当て直すことにした。
あわせて、GPU 側で canvas が作り直されたとき(contextrestored)とタブが前面に戻ったときにも、寸法と変換を当て直して全面を消す。
2026-09-13、「画面を放置すると処理がどんどん重くなる」という申告と一緒に、開発ノート一覧が全体に灰色に曇り、斜めの帯だけ元の明るさというスクリーンショットが届いた。
帯は一辺約 400px(倍率 2)の正方形を、いまブラックホールがいる位置まで斜めにつないだ形で、small の描画範囲(198px)と一致した。
つまりcanvas の中身がどこかの時点で暗く壊れ、部分消しが通り道だけを元に戻していた。開発ノートでは canvas を濃さ 0.34 で本文に重ねているので、黒く壊れるとちょうど灰色の幕に見える。
ヘッドレス Chrome で canvas を黒で塗ると、10 秒後も 91.6% が黒のまま残り、スクリーンショットと同じ見た目になった。
GPU プロセスを落とすと contextrestored は届くのに何もしておらず、変換が初期値に戻って本体が左上寄りの小さな点になったまま戻らなかった。
部分消しをやめて毎フレーム全面を消す案もあったが、全面の clearRect は GPU なら 1 回の塗りつぶしで、1 秒に 1 回なら部分消しの軽さをほぼ失わない。
何が中身を壊したかに関係なく 1 秒で消える形を選んだ。
本番で、黒で塗っても 2 秒後には本体だけに戻る(98.8% → 0.2%)ことと、GPU プロセスを落としても元の大きさで描き続けることを確認した。
実機で中身を壊したきっかけは特定できていない(スリープからの復帰などを疑っている)。「放置すると重くなる」も再現できていない。
トップと開発ノートを倍率 2 で 8 分放置し、描画プロセスと GPU プロセスのメモリ、CPU、コマの間隔、描画ループの回数、イベント監視の数を 30 秒ごとに取ったが、どれも増えなかった。
灰色の幕と同じきっかけで Chrome の描画が遅い方式に落ちている、というのは仮説のまま。
下の「背景を 3 段階に」で決めた「実行時の劣化は下方向へ 1 回だけ、上げ直さない」を撤回した。
濃いが重いと判定されても、その回だけ軽いに替えて、次の番でまた濃いを出す(sessionStorage の nzw:bg-demoted は廃止)。
判定そのものも、1 コマの遅れを 50ms で頭打ちにし、遅い状態が 90 フレーム続いたときだけ落とすように変えた。ウィンドウを狭めたときの「広げ直しても戻さない」も同じ理由でやめ、広げ直せば入れ替えを再開する。
10 秒ごとの入れ替えを出した直後に、「テーマを切り替えたら濃いが出なくなった」と言われた。本番で、濃いの表示中に時間帯テーマを昼へ切り替えると、0.4 秒後に軽いへ落ち、30 秒待っても濃いは戻らなかった。
テーマの切り替えは 8,000px のページを描き直すので、1 コマが 300〜500ms 止まる。
判定は指数移動平均(ema = 0.9·ema + 0.1·dt)が 24ms を超えたら落とす作りで、1 コマが約 90ms を超えるだけで閾値をまたいでいた。
それを「上げ直さない」がタブを閉じるまで固定していた。
「一度カクついた人を滑らかな側へ戻しても得が無い」という仮説で入れた決まりだったが、実際に引っかかったのは重い端末ではなくテーマ切り替えの一瞬で、しかも結果は「片方の絵柄が二度と出ない」だった。
10 秒ごとの入れ替えを入れたときもこの決まりはそのまま残しており、テーマを切り替えて初めて表に出た。
濃いの表示中にテーマを切り替えて 1 コマ 517ms 止まっても、濃いのまま 10 秒を終えて入れ替わるようになった(本番で確認)。
CPU を 6 倍遅くした状態では、濃いが 1 回軽いに落ち、約 12 秒後にまた濃いが出た。本当に重い端末の保護は残しつつ、誤判定で絵柄を失うことはなくなった。
背景が「自動」で、濃いに耐える端末のときは、最初の 1 枚を濃い/軽いのくじで決め、10 秒ごとに 0.7 秒のフェードで入れ替える。
手で段階を選んだとき、狭い画面・タッチ操作が主の端末など初めから軽いになる端末では入れ替えない。窓辺(quiet.html)は対象外にした。
あちらの段階は「描き込みの量」で、絵柄の出し分けではないため。
2026-09-13、同じマシンで撮った 2 枚のスクリーンショットで「この背景の違いは何か、どのタイミングで 1 枚目になるのか」と聞かれた。
答えは時間帯ではなく段階の違い(漂う粒=濃い/点を線で結ぶ=軽い)で、自動判定は端末とウィンドウで決まる。同じ端末なら毎回同じ側が選ばれるので、もう片方は設定を開かない限り見られない。
両方を見られたのは、ウィンドウの高さや重さの判定でたまたま落ちたときだけだった。2 つは作りも見た目もまったく別物なので、どちらも見てもらう側に倒した。
最初は「開くたびにくじ」にし、続けて「10 秒で切り替え」まで進めた。
段階を変えるたびに canvas と描画エンジンを作り直す作り(1 枚の canvas から 2 種類のコンテキストは取れない)なので、1 回きりの切り替えでは表に出なかった後始末の漏れが 10 秒ごとに効くようになった。
軽い版の stop() は resize の監視を外さないので destroy() に替え、bg-field.js は読み込み済みなら <script> を足さず、濃いの読み込み待ちを追い越されたときは世代番号で捨てる。
くじはページを開いたときに 1 回だけ引く(resize のたびに引き直すと、幅を変えただけで背景が入れ替わる)。5 分放置して、canvas は 4 枚のまま、resize の監視の数も増えないことを確かめた。
/ai-workspace の実体は pages/ai-workspace.html の 1 本と決め、html/static/ai-workspace.html は 24 行のリダイレクトに差し替えた。
教材レビュー(Bearise・Certly・Dotpile・声くらべ)の「テストエリア」タブは、複製側にしか無かったものを配信されている側へ移した。
「テストツールが無くなった」という申告から入ったが、消えていたのは導線だけで、4 本とも本番で 200 を返していた。
2026-09-06 にテストエリアを足したのは html/static/ 側で、docs/ai-workspace-features.md に手順として書いてあった pages/ へのコピーが実行されず、以後 2 ファイルが別々に育っていた(3276 行 / 2456 行)。
main.py には /static/ai-workspace.html → /ai-workspace の 301 があるが、nginx が /static/ を /var/www/html から直接返すのでこの 301 は一度も発火していない。
結果、ログイン不要で開ける旧 UI が生き続け、どちらを見ているかで画面が違った。
リダイレクトはハッシュを引き継ぐ形にした(changelog-daygrid.html が #daygrid、changelog-mydictly.html が #langbridge を付けてここを指しているため、meta refresh ではなく JS を先に置く)。
/static/ai-workspace.html#daygrid から DayGrid タブが開くところまで確認。
あわせて、複製側にだけあった旧 id を TAB_ALIASES で受けるようにし、トップページの /ai-workspace#mydictly がホームに落ちていたのを直した。
pages/ を入れる — 外していたのは偶然で保たれていただけだったホームは design-system.css の --nzw-* を読む専用クラス(.ws-*)で組み、ブレークポイントも @media で自前に持たせた。
そのうえで tailwind.config.js の content に ./pages/**/*.html を足した。
当初は足さない方針で始めたが、作業中に足さざるを得ない事実が出た。
content は ./html/**/*.html と ./html/static/js/**/*.js だけで、pages/ を見ていなかった。
ここに書いたクラスは CSS が生成されず、エラーも警告も出ずに当たらない。
最初は「だから pages/ には Tailwind を足さない」で済ませるつもりだった——tailwind.min.css は deploy.sh、pages/ は deploy-backend.sh と別のスクリプトで本番に出るので、content に足しても片方だけ回した瞬間に崩れる形が残るからだ。
ところが上の複製を畳んだ直後のビルドで、bg-green-500・bg-red-400・bg-slate-50/60・focus:border-sky-400・focus:ring-sky-100・gap-2.5・hover:bg-slate-300・mt-1.5 の 8 クラスが CSS から消えた。
どれも pages/ai-workspace.html が使っているもので、これまで生成されていたのは «html/ 側の複製がたまたま同じクラスを使っていた» という偶然に過ぎなかった。
複製を消すと同時に成り立たなくなる。
走査対象に pages/ を入れて 8 クラスとも戻し、変更前の CSS と比べて失われたセレクタが 0 であることを機械的に確認してから配った。
デプロイが 2 本に分かれている問題は消えていないので、pages/ にクラスを増やしたら deploy.sh も回す、を残した。
今回のホーム自体は新しいユーティリティを 1 つも使っていない。
枠線の色は --nzw-border に統一し、ブランド色はアイコンと hover の枠線だけに残した——5 枚が別々の色で縁取られていると、並べたときにどれが強いのか読めない。
単語帳・DayGrid・Certly・グループチャットの 4 本を src から data-src に変え、タブを開いたときに一度だけ src を入れる形にした。
テストエリアの 4 本は元から同じ作りになっていた。
初期 HTML に src を書くと、ホームを眺めているだけで 4 本とも走る。
LangBridge と Certly の main.dart.js が各 4MB 超、DayGrid は iframe の中でさらに Tailwind の Play CDN を読む。
ここにテストエリア(4 本で 15MB ほど)を足すと最大 8 本になる。postMessage も contentWindow も使っていないことを確認したうえで替えた。
ホームを開いた時点の iframe 読み込みが 4 本から 0 本になった。src を入れ直さず DOM に残す作りにしたので、タブを行き来しても読み直さない(入れ直すと読んでいた場所を毎回見失う)。
副作用が 1 つある。グループチャットは localStorage から前の部屋へ自動再入室するので、これまではワークスペースを開くだけで同室の人の画面に「参加しました/退出しました」が出ていた。
これがタブを開いたときだけになった。
トップだけに出していた起動スプラッシュを、アプリ・技術書・グッズ・開発ノート・デザインシステム・About・zerohalf の 7 画面に広げた。
仕組みは boot-splash.js 1 本のままで、絵だけをページごとに差し替える。JS が毎フレームやるのは --bs-p(0〜1)の書き込みと % の文字の 2 回だけで、何がどう動くかは知らない。
絵は /static/css/boot-splash.css が calc() で作る。data-splash の値は各ページが既に持っている data-site-nav-current と同じ語彙を使い、識別子を 2 系統に増やさない。
「1 回だけ出す」の単位も nzw:booted から nzw:booted:<key> に分け、回遊しても «そのページの初回» には出るようにした。
7 画面はどれも独自の視覚言語を持っているのに、開いた瞬間だけは無地だった。
サイトの作りが元々「骨格は 1 種類、皮だけ変える」(design-system.css → 各ページ CSS の上書き)なので、演出も同じ形にすれば新しい仕組みを増やさずに済む。
絵は新しい記号を持ち込まず、そのページが既に使っているものから取った — グッズは極太のタブular数字、開発ノートは 14px ピッチの方眼ドット、アプリはステータスを «形» で区別する点、技術書は低ポリのハチドリ、About は等高線とルート、zerohalf はロゴそのものの数直線。
下限は 600ms から 1200ms に伸ばした。静的 HTML は速く、実ロードに連動させるだけでは絵が一瞬で消えて «個性» が見えないため。
時間項の時定数も下限の半分に合わせないと、下限の時点でまだ .63 までしか来ておらず残りが一瞬で飛ぶ。
7 画面すべてで、初回に出る・同一タブの再訪では出ない・戻るでは出ない・6 秒のフェイルセーフが効く(JS と CSS をそれぞれ遮断して確認)ところまで通した。
フックの読み方で 1 度つまずいた。
テーマ側の window.NZW_SPLASH_HOOK を起動時に 1 度だけ捕まえる実装にしていたため、登録する inline script が boot-splash.js より後ろにあるページでは永久に null のままになり、ブラックホールが出なかった。
毎フレーム読む形に変えて解決。もう 1 つ、動きを減らす設定でスクロールをロックしないようにした。
About の動き抑制ブロックは animation-delay まで 0 に潰すので CSS の 6 秒フェイルセーフが即座に発火し、覆いが消えたのにロックだけが最大 4 秒残る — 「見えているのにスクロールできない」という最も質の悪い壊れ方になる。
下の「スプラッシュの絵を虹ラインへ差し替える」で外した canvas のブラックホールを、デザインシステムのページだけに戻した。
描画は既存の bh-wanderer.js をそのまま使い、bounds で画面中央の 420×220 の 暗いパネルの中だけに閉じ込める。
全面 canvas にはしない。VOID: xx.x% のタクティカルタグに読み込みの進捗を入れるため、ライブラリに autoDanger(既定 true)を足し、false のとき danger を外から与えられるようにした。
当時外した理由は 2 つあって、片方しか今回に当てはまらない。
「ブラックホールは Synapse Link 1 プロダクトの絵であって nzw.jp の看板ではない」は今も正しいが、戻す先はサイトの玄関ではなく、ブラックホールを解説しているページ 1 枚なので衝突しない。
もう片方の性能は、原因が «canvas 一般» ではなく「白背景に加算合成を重ねたこと」だった。
降着円盤が RGB 合計 17 しか変えないのに 1 フレーム 99.3ms のうち 75ms を占めていたのは、加算は白に足しても飽和するだけだから。
暗いパネルの中なら同じ絵が «見える» ぶんの仕事になる。加えて当時の 406 行の移植と違い、bh-wanderer.js はスプライト焼きとダーティ矩形が入っていて毎フレームの仕事が桁で違う。
スプラッシュは bh-wanderer.js の到着を待たない作りにした(使えるようになったフレームで初めて作り、読めなければバーだけで成立する)。
覆うはずのものが待ち時間を延ばす側に回らないための線引きで、ここだけは崩さない。
読み込みの置き場でも 1 つ踏んだ: ダウンロードを先行させようと defer で head に移したところ、defer は «body 末尾の非 defer» より後に走るため、ライブ見本の inline script と bh-ambient.js が BlackHoleWanderer 未定義のまま先に動き、どちらも黙って消えた。
実行位置は body 末尾のまま、preload で取得だけ先に始める形に倒した。
/about/ に移し、肩書きを「役割の列挙」から「一行の決め」に変えるAbout の URL を /about.html から /about/ へ移した。
実体は html/about/index.html、旧 URL は FastAPI のミドルウェアで 301(既に /static/ai-workspace.html で同じ形を使っている)。
あわせて肩書きを「AI活用のチームマネージャー/SaaS開発のプロダクトマネージャー」から 「PoC で終わらせない、AI プロダクトマネージャー」の一行に変え、所属と年数は下の小さい行(.profile-role-sub)へ逃がした。
URL の方は、/apps/・/book/・/goods/・/static/notes/ がすべてディレクトリで、About だけが拡張子付きで残っていたのが理由。
.html は配信の実装であって、読む側には関係がない。
肩書きの方は、元の一行が役割を 2 つ並べただけで、何をする人かが読み取れなかった。
職務経歴書側の芯は「PoC(0→1)で止めず、運用とスケール(10→100)まで持っていく」なので、そこを言い切って一行に置いた。
事実(エンジニア15年 → PjM → SaaS開発チーム マネージャー)を同じ行に足すと読点だらけになって何も残らないので、行を分けている。
サイト内のリンク 36 箇所と zerohalf.jp からの 2 箇所を /about/ に張り替え、canonical と og:url も差し替えた。
本番で /about.html → 301 → /about/ を確認し、旧ファイルはサーバーから削除(deploy.sh は --delete を付けていないので、消すには ssh が要る)。
/about(末尾スラッシュ無し)は StaticFiles(html=True) が 307 で拾う。
肩書きは About だけでなく、書籍 5 ページの著者紹介と zerohalf.jp の自己紹介の計 6 箇所も同じ一行に揃えた(同じ文言がサイト内に散っているので、片方だけ新しいと «どちらが本当か» が分からなくなる)。
box-shadow から background に変えるヘッダーの現在地下線を box-shadow: inset 0 -2px 0 で引くのをやめ、background-image の 2px 帯に変えた。
群の先頭([data-group-start])だけ background-size: calc(100% - 13px) で短くする。
群の変わり目の縦罫を .site-header-link::before=リンク自身の子として持たせているので、縦罫 1px + margin-right 12px がリンクの幅に入っている。
inset の影は幅いっぱいに出るため、About(「作り手について」の先頭)では下線が縦罫の下から始まり、アイコンと左端が揃わない。
影は引く幅を指定できないが、background なら指定できる。
ズレていたのは data-group-start が付く 2 項目(開発ノート・About)だけで、他は元から正しかった。
data-group-start は i > 0 の条件付きで付くので、先頭の「アプリ」に 13px の引き算が漏れる心配もない。
4 スキン(紙・白・夜・暗)すべてを本番で撮って確認した。
カード版と 6 幕から雨のオーバーレイをその日のうちに撤去した。雨を絵として持つのは窓辺だけにする。
代わりに、窓辺の「降るもの」(.qt-motes)に大雨の段を足した——雨脚は 1.9 倍の長さで 1.8 倍速く、揺れはさらに小さく、翳りを厚くし、本数は 24 → 60 本(「ふつう」なら 12 → 30 本)。
大雨のボタンは 3 画面とも残す。
「窓辺以外は雨のアニメーションは不要」という指摘。前面のオーバーレイは z-index: 9999 で、粒が本文の上を通る。
カード版と 6 幕は読ませる画面で、窓辺は眺める画面——同じ雨でも置く意味が違う。加えて、窓辺には最初から雨があった(data-qt-weather="rain" で舞うものが細い縦線に変わる)。
雨の実装が 2 系統あることに気づかないまま 3 つ目を前面に足していたのが実際のところで、消すべきは新しく足した方だった。
home.css から 5,978 文字、home-theme.js から雨の生成が丸ごと消えた。
この 2 画面で天気が効く先は背景の線と虹ラインの 2 つだけになり、前面に置く要素はゼロになった(#weather-overlay ごと削除)。
大雨のボタンを 3 画面に残したのは、天気の設定が localStorage で共有されているから——片方だけ選べないと、選んだ状態がもう片方で «どのボタンも押されていない» に化ける。
天気の「雨」を 小雨 / 雨 / 大雨の 3 段階に割った。段階は #weather-overlay[data-rain] が持ち、粒の長さ・太さ・傾き・色は CSS 側に集約して、JS が決めるのは何本・どの層に置くかだけにした。
大雨は粒を 200 本に増やすだけでなく、視界の濁り・風のむら・足元のしぶきの 3 枚を別に重ねる。どの判定にも入らず「無天気」だった雷雨(95〜99)も大雨に寄せて拾う。
実測が大雨になるのは年に数日なので、表示設定に手で選べる 6 つ目のボタンを足した(天気の行だけ 6 列になる)。
「大雨のアニメーションも考えて」という依頼で開けたら、.rain-drop の CSS がリポジトリのどこにも無かった。
JS は className を付けて色を #a5f3fc で塗るだけで、幅も高さも animation-name も持たない空の div が 100 枚積まれていた——初回コミットから一度も雨は降っていなかった(デザインシステムのページには降る前提で書いてある)。
仮に描けていても、水色を白い下地に置いていたので昼はほぼ見えない。だから大雨の前に、まず雨を降らせるところからになった。
その上で、大雨を粒の量と速度だけで作らないと決めた。粒を増やすと「たくさん降っている」にしかならず、強い雨として読まれるのは視界が濁ること・風でむらが出ること・足元が白く煙ることの方だから。
奥行きも 3 層に分けて速度差(1.55 / 1.0 / 0.72 倍)を距離として読ませる。
粒は小雨 45 / 雨 95 / 大雨 200 本(横 1280px 基準。狭い画面は 0.45 倍まで落ちる。実測 1440px で 200 本、390px で 90 本)。
大雨は落下 0.5 秒・傾き 14 度で、傾きと横流れ(35vh)は必ずセットで動かす——片方だけ変えると粒が自分の向きと違う方向へ滑って見える。
虹ラインは大雨だけ「静か→活発」の 1 軸から外し、周期 60 秒(晴れ寄りに速い)/彩度 0.82(雨より低い)にした。
動きは強いが色は濁る、という別の性格づけ。
背景(Canvas2D / WebGL2)にも大雨のプリセットを足し、判定の境目は 3 か所(home-theme.js の rainTierOf()・bg-synapse.js・bg-field.js)で揃えた。
prefers-reduced-motion では粒を 1 枚も作らない。共通の抑制ルールが animation-iteration-count を 1 にするので、作ってしまうと画面の下端に止まった粒が貼りついたまま残る。
開発ノートに プロダクトの系譜を新設した。縦を時間、横を扱う領域に取り、改名・後継・作り直し・開発中止だけを線で結ぶ。
ノードは全部それぞれのノートへのリンクにして、各プロダクトの中身はいっさい書かない。一覧ページには本数を数えるフィルタの外側に導線を 1 枚置いた。
Socrates → Grizzlingo → Bearise、LangBridge → MyDictly という血縁は、実際には各ノートのメタ表の「出自」「後継」行と、リポジトリのコミットログに散らばっていた。
1 本を読んでいるあいだは、その 1 本の話しか見えない。一覧に戻っても更新日順のカードが並ぶだけで、何が何から生まれたかは読み取れない。
ノートは「なぜそう決めたか」を書く場所なので、ここに系譜を足すと 1 本ずつが重くなる——だから線だけを持つページを別に立てることにした。
導線をフィルタの中に入れなかったのは、チップが数えているのが「ノートの本数」で、系譜図はノートではないから。
ノード 14 個・辺 7 本の 1 枚に収まった。
分かれ道は 4 つ——Socrates からのピボット、Grizzlingo の中止と Bearise への再構築、LangBridge から MyDictly への改名、そして Quick Query Question と DayGrid が名前を残したまま中身を作り直したこと。
最後のひとつは図を描くまで「同じ形の出来事が 2 回あった」と気づいていなかった。色は使わず線種と枠だけで区別している(実線=同じ血、破線=知見の受け渡し、破線枠+✕=開発を終えたもの、枠なし=開発ノートが無い)。
図が読み物の幅(768px)だと 7 割まで縮んで読めないので、.fig-wide で図だけ器の外へ出し、狭い画面では縮めずに横スクロールさせている。
通った跡を全部やめた。点の列 → 連続した溝 → 泥の層、と 2 日かけて作り込んだものを丸ごと外し、10〜24 秒に 1 個だけ小さな○を吐くだけにした。
○は本体から離れる向きへ 110px 流れながら 4.2 秒で消える。
「足跡はいらない、稀に小さな○を吐き出すくらいでよい」という指摘。作っている最中は「どう見せるか」ばかり考えて「そもそも要るか」を問い直していなかった。
跡は「どこを通ったか」という情報で、読み物のページに置くと本文より強い意味を持ってしまう。読者が追うべきものは本文であって、装飾の軌跡ではない。
うんこに見えたのも、溝が数珠つなぎに見えたのも、根っこは同じで跡が画面に長く居座りすぎていたことだった。形を直す方向で 2 回作り直したが、直すべきは形ではなく存在の方だった。
実測 3.4 個/分、画面に○が居る割合は 24%。同時に 2 個以上出ることは実質ない。
間隔を毎回引き直すのは、等間隔だと「一定周期で出る装置」に見えてたまたま出会うものにならないため。吐く向きを全周から引いているのは、進行方向の後ろに寄せると尾に見えて、やめたはずの跡に戻るから。
飛距離 110px は重力圏(tiny 60px / medium 130px)を抜けるための値で、短くすると本体の暈に飲まれて出たことすら分からない。
コードは 100 行以上減った——泥の二次曲線、溝の区間ごとの太さ計算、点の位相管理が全部消えた。
ヘッダーの真下にあった8 枚の横スクロールカルーセル(6 秒自動送り)を廃止し、トップを 6 つの幕として組み直した。
①サイトを一言で言う見出しと実数、②直近 90 日のコミット数、③いま見てほしい 3 つ、④やめたことで説明する、⑤つくったもの全部(35 枚)、⑥このページ自身のこと。
以前のデザインは /classic.html として凍結して残し、ヘッダーのボタンでいつでも往復できるようにした。出し分けは Cookie を見て main.py がサーバ側で行う。
既定は従来のデザインのまま置いた。作り替えた側を既定にすると、判断が正しかったかを自分で見比べる前に全員へ配ってしまう。
新しい方は「選んだ人が見る側」にして、ヘッダーのボタンを最初の 5 秒だけ薄く光らせて存在だけ知らせる。索引・canonical・OGP は既定側(classic.html)が持ち、新しい方は noindex にしてある。
きっかけは「もっとインパクトのあるサイトにしたい」という一言だったが、実際に測ると足りないのは派手さではなかった。grep -c "og:" が 0 で、<title> は「nzw.jp」の 4 文字。
SNS に貼っても無地のリンクにしかならない。ページ内にもサイト全体を語るコピーが 1 つも無く、いきなり販促スライドが始まる。
docs/VISION.md 自身が「ならないもの」に挙げた「ただの作ったものリスト」に、トップがいちばん近い状態だった。
カルーセルは書籍・グッズ・アプリ 4 種を平等に扱っており、平等は無印象。しかも読ませる前に流れる。
8 枚のうち 5 枚は下のグリッドにも同じものがあり、情報としては重複していた。
作り替えを「一度きりの入れ替え」にしなかったのは、判断が正しかったか自分で見比べられるようにするため。
移行のための並行ファイルがそのまま恒久の classic になるので、捨てる作業がゼロで済む。
index.html は 8,238 行 → 3,444 行(インライン CSS 1,900 行と JS 2,700 行を外部化、動かない canvas 5 枚と死にコード 650 行を削除、カルーセル 920 行を削除)。
canvas は 11 個 → 2 個。<title>・meta description・OGP・canonical を新設し、OG 画像は scripts/og/home.html を Playwright で撮って作り直せるようにした。
採用担当にいちばん効く数字として「直近 90 日で 477 コミット / 9 リポジトリ」を幕 1 と幕 2 に出した。
図は開発ノート一覧のものを activity-chart.js として切り出して共有している(あちら側が画素単位で変わっていないことを確認済み)。
数字は HTML に最終値をベタ書きし、JSON は答え合わせに使う——こうすると「JS 無し」「JSON が 404」「数字が後から入って版が飛ぶ」の 3 つが同時に消える。
CSS の scroll-driven animations(animation-timeline: view())を「使える環境だけの飾り」として重ねていたが、丸ごと撤去した。
出現は scroll-stage.js の IntersectionObserver 一本にする。content-visibility: auto も同じ理由で撤回した。
view() の進み具合は「スクロールできる量」で決まる。そのためページ下端の要素は animation-range の終わりへ到達できず、永久に半透明のまま残る。
実測で締めの一行が opacity 0.17 で固まった。そもそも Safari は未対応なので、ほとんどの読者は Observer 側を見ている。
読まれない方の経路が、この壊れ方を抱えたまま存在する価値は無い。content-visibility: auto は中身がレイアウトされない状態になるため、子要素が IntersectionObserver から見えなくなる。
飛び越えたときの掃き出しが矩形を読めず、幕 4 の 5 要素が隠れたまま残った。長いページなので効かせたかったが、出現とは両立しない。
実装しながら 3 つ踏んだ。(1) IntersectionObserver は「飛び越えた」要素に通知を出さない(交差の割合が 0 のまま 0 なので閾値をまたがない)。
どれか 1 つでも通知が来たときに控え全体を掃く形にした。(2) 1 要素に 2 つの監視を掛けると後勝ちでハンドラが消える。
幕 3 は出現と進捗の両方を掛けており、出現側が黙って上書きされていた。
(3) グリッドの札は表示の出し入れを home-cards.js が握っているので、出現もあちらから仕込まないと「もっと見る」で出た分が opacity 0 のまま残る。
いずれもエラーを出さずに静かに壊れる種類で、通しで動かして初めて見つかった。
CSS 側は「JS が属性を立てたときだけ隠す」書き方に統一してある(about/index.html の .reveal は素の状態で opacity 0 なので、IntersectionObserver が無い環境では永久に見えない。あれを繰り返さない)。
背景を 切 / 軽い(Canvas2D)/ 濃い(WebGL2)の 3 段階にした。端末を見て自動で選び、表示設定から手でも変えられる。
軽い方は bg-synapse.js を改造せず設定だけ絞って使い、濃い方は新規の bg-field.js(357 行・依存ゼロ)。
prefers-reduced-motion は段階に混ぜない。
軽い方を作り直さないのは、SynapseBackground が setConfig で粒数も結線距離も絞れるうえ、WEATHER_PRESETS と setWeather が既にサイトの天気系に繋がっているから。
新規実装するとそこを一から書くことになる。
濃い方にライブラリを入れないのは、描くのが draw call 2 回(全面の三角形 1 枚と gl.POINTS 1 回)だけで、シーングラフもカメラも要らないから。
three.js は 150KB gz あり、HTML 全体が 80KB gz のこのページでは単独で最大のアセットになる。
Play CDN の Tailwind をやめた経緯と真逆になってしまう。
点の動きは (種, 時刻) の閉じた関数なので GPGPU も transform feedback も不要で、静的な VBO 1 本と uniform 数個で足りた。
prefers-reduced-motion を「切」にマップしなかったのは、切が上書きできるユーザーの選好なのに対し、あちらは上書きされてはならない OS の設定だから。
同じ値にすると「濃い」を押しただけで OS 設定を蹴破れてしまう。抑制中は設定行そのものを隠す。
自動判定は bh-wanderer.js の _resolveProfile を型にした。写したのは「情報が無いことを理由に降格しない(|| 8 の楽観的な既定)」と「向きが変われば引き直す」の 2 点。
ソフトウェア実装の WebGL(SwiftShader / llvmpipe)も軽い方へ落とす——CPU ラスタライズの WebGL は Canvas2D より遅い。
実行時の劣化は下方向へ 1 回だけで、立ち上がりの 120 フレームは見ない(シェーダのコンパイル・初回ペイント・フォントの差し替え・スプラッシュの退場が最初の 2 秒に固まっているので、素朴な検出器は必ず誤爆する)。
上げ直さない。
踏んだ穴が 2 つ。
1 枚の canvas から 2 種類のコンテキストは取れない——一度 webgl2 を取った canvas に getContext('2d') を投げると null が返り、降格が「背景が消える」に化ける。
段階を変えるたびに canvas を作り直している。もう 1 つは明るいテーマで粒が主張しすぎて本文が読みにくくなったこと。
暗い地と同じ量を乗せると白の上でベージュの斑点になるので、地の明るさで不透明度と大きさを落とす。
ブラックホールの跡を、点を落とす方式から1 本の連続した溝に置き換えた。0.6 秒ごとに本体の位置を記録し、その折れ線を引く。
太さと濃さは新しいところほど大きく、古い側へ向かって細りながら 30 秒で消える。粒のスプライトと、粒ごとの位置・速度・寿命の管理は丸ごと消えた。
「うんこのように見える」という指摘。実物を見ると否定できなかった。
丸い染みが等間隔で並んでいる絵なので、明るい地の上ではそう読める。原因は形ではなく離散させたことにある。
跡というのは「通った」という連続した出来事の記録なのに、点にした時点でその連続性が落ち、あとに残るのは正体不明の丸だけになる。
線にすれば向きと速さがそのまま形に出る。
寿命を 24 秒から 30 秒へ伸ばしたのは、短いと溝が本体の重力圏(medium で直径 300px)の内側で終わってしまい、降着円盤の光に飲まれて跡として読めないため。
線を 2 本重ねているのは、このライブラリが置かれるページの地の明るさを知らないから。太く淡い紫(アプリ一覧の黒地用)と細く濃い黒(開発ノートの紙地用)を常に両方引いて、どちらの地でも片方が残るようにしてある。
区間ごとに太さと濃さが違うので 1 本の Path2D にまとめられず、描画は 1 フレームあたり 51 区間 × 2 本 = 約 100 ストロークになった。
そのかわり消す範囲は溝全体を囲む 1 枚だけにしてある(区間ごとに矩形を積むと、重なった領域を何度も消すことになって割に合わない)。
前の方式が持っていたスプライトの焼き直しと dpr 追従は不要になった。丸い線端が区間の継ぎ目で膨らみ、円が数珠つなぎに並んで見えるのは残った。
あとから、その円と円のあいだを埋める泥の層をいちばん下に 1 枚足して潰している(区間の中点を法線方向へずらした二次曲線を、太さとずれを 2.6 秒周期で揺らしながら引く)。
色は中間調 1 色で足りた——明るい地では暗く、暗い地では明るく転ぶため、芯と縁のように 2 本引く必要がない。位置を止めて時刻だけ 0.65 秒進めたとき、跡の画素の 47% が変わることを確認している。
About の空を作り替えた。(1) 太陽は filter: blur(34px) でぼかした円と、ぼかした三角の光条をやめ、同心円・方位環・細い直線の光条を持つ図に置き換えた。
にじみは CSS の blur ではなく SVG のグラデーション 1 枚。(2) 月は白丸をやめ、同じ半径の円をずらしたマスクで満ち欠けを作り、クレーターを塗りではなく細い輪にした。
欠け具合は今日の月齢から出す。(3) 操作卓にもう 1 群足し、季節(自動/春夏秋冬)の隣で時間帯(時計/朝昼夕夜)も選べるようにした。
「太陽や月のデザインがイケてない」という指摘。見に行くと、どちらもこのページの語彙から外れていた。
About は地形図(等高線・稜線・ルート破線)と 8bit で組んであり、線は細く、形は測れるものとして描いてある。そこにぼかした橙色の塊と白丸だけが、輪郭も情報も持たずに乗っていた。
太陽と月も同じ「測る道具」の側に寄せれば、装飾ではなくページの一部になる。
時間帯をヘッダーに出したのは、16 通り(山 4 × 時間帯 4)で作ってあるのに、訪問者は自分の時計が指す 4 通りしか見られなかったため。
季節はボタンで選べるのに時間帯だけ ?time= を手打ちしないと見られないのは、作りの非対称だった。月齢を本物にしたのは、季節を訪問者の暦から、時間帯を訪問者の時計から出しているのと揃えたかったから。
今夜の形と合っている月のほうが、毎回同じ形の飾りより面白い。
ぼかしを SVG のグラデーションに替えたので、毎フレーム合成していた blur が 3 枚ぶん消えた(輪郭も締まる)。
時間帯を動かせるようにしたぶん --sun-* / --star-o を @property に登録し、季節と同じ 1.4 秒のクロスフェードに乗せた。
--sun-size だけは min() を含むので登録せず(解決に失敗した環境で initial-value に落ちて絵が壊れる)、大小は数値の --sun-scale で付けている。
手こずったのは「昼」の置き場所。真上=画面上端は、帯(110px)とヒーローのカード(185px)に挟まれた 75px しか空いていない。
輪を持たせたぶん前より場所を食うので、芯をカードの上端に重ねて上半分だけを出す置き方に変えた。狭い画面では操作卓が 2 段に折り返して帯が 165px まで伸びるため、そこだけ別に下げている。
2 つ決めた。
(1) 開発ノートのブラックホールだけトーンを落とす — 濃さを 0.8/0.65 → 0.34/0.22、彩度を saturate(0.3)、6 層目(VOID タグと本体まわりのフレア)は data-label="false" で切る。
適用は notes.css の末尾と各ノートの script タグだけで、レンダラには手を入れない。アプリ一覧とデザインシステムは素のまま。
(2) 通った跡の粒を本体の半径に対する比にする — debrisSize: 2.6px(固定)を debrisSizeRatio: 0.13 に置き換えた。
(1) は前回のログで「紫がノートのモノクロから浮く」と書いて保留にしていた件。実物を見たら、いちばん邪魔だったのは色より本文の上に重なる「VOID: 12.7%」の文字だった。
読んでいる行の上に別の書体で数字が乗ると、意味を持った情報だと思って一瞬読んでしまう。彩度と濃度は下げるだけで済むが、これは消すしかない。
完全な grayscale にしなかったのは、紫がわずかに残るほうが「Synapse Link のあれだ」と分かるため。
(2) は固定 px だったのが単純に間違いだった。tiny(半径 10px)でも medium(35px)でも同じ 2.6px の粒が落ちるので、大きい個体ほど跡が相対的に細って見える。
跡は「何がここを通ったか」の手掛かりなのに、大きさの情報がそこだけ落ちていた。
粒は tiny 1.3px / small 2.6px(据え置き)/ medium 4.6px。0.13 という比は、それまでの 2.6px がちょうど small(半径 20px)ぶんだった値を採った。
絵そのものは触っていない——層数も係数も分割数も原典のまま、変えたのは粒の大きさの決め方と、ページ側の CSS 2 行だけ。
検証中に副産物として分かったのが、headless Chrome では rAF がほとんど回らないこと(20 秒待っても elapsed=0.0s)。
フレームを手で送り、toDataURL() で焼いてから撮る形にして、ようやく尾の見た目を確認できた。canvas の絵を CI で撮るときはこれが要る。
トップページ(index.html)からブラックホールを外し、アプリ一覧(/apps/)・開発ノート 18 ページ・デザインシステムへ置き換えた。
index.html に直書きしていた 140 行の起動処理は /static/js/bh-ambient.js に切り出し、canvas・当たり判定・CSS までそのファイルが差し込む形にしたので、ページ側は <script> を 2 本読むだけになる。
同時に、時間帯ごとの出現抽選と天気ごとの 4 通りの周期も落とした。
「トップページに動くブラックホールがあるのは不気味」という指摘。トップページは判断の場で、来た人はヒーローと製品カードを見比べて次にどこへ行くかを決めている。
その真上を黒い球が横切ると、意味の読めない動きに視線が引かれ、決めるための時間を削る。一方でアプリ一覧・開発ノート・デザインシステムは腰を据えて読むページで、視線は本文の列に固定されている。
同じ絵でも、視線の外側をゆっくり通り過ぎるだけなら邪魔にならない。しかも「Synapse Link のレンダラを移植した」という話そのものがこの開発ノートに書いてあるので、書いてある場所に現物が居るほうが筋が通る。
抽選をやめたのは、置き場所を狭めた以上「たまに出会う」を二重に掛ける必要が無くなったため(出現確率は data-chance で戻せるようにしてある)。
トップページから canvas 2 枚ぶんの CSS(76 行)と起動スクリプト(140 行)が消え、bh-wanderer.js の読み込みも 1 本減った。
移した先では 1 匹だけが手前(z-index: 30)と奥(z-index: -1)を 24 秒周期で往復する。地との相性は移した先で分かれた。
アプリ一覧は黒地の製図台なので、絵としてはトップに置いていた頃より馴染む。逆に開発ノートはモノクロ(Dot Matrix)に組み直したページなので、紫の降着円盤が浮く懸念が残っている。
ページ側から --bh-front-opacity / --bh-behind-opacity で濃さを下げられるようにして、判断は実物を見てからにした。
開発ノート一覧の頭に置く図を、ノート 16 本の最終更新日から直近 90 日のコミット数に差し替えた。
ノートが扱う 9 リポジトリ(nzw.jp / DayGrid / Bearise / Certly / Engly / MyDictly / Synapse Link / Quick Query Question / 書籍)の git log を scripts/gen_notes_activity.py が数えて activity.json に落とし、deploy.sh が毎回作り直す。
図は点=その日のコミット数、線=7 日移動中央値。
最初に置いたのは「ノートの最終更新日」の分布だったが、ノートは 16 本しかなく、点も 16 個しか出ない。しかも中身は各カードに書いてある日付の焼き直しで、図にした分の情報が増えていなかった。
開発ノートの頭に出すべきなのは「ノートをいつ触ったか」ではなく「その裏で何をどれだけ作っていたか」のはず。手元にあって嘘のつけない数字はコミット数だったので、そこから読む形にした。
生成 AI との対話時間も測れた(37 日で 137.3 時間)が、Claude Code の履歴保持が 37 日ぶんしか無く、放っておくと過去から消えて窓が前に滑るので採らなかった。
90 日で 466 コミット、手を動かした日は 65 日、最多は 1 日 53 コミット。最初は線形の縦軸で描いて失敗した——中央値が 2 なのに最大が 53 なので、普段の日が全部下端で潰れて死んだ図に見えた。
縦軸を平方根目盛に変え(ラベルは実数のまま)、跳ねた日は中央値の 3 倍以上のときだけリングで囲う形にして読めるようにした。
2 倍にすると 18 日が囲まれて「目立たせる」意味が消える。データはリポジトリ側にあるので、ノートを足しても図の面倒は増えない。
開発ノート 18 ページが各自持っていたページ内 <style>(合計 4,488 行)を捨て、/static/css/notes.css 1 枚に統合した。
同時に見た目を作り直し、罫線・箇条書き・進捗・時系列・区切りをすべて「点」で描くことにした。プロダクトごとのブランド色(--brand / --topic-brand、実数 14 色)は全廃し、色で意味を運ぶのをやめている。
例外は 3 つだけ:ヘッダー最上部のレインボーライン(サイトの識別子)、アプリアイコンとスクリーンショット(既定はモノクロ、hover で戻す)、カラーパレット見本(色が内容そのもの)。
きっかけは見た目の不満ではなくコピーの劣化。ノートを 1 本足すたびに 250 行前後の CSS を隣のページから貼っていたので、同じ .meta のラベル幅が 1% / 30% / 34% / 38% の 4 通りに分裂していた。
「開発ノートは判断の記録であって宣伝ではない」と書いておきながら、ページごとにブランド色を足していたのも噛み合っていない。
ステータスや警告を色で示していた箇所も、色を抜いたときに情報が消えるなら、そもそも色にしか意味を載せていなかったということになる。
だから色を抜くのを目的ではなく検算として使い、消えた分は点の塗り/中抜き/密度に置き換えた(公開中=二重丸、β版=中抜き、開発終了=薄い中抜き、注意=点の網)。
4,488 行 → 1 ファイル 1,471 行。以後ノートを足すときに CSS は書かない。
figure.fig の図版は --nzw-* トークンで描いてあったため、トークンを差し替えただけで図まで自動でモノクロになった。
逆に色を直書きしていた 2 枚(ブラックホール・重なり順)は .fig--ink として切り出し、hover でだけ元の色を出している。
cdn.tailwindcss.com(Play CDN)の読み込みと、index.html に直接書いていた tailwind.config を捨てた。
設定はリポジトリ直下の tailwind.config.js に一本化し、npm run build:css が吐く /static/css/tailwind.min.css を <link> で読む。
ビルドは deploy.sh に組み込み、流し忘れて「新しく足したクラスだけ効かない」が起きない形にした。
「トップが重い、とくに画像が出ない」という体感の原因を、最初は画像の重さ(15.5MB)だと思っていた。ところが本番をヘッドレスブラウザで実測すると、DOMContentLoaded まで 39.4 秒かかっていた。
同じ回線で about.html は 3.98 秒、/goods/ は 3.07 秒。違いは 2 つで、トップだけが 499KB あることと、トップだけが Play CDN を読んでいること。
Play CDN は DOM の変化を監視して JIT コンパイルを回すので、8,000 行の文書が流れ込む間じゅう全走査が繰り返される。
画像が出なかったのは帯域ではなく、先読みスキャナが画像を取り終えているのに DOM が組み上がらないためだった。30 秒経過時点で本文はまだ 838 要素しかできていなかった。
本番の DOMContentLoaded は 39.4 秒 → 31.6 秒(同時に測った about.html は前後とも 3.7〜4.0 秒で、計測環境が同条件だったことは確認済み)。
期待したほどは縮まなかった。ローカルの切り分けでは 56 秒 → 15 秒という大きな差が出たが、あとで uptime を見たらロードアベレージが 22(10 コア)まで上がっており、その値は計測マシンの負荷で汚染されていた。
信用できるのは本番で前後を同じ手順で測った 2 割の短縮のほうだけ。
ただし切り分け自体は残った: インライン <style> 16 個はほぼ無コストで、残りの支配項はインライン <script> 26 個の実行時間(スクリプトを全部外すと同条件で 4.66 秒 → 1.29 秒、つまり 7 割強)。
12 個の canvas の初期化を可視化まで遅らせるのが次の一手で、宿題として残す。
なお package.json には以前から build:css が書かれていたが、出力先が html/ の外で content グロブもトップを含んでおらず、一度も配信されていなかった。
写真・スクリーンショット系 15 枚を表示サイズに合わせて縮小し WebP へ置き換え、元ファイルは消した。ロゴ・アイコン 4 枚は拡張子を変えず PNG のままリサイズ(参照を書き換えずに済む)。
<img> 22 枚に loading="lazy" を入れ、ヒーロー第 1 パネルだけ eager + fetchpriority="high" を残した。
ただし og:image に使っている 2 枚だけは、リンクプレビューの互換性を優先して従来形式を維持した。
初回表示で 15.5MB を一斉に取りにいっていた。
内訳の偏りがひどく、blackhole2.png が 4.74MB(1290x2796 をカードのサムネイルに使用)、zerohalf-logo.png が 2.97MB(1626x1622 をヘッダーで 20px 表示、しかも全ページ共通)。
加えて loading="lazy" が 1 箇所も無く、プロジェクトカード 36 枚が全部マークアップにあった。
「9 枚だけ表示」は JS がパース後に display:none を付けているだけなので、先読みスキャナは既に 36 枚ぶん取り終えている。
派生ファイルを足す案もあったが、同じ絵が 2 つある状態は必ず片方が腐るので置き換えを選んだ。
15.5MB → 0.74MB(blackhole2 は 4.74MB → 26KB、ヘッダーロゴは 2.97MB → 23.8KB で全ページに効く)。
作業中に、書籍ページには既に <picture> + WebP の仕組みが入っていたのにトップだけが生の PNG を直接読んでいたことが分かり、そちらは既存の webp を指すだけで済んだ。
書籍ページ自体には手を触れていない。
起動スプラッシュから canvas を落とし、ヘッダー下とフッター上に敷いてあるのと同じ虹のライン 1 本だけにした。中央にサイトのアイコン、その下に LOADING、その下に虹の帯。
進捗の作り(実際の準備度に連動・0.92 で頭打ち・4 秒のハードタイムアウト・CSS 6 秒のフェイルセーフ)は一切触っていない。
差し替えたのは絵だけ。
「ブラックホールにこだわらず、サイトデザインに合ったものにしてほしい」という指摘。ブラックホールは Synapse Link という 1 プロダクトの絵であって、nzw.jp の看板ではない。
サイト全体で共有している視覚要素は虹のラインのほうで、これは既に全ページのヘッダーとフッターに敷かれ、デザインシステムにも .nzw-rainbow-line として載っている。
起動時にそれを見せれば、ページが出た瞬間にヘッダー下の同じ帯へ視線が繋がる。
boot-splash.js が 406 行 → 214 行。層 1〜7 の描画・飲み込み・白フラッシュがまるごと不要になり、毎フレームの仕事は transform: scaleX() の書き込み 1 回だけになった。
幅ではなく transform で伸ばすので、進捗が動いてもレイアウトを作り直さない。
下の「白背景では RGB 合計 17 しか変えていないのに 1 フレームの 75ms を占めていた」という問題は、間引きではなく絵そのものを差し替えたことで消えた。
トップに起動スプラッシュを置いた。
描画は Synapse Link の _LoadingBlackHolePainter の 7 層をそのまま Canvas 2D へ移植し、フェーズ(load → hold 200ms → expand 600ms easeInQuint → 白フラッシュ 400ms)も原典どおり。
進捗は固定タイマーではなく実際の準備度に連動させ、0.92 で頭打ちにして ready のときだけ 1.0 へ詰める。表示はタブのセッション中 1 回だけ。
ページを速くしても、この規模だと待ちはゼロにならない。それなら白画面のまま黙って待たせるより、このサイトが何を作っている場所かを見せる時間に変えるほうがいい。
徘徊するブラックホールは既に移植済みだったので、同じ絵を起動時にも使えば説明が要らない。固定 2 秒にしなかったのは、速い回線で無意味に待たせたくないから。
逆に遅いときにバーが止まって見えないよう、シグナルが全部滞っても時間だけで進む項を入れた。毎回出さないのは、ページ内リンクで戻るたびに待たされるのが最も嫌われるため。
スプラッシュ自身が待ち時間を延ばしていた。ページのパースと 26 個のインライン script と同じメインスレッドを 60fps の rAF で奪い合うので、覆うつもりのものが原因側に回る。
画素で測ると、降着円盤(3 層 x 60 セグメントの加算合成)は白背景では最大でも RGB 合計 17 しか変えていないのに、1 フレーム 99.3ms のうち 75ms を占めていた。
加算合成は白に足しても飽和するだけなので、当然といえば当然だった。読み込み中は円盤を描かず、飲み込みが始まって背景が黒くなってからだけ描くようにし、canvas の更新自体も 30fps に間引いた。
見た目は前後で区別がつかない。
もう一つ、フェイルセーフの検証のほうが設計より重かった。
当初スクロールロックを html:has(#boot-splash:not(.is-done)) という CSS だけで持たせ、「外し忘れる class が無いから安全」と考えていた。
ところが JS を 404 させて試すと、is-done が付かないのでロックだけが永久に残り、ページがスクロール不能になった。
CSS 側のフェイルセーフは opacity と visibility しか戻せない。結局 overflow は JS が付け外しする形に変え、スクリプトが読めない場合はロックも掛からない、という非対称に倒した。
「CSS だけで完結するから安全」は、CSS で戻せない性質のものには通用しない。
徘徊ブラックホールの low プロファイル(粗いポインタ / 短辺 700px 未満 / コア 4 以下)で、ビューポート大の canvas を捨てた。
canvas 自体を本体ぶんの正方形まで縮め、位置は transform: translate3d() で動かす。絵は一切落としていない。
分割数も層数もぼかしも原典のまま。減らしたのは描き直す回数と、描く場所の大きさだけ。
以前このライブラリを最適化したとき、分割数や層を削る案は「見た目が損なわれるわりに 1 秒あたり 45ms しか稼げない」として却下してある。
その判断を今回もひっくり返したくなかった。
一方で、本体は最大でも 300px 角しか塗っていないのに、毎フレーム画面全体ぶんの clearRect と drawImage を回し、430x932 の端末で 6.4MB のバッキングストアを抱えていた。
位置の更新をコンポジタに渡せば、絵に一切触れずに費用だけ下げられる。
バッキングストア 6.4MB → 0.91MB、毎フレームの CPU 描画は「文字列比較 1 回とスタイル書き込み 1 回」になった。
副次的に、モバイルで URL バーが開閉するたび innerHeight が変わって走っていた全画面ぶんの再確保も no-op になった。
移植できなかったのは VOID タグだけで、画面基準のクランプが成り立たないため「画面端では本体の反対側へ折り返す」に変えた。
またこの変更で、デザインシステムのデモが canvas モード前提の _resize を上書きしていることが露見し、スマホで開くと壊れる状態だったので quality: 'high' の明示で塞いだ。
zerohalf.jp だけ standalone だった証明書の認証方式を nginx に寄せ、3 枚すべてを同じ方式に揃えた。
今後ドメインを足すときも --nginx と --cert-name の明示を必須とし、standalone は使わない。
2026-08-30 に zerohalf.jp が証明書失効で開かなくなった。失効は 08-25 で、5 日間気づかなかった。
原因は standalone が 80 番を bind できず更新に失敗し続けていたことだが、本当の問題は「同じサーバーの証明書なのに 1 枚だけ更新経路が違う」状態を放置していたこと。
方式が混在していると、動いている証明書を見て安心してしまい、壊れている 1 枚に気づけない。揃えること自体が監視の代わりになると考えた。
再発行後、certbot renew --dry-run が 3 枚すべて success。有効期限は 2026-11-28 まで回復した。
一方で、certbot が pip / apt / snap の 3 系統入りタイマーが 2 本並走していること、nzw.jp-0001 という未使用の重複証明書があること、retrieval.nzw.jp が DNS も SAN も欠けたまま設定だけ残っていることが判明し、宿題として残った。
更新失敗が誰にも通知されない点は、同日に期限監視(後述)を入れて塞いだ。
夜は必ず出していたのをやめ、時間帯ごとの抽選にした。朝・昼・夕は 1/3、夜は 1/2。
大きさも原典の blackHoleSizes から tiny / small / medium を等確率で引き、速度は表どおり大きいほど遅くする。
さらに z-index を 30 と -1 で往復させ、コンテンツの前後を行き来させた。
毎晩かならず同じ大きさで同じように横切るものは、2 回目には目に入らなくなる。実際、実装直後に見たときの「何か動いている」という驚きが、翌日にはもう無かった。
出るかどうか・どの大きさか・手前か奥かが毎回変わるなら、見つけたときにだけ嬉しいものになると考えた。ずっと手前にいると読む邪魔になる問題も、同じ仕組みで解ける。
確率・サイズ・速度・奥行きの入れ替わりをヘッドレスブラウザで実測して検証した(抽選 2000 回で 0.32〜0.36 と 0.52、速度は表と 1% 以内で一致)。
副産物として、classList.remove() が属性を無条件に書き直すことに起因する MutationObserver の無限ループを検出でき、公開前に潰せた。
No.02「おつかれ!アクリルキーホルダー」を追加し、トップのヒーローでは No.02 を No.01 より前に置いた。
No.01(いくぞ!)を下げるのではなく 2 枚並べ、カルーセルは 7 枚から 8 枚に増やしている。絵柄は「おつかれ!」「山頂達成!」の文字が先に読める配置で、キャラクターは言葉の下に置いた。
No.01 は「準備は雑だけど、とりあえず行く」という自分に向けた絵だった。実際に持ち歩いてみると、人に見せたときに反応が返ってくるのは絵柄ではなく文字のほうで、キーホルダーは「自分の気分の表明」より「相手に言う一言」として機能していた。
2 点目は最初からその用途で設計したほうが素直だと考え、のぼり切ったあとをねぎらう絵にした。トップで新しい方を先に出したのは、グッズ導線が 1 本しか無い以上、最新でないものが先頭にあると「更新が止まっている棚」に見えるため。
ラインナップは .goods-card を 1 枚、詳細は .item を 1 ブロック足すだけで済み、goods.css は 1 行も触っていない(.item + .item の区切り線がそのまま効いた)。
ひとつ前の goods の項に書いた「2 点目以降はカードを複製するだけ」という想定は、実際に成立した。
No.02 の商品写真はキーホルダーがど真ん中に写っていて、そのまま全面に敷くと左に置く文字と重なった。
トリミングでも右に寄せられないため、写真の左端(ピントの外れた山と空)を反転して横に引き伸ばし、画面そのものを左へ拡張した 1800x1004 を書き出して使っている。
さらに最小幅では横に切ると 2 体のうち片方が必ず欠けるので、上に空を足した縦位置(900x1200)を <picture> で出し分けた。
候補は「文字を右に逃がす」「写真を小さく置く」「画を広げる」の 3 つ。前 2 つは No.01 のパネルと作りが変わり、同じ棚の商品が別レイアウトで並ぶことになる。
元の背景がもともとボケているので、引き伸ばしてぼかしても嘘にならないと判断した。反転してから引き伸ばせば継ぎ目の色が完全に一致するので、合成跡が出ない。
被写体が幅の 67% に来て、既存の scrim(左 0.90 → 右 0.00)をそのまま使い回せた。CSS はクラス名を .goods2-* に変えただけで、パネルの構造は No.01 と同一。
写真の都合でレイアウトを変えないという形に落ち着いた。
アクリルキーホルダーを作ったタイミングで /goods/ を新設し、トップのヒーローと Labs & Prototypes の両方から導線を張った。
EC・カート・価格表示は一切作らず、ステータスは「試作 / 構想」の 2 段階だけを持たせている。配布・販売の可能性は残すが、決まるまでは「未定」と明記する。
キーホルダーは 1 個しか手元になく、売る算段も配る算段も無い状態で出来上がった。この段階で「SHOP」を名乗ると、買えないものを並べた店になって信頼を落とす。
一方で置き場所を作らないと、アプリや書籍の副産物として生まれたキャラクターがどこにも残らない。「作ったものの延長線上にある記録」という枠なら、売れる前でも嘘にならないと判断した。
.goods-card)を複製するだけで追加できる形に落ち着いた。販売を始める場合も、ステータスを差し替えて導線を足すだけで移行できる。ページは静的 HTML(本番は nginx が直接配信)で高速に見せつつ、トップのカードは /v1/content-cards API から公開中のものを動的注入。
API 停止時は静かにスキップして静的閲覧を維持する。
--nzw-* トークンに集約し、フレームワーク非依存の CSS で全ページ共通化した。飛空艇を飛ばす(城と龍のあいだの頻度・夜は窓に明かり・降る日は出さない)/雲海の下に降らせる回は雲海を厚くする/雲と雲海の色を 1 組の定義にそろえる/雲海を積雲の形に(放射状のグラデーションの丸を密に重ねる)/大型怪獣が出たら地上の生き物は端へ寄って見守る/登山夫婦と猫夫婦は必ず組、山頂のテント(全員で中へ・煙・ときどき外で走る/珈琲)/自己紹介カードは既定を透明に(乗せると濃く、外すとすぐ戻る)/雨雪は雲の底から出して雲より奥に描く/春は花畑の中で猫が寝る(花の匂いを嗅ぐ回は廃止)/山頂の台地を画面幅に合わせて広げる・テント前でご飯と椅子/怪獣が近づいたら画面外へ逃げる/自動の回しは季節 30 秒・時間 20 秒/競走のうさぎは、目覚めたら «!» を出してから追いかける
天気 5 種 × 強さ 3 段階と風 4 種、雲海の下だけに降る範囲、雪の積もり、降る日の雨宿り(葉の傘・雪の鎌倉)/猫の部品抽選と性格・鬼ごっこ、登山夫婦、恐竜 4 種/操作パネルの整列、開閉は保存しない
山を奥と手前の 2 枚に分け、小動物をその間・恐竜をさらに奥へ/山の動物を 2 列の時計で出し、同じ動物は 1 分あける/キジ猫 2 匹の遊びと、恐竜を見て逃げる動き/大型の怪獣(クールな姿・縮まず歩いて出入り)/自己紹介カードの既定を不透明度 48%・放置 5 秒で 5%/太陽の近くで雲・城・龍も暗くなる/NUKERO! と Dotpile のリリース日を未定に
共通ヘッダーにお知らせのベルを足す(未読の数・最新 5 件)/メニューに「お知らせ」の行と一覧 /news//中身は news.json 1 本、足すたびに check_news.py で検査
About に空と生き物(鳥・龍・恐竜・うさぎとかめ・花火・流れ星・天の川・雲海など)を足す/出す条件を NZW_RULES の表 1 つに集め、山の後ろの層を稜線で切り抜く/自己紹介カードとヘッダーを半透明にし、5 秒放置でカードをほぼ透明にする
統計を CI が数え直す(fetch_repos.py が 12 本を treeless mirror で取る・毎日 05:00 JST の cron)/握り潰した警告と「止まっている」の検知を Slack と step summary へ/トップのベタ書きは gen_home_stats.py --fix-html が書き戻す
開発ノート 23 ページから徘徊するブラックホールを外す(bh-wanderer.js / bh-ambient.js の読み込みをやめる)/アプリ一覧とデザインシステムには残す/notes.css の彩度調整を apps.css へ移す
コミット数の図を 1 日 1 点・直近 90 日から週の合計・直近 52 週に変更(日ごとだと見張られているように感じるため)/7 日移動中央値と跳ねた日のリングをやめる/幕 1 と quiet 画面の実数も直近 52 週に揃える
Vol.4 の発売日を 2026-09-22 に変更(手で直した 24 箇所のうち 6 箇所が古いまま残った)/発売日と発売状態の源を /book/books.json 1 箇所にし、data-book の印 63 箇所へ sync_books.py がデプロイ時に書き戻す/既刊 Vol.1・Vol.2 の日付も同じ仕組みに載せる
放置した開発ノートが灰色に曇ったまま残る不具合を修正(部分消しに 1 秒ごとの全面消しを足す)/GPU 側で canvas が作り直されたとき(contextrestored)に寸法と変換を当て直す/「放置すると重くなる」は倍率 2・8 分の放置で再現できず未解決
背景の濃い/軽いを 10 秒ごとに入れ替える(最初の 1 枚はくじ、0.7 秒のフェード)/テーマを切り替えると濃いが二度と出なくなる不具合を修正(1 コマの遅れで «重い端末» と判定し、上げ直さない決まりが固定していた)/技術書の数から発売中止の巻を外す(4 → 3)
AI ワークスペースのホームを作り直し(中央 512px の 2 列 → --nzw-* の 3 セクション・1→2→3 列、カードは button にしてキーボードで辿れるように)/「テストエリア」タブを配信されている側へ戻す(Bearise・Certly・Dotpile・声くらべ)/複製だった html/static/ai-workspace.html をリダイレクトに畳み、iframe 4 本を開いたときに読む形へ
起動スプラッシュを 7 画面に広げ、絵はページごとの語彙から作る(仕組みは 1 本のまま --bs-p だけを渡す)/ブラックホールをデザインシステム 1 枚に戻し、暗いパネルに閉じ込めて VOID: xx.x% に進捗を入れる/動きを減らす設定ではスクロールをロックしないようにした
About を /about/ に移し旧 URL は 301(リンク 36 箇所を張り替え)/肩書きを「PoC で終わらせない、AI プロダクトマネージャー」の一行に/現在地の下線が群の先頭で 13px 左へ食み出していたのを background 方式で直す
読み物の前面に降らせるのをやめ、雨を持つ画面を窓辺だけに絞る(カード版・6 幕のオーバーレイを撤去)/窓辺の「降るもの」に大雨の段を足す(雨脚 1.9 倍・本数 24 → 60)/雨の実装が 2 系統あることに気づかないまま 3 つ目を前面に足していた
雨を小雨 45 / 雨 95 / 大雨 200 本の 3 段階に(段階は #weather-overlay[data-rain]、見た目は CSS 側に集約)/大雨は粒に加えて 濁り・風のむら・足元のしぶき の 3 枚を重ね、雷雨(95〜99)も大雨として拾う/.rain-drop の CSS が初回コミットから存在せず、雨は一度も降っていなかったのを直した
開発ノートに「プロダクトの系譜」を新設(ノード 14・辺 7 の 1 枚)/改名・後継・作り直し・開発中止だけを線で結び、中身は各ノートへ逃がす/一覧にはフィルタの外側から導線を 1 枚
スクロール中にブラックホールの回転が止まる不具合を修正(スプライトの描き直しを見送る scrollPauseMs を廃止)/守っていた負荷は実測 1 回 0.07〜0.10ms・CPU 占有 0.2% で、見た目の損に見合っていなかった/短い窓(短辺 700px 未満)だとデスクトップでも low プロファイルに落ちるのが発端
ブラックホールの跡を全廃し、10〜24 秒に 1 個だけ○を吐く形へ(実測 3.4 個/分・画面に居る割合 24%)/溝と泥のコード 100 行以上を削除/直すべきは跡の形ではなく存在の方だった
夜へ切り替えたときの太陽と月を「入れ替わり」から「運行」に(太陽は今の位置から右下へ沈み、月は左下から昇る)/沈みきってから面を暗くし、朝は日が出てから明るくする(transition-delay を行き先の状態に持たせて順番を作る)/向かい合う時間帯(夜⇄昼・朝⇄夕)は必ずあいだの位置を通す(transition は 2 点を直線で結ぶだけなので、間に 1 つ挟んで 2 本に分ける)/夜から出るときは横位置を地平線の下で飛ばして真上に昇らせる/運行を弧にする(横は等速・縦は上り下りで緩急。x と y に同じ緩急を当てると、どんな cubic-bezier でも 2 点を結ぶ直線にしかならない)/朝は遅らせずに緩急で「じわっと明けて、日が出た瞬間にぱっと」/時間帯にも 15 秒の自動回転を足す(既定は切。開いたときの時間帯は訪問者の時計のまま)/横と縦で緩急を変えて弧を描かせる(同じ緩急だと真っ直ぐな斜めの線になる)/空の明るみの中心を太陽の位置から切り離す(--glow-x/y。夜の光源は月なので、沈む太陽に持っていかれない)
トップを 6 幕のスクロール物語に組み直し(8 枚の自動送りカルーセルを廃止)/以前のデザインを /classic.html として恒久化しヘッダーのボタンで往復(出し分けは Cookie でサーバ側)/<title>・description・OGP・canonical を新設(それまで og: はゼロ)/背景を 切 / 軽い(Canvas2D)/ 濃い(WebGL2)の 3 段階に/index.html 8,238 → 3,444 行、canvas 11 → 2 個
ブラックホールの跡を点の列から連続した溝へ(丸い染みが並んで見えるのをやめる)/寿命 24 秒 → 30 秒で重力圏の外まで伸ばす/地の明るさが分からないので淡い紫と濃い黒の 2 本重ねに/継ぎ目の膨らみが数珠つなぎに見えるので、円と円のあいだを埋める泥の層をうっすら敷く(2.6 秒周期で揺れる)
太陽を同心円+方位環+細い光条の図に描き直し(CSS の blur 3 枚を SVG グラデーションへ)/月を満ち欠けするものにし、欠け具合を今日の月齢から出す/ヘッダーの操作卓に時間帯(時計/朝昼夕夜)を追加し --sun-* を @property 化
開発ノートのブラックホールを彩度 0.3・濃さ 0.34 まで落とし VOID タグを非表示に(notes.css と data-label)/通った跡の粒を本体の半径に比例させる(固定 2.6px → 比 0.13、tiny 1.3px〜medium 4.6px)
徘徊するブラックホールをトップページからアプリ一覧/開発ノート/デザインシステムへ移設/起動処理を bh-ambient.js に切り出し(index.html から CSS 76 行 + JS 140 行を削除)/時間帯の抽選と天気ごとの周期を廃止して 1 通りに
メニューのパネルを画面の高さいっぱいに使い、続きがあることを下端のぼかしで示す(利用規約まで辿れるように)/デザインシステムの横あふれを解消し(文書幅 515px → 390px)、目次が横に流れることを両端のぼかしで示す/起動スプラッシュの絵をブラックホールからサイトの虹ラインへ差し替え(406 行 → 214 行)
ノート 18 ページのページ内 CSS 4,488 行を notes.css 1 枚(1,471 行)に統合/見た目をドットのみで組み直しブランド色 14 色を全廃/一覧の頭に 9 リポジトリの直近 90 日コミット数グラフ(466 コミット)を追加
トップの DOMContentLoaded を 39.4 秒 → 31.6 秒に(Tailwind を Play CDN から事前ビルドへ。残りの支配項はインライン script と判明、宿題)/初回表示の画像を 15.5MB → 0.74MB にし lazy 読み込みを導入/Synapse Link のブラックホールで起動スプラッシュを作成/スマホの徘徊ブラックホールを全画面 canvas から本体サイズ + transform へ(6.4MB → 0.91MB)
第2弾「おつかれ!アクリルキーホルダー」を /goods/ に追加/トップのヒーローを 8 枚にして新しい方を先頭側へ/商品写真の左を伸ばしてヒーロー用の 16:9 を書き出し、最小幅は縦位置に出し分け
zerohalf.jp の証明書失効から復旧(standalone → nginx 認証に統一)/運用でつまずいた場所を本ノートに集約/期限監視スクリプトと日次 cron を追加し Slack 通知まで開通させて失効の見逃しを塞ぐ/certbot 3 系統・重複証明書・retrieval.nzw.jp の未整理を宿題として記録
Synapse Link の描画をトップページへ移植し徘徊させる/出現を時間帯ごとの抽選に変更/コンテンツの前後を天気に応じて往復
各プロダクトの Product Note(PRD / 設計ノート)を整備・公開
design-system.css で配色・トーンを標準化
『チームに溶け込む AI エージェント開発』を公開
Engly / DayGrid / LangBridge / Synapse Link を順次リリース、Flutter Web を同一オリジンに統合
FastAPI + 静的 HTML + DB コンテンツカードのハイブリッドとして運用開始
未決事項:MISSION(zerohalf 名義)と VISION(nzw.jp 名義)の主語の整理、B2B 導線の設計。