NOTE

なぜ AI 全盛の時代に、技術ブログをリニューアルしたのか

なぜ AI 全盛の時代に、技術ブログをリニューアルしたのかyantene

One reaction at a time. Picking another replaces it.

技術の使い方を調べるとき、筆者は Web 検索より先に AI で当たりをつけています。手順や API の仕様なら、たいていそれで済んでしまいます。原典に当たりたいときでさえ、Web 検索から始めることはずいぶん減りました。

筆者が最後にこのサイトへ記事を書いたのは、2017 年 12 月 25 日でした。修士 2 年の冬から 9 年近く、ブログには何も書いていません。技術ネタは全部、職場の times に流していました。

そもそも AI が手順を教えてくれて、情報発信にしても times なら数分で同僚からの反応が返る時代です。記事を配信するためだけに、わざわざ個人サイトを作り直す理由はありません。それでも作り直したのは、答えだけでなく、そこへ至るまでの思考も記録して、伝えてみたくなったからです。ついでに、読み終わった人が気軽に反応を返せるようにしてみました。

この記事では、times から離れて個人サイトへ戻った理由と、読者に記事を最後まで読んでもらうために、サイトにどんな機能を持たせたかをまとめました。

times は居心地がよすぎた

筆者の職場には times 文化があります。各自が自分のチャンネルを持ち、作業ログでも技術メモでも独り言でも好きに流します。筆者もそこに、調べたことや詰まったことを書いていました。

書いてから「書いてよかった」と思えるまでが、とにかく短いんですよね。誰かが読んで、質問や絵文字のリアクションを返します。「それ自分もハマりました」の一言が返ってくることもあります。書いてよかった、とすぐ思えます。

ただし、times に書いたものは社外に出ません。読む人は同僚に限られます。この 9 年近く、対外的な発信はほとんどありませんでした。届く範囲が社内に閉じると分かっていても、times は居心地がよすぎました。

読みたくなる記事には思考が書かれている

筆者自身が AI で先に調べるようになっても、個人のブログには最後まで読んでしまう記事があります。調べたかったことは最初の数行で済んだのに、そのまま惹き込まれて気がついたら読み終えてしまいます。

そういう記事には、答えだけでなく、思考の過程が書かれています。なぜその設計にしたのか、何を試して駄目だったのか、どこで詰まったのか。「A を採用した」という結論だけでは、ほかの案を退けた理由も、何を守るための選択だったのかも分かりません。答えは残っても、条件が変わったときに考え直す材料は残りません。

AI が手順を教える時代なら、人間が書く記事にはナラティブを意識した思考の過程を残したい。それを読んでもらう場所として、個人サイトをもう一度選びました。

情報発信だけのサイトをやめる

旧サイトは Jekyll と GitHub Pages でした。 Markdown を書いて push すると静的な HTML が生成されて配信される、よくある構成です。 2010 年から 2017 年までの記事が 27 本あって、高校生のときに書いたものも混ざっています。読み返すとなかなか恥ずかしいのですが、消さずに全部移しました。昔の URL も 308 リダイレクトで生かしています。

静的サイトのままでも記事は配信できます。でも今回ほしかったのは、ファイルの置き場だけではありません。 times のように、いろいろな emoji でリアクションできる機能がほしい。どうすれば人間に最後まで読んでもらえるのか、サイトを使って試してみたい気持ちもありました。

そこで、サイトを Cloudflare Workers の上に作り直しました。記事のメタデータは D1、本文と画像は R2、読者ごとの状態は KV に置いています。モダンな技術スタックで、いわば "対外的 times" を作ったわけです。

実装そのものは公開しているので、細かいところはリポジトリを読むか、AI に聞いたほうが早いはずです。よかったら覗いてみてください。

GitHub - yantene/yantene.netContribute to yantene/yantene.net development by creating an account on GitHub.GitHub

モダン Web の当たり前をそろえる

まず、モダン Web として求められる機能には一通り対応しました。

  • PWA: ホーム画面にインストールでき、一度読んだページは Service Worker によってオフラインでも開けます
  • レスポンシブ対応: スマートフォンから大きな画面まで、同じ記事を無理なく読めます
  • 画像: 遅延読み込みに対応し、同期時に取得した寸法を HTML に埋めてレイアウトシフトを防ぎます
  • アクセシビリティ: 操作部品に ARIA 属性を付け、端末の設定に応じてアニメーションを抑えます
  • メタデータ: OGP、構造化データ、Atom フィード、sitemap を出します
  • セキュリティ: スクリプトは nonce を使った Content Security Policy で縛ります

ここまでは、記事の中身以前の話です。

記事の原文を、そのまま渡す

読んだものを読者がどう扱うかは、読者が決めることです。手元に保存する、引用する、AI に要約させる。どれも書き手が口を出す話ではありません。

記事の URL の末尾に .md を付けると、原文の Markdown がそのまま返ります。 .md を付けなくても、Accept: text/markdown で名指しすれば同じものが返ります。もらったリンクをそのまま AI に渡すときに、URL をいじらずに済みます。

名指ししたときだけ、というのが要点です。ブラウザの Accept には必ず */* が入っているので、これを Markdown と数えると、記事を開いた全員に原文が配られます。ワイルドカードは HTML に倒し、Markdown は名指しでしか取れないようにしました。

返すのは、GitHub に置いた Markdown そのものです。フロントマターも含みますし、画像の相対パスも書き換えません。加工した Markdown を原文と呼びたくなかったからです。

「よく読まれている」を、どう決めるか

トップページには、よく読まれている記事のランキングを出しています。ナイーブに実装するなら、アクセス数の多い順に並べるでしょう。ただ、それでは新着記事に不利で、古い記事がいつまでも上位に居座ります。そこでアクセスカウンタの重みに半減期を設け、最近のアクセスほど優遇することにしました。

ところが、重みを時間とともに減らすには、全記事のスコアを定期的に計算し直すバッチが要ります。そこで発想を裏返しました。古い重みを減らすのではなく、新しいアクセスの重みを増やします。基準日から半減期 1 つぶん進むごとに、そのとき起きたアクセスの重みを 2 倍にします。こうすると、一度記録した値には二度と触らなくて済みます。バッチが丸ごと消えました!

ここからは記事の本筋を少し外れるので、急ぎの人は次の節まで飛ばし読みして OK です。とはいえ、趣味のサイト作りで一番楽しいのは、こういう寄り道ですよねw

重みは経過に対して指数的に膨らみます。基準日を t0t_0、半減期を HH 日とすると、tt 日目に起きたアクセスの重みはこうなります。

w(t)=2(tt0)/Hw(t) = 2^{(t - t_0)/H}

半減期を 30 日にすると、素の値はやがて倍精度に収まらなくなります。基準日を 2000 年 1 月 1 日に置いているので、ざっと計算すると限界は 2080 年代のどこかでしょうか。そのころ 90 歳近いやんてね翁は、きっとこの制約を忘れて記事を量産していることでしょう。そんな中、全記事のスコアが \infty になってランキングが壊れたら大変です。

そこで、スコアは対数で持つことにしました。

lnw(t)=tt0Hln2\ln w(t) = \frac{t - t_0}{H} \ln 2

指数が消えて、経過に対して線形にしか増えません。 100 万年でも 8.4×1068.4 \times 10^6 にしかならない計算です。

しかも、対数で持つと嬉しい副産物があります。対数は単調なので、順序は素の値のときと変わりません。保存した列をそのまま降順に並べれば人気順になります。読み出すときに計算を挟まずに済みます。

足し算にはいわゆる log-sum-exp を使います。素の値に戻すと溢れるので、対数のまま和を求めるやつです。 lna\ln alnb\ln b の大きいほうを mm、小さいほうを nn とすると、和の対数はこう書けます。

ln(a+b)=m+ln(1+enm)\ln(a + b) = m + \ln\left(1 + e^{\,n - m}\right)

大きいほうを括り出してから足すので、指数関数の引数が 0 以下に収まり、途中の計算でも溢れません。リアクションもこの足し算に乗せていて、1 つを閲覧 5 回ぶんとして数えています。

……と、ここまでは順調でしたが、最後にひとつ詰まりました。まだ読まれていない記事のスコアを 0 にすると、対数が -\infty になります。下限を「投稿日に 1 回読まれた」ぶんに引き上げて解決しました。おかげで初回かどうかで場合分けする必要がなくなり、まだ読まれていない記事同士も新しい順に並ぶようになりました。

この仕組みだと、古い記事が上位に来ることがあります。それは昔の記事がいまも読まれているということなので、正しい順位です。

リアクションは作り、コメント欄は作らない

冒頭に戻ります。 times で得ていたのは、速さと絵文字のリアクションでした。

リアクションは作りました。記事の上と末尾に置いたハートを押すと「いいね」になり、パレットから別の絵文字も選べます。押せるのは 1 記事につき 1 つです。

D1 が持つのは、どの絵文字が何回押されたかだけです。誰が押したかは残しません。その代わり、読者が何を押したかは、読者ごとのセッションに置いています。だから押し直しも取り消しもできますし、誰が何を押したかの一覧はどこにも残りません。

一方でコメント欄は作りません。感想も議論も、Bluesky や X に逃がします。

自前でコメント欄を持てば、悪口を書かれても非表示にできます。それでも、会話の場は読者が普段いる場所に置きます。

読者は筆者にコメントしたいというより、自分の意見を書いて他人とやりとりしたいことが多いはずです。会話の場は読者のホームであるべきで、筆者のサイトに呼びつけるのは筋が違います。閉じこもるより記事の宣伝にもなります。

リアクションを自分で持ちながらコメントを外に出すのは、ちょっとちぐはぐに見えますよね。リアクションは、記事が読まれた手応えを筆者が受け取るためのものです。コメントは、書き手も加わる会話です。前者だけ自分で持ちます。

会話を外に置いても、外で交わされた会話との接点までなくしたわけではありません。このサイトは Webmention に対応しています。他所のサイトからリンクされたことを、リンク元がこちらに通知する仕組みです。会話は外、参照は内。

速さのほうは諦めました。

times に書けば、数分で反応が返ります。ただし、読むのは同僚だけです。ここに書けば、反応は何日か経ってから来ます。届かないまま終わるかもしれません。

その代わり、読む人を同僚に限定しません。学生のころの記事も、筆者が放置していた 9 年近くのあいだ、そこに残っていました。

あとは面白い記事を書くだけ

今回、思い切って個人サイトを作り直しました。読むのを邪魔するものを減らし、原文をそのまま渡し、読み終わった人が反応を返せる場所にしました。まだ試したいアイデアはたくさんあります。

ただし、人に読まれる仕組みだけでは、人に読まれる記事になりません。面白い題材を見つけ、それを面白く伝える文章力が要ります。ここだけは Cloudflare Workers に載せても動きません。困ったもんですねw

ともあれ、サイトも文章もチューニングしながら、また書いていきます。これからよろしくお願いします!

One reaction at a time. Picking another replaces it.