カテゴリー: 技術系

  • ページを消すとき、一緒に消えるもの

    ページを消すとき、一緒に消えるもの

    実績とスキルの個別ページを、トップページの中へ統合しました。

    作業自体は「セクションにidを付けて、ページを2枚消す」だけです。消すほうに、決めることが集まっていました。

    この記事で扱うこと

    個別ページを持つ価値をどう判断したか、ページを消すときに一緒に消えてしまうもの、そして統合したあとに出た「リンクを押しても動かない」問題です。

    同じ内容が、2つのURLにあった

    トップページには、もともと実績・スキル・プロフィールのセクションがあります。そのうえで /projects と /skills という個別ページも持っていました。

    個別ページを持つ価値は、そこにしかない情報があるかで決まります。実績が20件あって絞り込みや並べ替えが要るなら、独立したページに意味があります。

    しかし実績は数件で、当面増える見込みもありません。結果として、ほぼ同じ内容が2か所にある状態でした。

    同じ内容が2つあると、選ばせることになる

    検索エンジンから見ると、似た内容のページが2つあります。どちらを検索結果に出すべきかを判断させることになり、評価も2つに分かれます。

    読む人にとっても、メニューの「実績」を押した先が、直前まで見ていたトップの内容とほぼ同じ、というのは肩透かしです。

    消すと、一緒に消えるもの

    ここが本題です。ページを削除すると、そのURLは404になります。失われるのは中身だけではありません。

    URLに付いていたもの 削除したとき
    検索エンジンからの評価 引き継がれず消える
    他サイトから貼られたリンク 404へ向かう
    ブックマーク・履歴 404が出る

    URLは外から指される名前です。中を整理した都合で、外からの参照を無効にしてよい理由にはなりません。

    そこで、旧URLを新しい位置へ転送する設定を入れました。

    転送の設定
    /projects  →  /#projects
    /skills    →  /#skills

    これで評価は転送先へ引き継がれ、古いリンクを踏んだ人も目的の場所に着きます。

    恒久か、一時的か

    転送には「もう戻らない」と「一時的に別の場所を見せている」の2種類があります。今回は戻す予定が無いので恒久を選びました。

    恒久転送は、ブラウザが覚える

    恒久として返すと、ブラウザはその対応を記憶します。次からはサーバーに聞かずに転送先へ行きます。速い代わりに、あとで気が変わっても、記憶を持っている人には届きません。

    迷いがあるうちは一時的な転送にしておくほうが安全です。恒久にするのは、本当に戻さないと決めてからで遅くありません。

    sitemapからは外した

    細かい点ですが、転送するURLを一覧に載せたままにしませんでした。

    sitemapは「このURLを登録してほしい」という一覧です。そこに転送するURLを書くと、「登録してほしい」と「そこへ行くな」を同時に伝えることになります。矛盾した指示は、送らないほうが良いはずです。

    統合したら、リンクが動かなくなった

    ここで問題が出ました。トップページ以外から「実績」を押しても、目的の場所へ行きません。

    原因は、ページの移動の仕方でした。

    同じ「#つきリンク」でも、起きることが違う
    同じページの中     → ブラウザが自分でスクロールする
    別のページから来た → 画面を読み込み直さず中身だけ差し替える
                         = ブラウザは「移動した」と思っていない
                         = URLの # を見て動いてくれない

    加えて、移動した直後は画面の先頭に戻されます。何もしないと「トップページの一番上に着いただけ」という結果になります。

    そこで、移動後にURLの # を読んで、その位置までスクロールさせる処理を足しました。飛び先がまだ描画されていない場合に備えて、1回だけ待って再試行しています。

    仕組みが変わると、当たり前が当たり前でなくなる

    # つきのリンクは、HTMLの最初期からある機能です。動いて当然と思っていました。

    しかし「画面を読み込み直さずに中身を差し替える」作りにした時点で、ブラウザが担当していた仕事の一部を、こちら側が引き取っています。引き取ったことに気づいていないと、こういう抜けになります。

    着地の位置も直した

    もうひとつ。アンカーで飛んだ先が、貼り付いたヘッダーの裏に隠れていました。

    飛び先の位置を計算するとき、ブラウザはヘッダーの存在を考慮しません。要素の上端を画面の上端に合わせるだけです。

    飛んだときだけ余白を確保する指定があるので、ヘッダーの高さぶんを足しました。

    今回の学び

    まず、個別ページを持つ価値は、そこにしかない情報があるかで決まること。ページ数が多いこと自体には価値がありません。同じ内容が複数の場所にあると、評価も注意も分散します。

    次に、URLは外から指される名前なので、中の都合で無効にしないこと。これはアンカーIDのときと同じ話でした。指す側は、指される側の事情を知りません。消すなら、行き先を用意してから消します。

    そして、恒久的な指定は、取り消しにくいこと。恒久転送はブラウザに記憶されるので、あとから変えても届かない相手が出ます。迷いがあるうちは一時的にしておく判断があります。

    最後に、仕組みを変えたら、ブラウザ任せだった部分を確認すること。画面を読み込み直さない作りにした時点で、いくつかの仕事がこちら側に移っています。移ったことに気づかないまま「昔から動くもの」として扱うと、静かに動かなくなります。

  • ゆっくり読む人ほど、読了にならなかった

    ゆっくり読む人ほど、読了にならなかった

    「ゆっくり読むと、読了にならないのではないか」。自分で読み返していて、そう思いました。

    調べたところ、そのとおりでした。丁寧に読む人ほど読了にならず、勢いよくスクロールした人ほど読了になるという、逆の作りになっていました。

    この記事で扱うこと

    割合で測るときに何を分母にするかで意味が変わるという話と、「最後まで到達した」と「読み終えた」を分けて考えた結果です。

    元の判定

    読了は、こう判定していました。

    元の計算
    進捗 = スクロール位置 ÷ (ページ全体の高さ − 画面の高さ)
    
    進捗が 95% を超えたら読了

    一見、妥当に見えます。95%にしているのも、末尾ぴったりまで行かなくてよいようにという配慮でした。

    実際に測ってみた

    推測で直すと、原因が別にあったときに気づけません。実際の記事のページで数字を出しました。

    測ったもの 値
    ページ全体の高さ 7,609px
    本文の高さ 6,153px
    本文より下に残っている高さ 684px
    本文を読み終えた時点の進捗 89.9%

    本文を最後の1行まで読んでも、89.9%にしかなりません。95%に届くには、そこからさらに344px下——前後の記事へのリンクと、フッターの中まで進む必要がありました。

    なぜ「ゆっくり」だと届かないのか

    ここが今回の肝でした。読む速さと、スクロールの止まる位置には関係があります。

    同じページでも、止まる場所が違う
    勢いよくスクロールした人
      → 慣性でページの最下端まで滑る → 100% → 読了
    
    文章を追って読んだ人
      → 本文の最後の行で手を止める   → 89.9% → 読了にならない

    速く動かした指は、そのまま惰性でフッターまで運ばれます。一方、読んでいる人は読むものが無くなった時点で止まります。当然の行動です。

    分母に、読まないものが入っていた

    ページ全体の高さには、フッターも、前後の記事へのリンクも、一覧へ戻るリンクも含まれます。どれも「読む」対象ではありません。

    測りたかったのは「本文をどこまで読んだか」なのに、分母は「ページをどこまで動かしたか」でした。名前は進捗でも、中身はスクロール量です。

    分母を本文に変えた

    基準を、ページ全体から本文の高さに変えました。

    新しい計算
    進捗 = (画面の下端 − 本文の開始位置) ÷ 本文の高さ

    本文の末尾が画面の下端に来た時点で100%になります。読み終えたら100%という、当たり前の対応になりました。

    ただし、これだけでは足りない

    末尾への到達だけを条件にすると、別の問題が残ります。目次から末尾の見出しへ飛んだ場合も、一気に下まで送った場合も、読了になります。

    「画面が下まで移動した」ことと「読み終えた」ことは、別のできごとです。

    そこで条件を2つにしました。

    条件 これが無いと
    本文の末尾まで到達している 開いて放置しただけで読了になる
    一定時間、実際に見ている 下まで送っただけで読了になる

    時間の基準は読了目安の4分の1にしました。8分の記事なら2分です。速く読む人を弾かない程度に緩く、しかし一瞬で下まで送った操作は通らない、という線を狙っています。

    「開いていた時間」ではなく「見ていた時間」

    単純に開いてからの経過時間で測ると、タブを開いたまま放置した時間まで読書時間になります。それでは条件の意味がありません。

    ブラウザには「このページがいま見えているか」を知る手段があります。見えている間だけ加算するようにしました。席を外して戻ってきた人が損をしません。

    一度到達したことを、忘れないようにする

    実装中に、条件が2つになったことで生まれる穴に気づきました。

    2つの条件は、同時に満たされるとは限りません。先に末尾まで読み、そのあと上に戻って読み返している最中に時間の条件を満たすことがあります。

    このとき現在位置だけを見ていると、「いま末尾にいないので読了ではない」と判定されます。一度は最後まで読んだという事実が、位置を戻した瞬間に消えてしまいます。

    起きたことは、消さずに覚えておく

    到達はできごとで、位置は状態です。できごとを状態から読み取ろうとすると、状態が変わったときに失われます。

    「一度でも到達したか」を別に記録するようにしました。読み返しても消えません。

    高さが変わることも見るようにした

    もうひとつ。本文の高さは、読んでいる最中にも変わります。画像が後から読み込まれる、目次を開く、画面を回す。

    元の実装は画面サイズが変わったときしか計算し直していませんでした。画像の読み込みでは、その通知は来ません。

    要素の大きさが変わったこと自体を教えてくれる仕組みに変えました。高さがずれたまま古い値で判定し続けることが無くなります。

    表示のほうも、記録に合わせた

    最後に、細かいけれど気持ち悪い食い違いを直しました。

    画面の「読了済み」という表示は、残り0%になった時点で出ていました。しかし実際に記録されるかどうかは、時間の条件も見ています。

    つまり画面には読了済みと出ているのに、記事一覧には読了マークが付かない状態があり得ました。

    表示のほうを、記録に従わせるようにしました。2つの真実を持たない、というだけの話です。

    今回の学び

    まず、割合で測るなら、分母が何かを疑うこと。今回は分母に、読む対象でないものが1割ぶん入っていました。式は正しく動いていて、測っているものが違っただけです。正しく計算された、意味の違う数字がいちばん見つけにくいと思います。

    次に、推測で直さず、まず測ること。「95%は厳しすぎるから90%に下げよう」でも症状は消えたはずです。しかしそれは、フッターの高さが変わるたびに再発します。89.9%という数字を出したから、分母の問題だと分かりました。

    そして、操作と、意味を区別すること。「下までスクロールした」は操作で、「読み終えた」は意味です。操作から意味を推し量るときは、必ずずれが出ます。ずれを埋める条件が要ります。

    最後に、できごとは、状態から読み取らないこと。一度起きたことは、現在の状態を見ても分からなくなることがあります。起きた時点で記録しておけば、あとから位置が変わっても失われません。

  • 長さが合っているのに、中身が壊れていた

    長さが合っているのに、中身が壊れていた

    作った画像を、ブラウザ越しにCMSへ上げる必要がありました。一度に送れない大きさだったので、分割して送り、向こう側でつなぎ直しています。

    つなぎ直した長さは、元のデータと1文字も違いませんでした。それでも上がった画像は真っ黒でした。

    この記事で扱うこと

    長さが合っているのに中身が壊れていた原因と、それを見つけるまでに遠回りしたこと。そして分割して送るときに、検証をどこに置くべきかです。

    長さが合うことは、無事の証明にならない

    最初に確認したのは長さでした。送った文字数と受け取った文字数が一致していたので、無事に届いたと判断しました。

    これが間違いです。どこかで1文字増えて、どこかで1文字減れば、長さは変わりません。実際に起きていたのはこれでした。

    確かめやすいことを、確かめたいことの代わりにしていた

    確かめたかったのは「中身が同じか」です。確かめたのは「長さが同じか」でした。長さは数えるだけで済むので、つい代わりに使ってしまいました。

    長さが違えば壊れていると分かりますが、長さが同じでも壊れていないとは言えません。片方向にしか使えない検査を、両方向に使っていました。

    どこで壊れたのかが分からない

    次に困ったのが、壊れた場所の特定です。

    全体をひとつの塊として扱っていたので、「どこかが壊れている」以上のことが分かりません。やり直すとしたら全体をやり直すしかなく、しかも同じ場所でまた壊れるかどうかは運任せになります。

    実際、一度送り直しても同じように壊れました。何が起きているか分からないまま、同じ操作を繰り返していました。

    分割して送るなら、検証も分割する

    やり方を変えました。送る側で分割したときに、各かたまりごとに短い検査値を計算して一緒に渡します。受け取る側は、つなぐ前にその場で照合します。

    全体だけを検証 かたまりごとに検証
    壊れたことに気づける すべて送り終えたあと そのかたまりを受け取った時点
    壊れた場所 分からない そのかたまりだと確定する
    やり直す範囲 全体 そのかたまりだけ

    これに変えた途端、壊れている場所が一発で出ました。しかも受け取り側が拒否を返すので、壊れたまま先へ進むことがなくなります。

    やり直しの範囲は、検証の粒度で決まる

    検証を全体にしか置かないと、失敗も全体になります。細かく検証すれば、失敗も細かくなります。

    分割する目的は「一度に送れないから」でしたが、分割したことの利点は、失敗を小さくできることのほうにありました。

    検証は、転送の「あと」ではなく「一部」にする

    ここが今回の考え方の変化でした。

    最初は、送り終えてから確かめる、という順番で組んでいました。転送と検証が別の工程になっていて、間に「壊れたまま進む余地」があります。

    いまは、検査値が合わないかたまりは受け取らない形にしています。検証は転送の後工程ではなく、転送の一部です。こうすると「受け取れた」と「正しい」が同じ意味になります。

    形式として正しくても、中身が正しいとは限らない

    もうひとつ、別の層の話があります。

    検査値が合って、データが完全に届いた。そこまで確認しても、上がった画像が意図したものかは、まだ分かりません。実際、最初に壊れた画像は「画像ファイルとしては正しい形式」で、開くこともできました。中身が真っ黒だっただけです。

    そこで、送り先に登録する前に、実際に画素を読むようにしました。画面には出さず、いくつかの座標の色を取って、想定した配色になっているかを見ます。真っ黒なら、そこで止まります。

    出す前に、出すものを見る

    「形式として妥当か」と「意味として正しいか」は別の検査です。前者は自動で分かりますが、後者は中身を見に行かないと分かりません。

    面倒に見えますが、やっているのは数点の色を読むだけです。壊れたものを世に出してから気づくコストと比べれば、はるかに安く済みます。

    今回の学び

    まず、長さの一致は、内容の一致ではないこと。確かめやすい指標を、確かめたい事実の代わりに使うと、一致しているのに壊れている状態を見逃します。

    次に、失敗の大きさは、検証をどの粒度に置いたかで決まること。全体にしか検証を置かなければ、やり直しも全体になり、原因も特定できません。分けて送るなら、分けて確かめるべきでした。

    そして、検証は工程の後ろに置くと、壊れたまま進む余地が残ること。転送の一部にしてしまえば、受け取れたことがそのまま正しさの証明になります。順番を変えただけで、確認という作業そのものが要らなくなりました。

    最後に、形式の正しさと意味の正しさは、別々に確かめる必要があること。壊れた画像は画像として開けました。開けることと、意図したものであることは、まったく別の話でした。

  • 測れるもので代用すると、測りたいものから離れる

    測れるもので代用すると、測りたいものから離れる

    「記事を最後まで読んだ人が何人いるか」を知りたい、というだけの話でした。単純に見えたので、すぐ作れると思っていました。

    実際には4回作り直しています。そのたびに別の壊れ方をしました。

    この記事で扱うこと

    読了の判定を作り直した4回ぶんの経緯と、そのたびに何を見落としていたかです。最後に、そもそも何を測ろうとしていたのかを整理します。

    1回目:下までスクロールしたかで測った

    最初は素直に、ページの何%までスクロールしたかで判定しました。95%まで来たら読了、という形です。

    これがうまくいきませんでした。分母にしていたページ全体の高さには、本文の下にある前後記事のリンクや一覧へ戻るリンク、フッターまで含まれています。実測すると、その下側だけで約680pxありました。

    分母に、読むものではないものが入っていた

    本文を最後まで読み終えても、進捗は90%前後にしかなりません。100%にするには、読むもののないフッターまでスクロールする必要がありました。

    勢いよくスクロールする人は慣性で最下端まで行くので満たせます。一方、文章を追って読む人は本文の終わりで手を止めます。ゆっくり読む人ほど読了にならないという、目的と逆の結果になっていました。

    2回目:本文の高さを分母にした

    分母を本文だけに変えました。本文の末尾が画面の下端に来た時点で100%になります。読み終えれば100%、という当たり前の対応になりました。

    ただ、これで別の穴が開きます。目次から末尾へ飛んだ場合も100%になります。位置だけを見ている限り、そこに至った経緯は分かりません。

    3回目:滞在時間の条件を足した

    そこで「そのページを実際に見ていた時間が一定以上ある」を条件に加えました。一瞬で駆け抜けた操作を弾くためです。

    条件が2つになった瞬間、画面の表示と記録がずれ始めました。

    回 何で判定したか そこで抜けたもの
    1 ページ全体のスクロール率 読む人ほど到達しない
    2 本文のスクロール率 飛んだ人も通る
    3 本文の到達+滞在時間 表示と記録がずれる
    4 同上(時間を緩和) 読まずに時間だけで通る

    進捗のバーは位置だけを表しているので、末尾に着いた瞬間に満杯になります。しかし記録は時間の条件を満たすまで行われません。バーは終わっているのに、読了済みにならないという食い違いが画面に残りました。

    条件を足すと、それを表す場所も増える

    判定に条件を足すのは簡単ですが、画面に出している表示のほうは1つのままでした。

    1つの表示で2つの条件を表そうとすると、必ずどちらかの状態を隠すことになります。隠された側が読み手には「動かない」ように見えます。

    間に挟まった、数字の問題

    ずれを隠そうとして、記録される前は「あと1%」で止める、という処理を入れました。0%と出しているのに読了済みに変わらない、という矛盾を避けるためです。

    これは失敗でした。今度は「1%のまま動かない」という別の分かりにくさになりました。残っていないものを数字で表そうとしたのが、そもそもの間違いでした。

    数字で表せない状態は、言葉で書く

    「残り」は数字で表せますが、「末尾には着いたが、まだ記録されていない」は数字ではありません。

    ここは数字をやめて、状態をそのまま言葉にするべき場所でした。無理に数値化すると、動かない数字という嘘が残ります。

    4回目:今度は、読まずに完了するようになった

    時間の条件を緩めたところ、逆の症状が出ました。読み終えていないのに、時間の経過だけで完了になります。

    設計上、位置と時間の両方を満たさないと読了にはならないはずです。それでも時間だけで通るということは、位置の条件が常に成立しているとしか考えられません。

    条件が2つあると、片方が壊れたときに気づけない

    2つの条件を「かつ」で結ぶと、片方が常に真になっても、全体としては動き続けます。残った条件が単独の判定として働くだけで、エラーにはなりません。

    気づけたのは、症状が「時間だけで通る」という形で表に出たからです。出方が違えば、そのまま気づかずにいた可能性があります。

    そもそも、測れるもので代用していた

    ここまで来て、やっと本題に気づきました。

    知りたいのは「読んだか」です。しかし測れるのは「画面がそこを通ったか」と「そのページが開かれていた時間」だけです。読んだかどうかは、ブラウザからは直接見えません。

    つまり最初から、測りたいものを測れるもので代用していました。代用そのものは避けられません。問題は、代用していることを忘れて、条件を足したり引いたりしていたことです。

    代用だと分かっていれば、条件の足し方が変わる

    条件を足すたびに「これは読んだことの何を近似しているのか」を書いていれば、位置と時間が別々の近似だと分かったはずです。

    別々の近似を「かつ」で結べば、片方が外れたときに残りが単独で判定を下します。足したのは精度ではなく、壊れ方の種類でした。

    いま考えている形

    次は、時間をやめて経路を見ようと思っています。

    記事をいくつかの区画に区切り、そのすべてが画面を通ったかどうかを判定します。目次から末尾へ飛べば途中の区画を通らないので読了になりません。ゆっくり読む人は必ず全区画を通るので、確実に読了になります。

    時間に頼らずに「飛ばした人を通さない」と「速く読む人を弾かない」の両方が満たせます。何より、近似しているものが1種類になるので、片方が壊れて気づかない、という形にはなりません。

    今回の学び

    まず、測りたいものと測れるものは違うこと。読んだかどうかは見えないので、必ず何かで代用します。代用だと自覚していないと、精度を上げているつもりで壊れ方を増やすことになります。

    次に、条件を「かつ」で結ぶと、壊れても動き続けること。片方が常に真になっても例外は出ません。残った条件がそのまま判定として働くので、症状が出るまで気づけません。条件を足すときは、それぞれが単独になったときに何が起きるかを見ておく必要がありました。

    そして、判定に条件を足したら、表示も足すか、表示のほうを言い換えること。1つの表示で2つの状態を表そうとすると、必ずどちらかが隠れます。隠れた側は、読み手には「壊れている」ように見えます。

  • 奥行きは、影だけの話ではない

    奥行きは、影だけの話ではない

    ダークモードを入れたとき、明るい配色で効いていた影が暗い背景でほとんど消えました。境目が分からなくなり、カードが背景に溶けます。

    そのとき初めて、自分が奥行きを影だけで作っていたことに気づきました。手段が1つしかないと、その手段が使えない場面でやり直しになります。

    この記事で扱うこと

    面を分けるための手段を並べて、それぞれが何で差をつけているか、どこで効かなくなるかを整理します。そのうえで、どういう順番で選べばいいかを決めます。

    「切り分ける」と「浮かせる」は別の目的

    まずここを混ぜないほうがいいと思いました。見た目は似ていますが、伝えたいことが違います。

    やりたいこと 読み手に伝えたいこと 向いている手段
    切り分ける ここまでとここからは別の話だ 余白、背景の明度差、罫線
    浮かせる これは本文より手前にある一時的なものだ 影、ぼかし、重なり

    切り分けたいだけの場所に影を付けると、本文と同じ平面にあるはずのものが浮いて見えます。逆に、閉じれば消えるものを罫線で区切ると、ページの一部として居座って見えます。

    手段を並べて、効かない場面を書き出す

    使える手段はそれほど多くありません。並べたうえで「どこで効かなくなるか」を一緒に書いておくと、選ぶときに迷わなくなります。

    手段 何で差をつけているか 効かなくなる場面
    余白 距離 画面が狭く、余白を取る余地がないとき
    背景の明度差 面の明るさ 差が小さすぎるとき。ただし配色を問わず成立する
    罫線 線 本数が増えたとき。画面が格子に見える
    枠線と角丸 囲い 入れ子にしたとき。内と外の区別が付かなくなる
    影 光の当たり方 暗い配色。背景より暗い影は見えない
    半透明とぼかし 前後関係 背景が単色のとき。透けても何も見えない
    影だけが、配色を前提にしている

    影は「上から光が当たっている」という状況を描いたものです。明るい面に暗い影を落とすから浮いて見えるのであって、面そのものが暗ければ落とす先がありません。

    他の手段は配色が変わっても意味を保ちますが、影だけは配色とセットです。ここを分けて考えていなかったのが、今回つまずいた原因でした。

    いちばん強いのは余白だった

    意外に思われるかもしれませんが、切り分ける力がいちばん強いのは余白です。

    近くにあるものは同じ仲間、離れているものは別の仲間、と読み手は勝手に判断します。線も影も無くても、距離だけで境界は伝わります。実際、罫線を引きたくなった場所の多くは、余白を広げるだけで解決しました。

    それでも線や面を足したくなるのは、余白が足りていないときです。狭いところに詰め込んだまま境界だけを描こうとすると、線が増えて画面が窮屈になります。順番としては、まず余白を疑うほうが早いと思います。

    暗い配色でも残るのは、明度差だった

    ダークモードで見直したとき、最後まで機能していたのは背景の明度差でした。

    明るい配色では、白い本文の上に少しグレーがかった面を置くと「一段下がった」ように見えます。暗い配色ではこれが逆になり、背景より少し明るい面が「一段上がった」ように見えます。

    明るいほうが手前、は暗い配色でも変わらない

    明暗を反転させると濃淡の関係も逆になりますが、「明るいほうが手前に見える」という関係だけは変わりません。

    だから、明るい配色では「背景より暗い面」で沈め、暗い配色では「背景より明るい面」で浮かせる。同じ考え方のまま、向きだけを入れ替えれば済みます。

    影のように配色ごとに作り直す必要がないので、迷ったらここに寄せるのが安全だという結論になりました。

    追従するものだけは、別の扱いが要る

    ひとつ例外があります。スクロールに追従して画面に貼り付くものです。

    このサイトでは、スマートフォンで目次が画面の上に貼り付きます。ここだけは本文の上を通過していくので、切り分けではなく前後関係を示す必要があります。少し透かして背景をぼかす指定を入れているのは、そのためです。

    透けて見えることで「下に本文が続いている」と分かり、ぼけていることで「いま読むのはこちらではない」と分かります。この2つはセットにして初めて意味を持ちます。透かすだけだと下の文字と重なって読みにくくなり、ぼかすだけだと下に何があるか分かりません。

    透過は、目立たなくするための手段ではない

    以前、追従しているあいだだけ目次を薄くする、という案を入れたことがあります。邪魔にならないようにするつもりでした。

    結果として、読みにくくなっただけで邪魔さは変わりませんでした。薄くしても場所は取り続けるからです。透過が答えになるのは「下が見えることに意味がある」ときだけで、「目立たせたくない」なら置く場所や大きさのほうを変えるべきでした。

    手段を重ねるほど、弱くなる

    ここが今回いちばん腑に落ちた部分です。

    ひとつの境界に、枠線を引いて、角を丸めて、背景を変えて、影を落とす。全部やれば強く分かれそうに思えますが、実際には逆でした。どの手段が境界を作っているのか分からなくなり、画面全体がぼんやりします。

    手段が増えるほど、他の場所との差も付けにくくなります。すべての箱が同じように装飾されていると、本当に浮かせたい1枚だけを浮かせる余地が無くなります。

    1つの境界には、1つの手段

    決めたのは単純な方針で、ひとつの境界を作るのに使う手段はひとつだけにする、というものです。

    面で分けたなら線は引かない。線で分けたなら背景は変えない。そうしておくと、あとから「ここだけ強調したい」と思ったときに、手段をひとつ足すだけで差が付きます。

    選ぶ順番を決めておく

    毎回考え直さなくて済むように、順番にしました。

    まず余白で足りないかを見ます。足りるなら何も足しません。次に、それが同じ平面の話なら背景の明度差で分けます。線を引くのは、面で分けると重くなる細かい区切りだけにします。

    手前に浮かせたいときだけ影を使い、その場合は明るい配色と暗い配色の両方で見え方を確かめます。追従して本文の上を通るものだけ、半透明とぼかしを使います。

    この順番にしてから、装飾で迷う時間がはっきり減りました。決めているのは見た目ではなく、何を伝えたいかのほうだからだと思います。

    今回の学び

    まず、奥行きの手段は、配色に依存するものとしないものに分かれること。影だけが光を前提にしていて、暗い配色では成立しません。手段を1つしか持っていないと、配色を増やした時点でやり直しになります。

    次に、いちばん強い区切りは、何も描かない余白だったこと。線を引きたくなったときは、たいてい余白が足りていませんでした。描き足す前に、空けられないかを先に見るほうが早いです。

    そして、手段を重ねると強くなるどころか弱くなること。全部の箱を同じように飾ると、本当に目立たせたい1つを目立たせる手段が残りません。1つの境界に1つの手段、と決めておくのは、いま作りやすくするためではなく、あとで差を付ける余地を残しておくためでした。

  • 読む邪魔をしない

    読む邪魔をしない

    スマートフォンで自分の記事を読んでいて、気になることがありました。

    画面の上に、ヘッダーと目次がずっと貼り付いたままです。どちらも便利なつもりで置いたものですが、読んでいるあいだは、ただ場所を取っています。

    この記事で扱うこと

    常に出しておくものと、必要なときだけ出すものの分け方。「消す」ではなく「薄くする」を選んだ理由。そして、2つの追従する要素が重なったときに起きた、位置の食い違いです。

    貼り付いたものは、面積を占め続ける

    スマートフォンの画面の高さは、だいたい本文15〜20行ぶんです。そこにヘッダーが1行、目次のバーが1行居座ると、実際に読める範囲は1割ほど減ります。

    減るだけならまだしも、目次のバーはいま読もうとしている行の上に半透明でかぶさります。スクロールしている最中がいちばん邪魔です。

    3つの変更

    対象 前 後
    ヘッダー 常に表示 下へ読み進む間は引っ込む
    目次(スマホ) 常に同じ濃さ スクロール中だけ薄くなる
    先頭へ戻る 手段なし ボタンを出す

    1. ヘッダーは「量」ではなく「向き」で判断する

    最初に考えたのは「300pxスクロールしたら隠す」でした。単純ですが、これだと読み進めるほど隠れっぱなしになります。メニューを開きたくなったとき、ページの先頭まで戻ることになります。

    採用したのは、スクロールの向きで判断する方式です。

    判断の基準
    下へ読み進んでいる  → 隠す(読むのに要らない)
    上へ戻し始めた      → 出す(何かしたいはず)
    ページの先頭付近    → 必ず出す

    上へのスクロールは、たいてい「前に戻りたい」「別の場所へ行きたい」という合図です。その動作を、ヘッダーを呼び出す操作として兼ねさせています。新しい操作を覚えてもらう必要がありません。

    細かいが要る配慮が3つあった

    ひとつめはわずかな揺れを無視すること。指の震えや慣性の揺り戻しで数px動くたびに出し入れすると、ヘッダーが点滅します。6px未満の移動は無視しています。

    ふたつめは画面の端で引っ張ったとき。iPhoneでは画面の一番上でさらに引っ張ると、スクロール位置がマイナスになります。そのまま計算すると「上に動いた」と誤判定します。0で止めるようにしました。

    みっつめはメニューを開いているあいだは隠さないこと。開いた本人の操作でヘッダーごと消えたら、何が起きたのか分かりません。

    2. 目次は「消す」のではなく「薄くする」

    目次も同じように隠す案がありました。やめました。

    この目次のバーには読了の進捗も出しています。消すと、どこまで読んだかが分からなくなります。

    邪魔なのは「濃さ」であって「存在」ではない

    困っていたのは、文字の上に別の文字が重なって読めないことでした。薄くすれば下の文字は読めます。位置と進捗は視界の端に残ります。

    指を止めれば元の濃さに戻ります。スクロール中しか邪魔にならないものを、常時消す必要はありません。

    開いているときは薄くしません。開いているのは読むためではなく選ぶためなので、そこで薄くすると目的の見出しを探せなくなります。

    3. 2つの追従が、食い違った

    ここで問題が出ました。

    目次は「ヘッダーの高さ(56px)ぶん下」に貼り付く指定になっています。ヘッダーがそこにある前提です。

    ところがヘッダーは引っ込むようになりました。すると——

    ヘッダーが消えたあと
    画面の上端
       ↓
    (56pxの空白)   ← ヘッダーが居たはずの場所
       ↓
    目次のバー       ← 宙に浮いて見える

    目次だけが、意味のない隙間を空けて浮きます。

    状態を、渡さずに共有する

    ヘッダーが隠れているかどうかを、目次に伝える必要があります。素直に考えると「上から順に受け渡す」ですが、この構成では間にあるページ全体を作り替えることになります。

    選んだのは、ページ全体に1つの値を置く方法です。

    ヘッダーが書き、目次が読む
    ヘッダー: --header-offset = 3.5rem(表示中)/ 0rem(隠れている)
    目次    : 貼り付く位置 = --header-offset

    ヘッダーは自分の状態を書くだけ、目次はその値を見るだけです。お互いを直接知らないまま、位置だけが揃います。

    あえてTailwindを使わなかった箇所

    このサイトのスタイルはTailwind CSSで書いています。今回の位置指定も書けますが、1か所だけ普通のCSSにしました。

    理由は、壊れ方です。Tailwindのクラス名を書き間違えると、CSSが生成されないだけで、ビルドは成功します。

    気づけない壊れ方を選ばない

    位置指定が消えると、目次は貼り付くのをやめて一緒に流れて消えます。エラーは出ません。実際に記事を開いてスクロールするまで分かりません。

    この部分は今回いちばん確かめにくい場所だったので、綴り間違いで静かに消えない書き方を選びました。

    4. 先頭へ戻るボタン

    最後に、よくあるボタンを置きました。ここでも1つだけ気を付けた点があります。

    薄くして消したボタンは、見えないのに押せます。透明度を0にしただけでは、そこにある扱いのままです。キーボードで操作している人には、見えない項目に順番が回ってきます。

    見えないあいだは押せない・順番が回ってこない・読み上げられないの3つを揃えました。見た目と実体を一致させています。

    あわせて、端末の「動きを減らす」設定を尊重しています。滑らかに移動するのが心地よい人ばかりではありません。

    今回の学び

    まず、常に出しておくものと、必要なときだけ出すものを分けること。便利だから常に出す、を積み重ねると、本文が読める面積だけが減っていきます。何のために画面を開いているのかに戻って決めるべきです。

    次に、「隠す」の前に「薄くする」を検討すること。困っているのが重なりなら、濃さを変えるだけで解決することがあります。消してしまうと、その要素が持っていた別の役割まで一緒に失います。

    そして、動かすものが増えたら、位置の前提を疑うこと。固定されている前提で書いた数値は、片方が動き出した瞬間に嘘になります。今回は「ヘッダーの高さ」がそれでした。

    最後に、確かめにくい場所ほど、壊れたら気づく書き方を選ぶこと。同じ結果になる書き方が2つあるなら、間違えたときに黙って消えないほうを選ぶ価値があります。

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

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

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

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

    この記事で扱うこと

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

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

    広告配信サービスの規約を読むと、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つ増やさずに済むなら、それだけ壊れる場所が減ります。