投稿者: sakurai_dev

  • リンク先は存在した。それでも404だった

    リンク先は存在した。それでも404だった

    連載の記事に「次回:〇〇」というリンクを書いています。そこを押すと404でした。

    リンク切れは事前に確認していました。結果は0件でした。確認の仕方が間違っていたのです。

    この記事で扱うこと

    存在するのに辿り着けないリンクの話です。確認が通ってしまった理由、直し方を3つ比べて1つを選んだ過程、そして表示するときに判定する実装を書きます。

    何を数えていたか

    確認の中身はこうでした。全記事の本文から /blog/ で始まるリンクを抜き出し、その行き先のスラッグがCMSに存在するかを見る。存在しないものがあれば、それがリンク切れです。

    この方法で、112本ぶんを突き合わせて0件でした。

    存在するかどうかを見ていた

    CMSの中には、公開済みの記事と、予約投稿の記事が両方入っています。

    連載の第1回を公開した時点で、第2回はまだ予約投稿の状態です。CMSには在るので、確認は通ります。しかしサイトに出ているのは公開済みのものだけなので、押すと404に着きます。

    2つの集合がずれている

    整理すると、こうなっていました。

    含まれるもの
    CMSにある記事 公開済み + 予約投稿 + 下書き
    サイトに出ている記事 公開済みだけ

    確認していたのは上の集合で、読む人が見ているのは下の集合でした。差の部分が、そのまま404になります。

    書いている側からは見えません。管理画面では第2回が普通に並んでいますし、編集もできます。書き手の視界には、公開前の記事も入っているからです。

    直し方を3つ比べた

    案 やめた理由 / 選んだ理由
    公開のたびに本文からリンクを外す 連載は5本あり、回が進むたびに手を入れる。忘れた回だけ404が残る
    「次回」を書かない 404は消えるが、次があることも伝わらなくなる。連載として損
    表示するときに判定する 採用。書く側は常に「次回」を書ける。公開日が来れば自動でリンクに戻る

    1つめを外したのは、人が覚えていることを前提にした手順だからです。今回そもそも見落としたのは、覚えていることを前提にした確認をしていたからでした。同じ形の対策を足すと、同じ理由で破れます。

    本文を通す場所が、もう1段増えた

    このサイトの本文は、表示する前にいくつかの処理を順番に通ります。商品カードを差し込み、リンクの末尾の形を揃え、見出しに目印を付ける。そこに1段足しました。

    const posts = await getPosts();
    
    // getPosts() は公開済みのものだけを返す。
    // ここに無いスラッグは「CMSには在るが、まだ出ていない」記事になる。
    const publishedSlugs = new Set(
      posts.map((p) => p?.slug).filter((s): s is string => Boolean(s)),
    );
    
    const rawContent = softenPendingLinks(
      normalizeInternalLinks(injectProducts(post.content)),
      publishedSlugs,
    );
    

    判定の材料はもともと手元にありました。前後の記事と関連記事を出すために、記事一覧をすでに取っています。取得の順番を入れ替えただけで、通信は1回も増えていません。

    リンクを消さずに、文字にした

    return html.replace(POST_LINK, (whole, _before, slug, _after, text) => {
      if (publishedSlugs.has(slug)) return whole;
      return `<span class="pending-link">${text}<span class="pending-link-note">(近日公開)</span></span>`;
    });
    

    行き先が無いなら丸ごと消す、という手もありました。選ばなかったのは、予告そのものには意味があるからです。

    押せないことと、無いことは違う

    「次回:打ち方が違っても、同じ言葉として扱う」という一文は、続きがあることと、その内容を伝えています。押せるかどうかとは別の仕事です。

    消すと、その情報まで一緒に消えます。薄い文字にして「(近日公開)」を添えれば、押せないことは分かり、何が来るかは残ります。

    「(近日公開)」をCSSの装飾ではなく本文の一部として出しているのは、読み上げや、文字を選んでコピーしたときに落ちないようにするためです。押せない理由は、飾りではなく中身として持たせることにしました。

    置き換えで壊しそうな場所

    本文のHTMLを書き換える処理は、意図しないものまで巻き込みます。6つ試しました。

    試したこと 期待
    公開済みへのリンク そのまま残る
    1つの行に「前回」と「次回」が並ぶ 前回は残り、次回だけ文字になる
    class や title が前後に付く 拾える
    連載の目次(/series/)へのリンク 触らない
    外部サイトのURL 触らない
    リンクの文字に <strong> が入る 中のタグを保つ

    5つめは見落としやすい形をしています。外部のURLにも /blog/ という並びが現れることがあるので、行き先がサイト内かどうかを先に判定する必要がありました。

    確認そのものを直す

    今回いちばん効いたのは、コードの修正ではありません。確認の条件を書き直したことです。

    見ていたもの
    前 リンク先のスラッグが、CMSに存在するか
    後 リンク先のスラッグが、公開済みの一覧に在るか

    条件を書き直して数え直したら、公開中の57本のうち、公開前を指しているリンクは1件でした。指摘を受けたその1件です。数が合ったことで、見落としの範囲が確定しました。

    公開日が来たら、本当に戻るのか

    「公開日が来れば自動でリンクに戻る」と書きましたが、これは作った側の見込みでしかありません。確かめる機会は、2日後に来ました。

    第2回の公開日に、第1回と第2回を両方開いて、連載の案内を見比べました。

    記事 「次回」の行き先 表示
    第1回 第2回(その日に公開) リンクに戻っていた。押すと開く
    第2回 第3回(まだ予約) 文字のまま、「(近日公開)」付き

    第1回の「次回」は、2日前まで押せない文字でした。その日の朝、誰も何も触らないまま押せるリンクになっていて、代わりに第2回の「次回」が押せない文字になっていました。1本ずつ、順送りに開いていく形です。

    戻ることまで確かめて、はじめて直ったと言える

    押せなくする側だけを見て終わりにしていたら、「公開後もずっと押せないまま」という別の壊れ方に気づけませんでした。

    表示を止める仕組みは、止めたものを再開させる側まで確認してようやく半分ではなくなります。

    今回の学び

    まず、存在することと、辿り着けることは別だということ。手元のデータで「在る」と確認できても、読む人に配っている集合に入っていなければ意味がありません。確認は、読む人が見るほうの集合でやる必要がありました。

    次に、書き手の視界は広すぎること。管理画面には公開前のものも並びます。そこを基準に考えると、自分にだけ見えているものを、全員に見えていると錯覚します。

    そして、人が覚えていることを前提にした対策を足さないこと。見落としの原因が「覚えていること前提の確認」だったのに、対策も同じ形にしたら、同じ理由で破れます。表示するときに判定する形にしたのは、そこを繰り返さないためでした。

  • 人が書けて、プログラムが書けない — 403の犯人は権限ではなかった

    人が書けて、プログラムが書けない — 403の犯人は権限ではなかった

    記事の下書きを自動で登録する仕組みを使っています。ある日、それが403を返しました。

    403は普通、「権限がない」という意味です。ところが同じアカウントで管理画面を開くと、手では問題なく書けます。

    この記事で扱うこと

    「人が操作すると通るのに、プログラムから叩くと落ちる」状態をどう切り分けたか。権限の問題ではないと判断するまでに確かめた5つと、そこで分かった犯人です。

    連載「弾かれる」(全5回)

    第1回:症状の切り分け(この記事)/第2回:二分探索で原因を絞る/第3回:エスケープが効かなかった理由/第4回:意味を変えずに書き直す/第5回:正しく止められたときに何を残すか

    最初に疑ったのは、全部はずれだった

    思いつく順に確かめました。

    疑ったこと 確かめ方 結果
    ログインが切れた 自分の情報を取得する問い合わせを投げる 通る。切れていない
    権限が足りない 管理画面から同じ操作をする 書ける。足りている
    投げ先が違う 一覧の取得を同じ入口に投げる 通る。入口は合っている
    記事が壊れている タイトルだけで空の下書きを作る 通る
    本文が長すぎる 本文を半分にして投げる 通る

    ここで方向が決まりました。同じ人・同じ入口・同じ操作で、本文の中身によって結果が変わっています。

    権限の問題なら、中身では変わらない

    権限が足りていないなら、空の下書きも落ちるはずです。空なら通って、ある本文だと落ちる。この時点で権限は完全に容疑から外れます。

    「403だから権限」と思い込むと、この切り分けに辿り着きません。番号ではなく、何を変えたら結果が変わるかで見るほうが早い。

    返ってきたものが、そもそも違った

    決定打は、落ちたときの応答の中身でした。

    返ってきたもの
    通常の権限エラー CMSが作ったJSON。原因を示す符号が入っている
    今回 サーバーが作ったHTMLのページ。CMSの符号は一切ない

    CMSが断ったなら、CMSの形式で断ります。CMSの言葉で返ってきていないということは、要求がCMSに届く前に止められているということでした。

    前段に立っているものがいた

    このサイトのCMSは共用サーバーに置いています。そこには、契約時から攻撃を検知して遮断する仕組みが入っていました。

    管理画面の設定一覧を見ると、確かに有効になっています。自分で入れた覚えはありません。最初から入っていて、これまで一度も邪魔をしてこなかったので、存在を忘れていました。

    自分のサーバーの機能を、自分が知らない

    この仕組みは、要求の中身を見て「攻撃の形をしているか」を判定します。判定に引っかかると、CMSに渡さずにその場で断ります。

    問題は、断った理由がこちらに見えないことです。何が引っかかったかは教えてくれません。403という数字だけが返ってきます。

    引っかかっていたのは、記事の中のサンプルコードだった

    落ちていたのは、コードの書き方を説明した記事でした。本文にプログラムの断片が何か所も入っています。

    攻撃を検知する仕組みから見ると、プログラムの断片と、送り込まれた攻撃コードの区別がつきません。どちらも同じ形をしています。

    つまり、コードの話を書こうとすると落ちるという状態でした。書きたい内容と、守りたい形が正面からぶつかっています。

    ここまでで分かったこと

    分かったこと
    誰が断ったか CMSではなく、その手前にいる遮断の仕組み
    何で判定しているか 送った本文の中身
    どこが悪いか 不明。教えてくれない

    本文は1500文字ほどあります。そのどこか1か所が引っかかっている。読んで探しても、どれも普通のコードにしか見えません。

    次回は、この「どこか1か所」を突き止めた方法を書きます。読むのをやめて、機械的に半分ずつ削っていく手順です。

    今回の学び

    まず、エラーの番号より、何を変えたら結果が変わるかを見るということ。403から権限を疑うのは自然ですが、空の本文で通った時点で権限は関係ありませんでした。変えた条件と結果を並べるほうが、番号を読むより速い。

    次に、誰が断ったかは、断り方の形で分かること。CMSが断ればCMSの形式で返ってきます。違う形式で返ってきたということは、そこまで届いていないということでした。中身を見ずに「403だ」で終わらせると、この情報を捨てます。

    そして、使っていない機能は、存在ごと忘れること。この遮断の仕組みは契約時から動いていて、これまで一度も邪魔をしませんでした。邪魔をしない期間が長いほど、思い出すきっかけが無くなります。

  • 除外しているつもりで、1件も除外していなかった

    除外しているつもりで、1件も除外していなかった

    アクセス解析に、自分のアクセスを入れないための設定があります。ずっと「テスト」のままになっていたので、「有効」に切り替えました。

    それでも、何も除外されません。設定がもう一段、奥にありました。

    この記事で扱うこと

    自分のアクセスを除外していたつもりで、1件も除外できていなかった話です。設定が2段構えになっていること、片方だけでは何も起きないこと、そしてそれが画面からは分からない理由を書きます。

    印を付ける側と、捨てる側

    この除外は、2つの設定が噛み合ってはじめて動きます。

    置き場所 役割
    内部トラフィックの定義 指定したIPからのアクセスに「内部」という印を付ける
    データフィルタ その印が付いたものを捨てる

    片方だけでは何も起きません。印を付けても捨てる設定が無ければ残りますし、捨てる設定があっても印が付いていなければ、捨てる対象が存在しません。

    捨てる側だけがあった

    確認したところ、こうなっていました。

    設定 状態
    データフィルタ あり(内部の印が付いたものを除外)
    内部トラフィックの定義 ルールが1件も作られていない

    つまり、どのアクセスにも「内部」の印が付いていませんでした。フィルタは、一致するものが1件も無い状態で回っていたことになります。

    「テスト」のままだったことを問題だと思っていましたが、有効にしたところで結果は変わりません。除外対象がゼロだからです。

    なぜ気づかなかったか

    一覧の画面には、フィルタが1件、名前も種類も状態も表示されていました。

    設定が「ある」ことと、「効いている」ことは別

    見えるのは、フィルタという入れ物があることだけです。その中身が参照している印を、誰が付けているのかまでは出てきません。

    設定の一覧は、設定した本人が覚えている前提で作られています。半年前の自分が何をしたかは、そこには書いてありません。

    もうひとつ、状態の表示も助けになりませんでした。「テスト」は評価はするが捨てないという意味で、一致が0件かどうかは区別されません。一致が無いことと、一致したが捨てていないことが、同じ表示になります。

    効いていないことを、どう確かめるか

    この種の設定は「何も起きないこと」が正常な状態なので、確かめ方を別に持っておく必要があります。

    噛み合っているかを、1本の線で追う

    除外の設定を確認するときは、フィルタの画面を見るだけでは足りません。

    「誰が印を付けるのか」から順に辿る必要があります。今回で言えば、IPのルールがあるか、そのルールが付ける印の名前が、フィルタが探している名前と一致しているか、という2点です。

    実際に確認したら、印を付ける側の画面には「ルールはまだ作成されていません」と1行だけ書かれていました。探しに行かなければ、ずっと見ない場所です。

    直すときに気をつけたこと

    ルールを作るには、自分のIPアドレスを登録します。ここで、手元のメモに残っていた古い値を使うのはやめました。

    古いIPは、いま別の人が使っている

    家庭の回線のIPは、契約が同じでも変わることがあります。

    変わったあとの値を登録すると、自分は除外されず、代わりに無関係な誰かが除外されます。しかも、どちらも画面からは分かりません。

    設定画面に現在のIPを表示するリンクがあるので、そこで確認した値を使いました。複数の回線から見るなら、条件を並べて足せます。

    さかのぼっては、消えない

    最後にひとつ。この設定が効くのは、これから溜まるデータだけです。

    これまでに入ってしまった自分のアクセスは、あとから消えません。過去の数字は、自分が混ざったまま読むことになります。どのくらい混ざっているかも分からないので、公開直後の数字は参考程度に扱うのが正しい、ということになりました。

    今回の学び

    まず、設定が2段構えのときは、片方だけ直しても何も変わらないということ。「テストを有効にする」は正しい操作でしたが、直す場所がそこではありませんでした。

    次に、一覧に出ているのは入れ物だけだということ。名前も種類も状態も表示されますが、それが誰と噛み合う想定なのかは出てきません。設定を確認するときは、一覧ではなく経路を辿る必要がありました。

    そして、古い値を使い回さないこと。合っていれば自分が消え、間違っていれば他人が消えます。どちらの結果も画面には出ないので、確かめられる方法で取り直すほうが安全でした。

  • 画面幅と設計 — CSS応用(第3回)

    画面幅と設計 — CSS応用(第3回)

    PCでは完璧なのに、スマホで崩れる。そして直そうとすると分岐が増えていく。

    順番を逆にすると、この2つが同時に減ります。狭い画面を初期状態にして、広い画面に足していく書き方です。

    そもそもレスポンシブデザインとは

    1つのHTMLで、スマホでもPCでも適切に表示する作り方です。

    かつてはPC用サイトとスマホ用サイトを別々に作っていました。しかし端末の画面サイズは無数にあり、作り分けは現実的ではありません。そこで、画面幅に応じてCSSの当たり方を変えるという方法が標準になりました。

    今のWebアクセスは半分以上がスマートフォンです。「まずスマホで成立させ、広い画面には余裕を足す」という順番で考えるのが現在の基本になっています。

    あわせて出てくるSassは、CSSを書きやすくする拡張です。変数を使ったりファイルを分けたりでき、最終的には普通のCSSに変換されます。

    この回で解ける困りごと

    「PCでは完璧なのにスマホで崩れる」「CSSが膨らんで、どこを触ると何が壊れるか分からない」。

    よくある書き方(Before)

    .title { font-size: 32px; }
    @media (max-width: 768px) { .title { font-size: 24px; } }
    @media (max-width: 375px) { .title { font-size: 20px; } }
    

    端末が増えるたびに分岐が増えます。

    直した書き方(After)

    .title { font-size: clamp(1.25rem, 4vw, 2rem); }
    

    最小1.25rem、最大2rem、その間は画面幅に応じて滑らかに変化します。分岐ゼロです。

    なぜそうなるか

    モバイルファースト

    max-width で「大きい画面から削っていく」より、min-width で「小さい画面に足していく」ほうが記述が減ります。狭い画面は縦一列が基本形なので、そこを初期状態にするのが自然です。

    .list { display: grid; gap: 16px; }
    @media (min-width: 768px) { .list { grid-template-columns: repeat(3, 1fr); } }
    

    命名で壊れなくする(BEM)

    .card {}            /* Block  かたまり */
    .card__title {}     /* Element  かたまりの部品 */
    .card--featured {}  /* Modifier  状態違い */
    

    クラス名を見ただけで所属が分かるので、他の場所に影響しません。SMACSSなど他の流儀もありますが、チームで1つに統一されていることが本質です。

    アニメーション

    .card { transition: transform .2s ease-out; }
    .card:hover { transform: translateY(-4px); }
    

    動かすプロパティは transform と opacity を優先します。width や top を動かすとレイアウト計算が毎フレーム走り、カクつきます。

    Sass

    変数・入れ子・ファイル分割ができます。ただし入れ子を深くしすぎると詳細度が上がって前回の問題が再発するので、3階層までを目安に。

    手を動かす課題

    一覧ページをスマホ幅(375px)で開き、横スクロールが出ないところまで直してください。仕上げに、カードのホバーで浮き上がる動きを transition で付けます。

    自己チェックリスト

    • □ DevToolsのデバイスモードで375px・768px・1280pxを確認した
    • □ 横スクロールバーが出ていない
    • □ 画像に max-width: 100% を指定した
    • □ クラス名の命名ルールがページ全体で揃っている

    次回はSEOです。実装者が責任を持つ範囲だけに絞って扱います。

    今回の学び

    まず、大きい画面から削るより、小さい画面に足すほうが記述が減るということ。狭い画面は縦一列が基本形なので、そこを初期状態にするのが自然でした。

    次に、命名の目的は、影響範囲を閉じることだということ。クラス名の流儀はいくつもありますが、どれを選ぶかより、1つに統一されていることのほうが効いていました。

    そして、動かすものを選ぶと、カクつきが消えること。幅や位置を動かすと毎フレーム計算が走ります。同じ見た目でも、何を動かすかで滑らかさが変わります。

  • 設定を足しただけでは、変わらなかった

    設定を足しただけでは、変わらなかった

    問い合わせフォームの送信先を設定して、保存しました。画面を開き直しても、フォームは「準備中」のままでした。

    設定は間違っていません。読まれるタイミングが違っただけでした。

    この記事で扱うこと

    問い合わせの受け取りを外部サービスに任せるまでの判断と、その設定でつまずいた3点です。環境変数がいつ読まれるか、保存したら見られなくなる値のこと、そして「正しく弾けているか」の確かめ方を書きます。

    なぜ自分でメールを送らないのか

    このサイトには、メールを送る手段がありません。足すこと自体はできますが、そこから先が長い。

    迷惑メール扱いされないためには、そのドメインから送ってよいという宣言をDNSに置く必要があります。SPFとDKIMと呼ばれるものです。

    SPFは1ドメインに1本しか置けない

    すでに何かの設定が入っている状態で足すと、書き方を誤った瞬間にメールが届かなくなります。

    しかも壊れたことは、こちらからは分かりません。誰かに「返信が来ない」と言われるまで、正常に見え続けます。

    問い合わせを1本受け取るために、この重さを引き受けるのは割に合わないと判断しました。受け取りだけ外に出すことにします。

    求めているのは、転送先1つだけ

    ボット対策も、エラーの出し分けも、入力の確認も、すべてサイト側に置いてあります。外のサービスに任せているのは「受け取って、メールで転送する」という一点です。

    この形にしておくと、乗り換えの費用が小さくなります。別のサービスに移すときに変えるのは送信先のURL1つで、コードには触りません。

    最初から小さく繋いでおく

    外部サービスの機能を使い込むほど、そのサービスの形に合わせた作りになります。便利ですが、離れにくくもなります。

    「何を任せないか」を先に決めておくほうが、あとから選び直せます。

    つまずき1:環境変数は、ビルドのときに読まれる

    送信先のURLは、コードに書かずに環境変数として置いています。保存して、サイトを開き直しました。変わりません。

    思っていたこと 実際
    設定を保存すれば、次のアクセスから反映される 作り直すまで、前の状態のまま

    このサイトはページを先に作ってしまう構成なので、環境変数を読むのはページを作るときです。あとから設定を足しても、すでに作られたページは古い状態を抱えたままになります。

    配信サービスの画面にも「変更を反映するには新しいデプロイが必要です」と出ていました。読んでいませんでした。保存した時点で終わったつもりになっていたからです。

    つまずき2:保存すると、見られなくなる値がある

    環境変数を保存するとき、種別を2つから選べました。

    種別 あとから値を見られるか
    設定値 見られる
    秘密 見られない

    今回は「秘密」を選びました。訪問者に配るファイルへ混ざってほしくない値だからです。

    ただしこれは、自分でも二度と確認できないという意味でもあります。今回は発行元の画面でいつでも見られるURLなので困りませんが、そうでない値なら、保存する前に控える必要がありました。

    つまずき3:相手側の防御が、こちらの正常な送信を弾く

    受け取り側のサービスにも、独自のボット対策が用意されています。画像の文字を読ませるあれです。

    ブラウザ前提の仕組みは、サーバー間では通らない

    この対策は「人がブラウザで操作している」ことを前提にしています。

    今回はサイトのサーバーから相手のサーバーへ送る形なので、有効になっているとこちらの正常な送信まで弾かれます。

    今回は既定で無効になっていたので何もせずに済みましたが、有効な状態で繋いでいたら「設定は合っているのに届かない」で長く悩むところでした。二重に掛けた対策が、互いを打ち消すことがあります。

    正しく弾けているかは、画面では分からない

    繋いだあと、4つ試しました。

    送ったもの 期待 結果
    普通に書いて送る 通る 通った
    開いた直後に送る 断る 断られた
    アドレスの形が違う 断る 断られた
    見えない欄を埋めて送る 捨てる 「送信しました」と出た

    最後のものは、わざとそうしています。機械に「その欄を避ければ通る」と分からせないためです。

    そのぶん、画面を見ているだけでは、弾けたのか通ったのかが区別できません。確認するには受信箱を見て、届いていないことを確かめる必要があります。実際に届いていたのは4件のうち1件だけでした。

    「何も起きないこと」を確かめる手順を用意しておく

    正しく動いた証拠が「画面に出ない」「メールが来ない」という形をしている場合、確認の手順を別に決めておかないと、確認そのものを忘れます。

    今回の学び

    まず、設定を保存した時点では、まだ何も変わっていないことがあるということ。読まれるのがページを作るときなら、作り直すまでは古いままです。保存できたことと、効いていることは別でした。

    次に、隠すと、自分からも見えなくなること。訪問者に見せたくない値を隠すのは正しい判断ですが、その値を次に必要とするのは自分かもしれません。保存する前に控えるかどうかを決めておく必要がありました。

    そして、対策は二重に掛けると打ち消し合うことがあること。自分の側で弾いているのに相手の側でも弾いていると、通したいものが通りません。どこで何を見ているかを1回並べて書き出すのが、結局早い道でした。

  • 動かしていなかったから、漏れていなかっただけ

    動かしていなかったから、漏れていなかっただけ

    問い合わせフォームにボット対策を入れようとして、先に別のことが分かりました。

    いまの作りでは、ブラウザ側にどんな対策を入れても効きません。

    この記事で扱うこと

    送信先のURLが配信されるJavaScriptに入る作りになっていた話と、その直し方です。あわせて、画像の文字を読ませる方式を選ばなかった理由も書きます。

    先頭の6文字が、置き場所を決めていた

    このサイトはNext.jsで作っています。環境変数の名前には、決まりがあります。

    名前 どこから見えるか
    CONTACT_ENDPOINT サーバーだけ
    NEXT_PUBLIC_CONTACT_ENDPOINT ブラウザにも配られる

    名前の先頭に NEXT_PUBLIC_ が付いていると、その値は組み立てのときにJavaScriptの中へ埋め込まれます。訪問者に配られるファイルの一部になる、ということです。

    フォームはブラウザの中で動く部品なので、送信先を知る必要がありました。だから素直に NEXT_PUBLIC_ を付けていました。付けなければ読めないので、それ以外に選びようが無いように見えていました。

    鍵の掛かっていない扉が、隣に並ぶ

    この形だと、送信先のURLはフォームを開かなくても分かります。配られたファイルを開けば書いてあります。

    フォームを通さずに送れる

    見えない入力欄を足しても、送信までの時間を測っても、それらはフォームを使った場合にだけ効きます。

    送信先を直接叩く相手には、そもそも通り道が違うので関係がありません。鍵を掛けた扉の横に、鍵の掛かっていない扉が並んでいる状態でした。

    動かしていなかったから、漏れていなかった

    ここで確認したところ、本番の環境変数は設定されていませんでした。

    フォームは、送信先が無いときは「お問い合わせ先は準備中です」と出す作りにしてあります。つまり公開されてからずっと、フォームは表示されておらず、URLもどこにも出ていませんでした。

    安全だったのではなく、使っていなかっただけ

    結果として何も漏れていませんが、設定を入れた瞬間に漏れる形のまま置いてありました。

    この種の問題は「まだ起きていない」と「起きない」の区別が付きにくく、動かし始めた日にまとめて効いてきます。

    ついでに言うと、フォームが出ていなかったこと自体も問題でした。問い合わせ先として案内していた場所が、ずっと準備中だったということになります。

    自分のサイトに窓口を作る

    直し方は、間に1枚挟むことでした。

    前 後
    送信先 ブラウザ → 外部サービス ブラウザ → 自分のサイト → 外部サービス
    URLの在り処 配られるJavaScript サーバーだけが持つ
    判定の場所 ブラウザ(迂回できる) サーバー(迂回できない)

    ブラウザから見えるのは /api/contact だけになります。ここを直接叩くことはできますが、叩いた先で何を見ているかは分かりません。

    画像の文字を読ませる方式は、入れなかった

    ボット対策というと、ゆがんだ文字や、信号機の写真を選ばせるものが思い浮かびます。今回は入れていません。

    読みにくい文字は、機械より先に人が脱落する

    問い合わせは、そもそも件数が多くありません。1日に何百件も来るものではない。

    それに対して、1件取りこぼす損のほうが大きいと考えました。迷惑な送信を100件止める価値より、相談しようとした人が1人あきらめる損のほうが重い、という判断です。

    人がやらないことを、3つ見る

    代わりに置いたのは、訪問者の手間が一切増えない3つです。

    見ること 人がやらない理由
    見えない欄が埋まっている 画面に出ていないので、触りようがない
    開いてすぐ送っている 3秒で文章は書けない
    短時間に何度も送っている 同じ相談を10分に4回はしない

    どれも、正しく使っている人には何も起きません。止めるために何かをさせるのではなく、やっていないことを確認する形にしています。

    見えない欄の名前を、慎重に決めた

    ひとつだけ、注意が必要な部分があります。見えない欄の名前です。

    この欄を name や url のような一般的な名前にすると、ブラウザやパスワード管理ソフトの自動入力が、画面に出ていなくても勝手に埋めることがあります。

    人の送信が、黙って捨てられる

    この欄が埋まっている送信は、機械とみなして捨てます。しかも成功したように見せて捨てます。理由を返すと、次からその欄を避ければ通ると分かってしまうからです。

    つまり自動入力に埋められた人は、送信したつもりで、何も届いていない状態になります。これがこの仕組みの一番の失敗です。

    自動入力が狙わない名前にしたうえで、キーボードの移動先からも読み上げの対象からも外しています。

    回数の数え方には、限界がある

    正直に書いておくと、短時間の回数を数える部分は完全ではありません。

    このサイトのサーバー側の処理は、必要なときに起動して、しばらくすると消えます。数えた記録は共有されず、消えるときに一緒に無くなります。厳密には数えられません。

    それでも、続けて叩いてくる相手は同じ実体に当たりやすいので、一番困る「短時間に何百件」は止まります。正確さが要るようになったら、外に記録を置くことになります。いまの流量でそこまで持つ必要は無いと判断しました。

    今回の学び

    まず、名前の先頭6文字が、置き場所を決めていたということ。NEXT_PUBLIC_ は便利な省略ではなく、「これは配る」という宣言でした。付けなければ読めないから付ける、では順番が逆でした。

    次に、ブラウザ側の判定は、通り道が1本のときにだけ効くこと。もう1本、鍵の掛からない道が並んでいれば、そちらが使われます。対策を足す前に、道が何本あるかを数える必要がありました。

    そして、止めるために手間を増やさないこと。問い合わせのように件数が少ないものでは、止めた数より取りこぼした数のほうが重くなります。人がやらないことを確認するだけなら、正しく使う人には何も起きませんでした。

  • 勉強法(6) 効いたのは、技術そのものではなかった

    勉強法(6) 効いたのは、技術そのものではなかった

    この連載の最後に、身も蓋もない話を書きます。

    仕事につながった場面を振り返ると、効いていたのは技術そのものではありませんでした。

    この記事で扱うこと

    実際に役に立った3つ。どれも技術の名前ではなく、技術の扱い方の話です。このサイトで実際にやったことと対応させて書きます。

    1. 説明できること

    「なぜそうしたのか」を聞かれて答えられるかどうか。ここでいちばん差が出ました。

    動くものを作れる人は多い。ところが「その方法を選んだ理由」になると、急に答えが揃わなくなります。

    このサイトで言えば、同意の仕組みがそうでした。先に読み込んでおいて送信の可否だけ切り替える方法と、同意まで読み込まない方法がある。後者を選んだ理由は、前者だと自分のコードを全部読んでも「送っていない」と言い切れないからです。

    説明できる形を選ぶ、という選び方がある

    この判断は、技術的な優劣で決めていません。あとで説明を求められたときに答えられるかで決めています。

    仕事だと、この視点が要る場面のほうが多い。「動きます」で終わらせられる相手ばかりではありません。

    2. 引き継げること

    自分で書いていないコードを直す仕事が続いた時期があります。書いた人はもういない。仕様書もない。あるのは動いているコードだけ。

    そのとき欲しかったのは、常に判断の理由でした。

    コードから分かること コードから分からないこと
    何をしているか なぜその方法を選んだか
    どういう順番で動くか なぜこの順番でなければならないか
    この値を除外していること 除外しないと何が起きるのか

    右の列が分からないと、触っていいかどうかが判断できません。一見無駄に見える処理が、過去の不具合への対処だったということは珍しくない。理由が書いていないと、消して初めて分かります。

    いま自分が書くコードには、処理の説明ではなく理由を書いています。引き継ぐ側に回った経験が、そのまま書き方に効きました。

    3. 止まらないこと

    3つめは、壊れたときにどうなるかを先に決めておくことです。

    このサイトのCMSは共用サーバーにあって、混むと応答しません。最初はそれで公開作業ごと止まっていました。

    直したのは速度ではなく、失敗する前提で書き直したことです。

    場面 決めておいたこと
    CMSが500を返す 間を置いて投げ直す。ただし要求が違う場合は投げ直さない
    記事が取れない 0件のフィードを返す。エラーにしない
    同意の記録が読めない 「同意していない」扱いにする

    どれも「うまくいく道」ではなく、うまくいかなかったときにどちらへ倒れるかの話です。

    止まらない作りは、能力ではなく事前の判断

    特別な技術は使っていません。分からないときにどちらへ倒すかを、書く時点で決めただけです。

    決めていないと、そのときの都合で倒れます。都合で倒れたものは、あとから説明できません。

    3つに共通していたこと

    並べてみると、全部「あとから来る人」への態度でした。

    誰のためか
    説明できること 判断を求める相手
    引き継げること 次に触る人
    止まらないこと 壊れたときに困る人

    3つとも、書いている自分には直接の得がありません。書いた時点では、誰も困っていないからです。

    効いてくるのは、いつも後からでした。そして後から効いたものだけが、次の仕事につながっていました。

    技術が要らない、という話ではない

    誤解のないように書いておくと、土台としての技術は要ります。読めないコードは引き継げないし、仕組みを知らなければ壊れ方も予想できません。

    ただ、そこは足りていないと何も始まらないが、足りたからといって差がつく場所ではないという感覚です。差がついていたのは、同じものを作れる人どうしの間でした。

    今回の学び

    まず、動かせる人は多いということ。差がついていたのは動くかどうかではなく、なぜそうしたかを説明できるかどうかでした。

    次に、引き継ぐ側の経験が、書き方を変えること。読めないコードに困った経験がないと、理由を残す動機が湧きません。困った側に一度回ると、書き方が変わります。

    そして、効いてくるのは全部あとからだということ。3つとも、書いた時点では誰も困っていません。得が見えないうちにやっておいたものだけが、後で効きました。

  • あと34本で、記事が消え始める作りだった

    あと34本で、記事が消え始める作りだった

    サイト全体を見直していて、あと34本で記事が消え始める作りになっていることに気づきました。

    エラーは出ません。ビルドも通ります。ただ、書いた記事が一覧にもサイトマップにも出なくなります。

    この記事で扱うこと

    件数の上限を置いたまま忘れると何が起きるか。なぜ気づけないのか。そして上限を上げるのではなく、外した話です。

    1行だけの問題だった

    CMSへの問い合わせに、こう書いてありました。

    posts(first: 100) {

    「最初の100本を取ってくる」という意味です。書いた当時は記事が10本もなく、100は十分に大きい数でした。

    いま66本あります。残り34本。

    101本目に何が起きるか

    ここが厄介なところです。何も起きません。

    確かめる場所 101本目を書いたあと
    CMSの管理画面 記事はある。公開済みになっている
    サイトの表示 正常。崩れない
    ビルド 成功する
    エラーの通知 来ない
    一覧・サイトマップ その記事だけ載らない

    気づく手段が、「あれ、あの記事どこへ行った」と思うことだけです。しかも消えるのは一番古い記事か新しい記事かも、並び順次第で変わります。

    上限を書いた本人は、上限のことを覚えていない

    この行を書いたのは半年近く前で、そのときは「100もあれば当分平気」と思って書いています。実際そのとおりでした。

    問題は、平気な期間が長いことです。長く問題が出ないと、その数字があること自体を忘れます。そして忘れたころに到達します。

    上げるか、外すか

    いちばん簡単なのは、100を500にすることです。1文字の修正で済みます。

    ただ、それだと同じ壊れ方が先に延びるだけです。500本書いたときに、同じように何も知らせずに消え始めます。そのときの自分は、いまより確実に忘れています。

    そこで、区切りそのものを外しました。

    posts(first: 100, after: 前回の続きの位置) {
      pageInfo { hasNextPage endCursor }
      ...
    }

    100本ずつ取って、「まだ続きがある」と言われているあいだは取りに行き続けます。記事が何本になっても、全部取れます。

    念のための打ち切りは、別の理由で置いた

    実は上限を1つ残しています。30回まで、という繰り返しの回数です。

    これは記事数のためではありません。相手が壊れていて「まだある」と言い続けたときに、公開作業が終わらなくなるのを防ぐためです。100本ずつなので、ここに達するのは3000本を超えてからになります。

    同じ「上限」でも、普通に使っていて到達するものと、異常時にしか到達しないものは別ものです。

    同じ数字が、もう1か所にあった

    直したあと、他にも同じ書き方が無いか探しました。サイトマップを作る処理にも、同じ100が入っていました。

    こちらのほうが影響は大きい。一覧に出ないだけなら内部リンクからは辿れますが、サイトマップから漏れると検索エンジンに存在を知らせる手段が減ります。

    両方とも同じ形に直しました。

    気づけたのは、数えたから

    この問題は、使っていても出てきません。見つかったのは、サイト全体を機械的に確かめていたときでした。

    やったのは単純で、数を数えることです。記事が何本あるか、サイトマップに何件載っているか、一覧に何枚並んでいるか。

    数えたもの 分かったこと
    記事の本数(66) 上限100まであと34本
    一覧のHTMLの大きさ 202KB。ほとんどが画面外のカード
    代替テキストの有無 62枚中21枚が未設定で、混ざっていた

    どれも見た目には出ていません。数えないかぎり、全部「問題なし」に見えます。

    今回の学び

    まず、「当分平気な数字」は、平気なあいだに忘れられるということ。書いた時点では正しい判断でしたが、正しさが長く続くほど、その存在を思い出すきっかけが無くなります。

    次に、上限を上げるのは、同じ壊れ方を先送りにするだけだということ。500にしても、500本目に同じことが起きます。そのときの自分が覚えている見込みは、いまより低い。

    そして、使っていて気づけない問題は、数えるしかないこと。表示は正常、ビルドも成功、エラーも無し。この状態で問題を見つける手段は、動かすことではなく数えることでした。

  • 勉強法(5) 手順は残らない。判断は残る

    勉強法(5) 手順は残らない。判断は残る

    このサイトの作業は、かなりの部分をAIと一緒に進めています。

    気になっていたのは、それで自分に何か残るのかということでした。60本を超えた時点で、残ったものと残らなかったものがはっきり分かれています。

    この記事で扱うこと

    何が残って何が残らなかったか。そして、残るかどうかを分けていたのが何だったかです。

    残らなかったもの

    正直に書くと、手順は覚えていません。

    やったこと いま書けるか
    DNSにTXTレコードを足す 画面を見れば分かるが、手順は覚えていない
    構造化データの書き方 項目名を覚えていない
    日付を決まった形式に直す 曜日と月名の並びは調べ直す

    ここは開き直っています。調べれば出るものを覚えておく理由が薄いからです。AIと組む前から、この手のものは毎回調べていました。

    残ったもの

    残ったのは、「なぜそれを選んだか」のほうでした。

    たとえば、同意を取る仕組みを作ったとき。先に読み込んでおいて送信の可否だけ切り替える方法と、同意まで読み込まない方法があります。後者を選びました。

    理由は、前者だと自分のコードを全部読んでも「送っていない」と言い切れないからです。判断が相手の設定に移ってしまう。

    この判断は覚えています。手順は忘れても、何を基準に選んだかは残りました。

    残っているのは、迷った場所だけ

    並べてみると、覚えているのは全部迷った場所でした。

    すんなり決まったところは覚えていません。2つの案を比べて、片方を捨てた場所だけが残っています。捨てた理由が、そのまま基準になっているからだと思います。

    分かれ目は「確かめられるかどうか」だった

    残るものと残らないものを分けていたのは、時間でも回数でもありませんでした。自分で確かめたかどうかです。

    分かりやすい失敗があります。

    読了の判定が正しく動いているかを、ブラウザを自動で動かして確かめました。記事を最後までスクロールさせても、読了として記録されない。「不具合がある」と結論を出しかけました。

    念のためスクロールのイベント回数を数えたら、0回でした。自動で動かしているタブでは、そもそもイベントが発生していません。不具合が無くても同じ結果になります。

    確かめずに直していたら、動いているものを壊していた

    ここで分かったのは「読了が記録されない」ではなく、「この方法では確かめられない」ということだけでした。

    AIと組んでいると、答えは早く出ます。早く出た答えを確かめる手段を持っていないと、間違いも同じ速さで進みます。速いぶん、戻る距離も伸びます。

    この一件は強く残りました。自分で数えて、自分で結論を取り下げたからです。

    残す作業を、意識的にやっている

    放っておくと残らないので、2つだけ決めています。

    1つめは、コードに「なぜ」を書くこと。処理の説明ではなく、その選択をした理由と、選ばなかった案を書きます。書こうとすると、自分が理由を説明できるかが分かります。書けないときは、分かっていません。

    2つめは、記事にすること。このサイトの記事は、ほぼ全部が詰まった話です。人に読める形にするには、何が起きて、なぜそうしたかを順に並べる必要があります。並べられない部分が、理解の穴です。

    やらないと やると
    コードの「なぜ」 半年後に同じ場所で悩む 判断ごと残る
    記事にする やった記憶だけ残る 説明できない部分が見つかる

    どちらも、AIには代わりにやってもらえません。説明できるかどうかを確かめているのは自分の理解なので、代わりに書いてもらった時点で意味が無くなります。

    向き不向きはある

    全部が同じようにうまくいったわけではありません。

    手を動かして覚えるもの——たとえば攻撃の手順を実際に試すような学び方は、読んだだけでは半分しか入りませんでした。体で覚える部分は、代わりにやってもらうと何も残りません。

    逆に、調べれば分かることを大量に集めてくる作業は、任せたほうが早い。分ける基準は「自分の判断が要るかどうか」でした。

    今回の学び

    まず、手順は残らないが、判断は残るということ。覚えているのは全部、2つの案を比べて片方を捨てた場所でした。捨てた理由が基準として残っています。

    次に、確かめる手段を持っていないと、間違いも同じ速さで進むこと。早く答えが出ること自体は良いのですが、確かめずに進むと戻る距離が伸びます。速さの価値は、確かめ方とセットでした。

    そして、残す作業だけは自分でやる必要があること。理由を書く、記事にする。どちらも「説明できるか」を試す作業なので、代わりにやってもらうと試すもの自体が無くなります。

  • フォームは作ってあった。行き方が無かった

    フォームは作ってあった。行き方が無かった

    このサイトには、問い合わせフォームが置いてあります。プロフィールページの一番下です。

    リンクを探したところ、サイトのどこにもありませんでした。

    この記事で扱うこと

    作ってあるのに辿り着けなかったフォームの話です。なぜ気づかなかったのか、フォームを増やさずに直した理由、そして同じ原因で他にも抜けが見つかった話を書きます。

    辿り着ける人が限られていた

    フォームに行く方法は、実質ひとつしかありませんでした。プロフィールページを最後まで読むことです。

    場所 フォームへのリンク
    ヘッダー 無し
    フッター 無し
    トップページ 無し
    記事ページ 無し
    プロフィールの末尾 フォームそのもの

    プロフィールは経歴と対応範囲が並ぶ長いページです。そこを下まで読み切った人だけが、フォームの存在を知ることになります。

    トップページが、記事一覧へのリンクで終わっていた

    もうひとつ、流れの問題がありました。

    トップページは、実績 → スキル → プロフィール → ブログ、という順に並んでいます。そして最後が「ブログ記事をすべて見る →」で終わります。

    一通り見た人の行き先が無い

    実績を見て、できることを見て、書いているものを見て、「頼めるか聞いてみよう」と思った人が、そこから行ける場所を持っていませんでした。

    案内があるのは記事一覧だけです。読み物としては続きますが、相談したい人にとっては話が逸れます。

    なぜ気づかなかったか

    作った本人は、フォームの場所を知っています。確認するときは /profile を開いて下までスクロールすればよく、「どうやってそこへ行くか」を試す必要がありません。

    動いているかどうかは何度も確認しました。送信できるか、エラーが出るか、文面は適切か。全部見ています。見ていなかったのは、その手前でした。

    フォームは増やさなかった

    直し方として、トップページにもフォームを置く案がありました。すぐ書けます。

    やめました。同じものが2か所にあると、直すときに片方だけ直ります。

    案内と、実体を分ける

    トップページに置いたのは案内だけです。何を相談できるか、返事がどう来るかを書いて、フォームのある位置へ送ります。

    フォームは1つのまま。文面や項目を直すときに、探す場所が1か所で済みます。

    フッターにもリンクを足しました。記事を下まで読み終えた人が、そのまま相談に移れる位置です。

    同じ原因で、他にも抜けていた

    フッターを触ったついでに気づいたのですが、フッターのリンク一覧には連載と用語集が入っていませんでした。

    どちらも後から足したページで、ヘッダーには入れたのにフッターには入れ忘れていました。記事を最後まで読んだ人が次へ移ろうとすると、いったん上まで戻らないと辿り着けない場所があったことになります。

    足す側は覚えているが、足した先は2か所ある

    新しいページを作ったとき、意識は「そのページ」に向いています。ヘッダーに入れた時点で、行けるようになったと感じてしまいます。

    入口は複数あるので、片方だけ更新された状態はそのまま残ります。

    「#」で飛ぶ処理が、1ページ専用だった

    もうひとつ、直しが必要になった箇所があります。

    フォームへのリンクは /profile#contact の形です。ところがこのサイトは画面を読み込み直さずに中身だけ差し替えるため、URLの「#」を見てブラウザが動いてはくれません。その位置まで運ぶ処理を自分で書いてあります。

    その処理を、トップページの中にだけ置いていました。行き先が /#projects のようにトップの中しか無かったからです。プロフィールが行き先になった時点で、足りなくなりました。

    置き場所を全ページ共通の場所へ移して、どのページでも効くようにしています。行き先が増えたときに、仕組みの置き場所も見直す必要があったということでした。

    今回の学び

    まず、動くことと、辿り着けることは別だということ。フォームは何度も動作確認をしていましたが、確認していたのは中身だけで、そこへ行く道は一度も試していませんでした。

    次に、作った人は道を探さないこと。場所を知っている人は直接開きます。行き方が無いことは、知らない人の立場に立たないと見えません。

    そして、同じものを2か所に置かないこと。案内は何か所あってもよく、むしろ多いほうがよい。ただし実体は1つにしておかないと、直したつもりの片方が残ります。