PRODUCT NOTE / PLATFORM PRD
個人開発者 nzw のポートフォリオ兼 AI プラットフォーム。
この文書は、このサイトそのものをなぜ・誰のために・どんな思想で作っているかから記録した開発ノートです。
| ステータス | Active(継続運用・開発中) |
|---|---|
| ドメイン | nzw.jp(姉妹サイト zerohalf.jp と別ドメインで共存) |
| 運営 | nzw(Hidekazu Nozawa) — PM × 作家 × バイブコーダー |
| 載せているもの | 技術書 / モバイルアプリ / Web ゲーム / AI ツール / 開発ノート / グッズ |
| 最終更新 | 2026-08-30 |
| 技術構成 | FastAPI ・ Tailwind ・ Canvas ・ Flutter Web ・ Nginx ・ EC2 / Docker |
| 関連リンク | トップ ・ About(運営者) ・ グッズ ・ 開発ノート一覧 |
nzw.jp は、1 人の開発者がつくるアプリ・ゲーム・AI ツール・技術書・開発ノートを一元公開する場です。単なる作品展示場ではなく、「AI に任せる」ではなく「AI を使うことで、あなた自身が強くなる」という設計思想(zerohalf ミッション)を体現するプラットフォームを目指しています。
土台にあるのは zerohalf のミッション「AI が全てできる時代に、人間としての価値を育てる」。
そこで、理論(本)・実証(プロダクト)・記録(開発ノート)をひとつの場で束ねて見せるプラットフォームとして nzw.jp を運営している。
「PM × 作家 × バイブコーダー」を 1 人で横断する交差点そのものが参入障壁。汎用 AI が持てない「偏愛」と「積み重ねた文脈」で勝負する。
「思想とプロダクトが結びついて届く」状態かを見る。指標は仮置き
トップページには、ときどきブラックホールが現れて画面上をゆっくり横切る。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/秒。
ずっと手前にいると邪魔になり、ずっと奥だとほぼ見えない。そこで 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 秒の上限を置いた)。MutationObserver で監視するので、AUTO・手動・ランダムのどれでテーマが変わっても追従する。prefers-reduced-motion のときはキャンバスを非表示にし、スクリプト自体を起動しない。踏んだ罠 classList.remove() は消す対象が無くても class 属性を書き直す。後片付けを毎回無条件に呼んだ結果、MutationObserver → 後片付け → 属性の書き換え → MutationObserver …… とマイクロタスクが循環し、ページが完全に固まった。動作中かどうかのフラグを見て、必要なときだけ片付けるようにして解決。
サイトが落ちるときは、たいてい作り込みではなく足回りで落ちる。同じ穴に二度落ちないよう、実際に踏んだものだけを残す。
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)。sudo の secure_path の並び順で実際に動くのは pip 版。自動更新のタイマーも certbot.timer と snap.certbot.renew.timer の 2 本が並走し、どちらも同じ /etc/letsencrypt を触る。実体ごとに持っているプラグインが違うので、触る前に sudo which certbot で実体を確かめる。1 枚だけ standalone になっていたのも、nginx プラグインを持たない実体で発行した名残とみるのが自然。nzw.jp と nzw.jp-0001 が同一の 3 ドメイン(nzw.jp / www.nzw.jp / api.nzw.jp)で発行済み。nginx が読むのは前者だけで、後者は誰にも使われないまま更新され続けている。-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.html を含むそれ以外はすべて location / 経由で FastAPI に流しているので、nzwjp-api.service が落ちるとサイト全体が 502 になる。「カード API だけ落ちても閲覧は維持される」話と、配信そのものが落ちる話は別物。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 で書く。ルート相対で書くと zerohalf.jp 側を探しに行って 404 になる。今回いちばん効いたのは更新の失敗そのものではなく、失敗が誰にも届かなかったことだった。そこで /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 点で残す。
standalone だった証明書の認証方式を nginx に寄せ、3 枚すべてを同じ方式に揃えた。今後ドメインを足すときも --nginx と --cert-name の明示を必須とし、standalone は使わない。certbot renew --dry-run が 3 枚すべて success。有効期限は 2026-11-28 まで回復した。一方で、certbot が pip / apt / snap の 3 系統入りタイマーが 2 本並走していること、nzw.jp-0001 という未使用の重複証明書があること、retrieval.nzw.jp が DNS も SAN も欠けたまま設定だけ残っていることが判明し、宿題として残った。更新失敗が誰にも通知されない点は、同日に期限監視(後述)を入れて塞いだ。blackHoleSizes から tiny / small / medium を等確率で引き、速度は表どおり大きいほど遅くする。さらに z-index を 30 と -1 で往復させ、コンテンツの前後を行き来させた。classList.remove() が属性を無条件に書き直すことに起因する MutationObserver の無限ループを検出でき、公開前に潰せた。/goods/ を新設し、トップのヒーローと Labs & Prototypes の両方から導線を張った。EC・カート・価格表示は一切作らず、ステータスは「試作 / 構想」の 2 段階だけを持たせている。配布・販売の可能性は残すが、決まるまでは「未定」と明記する。.goods-card)を複製するだけで追加できる形に落ち着いた。販売を始める場合も、ステータスを差し替えて導線を足すだけで移行できる。/v1/content-cards API から公開中のものを動的注入。API 停止時は静かにスキップして静的閲覧を維持する。--nzw-* トークンに集約し、フレームワーク非依存の CSS で全ページ共通化した。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 導線の設計。