投稿者: sakurai_dev

  • 使っていないものは、書かない

    使っていないものは、書かない

    広告を入れて収益にできないか、という話から始まりました。

    調べていて手が止まったのは、広告そのものではありません。プライバシーポリシーのページが、このサイトに無かったことです。

    この記事で扱うこと

    広告を入れるかどうかに関係なく、すでに必要だったという話。テンプレートをそのまま貼らなかった理由。そして、置く場所と管理の仕方をどう決めたかです。

    広告の話から入ったが、広告の問題ではなかった

    広告配信サービスの規約を読むと、Cookieを使うことと、利用者が無効にできることを、プライバシーポリシーに明記するよう求められています。

    そこで自分のサイトを見て、ページ自体が無いことに気づきました。

    ただ、よく考えると広告の話が出る前から必要でした。

    すでにやっていたこと 本来必要だったこと
    アクセス解析でCookieを使う 何を集めているか、断る方法の説明
    問い合わせフォームを作った 受け取った情報の使い道の説明
    読了状態をブラウザに保存 保存していること自体の説明

    広告は4つめにすぎません。

    新しいことを検討して、古い抜けが見つかる

    今回は「広告を入れるには何が要るか」を調べたことで、すでに必要だったのに無かったものが出てきました。

    抜けは、抜けているという理由では見つかりません。誰も困っていないからです。別のことを始めようとしたときに、ついでに見つかることのほうが多いと思います。

    テンプレートをそのまま貼らなかった

    この手の文書には雛形がたくさんあります。貼れば5分で終わります。

    ただ、実際に読んでみると使っていないサービスの記述が入っていました。広告配信サービスを利用しています、という一文もそのひとつです。

    これを貼ると、使っていないのに使っていると書いてある状態になります。

    書いてあることは、確かめられる

    プライバシーポリシーは、読む人が確かめるための文書です。書いてあるとおりかどうかは、ページのソースを見れば分かります。

    実態と違うことが書いてあれば、それは説明ではなく、ただの飾りです。飾りを置くくらいなら、無いほうがまだ正直かもしれません。

    そこで、現時点では広告配信を行っていないと明記しました。始めたら、そのとき追記します。

    見えないことをしているなら、書く

    もうひとつ迷ったのが、読了状態の保存です。

    これはブラウザの中だけに記録していて、こちらのサーバーには届きません。個人を特定する情報でもありません。書かなくても問題にはならない種類のものです。

    それでも書きました。理由は、利用者から見えないからです。

    記事一覧に「読了済み」と出るのは、どこかに記録されているからです。読む人がそれに気づいたとき、どこに何が保存されているのか分かる場所があるほうがいい。法的に必要かどうかと、説明したほうがいいかどうかは、別の基準です。

    置き場所と、管理の仕方

    フッターの最下部に置いた

    ナビゲーションには入れませんでした。普段見たいものではないので、並べると他の項目の邪魔になります。

    一方で、探しに来た人が見つけられないのは困ります。フッターの最下部は、この種の文書が置かれる場所として広く定着しています。決まった場所にあることが、探す側にとっての手がかりになります。

    CMSから取る形にした

    文面をコードに直接書くこともできました。やめた理由は、更新のされ方です。

    コードに書いた場合
    サービスを1つ増やす
       ↓
    ファイルを編集して、ビルドして、デプロイする
       ↓
    面倒なので後回しになる
       ↓
    実態と食い違ったまま残る

    この文書は正しさが時間とともに失われる種類のものです。使っているサービスが変われば、その日から内容がずれます。

    用語集と同じく、管理画面から直せる形にしました。手順が軽いほど、実際に更新されます。

    今回の学び

    まず、新しいことを始める前に、足元の抜けが出てくること。広告の検討をしなければ、しばらく気づかないままでした。何かを増やそうとするときは、いま動いているものの前提も一緒に点検する機会だと思います。

    次に、テンプレートは、書いてある内容が自分に当てはまるか確かめてから使うこと。雛形は考える手間を省いてくれますが、確かめる手間まで省くと、事実と違う文書ができあがります。

    そして、法的に必要かどうかと、説明したほうがいいかどうかは別だということ。見えないところで何かをしているなら、書いておく理由はそれだけで十分です。

    最後に、更新されない場所に置くと、正しさは時間とともに失われること。書いた瞬間だけ正しい文書は、しばらく経つと嘘になります。直しやすい場所に置くことは、内容を正しく保つための設計の一部です。

  • 届かない問い合わせ

    届かない問い合わせ

    問い合わせフォームを作りました。

    実装そのものは小さいのですが、決めることが3つありました。どれも「動くかどうか」ではなく「壊れたときにどう見えるか」の話でした。

    この記事で扱うこと

    メールアドレスを載せない選択、送信先が未設定のときにフォームを出すかどうか、そして失敗をどう伝えるか。共通しているのは「成功したように見える失敗」をどう防ぐかです。

    1. メールアドレスを直接載せない

    いちばん簡単なのは、ページにメールアドレスを書くことです。実装が要らず、壊れる要素もありません。

    ただし、公開した以上は自動で収集され、迷惑メールが届き始めます。個人サイトでも例外ではありません。

    そこで、フォームの送信先だけを外部サービスに任せる形にしました。受け取り用のアドレスがブラウザ側に出ません。

    方式 手間 アドレスの露出
    アドレスを載せる ゼロ する
    自前でメール送信 サーバーと送信サービスの契約が必要 しない
    外部サービスへ送信 登録して1行設定するだけ しない

    自前で用意する案もありましたが、問い合わせ1つのために管理するものが増えます。メール送信サービスの契約、鍵の管理、送信失敗時の扱い。得られるものに対して重すぎると判断しました。

    2. 送信先が未設定のとき、フォームを出すか

    ここが今回いちばん考えた点です。

    外部サービスの送信先URLは、環境変数で渡します。つまり設定を入れるまでは、送信先が空のままです。

    このとき、フォームをどう扱うか。素直に実装すると、こうなります。

    何も考えずに書くと
    フォームは表示される
       ↓
    入力して送信できる
       ↓
    送信先が空 → どこにも届かない
       ↓
    それでも「送信しました」と表示される

    これが最悪の状態です。

    送った側は、届いたと思って返信を待ちます。こちらには何も残りません。問い合わせが来なかったのか、届かなかったのかも分かりません。

    届かない問い合わせは、無いより悪い

    フォームが無ければ、相手は別の連絡手段を探すか、諦めます。どちらにしても相手は状況を把握できます。

    しかし届かないフォームは、相手に「連絡した」と誤認させます。返信が来ないことで、対応が悪いという印象だけが残ります。何もしないより損をしています。

    そこで、設定が無いときはフォームごと出さないことにしました。

    実装
    const ENDPOINT = process.env.NEXT_PUBLIC_CONTACT_ENDPOINT;
    
    if (!ENDPOINT) {
      return <p>お問い合わせ先は準備中です。</p>;
    }

    3行です。「準備中」と正直に出すほうが、動くように見えて届かないより、はるかにましです。

    3. 失敗を隠さない

    設定が正しくても、送信は失敗します。通信が切れる、外部サービスが落ちている、上限に達している。

    このとき、画面をどうするか。「送信しました」と出してしまうのは簡単です。実装が楽ですし、その場では体験が滑らかに見えます。

    しかし結果は2番目と同じで、相手は届いたと思って待ちます。

    失敗したときは失敗したと表示し、時間をおいて試すよう伝える形にしました。気まずさより、事実のほうが役に立ちます。

    3つに共通していたこと

    並べてみると、判断の軸は1つでした。

    避けたかった状態
    うまくいったように見えて、実は何も起きていない

    この状態が厄介なのは、誰も気づけないことです。エラーも出ず、画面も正常で、記録も残りません。問題があること自体が分からないので、直しようがありません。

    失敗するなら、見えるところで失敗させる

    機能が動かないことより、動かないのに動いて見えることのほうが高くつきます。「準備中です」と出ていれば、設定を入れ忘れていることに自分でも気づけます。静かに成功したふりをする実装は、自分自身も欺きます。

    今回の学び

    まず、簡単な方法には、たいてい代償があること。アドレスを直接載せるのは実装ゼロですが、迷惑メールという継続的なコストを払います。手間の総量で比べる必要があります。

    次に、未設定の状態を、設計に含めること。設定が入った状態だけを想定して作ると、入っていない期間の挙動が事故になります。「まだ設定していない」は必ず通る道です。

    そして、成功の表示は、成功したときだけ出すこと。当たり前に聞こえますが、実装を楽にするために崩れやすい原則でもあります。表示は事実に従わせるべきです。

    最後に、静かに失敗する仕組みを作らないこと。これは今回に限らず、繰り返し出てくる主題です。エラーが出る失敗は直せます。直せないのは、失敗したことが分からない失敗のほうです。

  • 順番で決まる名前は、いつか壊れる

    順番で決まる名前は、いつか壊れる

    用語集の102語と、記事のあいだにリンクを張ろうとしました。

    ところが張る前の下調べで、そのリンクが最初から壊れる仕組みになっていることが分かりました。順番を変えただけで壊れます。

    この記事で扱うこと

    並び順から自動でIDを作ることの危うさ、公開前の記事へリンクしてしまっていた話、そして「直したつもり」で終わらせないための確認方法です。

    用語ページの中身を、直接指せるようにしてある

    用語集は分野ごとに1ページで、1ページに10〜18語ほど入っています。個々の語は見出しに id を振ってあり、こう書けば特定の語へ直接飛べます。

    語を直接指す
    /glossary/security/#waf

    記事から用語へリンクするには、この id が要ります。そこで中身を確認しました。

    半分以上が、意味のない名前だった

    IDは用語名から機械的に作っていました。英数字だけを残し、それ以外を捨てる方式です。

    用語 できたID
    WAF waf
    DNS dns
    ネームサーバー term
    名前解決 term-2
    サブドメイン term-3

    日本語の語は、英数字を残すと何も残りません。空になったものには、衝突しないように連番を振っていました。

    102語のうち55語がこの状態でした。

    なぜ、これが壊れるのか

    問題は読みにくさではありません。この連番が「何番目に出てきたか」で決まることです。

    用語を1語追加すると
    追加前                    追加後
    term    ネームサーバー     term    ネームサーバー
    term-2  名前解決           term-2  【ここに1語追加】  ← 割り込む
    term-3  サブドメイン       term-3  名前解決           ← ずれた
                              term-4  サブドメイン       ← ずれた

    用語を1語足すと、それ以降のIDが全部1つずつずれます。

    そして、ずれてもエラーは出ません。ページは正常に表示されます。リンクを踏んだ人が、ページの先頭に着地して「あれ?」と思うだけです。

    外から指される名前は、中の都合で変えてはいけない

    IDは、他の場所から指すための名前です。指す側は、指される側の並び順を知りません。

    用語集を充実させるほど、つまり良い方向に手を入れるほど、リンクが壊れていく。更新すると壊れる仕組みを先に作ってしまっていました。

    手で書いて、増えないようにした

    自動生成をやめ、102語すべてに固定のIDを手で割り当てました。

    対応表を1枚用意する
    "ネームサーバー": "nameserver",
    "名前解決":       "name-resolution",
    "サブドメイン":   "subdomain",

    手で書くのは面倒に見えますが、面倒なのは最初の1回だけです。追加する語は1行足すだけで、既存のIDは動きません。

    あわせて、対応表に無い語が来たら止まるようにしました。

    黙って埋めるより、止まったほうがいい

    未登録の語に自動で連番を振る作りだと、また同じ問題が静かに戻ってきます。止まれば、その場で1行足すだけで済みます。

    「気を付ける」で運用するより、間違えられない形にしておくほうが確実です。

    もうひとつ、公開前の記事へリンクしていた

    用語集には「関連記事」を載せています。ここも確認したところ、22件のうち8件が、まだ公開していない記事を指していました。

    用語集のページは公開済みです。つまり読者から見ると、リンクを踏むと404になります。

    原因は単純で、記事を書く前提でリンクを先に書いていたためです。

    そこで、公開済みの記事の一覧を持たせ、そこに載っていないリンクは出力しないようにしました。記事を公開したときに一覧へ1行足せば、リンクも一緒に出てきます。

    ついでに、リンクの文字をスラッグから記事タイトルに変えました。apex-vs-www と書かれても、押す前に中身が分かりません。

    直したあと、本当に直ったかを確かめる

    ここが今回いちばん大事な部分です。

    本番のページを書き換える必要がありましたが、「更新しました」という応答は、内容が正しいことを意味しません。途中で一部だけ置き換わっていても、成功として返ってきます。

    そこで、手元で作ったHTMLと、本番に入ったHTMLを数値に変換して突き合わせました。

    確認の流れ
    手元で組み立てたHTML   → 数値に変換 → A
    本番に入ったHTML       → 数値に変換 → B
    
    A と B が一致 → 1文字も違わない
    

    文章を1文字ずつ見比べるのは現実的ではありません。全体を1つの数値に潰してしまえば、一致するかどうかだけを見れば済みます。

    8ページすべてで一致しました。さらに、書き換えたあとで全リンクの飛び先が実在するかも機械的に確認しています。

    確認したこと 件数 壊れ
    記事 → 用語集のリンク 230 0
    用語集 → 記事のリンク 38 0
    下書きのまま残っているか 10件すべて —
    確認まで自動にしておく

    268本のリンクを手で踏んで確かめることはできません。目視で確認できない量になった時点で、確認する仕組みのほうを用意する必要があります。

    「たぶん大丈夫」と「1件も壊れていないことを確かめた」は、別のことです。

    今回の学び

    まず、外から指される名前は、中の都合で変わってはいけないこと。並び順・件数・登録した順番といった内部の事情でIDが決まると、中を整理するたびに外が壊れます。名前は約束なので、勝手に変えられない形にしておくべきです。

    次に、自動生成は、入力の性質を確かめてから使うこと。英数字を残す処理そのものは間違っていません。日本語の用語に対して使ったことが間違いでした。半分以上で空になると分かっていれば、最初から別の方法を選んでいたはずです。

    そして、足りないときは黙って埋めずに止めること。埋めてくれる親切な仕組みは、間違いも一緒に埋めます。止まれば気づけます。

    最後に、直したことと、直ったことは違うこと。更新が成功したという応答は、内容が正しい証明にはなりません。手元の正解と本番を突き合わせて初めて、直ったと言えます。この確認自体が数秒で終わるなら、やらない理由はありません。

  • 1語1ページにしなかった理由

    1語1ページにしなかった理由

    用語集を作ることになりました。記事に出てくる専門用語を、初学者向けに解説するページです。

    問題は入れ物の選び方でした。1語につき1ページか、まとめて1ページか。この判断に、思ったより多くの要素が絡んでいました。

    この記事で扱うこと

    同じ内容でも置き方で使いやすさが変わるという話と、途中で前提が変わったときにどう判断を修正したか。そして「正攻法が使えないと分かったとき」の落としどころの決め方です。

    最初の判断は「1枚にまとめる」だった

    理由は運用でした。用語は50個、100個と増えていくものです。1語ずつ投稿画面を開いて登録する形にすると、そのうち止まります。

    1枚のページに追記していく形なら、思いついたときに1行足すだけで済みます。続けられる形を優先するという判断でした。

    やりたいことが1つ増えて、前提が変わった

    次に出てきたのが「初学者にも分かる解説を加えたい」という考えでした。

    これを入れると、1語あたりの分量が変わります。定義1行では足りず、なぜそれが存在するのか、具体例、つまずきやすい点まで書くことになります。1語400〜800字といったところです。

    単純な掛け算
    102語 × 600字 ≒ 61,000字

    1枚にまとめると6万字を超えます。目当ての語にたどり着くまでスクロールし続けることになり、用語集として使えません。

    前提が変わったら、結論も変わる

    「1枚がいい」という判断は、1語が短いという前提の上に立っていました。深く書くほど、その前提は崩れます。最初の結論に固執せず、条件が変わった時点で見直す必要がありました。

    1語1ページを検討した

    用語ごとにURLを持たせる案には、はっきりした利点があります。

    「WAF とは」で検索した人に、その1語のページを届けられます。1枚にまとめると、ページ全体で1つのURLなので、個々の語では上位に出にくくなります。

    記事から特定の語へ直接リンクすることもできます。

    ところが、逆効果になる可能性があった

    調べていて引っかかったのが、内容の薄いページを大量に作ることのリスクです。

    1語400字のページが100枚あるサイトは、検索エンジンから「中身の薄いページが多いサイト」と見なされる可能性があります。個々の語で上位を狙うつもりが、サイト全体の評価を下げかねません。

    1ページあたりの分量 評価
    1語1ページ 約400〜800字 薄いと判断される可能性
    分野別にまとめる 約7,000〜9,000字 読み物として成立する

    「個別ページのほうが検索に強い」という一般論が、この規模では当てはまらないことが分かりました。

    もうひとつ、実装上の壁があった

    1語1ページにするには、WordPressに専用の投稿タイプを追加する必要があります。ところが調べると、既存の「実績」「スキル」はサーバー上のテーマファイルに直接書かれていました。

    つまり追加するには、本番のPHPファイルを編集することになります。

    本番のPHPを、ブラウザの入力欄から編集しない

    PHPは文法エラーが1文字あるだけで動かなくなります。そしてWordPressの管理画面にも入れなくなります。その状態から戻すにはFTPが必要です。

    「たぶん大丈夫」で触る場所ではありません。復旧手段を確保してから行うべき作業です。

    分野別に8ページ、という落としどころ

    結果として選んだのは、分野ごとに1ページ、全8ページという形です。

    構成
    /glossary/            分野の一覧
    /glossary/infra/      インフラ・ネットワーク  14語
    /glossary/security/   サーバー・セキュリティ  12語
    ...
    

    各ページの先頭に語の索引を置き、それぞれの語には id を振りました。/glossary/security/#waf のように、外部から特定の語を直接指せます。

    1語1ページの利点である「直接リンクできる」は、これでほぼ回収できます。失うのは「その1語だけで検索結果に出る」ことですが、前述の通り、この規模では狙わないほうが安全でした。

    一覧が、それ自体で使える

    索引を置いたことで、ページを開いてすぐ全体を見渡せます。1枚にまとめる案の良さだった「一望できる」は、分野ごとに残っています。

    実装で、新しいものを作らずに済んだ

    もうひとつ判断があります。用語集のページをどう取得するか、です。

    新しい問い合わせを追加すると、このプロジェクトでは型を作り直す必要があります。作り直しにはCMSへの接続が要るので、作業できる環境が限られます。

    ところが既存の問い合わせを見ると、こう書かれていました。

    既にあったもの
    getProfilePage(uri: string = "/profile/")

    URIを引数に取る作りになっていました。プロフィール専用に見えて、実際には「URIから固定ページを引く」という汎用の処理です。

    名前だけを分けて公開し、用語集のURIを渡すことで、問い合わせを1つも増やさずに実装できました。

    名前が用途を狭く見せていた

    getProfilePage という名前のせいで、プロフィール専用のものに見えていました。中身は汎用なのに、名前が可能性を隠していたわけです。

    新しく作る前に、既にあるものが本当に使えないかを見る価値はあります。特に、名前が具体的すぎるものほど確認する意味があります。

    今回の学び

    まず、前提が変わったら結論を見直すこと。「1枚にまとめる」は、1語が短いという前提の上での正解でした。解説を厚くすると決めた時点で、その前提は崩れています。最初の判断を守ることが目的ではありません。

    次に、一般論が自分の規模で成立するかを確かめること。「個別ページのほうが検索に強い」は間違いではありませんが、1ページあたりの中身が薄ければ逆に働きます。条件付きの正解を、無条件の正解として受け取らないことです。

    そして、正攻法が使えないときの落としどころを、妥協と考えないこと。今回は本番のPHPを触れないという制約がありましたが、その制約が無くても分野別を選んだと思います。制約から入って、結果的により良い形にたどり着くことはあります。

    最後に、新しく作る前に、既にあるものを見ること。名前が具体的すぎるだけで、中身は汎用ということがあります。1つ増やさずに済むなら、それだけ壊れる場所が減ります。

  • Bose QuietComfort Ultra Earbuds — 「自動で切り替わる」に期待しなくなった

    Bose QuietComfort Ultra Earbuds — 「自動で切り替わる」に期待しなくなった

    作業はMacBookとiPadを行き来しながら進めます。調べものや資料はiPad、書くのはMacBookという分け方です。

    音は完全ワイヤレスのイヤホン1つで賄っています。2台あるのにイヤホンは1つなので、どちらの音を出すかを決める作業が毎回発生します。

    この記事で扱うこと

    2台を行き来する使い方で何が面倒になるか、自動で切り替わることに期待しなくなった理由、そしてノイズキャンセリングを常時オンにしている理由です。

    2台をまたぐと、必ず「どっちの音か」が出てくる

    1台だけで使っているうちは、この問題は存在しません。つなげば鳴る、それだけです。

    2台になると、イヤホンはどちらの音を出すかを選ばなければいけません。そして選び方は2つしかありません。機械に決めさせるか、自分で決めるか。

    最初は前者を期待していました。音が鳴ったほうに勝手に移ってくれるのが、いちばん手数が少ないはずだと思っていたからです。

    期待していたことと、実際にやっていること

    買う前に期待していたこと いま実際にやっていること
    音が鳴った側へ勝手に移る イヤホンを長押しして、自分で移す
    意識しなくていい 移すときだけ意識する
    手数はゼロ 手数は1回

    手数だけ見れば期待どおりにいっていません。それでも、いまのほうが困っていません。

    推測が外れたときのほうが、高くつく

    機械に決めさせるということは、「いま鳴っているこの音を、この人は聞きたいはずだ」と推測させるということです。

    推測は当たることもあれば外れることもあります。外れたときは、なぜ音が来ないのかを調べ、どちらにつながっているかを確かめ、戻す、という手順を踏むことになります。1回の手数を惜しんだ結果、何倍もの手数を払います。

    長押しで、自分で移している

    いまはイヤホン側を長押しして、つなぐ先を自分で切り替えています。

    作業の流れとしては、iPadで資料を見ながら音を聞いていて、書く段になってMacBookへ移る、という場面がほとんどです。移るタイミングは自分が決めているので、切り替えの操作もその流れの中に収まります。

    自分で決めるなら、外れない

    手数は1回増えますが、その1回は必ず狙いどおりになります。推測が外れる可能性がゼロになるので、つながり先を確かめる手間が消えます。

    「自動でやってくれる」の価値は、手数が減ることではなく考えなくてよくなることにありました。考える必要が残るなら、手数だけ減っても意味がありません。

    ノイズキャンセリングは、常にオンにしている

    ここも同じ話です。

    環境に合わせて切り替える使い方もできますが、自分は常時オンで固定しています。理由は静かだからというより、「いま要るか」を毎回考えなくて済むからです。

    切り替えられる機能は、切り替えるかどうかの判断を毎回発生させます。1回あたりは数秒でも、1日に何度も出てくると、そちらのほうが重くなります。

    固定してしまえば、イヤホンを着けた瞬間から作業に入れます。判断を1つ減らすために、選べる自由のほうを捨てている形です。

    合わなかったところ

    長押しでの切り替えは片手でできますが、どちらに移ったかは音でしか分からないのが少し不便です。無音のときに切り替えると、確かめる方法が「何か鳴らしてみる」しかありません。

    それと、これは製品というより自分の使い方の話ですが、2台とも音を出している状況には向きません。片方で動画を流しながらもう片方で作業する、といった使い方をするなら、イヤホンを分けたほうが早いはずです。

    まだ書けないこともあります。長時間つけたときの耳の疲れ方と、電池の持ちが1年後にどうなっているかは、いま判断できる段階にありません。

    今回の学び

    まず、自動化の価値は手数ではなく、考えなくてよくなることだということ。手数が1回減っても、確かめる手間が残るなら差し引きで損をします。ここを取り違えると、自動のほうを選んで後悔します。

    次に、推測させる仕組みは、外れたときのコストで評価すること。当たっているあいだは快適なので、外れる場面を想像しないまま良いものだと判断してしまいます。実際に効いてくるのは外れたときのほうです。

    そして、選べることは、それ自体がコストになること。切り替えられる機能は、切り替えるかどうかを毎回考えさせます。使い方が決まっているなら、固定してしまうほうが軽くなりました。

  • よく使うGitコマンドと、それが何をしているか

    よく使うGitコマンドと、それが何をしているか

    Gitのコマンドは、覚えるより「いま何がどこにあるか」を掴むほうが早いです。

    この記事では、実際に毎日使っているコマンドを、何をしているかの説明つきで並べます。順番は使う頻度ではなく、理解の順番にしました。

    この記事で扱うこと

    Gitが管理している3つの場所という考え方と、それに沿ったコマンドの整理。特に「戻す」系は似たコマンドが多く、取り違えると作業が消えます。実際に消した経験も含めて書きます。

    まず、3つの場所を知る

    Gitのコマンドが分かりにくいのは、ファイルが3つの場所に同時に存在するからです。ここを掴むと、大半のコマンドは「どこからどこへ動かすか」で説明がつきます。

    場所 何があるか 失うとどうなるか
    作業ツリー いまエディタで見ているファイル 戻せない
    ステージ 次のコミットに含めると決めたもの 作業ツリーから入れ直せる
    履歴 コミット済みのもの だいたい取り戻せる

    右の列が重要です。コミットしていないものだけが、本当に消えます。逆に言えば、コミットさえしてあれば、たいていの失敗は取り返せます。

    流れ
    作業ツリー  --git add-->  ステージ  --git commit-->  履歴
       (編集中)              (準備中)           (記録済み)

    状態を見る

    作業の8割はこれです。何かする前に、いまどうなっているかを見ます。

    git status –short
     M frontend/src/app/page.tsx      ← 変更あり(未ステージ)
    M  frontend/src/lib/data.ts       ← 変更あり(ステージ済み)
    ?? frontend/src/lib/new-file.ts   ← 新規(Gitがまだ知らない)

    左の2文字が、ステージと作業ツリーの状態を表します。--short を付けると1ファイル1行になり、ファイル数を数えられます。これが後で効きます。

    git log –oneline -5
    c5ce23e Merge pull request #11 from kisaku39/feat/next-16
    29aa412 chore(deps): Next.js 16 / React 19 へ移行
    ba625e3 Merge pull request #10 from ...

    直近5件の履歴。--oneline が無いと1コミットが5行になり、画面が埋まります。

    変更内容を見る
    git diff              # 作業ツリーの変更(未ステージ)
    git diff --staged     # ステージ済みの変更
    git diff --stat       # ファイル名と増減行数だけ

    --stat は、変更が意図した範囲に収まっているかを一目で見るときに使います。

    記録する

    基本の3つ
    git add ファイル名        # 指定したものをステージへ
    git add -A               # 変更・新規・削除をすべてステージへ
    git commit -m "説明"      # ステージの中身を履歴へ記録

    git add -A は便利ですが、意図しないファイルまで巻き込みます。実行前に git status --short で並んでいるものを確認する習慣にしておくと安全です。

    ファイル数を数える

    私は一度、5ファイル直したはずのコミットに2ファイルしか入っていない状態でpushしました。ビルドも通り、デプロイも成功し、何も直っていませんでした。git status --short の行数を数えるだけで、pushする前に気づけた失敗です。

    直前のコミットをやり直す
    git commit --amend -m "新しい説明"

    直前のコミットを作り直します。メッセージの誤字直しや、入れ忘れたファイルの追加に使います。push済みのコミットには使わないでください。履歴が別物になり、他の人の手元と食い違います。

    戻す ── ここが一番危ない

    似た名前のコマンドが複数あり、取り違えると作業が消えます。用途で分けて覚えるのが安全です。

    やりたいこと コマンド 危険度
    ステージから降ろす(編集は残す) git restore --staged ファイル 安全
    ファイルの編集を捨てる git restore ファイル 戻せない
    コミットを打ち消す新しいコミットを作る git revert コミットID 安全
    履歴を巻き戻す(編集は残す) git reset --soft コミットID 注意
    履歴を巻き戻して全部捨てる git reset --hard 非常に危険

    reset は「取り消し」ではなく「破棄」

    git reset --hard は、直前の操作を取り消したいときに手が伸びやすいコマンドです。しかし実際にやることは違います。

    git reset –hard が消すもの
    コミット済みの変更   → 履歴に残っているので取り戻せる
    未コミットの編集     → 消える。取り戻す手段は無い

    履歴に無いものは、履歴から復元できません。当たり前ですが、この当たり前が事故の形になります。私はこれで、書き換えたばかりの5ファイルを失いました。

    状態を整えたいだけなら stash

    「一旦きれいにしたい」「ブランチを切り替えたい」という理由でresetを使いたくなったら、その用途は git stash のほうです。捨てずに退避してくれます。

    revert と reset の違い

    どちらも「戻す」ですが、履歴の扱いが逆です。

    同じ「戻す」でも
    reset  … 履歴そのものを巻き戻す(無かったことにする)
    revert … 「打ち消す」コミットを新しく積む(戻した記録が残る)

    すでにpushしたものを戻すときは、必ずrevertです。resetで消すと、他の人の手元にある履歴と食い違い、収拾がつかなくなります。

    退避する

    git stash
    git stash              # 変更を退避して、作業ツリーをきれいにする
    git stash -u           # 未追跡(新規)ファイルも一緒に退避
    git stash list         # 退避したものの一覧
    git stash pop          # 最後に退避したものを戻す
    git stash drop         # 退避したものを捨てる

    -u を付ける場面が意外と多いです。既定のstashは、Gitがまだ知らない新規ファイルを退避しません。そのままpullすると「未追跡のファイルを上書きすることになるので中断する」と止められます。

    stash は名前を付けられる

    git stash push -m "pull前の退避" のようにメモを残せます。stashは溜まると何が何だか分からなくなるので、いつ・なぜ退避したかを書いておくと後で助かります。

    ブランチを扱う

    よく使うもの
    git branch                    # ブランチ一覧(* が現在地)
    git branch --show-current     # 現在のブランチ名だけ
    git checkout -b feat/xxx      # 新しいブランチを作って移動
    git checkout main             # 既存のブランチへ移動
    git branch -d feat/xxx        # ブランチを削除

    git branch -d はマージ済みのブランチしか消しません。未マージの変更が残っていれば拒否してくれます。大文字の -D は確認なしで消すので、日常では使わないほうが安全です。

    なぜブランチを切るのか

    本番に直接入れると、壊れていた場合に「戻す」しか選択肢がありません。ブランチで作業すれば、本番を触らないまま動作確認できます。デプロイ先によっては、ブランチごとに確認用のURLを作ってくれます。確認の機会を1回増やすだけで、事故の大きさが変わります。

    共有する

    やり取りの3つ
    git fetch      # リモートの状態を取ってくる(作業ツリーは変えない)
    git pull       # fetch + 自分のブランチに取り込む
    git push       # 自分のコミットをリモートへ送る

    git push が拒否されることがあります。

    よくある拒否
    ! [rejected]  main -> main (non-fast-forward)
    hint: Updates were rejected because the tip of your current
    hint: branch is behind its remote counterpart.

    これは正しい動作です。手元がリモートより遅れている状態でpushすると、リモート側の新しいコミットが消えます。Gitはそれを防いでいます。

    対処は「先に取り込む」です。git pull してから、あらためてpushします。ここで --force を使うと、止めてくれた意味が無くなります。

    困ったときの2つ

    操作の履歴をたどる ── reflog

    git reflog
    c5ce23e HEAD@{0}: commit: ...
    682faf3 HEAD@{1}: reset: moving to HEAD    ← ここで何かした
    682faf3 HEAD@{2}: pull: Fast-forward

    git log がコミットの履歴なのに対し、reflog は「自分がGitに何をしたか」の履歴です。checkout、reset、pull、commit——すべて記録されています。

    「変更が消えた」「いつのまにか違うブランチにいる」というときは、まずこれを見ます。何が起きたかを推測せずに済みます。私も、消えた原因がresetだったことをこれで特定しました。

    「別のプロセスが動作中」と言われる ── index.lock

    こんなエラー
    fatal: Unable to create '.git/index.lock': File exists.
    Another git process seems to be running in this repository.

    Gitは操作のたびに .git/index.lock という目印のファイルを作り、終わったら消します。何らかの理由で消し損ねると、このファイルが残り続けます。

    そしてロックが残っている間、Gitはすべての操作を拒否します。本当に別のプロセスが動いている可能性があるからです。

    対処
    # 本当に他でGitを動かしていないことを確認してから
    rm -f .git/index.lock

    このファイルは0バイトで、中身がありません。Gitのデータは一切入っていない、単なる目印です。他でGitを動かしていないことさえ確かなら、消して問題ありません。

    同じリポジトリを複数の場所から触らない

    私がこれを踏んだのは、同じリポジトリに2つの経路から触っていたためでした。片方に書き込み権限が足りず、ロックを作れたのに消せませんでした。.git は排他制御を前提に作られているので、複数の経路から同時に触る構成は避けるべきです。

    1日の流れに並べると

    典型的な作業
    git status --short            # 汚れていないか確認
    git pull                      # 最新を取り込む
    git checkout -b feat/xxx      # 作業用のブランチへ
    
    (編集する)
    
    git status --short            # ファイル数を数える
    git diff                      # 中身を確認
    git add -A
    git commit -m "..."
    git push -u origin feat/xxx   # ブランチのままpush
    
    (確認用URLで動作確認)
    (問題なければマージ)
    
    git checkout main
    git pull
    git branch -d feat/xxx        # 役目を終えたブランチを削除

    この中でいちばん省かれやすいのが、最初と真ん中の git status --short です。そして今日書いた失敗は、どちらもここを飛ばしたことが原因でした。

    まとめ

    まず、コミットしていないものだけが本当に消えること。逆に言えば、こまめにコミットしておけば、たいていの失敗は取り返せます。危ない作業の前にコミットを1つ挟むのは、いちばん安いお守りです。

    次に、reset は取り消しではなく破棄だということ。状態を整えたいだけなら stash を使います。用途で選び分けるだけで、事故の大半は避けられます。

    そして、何かする前に状態を見ること。git status --short と git diff --stat の2つを挟むだけで、「意図しないものが混ざっている」「意図したものが入っていない」の両方に気づけます。

    最後に、止められたときは理由があること。pushの拒否も、ロックによる中断も、Gitが壊れないように守っている動作です。--force や強制削除で押し通す前に、なぜ止まったのかを読むほうが、結果的に速いです。

  • 7日間でハッキングをはじめる本 — 守り方は分かる。何から守るのかは分からない

    7日間でハッキングをはじめる本 — 守り方は分かる。何から守るのかは分からない

    自分のサイトを公開したあと、脆弱性の検知と監視の仕組みを作りました。依存関係を見張って、警告が出たら気づけるようにする、という内容です。

    そのとき読んでいた情報は、全部が守る側のものでした。

    この記事で扱うこと

    守る側の情報だけを読んでいて何が抜けていたか、体験して覚えることと読んで覚えることの違い、そして学んだことをどこまで使ってよいかです。

    守り方は分かる。何から守るのかは分からない

    「この設定を入れる」「このヘッダーを付ける」「このバージョンに上げる」。守る側の情報は、やることが具体的に書いてあります。そのとおりにやれば作業は終わります。

    ただ、終わったあとに本当に守れているのかが判断できませんでした。

    理由ははっきりしていて、攻撃がどういう手順で進むのかを知らなかったからです。手順を知らないまま対策だけ並べると、全部が同じ重さに見えます。

    優先順位は、攻撃側の手順から決まる

    対策のリストには、放置すると即座に踏まれるものと、他がすべて破られた後にようやく効いてくるものが混ざっています。

    順番が分からないと、上から順にやるか、やりやすいものからやるかしか選べません。

    手を動かす本を選んだ

    そこで読んだのがこの本です。TryHackMeという、攻撃を試すために用意された環境を使いながら進みます。

    解説を読むだけの本ではありません。指示されたとおりに調べ、見つけ、入る。その手順を自分でなぞります。

    やってみて分かったのは、残り方が違うということでした。読んだだけの知識は「そういうものがあるらしい」で止まりますが、自分の手で手順を踏むと、どこが引っかかりやすいかまで体に残ります。

    攻撃は、ひらめきではなく手順だった

    やる前は、攻撃というものをもっと特別なものだと思っていました。実際には調べる、試す、記録するの繰り返しです。

    特別ではないと分かったことが、いちばん大きい収穫でした。特別でないなら、こちらも手順で備えられます。

    読むだけでは、半分しか入らない

    この本の注意点はここです。手を動かす前提で書かれているので、読むだけだと半分で終わります。

    環境の準備や、うまくいかないときに止まる時間も含めて、それなりにまとまった時間が要ります。通勤中に読み進める種類の本ではありません。時間を確保してから始めるほうがいいと思います。

    7日間という題名ですが、1日ぶんに何時間かかるかは書いてある内容と自分の慣れ次第です。自分は日によってかなり差がありました。

    使ってよい範囲について

    ここは書いておきます。

    この本で扱うのは、そのために用意された練習用の環境です。同じ手順を、許可を得ていない相手に対して試してはいけません。技術として成立するかどうかと、やってよいかどうかは別の話です。

    自分の場合、試す先は自分のサイトと練習環境だけにしています。攻撃側の手順を知る目的は、守る順番を決めるためであって、それ以上ではありません。

    今回の学び

    まず、守る側の情報だけでは、優先順位がつかないこと。対策は列挙できても、どれを先にやるべきかは攻撃の手順を知らないと決まりませんでした。

    次に、体験は、残り方が違うこと。読んで分かったつもりになっていたことでも、自分の手で通してみると引っかかる場所が出てきます。その引っかかりのほうが、あとで効きます。

    そして、知ることと、やってよいことは別だということ。手順が分かるほど、その線を自分で引いておく必要が出てきます。本の中で扱われている環境が、練習のために用意されたものである意味は、そこにあります。

  • たのしいRuby 第6版 — フレームワークから入ると、言語が抜ける

    たのしいRuby 第6版 — フレームワークから入ると、言語が抜ける

    CMSを作っている会社にいたころ、製品のベースがRailsでした。仕事は管理画面やフロントの改修です。

    コードは目の前にあります。動いてもいます。それなのに、何をしているのか読めませんでした。

    この記事で扱うこと

    フレームワークの側から入ると何が抜けるのか、なぜ言語の本を読み直したのか、そしてこの本で埋まった部分と埋まらなかった部分です。

    読めない、の中身

    最初は「経験が足りないから読めない」のだと思っていました。実際には、もっと具体的な詰まり方をしていました。

    目の前の1行が、Railsが用意した仕組みなのか、Rubyの文法なのかが区別できない。

    これが分からないと、次の一手が決まりません。

    その行が何か 調べに行く先
    Railsの機能 Railsのドキュメント、そのバージョンの仕様
    Rubyの文法 言語のリファレンス
    製品独自の実装 社内のコードと、書いた人

    3つのどれかは分かるのに、どれなのかが分からない。結果として、調べ始める場所が決まりませんでした。

    いちばん時間を食ったのは、書くことではなかった

    手が止まっていたのは、書き方が分からないときではありません。どこを調べればいいか分からないときでした。

    検索の言葉すら決まらないので、当てずっぽうで探すことになります。

    フレームワークから入ると、言語が抜ける

    入り方が逆だった、というだけの話です。

    ふつうは言語を覚えてからフレームワークに進みます。自分は製品の改修という入口から入ったので、Railsの決まりごとは仕事の中で覚えていきました。「こう書けば動く」は増えていきます。

    ただ、その下にある言語そのものは通っていません。だから、Railsの型に当てはまらない行に出会った瞬間に読めなくなります。

    動かせることと、読めることは別

    フレームワークの作法だけでも、決められた形の改修はできます。できないのは、形から外れたときの判断です。

    「動いているから正しい」以上のことが言えないまま進むことになります。

    言語の本に戻った

    そこで読んだのがこの本でした。Railsの本ではなく、Rubyそのものの入門書です。

    文法を順番に追う構成なので、フレームワークの側から入って穴が空いている状態を埋めるのに向いていました。「ここはRubyの書き方だ」と分かるようになると、調べに行く先が決まります。それだけで手が止まる回数がはっきり減りました。

    特に効いたのは、ブロックやシンボルのようなRubyでは当たり前だが、他の言語から来ると初見のものです。Railsのコードにはこれらが大量に出てきます。知らないまま読むと全部が「Railsの魔法」に見えます。

    この本で埋まらなかったこと

    当然ですが、Railsの作法はこの本には書いてありません。言語を埋めるための1冊と割り切る必要があります。

    それと、他の言語をすでに書ける人にとっては、前半は既知の内容が続きます。飛ばしながら読んで、知らない書き方が出てきたところで止まるという読み方になりました。最初から順に読む本としては、少し長く感じるかもしれません。

    今回の学び

    まず、入る順番は逆でもいいが、どこかで戻る必要があること。フレームワークから入るのは実務では珍しくありませんし、そのほうが早く戦力になります。ただ、戻らないままだと「形から外れた瞬間に止まる」状態が続きます。

    次に、詰まりの正体を言葉にすると、対処が決まること。「読めない」のままでは何を勉強すればいいか分かりませんが、「言語かフレームワークかの区別がつかない」まで分解できれば、読むべき本は1冊に決まります。

    そして、調べる場所が決まらないことが、いちばん時間を食うこと。答えを探す時間より、探す場所を決められない時間のほうが長かった。ここは知識の量ではなく、切り分けの問題でした。

  • Keychron Nape Pro — 1週間かかる、は誰の話か

    Keychron Nape Pro — 1週間かかる、は誰の話か

    キーボードの手前に置くポインティングデバイスを使い始めました。マウスへ手を伸ばす動作が消えます。

    レビューを読むと、だいたい「慣れるまで1週間はかかる」と書いてあります。自分は2〜3日で普通に使えるようになりました。

    速く慣れたという話ではありません。その1週間を、自分はすでに別のところで払い終えていただけです。

    この記事で扱うこと

    「慣れるまで1週間」という数字が誰の話なのか、慣れる対象を分けて数えるとどうなるか、そして買う前に見るべきなのは期間ではないという話です。

    何を置き換えたのか

    これはトラックボールの一種で、キーボードの手前に置いて、親指や指でボールを回してカーソルを動かします。

    マウスを使っていると、カーソルを動かすたびに右手をキーボードから離して横へ振ります。1回は小さな動作ですが、1日に何百回も繰り返します。手前に置くデバイスなら、その振る動作がなくなります。

    肩の開き方が変わるので、長時間の作業で姿勢が戻らなくなる感覚は減りました。ただ、それはこの製品でなくても、手前に置くデバイスなら得られる効果です。

    実際の置き方

    HHKBの手前にKeychron Nape Proを置き、左右に分かれた木製パームレストの真ん中に収めた机の上
    奥がHHKB、手前がNape Pro。左右の木がパームレストで、その間にちょうど収まっている。

    キーボードは HHKB Professional HYBRID Type-S で、その手前に Nape Pro を置いています。

    パームレストは左右に分かれていて、その真ん中に Nape Pro が入ります。分離型を選んだのは見た目の好みではなく、ここに置き場所が必要だったからです。

    一体型だと、この場所が空かない

    横に長いパームレストを置くと、キーボードの手前はそれで埋まります。手前に置くデバイスを使うなら、パームレストの形まで連動で決まります。

    デバイスを選んだ時点では気づいていませんでした。

    手は左右のパームレストに載せたままで、指だけでボールに届きます。木なので沈み込まず、手首の高さが一定に保たれます。

    キーボードに貼ってある付箋は、接続先の番号です。Nape Pro もキーボードも複数の機器に繋がりますが、どの番号がどの機器かは覚えていられませんでした。覚えておくことを前提にした仕組みは、たいていこうなります。

    「慣れる」は1つのことではない

    ここが今回いちばん考えた部分です。

    「慣れるまで1週間」と書かれると、1つの壁を1週間かけて越えるように読めます。実際には別々のものが3つあり、それぞれ独立して越える必要があります。

    慣れる対象 中身
    ボールでカーソルを動かす 指の動きと画面の動きが結びつくまで。狙った場所で止まらない期間がある
    手を右へ振らない姿勢 マウスを取りに行く動作が体に残っている。空振りする
    ボタンの割り当て どの操作をどこに置くか。決めて、指が覚えるまで

    マウスから乗り換えた人は、この3つを同時に相手にします。だいたい1週間、というのは妥当な数字だと思います。

    自分の場合、1つめは終わっていた

    その前に使っていたのが、ロジクールのトラックボールでした。

    つまり「ボールでカーソルを動かす」は、乗り換える前から体に入っていました。残っていたのは置き位置とボタンの割り当てだけです。2〜3日というのは、そのぶんが短くなった数字にすぎません。

    払っていないのではなく、先に払っていた

    そのロジクールのトラックボールを使い始めたときは、1〜2週間ほど操作に苦労しました。

    狙ったところで止まらない、細かい選択ができない、そのたびに手が止まる。あの期間はきちんとあって、しかも今回より長かったです。

    並べてみると分かりやすくなります。マウスからトラックボールへ移った時点で1〜2週間、そこからこの製品へ移って2〜3日。合計すれば、結局「1週間より長い」ところに着地しています。

    レビューの「1週間」は、間違っていない

    ここを取り違えないようにしたいと思いました。

    1週間という数字は、マウスから来た人にとっては正しいのだと思います。3つを同時に越えるならそのくらいかかります。書いた人が誇張しているわけではありません。

    問題は、その数字に「どこから来たか」という前提が書かれていないことです。前提が省かれた数字は、条件の違う人にそのまま当てはめられてしまいます。

    数字には、書いた人の出発点が含まれている

    「慣れるまで◯日」は、製品の性質だけで決まる値ではありません。製品と、その人が何を使っていたかの差で決まります。

    同じ製品でも、出発点が違えば別の数字になります。どちらも嘘ではありません。

    買う前に見るべきなのは、期間ではない

    そう考えると、検討している人が確かめるべきなのは日数ではなくなります。

    いま何を使っているか。マウスなら、上の3つを全部越えることになります。すでにトラックボールなら、越えるのは2つです。ここが分かれば、レビューの数字を自分向けに読み替えられます。

    逆に言うと、「何日で慣れました」とだけ書かれたレビューは、そのままでは使えません。読むときは、その人が前に何を使っていたかを探すことになります。

    正直に書いておくこと

    この製品はクラウドファンディングで手に入れたので、一般の発売より前から使っています。そのぶん使用期間はありますが、周辺の情報はまだ少ない状態です。困ったときに検索して出てくる記事の数は、定番の製品とは比べものになりません。

    それと、ここまで書いてきたのは操作に慣れるまでの話だけです。長く使ったときに手や肩がどうなるかは、まだ判断できる段階にありません。分かったらあらためて書きます。

    実際に割り当てているもの

    3つめの「ボタンの割り当て」は、慣れるというより決める作業です。決まっていないあいだは指が迷うので、そこが長引くと「まだ慣れない」と感じます。

    いま入れているのは次のとおりです。ボタンは短く押したときと長く押したときで別のものを出せるので、2つ書いてある行はその区別です。

    場所 短押し 長押し
    M1(手前・上) 右クリック —
    M2(手前・下) 左クリック —
    01(左上) 英数 Win
    02(左下) 戻る —
    03(右上) かな ボールジェスチャ
    04(右下) 進む —
    ホイール 上スクロール 下スクロール

    決め方の基準はひとつだけで、キーボードから手を離さずに済むものを優先することです。

    英数とかなを左右に分けて置いたのは、日本語入力の切り替えがいちばん回数の多い操作だからです。ここをキーボード側に取りに行くのなら、手前に置いた意味が半分なくなります。

    戻ると進むも、調べものをしているあいだずっと使います。これも本来は別の場所にあった操作でした。

    割り当ては、覚える数を増やさないほうに寄せる

    ボタンがあると、つい何か入れたくなります。ただ覚えていない割り当ては、無いのと同じです。

    いま入れているのは、どれも元から毎日やっていた操作の置き場所を変えただけで、新しく覚えることは増やしていません。3つめが早く終わったのは、そのぶんもあります。

    今回の学び

    まず、「慣れるまで◯日」は製品の値ではないこと。製品と、その人の出発点との差で決まります。数字だけを取り出すと、自分に当てはまるかどうかが判断できません。

    次に、前提が省かれた数字ほど、そのまま信じられてしまうこと。1週間という数字には「マウスから来た場合」という条件が付いていましたが、書かれていませんでした。書いた人にとって当たり前すぎる前提ほど、文章から抜け落ちます。

    そして、自分が書くときも同じ立場になること。「2〜3日で慣れた」とだけ書けば、同じ落とし穴を作ることになります。前に何を使っていたかを一緒に書いておかないと、読んだ人の役に立ちません。

  • 通ったビルドが、何も直していなかった

    通ったビルドが、何も直していなかった

    ビルドが通った。テストも落ちていない。デプロイも成功した。

    それでも、直したかったものは1つも直っていませんでした。

    この記事で扱うこと

    変更が丸ごと消えた状態でコミットが成功してしまった話と、それがなぜ「成功」に見えたのか。そして、こういう失敗を見つけるための確認が「動くかどうか」ではなく「入っているかどうか」だという話です。

    やろうとしていたこと

    2つの修正を、1回の作業でまとめる予定でした。

    直したいもの 理由
    記事一覧の取得上限(20→100) 公開記事がちょうど20本。次の1本から漏れ始める
    構造化データの日時をGMTに 時差の情報が無い時刻を、機械に渡していた

    どちらもGraphQLのクエリを変える作業です。このプロジェクトはクエリから型を自動生成しているので、クエリを変えたら型を作り直す必要があります。作り直しにはCMSへの接続が要るため、手順はこうなりました。

    予定していた手順
    1. ソースを5ファイル書き換える
    2. npm run codegen   (型を作り直す)
    3. npm run build     (手元で通ることを確認)
    4. commit → push

    全部が「成功」した

    実行結果は、どこにも問題がありませんでした。

    codegenは通りました。ビルドも通りました。コミットもpushも成功し、Vercelのデプロイも緑になりました。本番サイトは変わらず表示されています。

    ところが本番の構造化データを見ると、日時が変わっていません。

    期待していたもの / 実際
    期待  "datePublished": "2026-08-22T04:20:00Z"
    実際  "datePublished": "2026-08-22T13:20:00"

    デプロイの遅れかと思い、キャッシュを避けて何度か取り直しました。変わりません。

    コミットの中身を見た

    ここでようやく、コミットに何が入っているかを確認しました。

    git show –stat
    frontend/src/gql/gql.ts     | 5 +++++
    frontend/src/gql/graphql.ts | 8 +++++++-
    2 files changed, 12 insertions(+), 1 deletion(-)

    2ファイル。しかも両方とも、自動生成されたファイルです。手で書き換えたはずの5ファイルが、1つも入っていません。

    作業ディレクトリを見ると、posts(first: 20) のまま戻っていました。書き換えたはずの内容が、どこにも残っていません。

    何が起きていたか

    git reflog に答えがありました。

    git reflog
    d370f37 HEAD@{0}: commit: fix: 記事一覧の上限を100に上げ...
    682faf3 HEAD@{1}: reset: moving to HEAD     ← これ
    682faf3 HEAD@{2}: pull: Fast-forward

    pullの直後、コミットの前に reset が挟まっています。git reset --hard は、コミットしていない変更を問答無用で消します。元に戻す手段はありません。

    つまり、書き換え → リセットで消滅 → その状態でcodegen → コミット、という順番で進んでいました。

    reset は「取り消し」ではなく「破棄」

    直前の操作を取り消したいときに git reset --hard を使いたくなりますが、これはコミット済みの履歴を戻す道具です。まだコミットしていない作業は、履歴に無いので戻せません。作業を残したまま状態を整えたいなら git stash のほうです。

    なぜ「成功」してしまったのか

    ここが今回いちばん考えさせられた点です。

    変更が消えているのに、なぜビルドが通ったのか。答えは、消えた結果として、全部が整合していたからです。

    クエリ側 生成された型 結果
    意図した状態 first: 100 first: 100 通る
    実際の状態 first: 20 first: 20 通る
    もし片方だけ残っていたら first: 100 first: 20 落ちる

    中途半端に消えていれば、ビルドが落ちて気づけました。きれいに全部消えたので、辻褄が合ってしまったわけです。

    ビルドが検証しているのは「矛盾が無いこと」であって、「意図が反映されていること」ではありません。何も変えていないコードは、当然のように矛盾しません。

    緑は「壊れていない」の意味

    CIやビルドの成功は、壊れていないことの証明です。直っていることの証明ではありません。この2つを同じものだと思っていると、今回のように「全部が緑なのに何も進んでいない」状態を見逃します。

    では、生成されたファイルの差分は何だったのか

    もうひとつ不思議だったのが、消えているのに gql.ts だけ差分が出ていたことです。

    調べると、生成されていたのは別のクエリの型でした。sitemap用に書いた、こういう文字列です。

    sitemapPosts.ts
    const POSTS_FOR_SITEMAP = /* GraphQL */ `
      query GetPostsForSitemap {
        posts(first: 100) { ... }
      }
    `;

    このファイルは、型生成に依存したくないからこそ素の文字列で書いていました。ところが先頭に付けた /* GraphQL */ という目印を、codegenが「これはクエリだ」と認識して走査対象に含めていました。

    結果として、型生成を避けるために作ったファイルが、型生成の対象になっていたわけです。意図とは正反対でした。

    失敗のおかげで見つかった

    この矛盾は、単体では表に出ません。使われない型がひっそり生成されるだけで、何も壊れないからです。別の失敗を調べていて、たまたま見つかりました。目印を外して、対象から除きました。

    次から何を見るか

    再発を防ぐために増やした確認は、1つだけです。

    コミットする前に
    git status --short

    並んでいるファイルの数を数えます。5ファイル触ったなら、5ファイル並んでいるはずです。2つしか無ければ、その時点でおかしい。

    やり直したときは、実際にこれで確認しました。7ファイル(手で書いた5つ + 生成された2つ)が並んでいることを見てからコミットしています。

    1回目 2回目
    コミットに入ったファイル 2 7
    ビルド 通った 通った
    本番の日時 変わらず Z が付いた

    1行目だけが、両者を区別できていました。

    今回の学び

    まず、ビルドの成功は「壊れていない」であって「直っている」ではないこと。今回は変更が丸ごと消えたことで整合が取れてしまい、すべての工程が緑になりました。矛盾を検査する仕組みは、何もしていない状態を歓迎します。

    次に、結果ではなく、入力を確認すること。「本番の表示が変わったか」で気づくのは最後です。コミットに何ファイル入ったかを見れば、pushする前に止められました。確認は下流より上流に置くほうが安いです。

    そして、git reset --hard は取り消しではないこと。コミットしていない作業に対しては、単なる破棄です。状態を整えたいだけなら stash を使う、という区別を持っておくと事故が減ります。

    最後に、避けたはずの仕組みに、目印ひとつで巻き込まれること。型生成から独立させるつもりのファイルが、コメント1つで走査対象になっていました。意図は、コードのどこかに具体的な形で表れていないと守られません。