考察記事の顔を、問いの文字に変えた ― 次の記事から自動でできる

ブログやメディアの記事一覧には、記事ごとに小さな絵(サムネイル)が並びます。このサイトの「考察記事」は、ロボットやフィジカルAI(カメラやセンサーで周りを見て、AIが動きを決め、ロボットの体を動かす仕組み)について考えたことを、AIとの対話の形で残している記事です。その67本のサムネイルは、どれも Google の画像生成 AI「Gemini」に1本ずつ頼んで描いてもらった、4コマ漫画のような絵でした。前に数えたときは(LOG147)、頼んで、確かめて、直してもらうまでに1本5〜10分かかっていました。記事を書くたびに、別の画面で絵を頼む手間が残っていたわけです。

考察記事のサムネイルを新しくしようと思います(Geminiにサムネイルを作ってもらっていますが、わざわざ頼むのが地味に大変なので、通常のサムネイルと一緒に作ってもらいたいです)

そこで、絵をやめて、記事の「問い」を文字で組む形に変えました。たとえば「70万円のアームで、仕事を請けられるのか。」という問いを縦書きで置き、その中の「70万円」だけを大きく、問いの結びの「請けられるのか。」を中くらいに、残りを小さく組みます。文字の大きさと太さの差で、絵がなくても1枚ずつ違う顔になるようにしました。

作り直したあとの考察記事の一覧。どのカードも、記事の問いを明朝の文字で組んだサムネイルになっていて、「70万円」「成否」「差」「フィジカルAI」など問いの中の一語が大きく組まれている

最初から今の形だったわけではありません。問いを縦書きで1つ置くだけの形を12枚並べたところ、同じ形が続いて見分けにくかったので、組み方の違う案をいくつも作って、一覧に並べた画面で比べながら決めました。

やったことなぜ結果
サムネイルの方向を3つ出した(問い・対話・一語)
他のシリーズの4コマや図と違う顔にするため
記事の問いを置く形に決めた
問いを縦書きで1つ置くだけの形を12枚並べた
一覧で文字だけが並ぶと見分けにくいかを確かめるため
同じ形が続いて単調だった
一語を大きく・黒い面で分ける・文の強弱、の3案を作った
文字の強弱と組み方で見分けがつくかを比べるため
文の強弱(案3)にした
案3の強弱の付け方を3通りにした
差の大きさで印象がどう変わるかを比べるため
3段階(3-B)にした
余白・行の間・縦の列の中心・書体の読み込みを直した
一覧に並べた実際の画面で、ずれや隙間が見えたため
読み込みのたびに位置が変わる不具合もなくした
67本の問いを書き、一覧・記事の上・SNS の画像を差し替えた
考察記事すべてを同じ形にそろえるため
最初に見える12枚の画像の重さが約10.2MBから約2.3MBになった
記事の頭の2行からサムネイルを作る仕組みにした
次の記事から絵を頼まずに済ませるため
67本を書き出すのに0.06秒
サイトのトップのシリーズのカードを直した
日本語のサイトで英語が先に来ていたため
日本語を先にし、説明も日本語に。考察記事のカードも問いの形に

絵をやめて問いを文字で組んだことで、考察記事のサムネイルは記事の中身から自動で決まるようになり、別の画面で頼む手間がなくなりました。 次の考察記事からは、記事の頭に「問い」と「大きく見せる語」の2行を書くだけで、サイトに入れるときにサムネイルができます。

ここから下は、どの案をどう比べ、何を直したかの作業ログです。最後に、使ったコードと測った結果を全量で載せます。

作業ログ ― 3つの方向から、67本を差し替えてトップに出すまで

この記事の「見込める効果」のうち、重さと時間は実際に測った値です。読みに来た人が増えるかどうかは、まだ測っていない見込みです。比べた画像は、最後に決めた問いの文で書き出し直したものなので、作業の途中で見ていた画面と言い回しが少し違うところがあります。

考察記事のサムネイルは、1本ずつ Gemini に頼んだ4コマの絵だった

作り直す前の一覧です。4つの枠に、ロボットアームや道具のイラストと英語の見出しが入っています。描き方はそろっていて、一覧としての統一感はありました。困っていたのは見た目ではなく、作るたびに別の画面で頼む手間のほうでした。

作り直す前の考察記事の一覧。どのカードも、4つの枠にロボットアームや道具のイラストと英語の見出しを描いた絵になっている

方向は「問い・対話・一語」の3つから、問いを縦書きで置く形を選んだ

ほかのシリーズのサムネイルは、黒い地や紙の色の地に英語の見出しと図を置いたものが多く、トップの「ひとり研究所の裏側」には実物の画面を使ったものもあります。考察記事だけは違う顔にしたい、という希望だったので、最初に方向を3つ出しました。

案主役見本の中身
A問い記事の問いを縦書きで1つ置く
B対話
問いと答えを1往復だけ置く(Q と A)
C一語
記事の鍵になる語を大きく置き、問いを小さく添える

A を選んだのは、考察記事が「問いから始めて、AIとの対話で考える」記事だからです。カードの下にはタイトルが出るので、サムネイルには同じことを繰り返さず、タイトルとは別に「何を問うた記事か」を置く役目にしました。紙の色と明朝の書体は、考察記事のページの上にある「いま考えていること」のページへの入口(縦書きの問い「ロボットに、何を任せればいいのか。」)とそろえています。

縦書きの問いだけを12枚並べると、同じ形が続いて見分けにくかった

まず、一覧で最初に見える12本だけを差し替えた確認用のページを作りました。検索には出さない設定にして、どこからもリンクしていません。

最初は問いの長さに合わせて文字の大きさを変えていましたが、並べると大きさがばらばらに見えたので、全部同じ大きさにそろえ、1行を8字までにしました。それでも、12枚とも「紙の色の地に、縦書きの文字が右に寄っている」同じ形でした。

最初の試作を一覧に並べた画面。12枚とも紙の色の地に、問いを縦書きで右に寄せて置いた同じ形になっている

流石に単調だね。 レイアウトを色々組んだり、文字の強弱でグラフィカルにみせる案を作ってみて

一語を大きく・黒い面・文の強弱の3案を作り、文の強弱を選んだ

どの案も、組み方を記事ごとに4通り(縦書きで右寄せ・横書きで左下・縦書きで左寄せ・横書きで右上)順に回すようにしました。一覧で同じ形が隣に続かないようにするためです。色は紙と墨の2色だけにしています。

案組み方並べたときの印象
案1 一語を大きく
問いの中の一語を絵のように大きく置き、問いは小さく添える。一語はカードの端からはみ出すこともある
文字の強さはいちばん大きい
案2 黒い面で分ける
カードを黒い面と紙の面に分け、一語を黒い面に白で抜く
一覧でいちばん目に入る
案3 文の強弱
問いの文をそのまま組み、一語だけを大きくする
文として読める。整い方はいちばんゆるい
最初の試作・案1・案2・案3で、同じ4つの問いを組んで並べた比較。案1は一語が大きくはみ出し、案2は黒い面に白い文字、案3は問いの文の中で一語だけが大きい

選ばれたのは案3でした。続けて、強弱の付け方だけを変えた案を頼まれました。

案3で、フォントの強弱の差の別パターンを3つ用意して

強弱の付け方を3通りに変え、3段階の 3-B に決めた

組み方(位置と縦・横の回し方)は案3のままにして、文字の大きさと太さの付け方だけを変えました。

案大きい一語問いの結びの行残り
3-A 差を控えめに今の案3の半分ほど・太字残りと同じ大きめ・ふつうの太さ
3-B 3段階いちばん大きく・極太中くらい・太字小さく・細め
3-C 太さだけで差残りと同じ大きさ・極太残りと同じ同じ大きさ・極細

3-C は、最初は大きさの差を少し残していました。並べると 3-A とほとんど同じに見えたので、大きさの差をなくし、太さだけで差をつける形に作り直しています。3つの違いがはっきりしないと、比べる意味がないためです。

3-A・3-B・3-C で同じ4つの問いを組んだ比較。3-A は一語が控えめ、3-B は一語・結びの行・残りの3段階、3-C は大きさが同じで一語だけが極太

12枚並べたときの強さは 3-B、落ち着きは 3-C が上でした。決まったのは 3-B です。

  • ねらい:一語で目を止め、問いの結びの行まで目を流して、問い全体を読ませる
  • 変わったこと:文字の大きさが3段階になり、問いの最後(「請けられるのか。」「覚えられるのか。」)が太字で残るようになった
  • 見込める効果:一覧で目に入る語と、記事が問うていることの両方が、タイトルを読む前に伝わる(読みに来た人が増えたかは、考察記事のページの押された数で確かめる)

3-B で直した4つ:余白、行の間、大きく見せる語、縦の列の中心

3-B に決めたあと、一覧に並べた実際の画面を見ながら、1つずつ直しました。

3-Bで、余白をもう少し減らしたい

外側の余白を、組みによって違っていた幅(カードの幅の5〜7%)から 4.5% にそろえ、3段階の文字を全部ひと回り大きくしました。

ここの隙間が空きすぎかな?

大きい一語がある行は、その一語の大きさに合わせて行の高さ(縦書きでは列の幅)が広がります。そのため、前後の行との間が開いていました。一語の行の高さを文字の1.12倍、問いの結びの行を1.25倍に詰めました。これは 3-B だけでなく、案3の4つすべてに入れています。

フィジカルAIが大きい文字に変更

LOG324 の大きく見せる語を「AI」から「フィジカルAI」に変えました。長い語なので、後ろの「にあるものは何か。」と同じ行に置くと小さくなります。そこで、大きく見せる語が上限の大きさの8割に届かないときは、その語の後ろで改行する決まりを足しました(「フィジカルAIに/あるものは何か。」)。

この辺修正(テキストセンタリング合わせ) 4万円は大きくていいよ

縦書きで、大きい一語と同じ列に並ぶ小さい文字(「の教材と、」「は」)が、列の中心から右に寄っていました。文字の大きさが違うものを同じ列に並べると、小さい文字が列の端にそろうためです。大きさを指定した文字だけを列の中心にそろえました。あわせて、縦書きの「AI」は1字の幅に収まらず列からはみ出していたので、英字は1字ずつ縦に立てて組み、数字(70・4)だけを縦の1字分に横に並べる形にしています。「4万円」も、横書きと同じ改行の決まりを縦書きにも広げて大きくしました(「4万円の/教材と、」)。

読み込むたびに数字の位置が変わったのは、書体が届く順番のせいだった

読み込むごとにセンタリングが変わるみたいだね

同じページを読み込み直すと、「70万円」の「70」の位置が変わっていました。

同じ一覧を2回読み込んだ画面の比較。上と下で、縦書きの「70万円」の「70」の位置と、隣の小さい文字との間が変わっている

原因は、書体の届く順番でした。 明朝の書体(Noto Serif JP)は、文字ごとに細かく分かれて届きます。届くまでの間、一部の文字は Mac に入っている別の明朝で並びます。縦書きの数字は、そのときの書体で幅が決まったまま残ることがあり、読み込むたびに位置が変わっていました。

書体がすべて届くまで文字を出さず、届いてから組み直して出すようにしました。届かないときも4秒で出します。同じサムネイル5本を、新しいブラウザで5回ずつ読み込み直して画面を比べ、5本とも5回とも同じ画面になることを確かめています。

  • ねらい:開くたびに見え方が変わらないようにする
  • 変わったこと:書体が届くまで文字を出さない。届いたら組み直して出す
  • 見込める効果:測った値で、5本×5回の読み込みが、どれも1通りの画面だけになった(直す前は、読み込むたびに数字の位置が変わっていた)

67本の問いを書き、語の途中で改行しない決まりを足した

OK!これで参考記事全部のサムネイル変更。

確認用の12本以外の55本は、記事の冒頭を読んで問いと大きく見せる語を書きました。全部を並べて見たところ、改行の決まりが語の途中で切っていたものがありました(「欲しい」の後ろの「ロ/ボット」、「評価」の後ろの「す/る」など)。改行するのは「大きく見せる語+助詞1字(の・に・は・を・で・が・と・も)」の後ろだけにし、残りが3字未満なら切らない決まりに直しました。それでも読みにくかった6本は、問いの区切り方を書き直しています。

差し替えた考察記事67本のサムネイルを8列に並べた一覧。縦書きと横書き、右寄せと左寄せが順に入れ替わり、どれも問いの中の一語が大きい

一覧・記事の上・SNS の画像を、考察記事ならこのサムネイルにした

これまでの考察記事は、Gemini の絵を「表紙の画像」として持っていて、一覧・記事の上・SNS に貼ったときの画像のすべてにその絵が出ていました。考察記事のときだけ、表紙の画像より新しいサムネイルを使うようにし、SNS 用の画像(1200×675)も67本書き出しました。

  • ねらい:考察記事の顔を、一覧・記事の上・SNS のどこでも同じにする
  • 変わったこと:Gemini の絵(1枚平均約259KB、最大約2.9MB)が、問いの文字のサムネイル(1枚平均約2.4KB の HTML)に替わった
  • 見込める効果:測った値で、考察記事の一覧を開いて最初に見える12枚は、これまで画像だけで約10.2MBだった。作り直したあとは、サムネイルの HTML が約0.1MB と書体のファイルが約2.2MB で、合わせて約2.3MB。書体のファイルは、同じページの文字と入口のバナーとも分け合っている

次の考察記事からは、記事の頭の2行でサムネイルができる

記事をサイトに入れる道具(tools/add_article.py)が、考察記事のときは記事の頭の question:(問い)と key:(大きく見せる語)を読んで、サムネイルを書き出すようにしました。2行がなければ止まって、書くように知らせます。組み方は記事ごとに固定し、新しい記事は一覧で隣になる記事の続きの組みが自動で付きます。すでにあるサムネイルの見え方は変わりません。試しに架空の LOG999 を足して、組みが前の記事の続きになることと、元に戻したあと LOG375 のサムネイルが1字も変わっていないことを確かめました。

  • ねらい:考察記事を書くたびに、別の画面で絵を頼む手間をなくす
  • 変わったこと:サムネイルは記事の頭の2行から作る。Gemini に頼む工程がなくなった
  • 見込める効果:測った値で、67本のサムネイルを書き出すのに0.06秒、SNS 用の画像は10本で約33秒。これまでは、頼んで確かめて直すまでに1本5〜10分かかっていた

サイトのトップのシリーズのカードを、日本語を先にして説明も日本語にした

TOPのサムネイルも提案ください。 それとTOPの日本語と英語のタイトルの位置を前後ろ入れ替え(他のシリーズも!)

トップの「シリーズ」に並ぶ7枚のカードは、英語の名前(Deep Dive)が大きく先に、日本語(考察記事)が小さく後ろにありました。シリーズのページの見出しは日本語が先なので、それにそろえて「考察記事 Deep Dive」の順にし、日本語を大きくしました。

TOPのシリーズのセクションの各カードの説明文が英語なので、日本語に直してください(全カード修正)

説明文も英語だったので、7枚とも日本語にしました。各シリーズのページの説明を短くまとめたものです。

考察記事のカードの絵は、3つ出しました。A は「いま考えていること」のページと同じ問い「ロボットに、何を任せればいいのか。」を 3-B の形で組んだもの、B は記事ごとの大きく見せる語を大きさを変えて敷き詰めたもの、C は新しい4本のサムネイルを2×2で並べたものです。選ばれたのは A でした。

作り直したあとのトップのシリーズのカード7枚。日本語の名前が先に大きく、説明も日本語。考察記事のカードは縦書きの問い「ロボットに、何を任せればいいのか。」になっている

トップのカードは画像で表示しているので、問いを組んだページを画像に書き出して載せました。すぐ上にある「いま考えていること」のページへの入口と同じ紙の色と書体なので、ひと続きに見えます。

7通りの組み方を、同じ4つの問いで切り替えて比べる

この記事で比べた7通りの組み方を、同じ4つの問いで切り替えられるようにしました。

作業の流れ(サイトの commit の一覧)

確認用のページを作ってから、トップのカードを差し替えるまでの commit です。時刻は日本時間。

ea180fa 11:19 確認用:考察記事のサムネイルを問いの縦書きにした一覧(/preview/deep-dive-thumbs)
6808459 11:26 確認用:考察記事のサムネイルに3案(一語を大きく・黒い面で分ける・文の強弱)を足した
9dcae55 11:31 確認用:案3(文の強弱)の強弱の付け方を3つ足した(差を控えめに・3段階・太さだけで差)
0599ecd 11:35 確認用:3-B(3段階)の余白を詰め、文字を大きくした
831b403 11:41 確認用:案3(4つとも)の大きい一語と問いの結びの行の、行の間を詰めた
dec65b8 11:47 確認用:LOG324 の強く見せる語を「フィジカルAI」にした
6478dcf 11:52 確認用:縦書きのサムネイルで、大きさの違う文字を列の中心にそろえた
432058f 11:53 確認用:サムネイルの見え方が読み込むたびに変わるのを直し、縦書きの列の中心をそろえた
5a9344e 12:11 考察記事67本のサムネイルを、問いを文字で組んだ形(3-B)に差し替えた。TOP のシリーズのカードは日本語を先に
787fe12 12:14 TOP のシリーズのカードの説明文を日本語にした(全7枚)
f90e771 12:17 TOP のシリーズのカード(考察記事)の絵を、案A「ロボットに、何を任せればいいのか。」にした

作業ログ(全量)― コード・データ・測った結果

この記事のコード・ログ・データは、1ファイルずつ下に置いています。全文はリンク先にあります。

ファイルの一覧(7本)

01_sec02.py.txt ― コード:tools/deep_dive_thumb.py(サムネイルを書き出す道具)・14,169字

02_sec02.json ― データ:tools/deep_dive_thumbs.json(67本の問いと大きく見せる語)・7,563字

03_sec02.txt ― コード:src/lib/records.ts(考察記事ならこのサムネイルを使う)・7,983字

04_sec02.txt ― コード:src/pages/record/[slug].astro(記事の上と SNS の画像)・18,310字

05_sec02.py.txt ― コード:tools/add_article.py(記事を入れるときにサムネイルを作る)・3,404字

06_sec02.txt ― コード:src/pages/index.astro(トップのシリーズのカード)・11,528字

07_sec02.txt ― コード:src/pages/preview/deep-dive-thumbs.astro(確認用のページ)・6,177字

ここから下は、この記事で使ったコードと、書き出したデータ、測った結果の全量です。

コード:tools/deep_dive_thumb.py(サムネイルを書き出す道具)

問いと大きく見せる語から、7通りの組み方のサムネイルを書き出す道具の全文です。採用した 3-B は p3b、書体が届くまで文字を出さない処理は HEAD の中の script です。

01_sec02.py.txt14,169字・319行・python
"""考察記事のサムネイル(問いを文字で組んだ形)を HTML で書き出す。

考察記事は Gemini の絵(4コマ)ではなく、記事の問いを文字で組んだサムネイルにする(2026-10-26 から。採用は 3-B「3段階」)。
問いと強く見せる一語の正は tools/deep_dive_thumbs.json(記事1本=1項目)。
  question : 記事の問い。縦書きで改行したい所に「/」。1行9字まで(数字の並びは1字、英字は1字ずつ数える)
  key      : 強く見せる一語。問いの1行の中にある語
  layout   : 組み(0 縦・右寄せ/1 横・左下/2 縦・左寄せ/3 横・右上)。記事ごとに固定し、新しい記事が増えても既存の見え方は変えない
別のタブで開く保存

データ:tools/deep_dive_thumbs.json(67本の問いと大きく見せる語)

考察記事67本の問いと大きく見せる語、組み(layout:0 縦・右寄せ/1 横・左下/2 縦・左寄せ/3 横・右上)の全量です。サムネイルの正はこのファイルです。

02_sec02.json7,563字・406行・json
{
  "items": [
    {
      "log": 375,
      "question": "70万円の/アームで、/仕事を/請けられるのか。",
      "key": "70万円",
      "layout": 0
    },
別のタブで開く保存

出力:書き出したサムネイルの1本(public/thumbs/LOG375.html)

道具が書き出すサムネイルの実物です。1本は約2.4KB の HTML で、画像を持ちません。

<!doctype html>
<html lang="ja" class="wait">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>LOG375 thumbnail</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+JP:wght@200;300;400;700;900&display=block" rel="stylesheet">
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  html, body { background: #f5f3ee; }
  .wait .wrap > * { visibility: hidden; }
  .wrap { width: 100%; aspect-ratio: 16 / 9; container-type: inline-size; position: relative; overflow: hidden; background: #f5f3ee; color: #22201c;
    font-family: "Noto Serif JP", "Hiragino Mincho ProN", "Yu Mincho", serif; }
  .v { writing-mode: vertical-rl; white-space: nowrap; }
  .h { white-space: nowrap; }
  .tcy { text-combine-upright: all; letter-spacing: 0; }
  .up { text-orientation: upright; }
  .abs { position: absolute; }
  .blk { position: absolute; background: #22201c; color: #f5f3ee; }
  .key { font-weight: 900; line-height: 1; letter-spacing: 0; }
  .sm { font-weight: 400; letter-spacing: .06em; }
  .b { font-weight: 900; }
  .t { right: 6.0cqw; top: 4.5cqw; font-size: 4.2cqw; line-height: 1.55; letter-spacing: .04em; font-weight: 300; }
  .t > span[style] { vertical-align: middle; }
</style>
</head>
<body>
<div class="wrap"><p class="abs v t"><span style="font-size:13.46cqw;font-weight:900;line-height:1.12;"><span class="tcy">70</span>万円</span>の<br>アームで、<br>仕事を<br><span style="font-size:5.57cqw;font-weight:700;line-height:1.25;">請けられるのか。</span></p></div>
<script>
  // 書体がすべて届いてから組み直して出す。日本語の書体は文字ごとに分かれて届くため、途中は別の明朝で並び、
  // 縦書きの数字(縦中横)の幅が最初の書体のまま残って、読み込むたびに位置が変わっていた(2026-10-26)
  (function () {
    var done = false;
    function show() {
      if (done) return; done = true;
      var w = document.querySelector('.wrap');
      w.style.display = 'none'; void w.offsetHeight; w.style.display = '';
      document.documentElement.classList.remove('wait');
    }
    if (document.fonts && document.fonts.ready) document.fonts.ready.then(function () { requestAnimationFrame(show); });
    else show();
    setTimeout(show, 4000);  // 書体が届かないときも4秒で出す
  })();
</script>
</body>
</html>

コード:src/lib/records.ts(考察記事ならこのサムネイルを使う)

一覧のカードを作るところで、考察記事のときだけ表紙の画像(Gemini の絵)より新しいサムネイルを使う withDeepDiveThumb を足しました。

03_sec02.txt7,983字・87行・ts
import { getCollection } from 'astro:content';
import cards from '../data/cards.json';
import mangaCards from '../data/mangaCards.json';
import englishCards from '../data/englishCards.json';
import deepDiveThumbs from '../../tools/deep_dive_thumbs.json';

// 考察記事のサムネイルは、問いを文字で組んだ形(public/thumbs/LOG{N}.html。tools/deep_dive_thumb.py で書き出す。2026-10-26 から)
// Gemini の絵(cover)より優先する。正は tools/deep_dive_thumbs.json
別のタブで開く保存

コード:src/pages/record/[slug].astro(記事の上と SNS の画像)

記事のページでも同じ関数を通し、記事の上の画像と SNS 用の画像(og:image)を新しいサムネイルにしています。

04_sec02.txt18,310字・271行・astro
---
import Base from '../../layouts/Base.astro';
import { ymd } from '../../lib/format';
import { mediaHtml } from '../../lib/media';
import { fixBody, textOf, abs, AUTHOR, PUBLISHER, SITE, SITE_NAME } from '../../lib/seo';
import { SERIES_PAGES, allCards, withDeepDiveThumb } from '../../lib/records';
import RecordCard from '../../components/RecordCard.astro';
import { getCollection, render } from 'astro:content';
別のタブで開く保存

コード:tools/add_article.py(記事を入れるときにサムネイルを作る)

考察記事を入れるときに、記事の頭の question: と key: を読んでサムネイルを書き出すようにした、記事を入れる道具の全文です。

05_sec02.py.txt3,404字・79行・python
"""記事の作業フォルダ(iCloud)から、記事1本をサイトに入れる。

使い方:
  python3 tools/add_article.py <作業フォルダ> <LOG番号> <シリーズ名> [--no-split]

やること:
- LOG{N}_article.md      → src/content/records/LOG{N}.md(先頭の項目に series を足す)
- LOG{N}_thumbnail.html  → public/thumbs/LOG{N}.html(考察記事は使わない。記事の頭の question・key から tools/deep_dive_thumb.py で書き出す)
別のタブで開く保存

コード:tools/render_og.py(SNS 用の画像の書き出し)

書体が届いてから文字を出すようにしたので、SNS 用の画像を撮る道具も、文字が出るまで待つようにしました。

"""記事のサムネイル(public/thumbs/*.html)を、SNS・検索結果に出す画像(public/og/*.jpg)に書き出す。

XやLinkedInで記事のリンクを貼ったときに出る画像は、HTMLでは出せないため、画像にしておく。
記事ページは、この画像があれば og:image に使い、なければサイト共通の画像を使う。

使い方(Chromium と Playwright のある Claude のクラウドの作業環境で動かす。Mac には Playwright がない):
  python3 tools/render_og.py            # まだ画像がないサムネイルだけ書き出す
  python3 tools/render_og.py LOG344     # 指定したものだけ書き出し直す(ファイル名から .html を除いた名前)
  python3 tools/render_og.py --all      # 全部書き出し直す

大きさは 1200×675(16:9)。Google が大きな画像として扱う幅 1200 以上にそろえる。
"""
import os
import sys
from pathlib import Path

from playwright.sync_api import sync_playwright

W, H = 1200, 675
site = Path(__file__).resolve().parent.parent
thumbs = site / "public/thumbs"
out_dir = site / "public/og"
out_dir.mkdir(parents=True, exist_ok=True)

args = sys.argv[1:]
names = sorted(p.stem for p in thumbs.glob("*.html"))
if "--all" in args:
    targets = names
elif args:
    targets = [a.removesuffix(".html") for a in args]
else:
    targets = [n for n in names if not (out_dir / f"{n}.jpg").exists()]

exe = os.environ.get("CHROMIUM_PATH") or next(
    (str(p) for p in Path("/opt/pw-browsers").glob("chromium-*/chrome-linux/chrome")), None)

done = []
with sync_playwright() as pw:
    browser = pw.chromium.launch(executable_path=exe) if exe else pw.chromium.launch()
    page = browser.new_page(viewport={"width": W, "height": H}, device_scale_factor=1)
    for name in targets:
        src = thumbs / f"{name}.html"
        if not src.exists():
            print(f"見つからない: {src}")
            continue
        page.goto(src.as_uri(), wait_until="networkidle")
        page.evaluate("document.fonts.ready")
        # 考察記事のサムネイル(tools/deep_dive_thumb.py)は、書体が届いてから文字を出す。出るまで待つ
        try:
            page.wait_for_function("!document.documentElement.classList.contains('wait')", timeout=8000)
        except Exception:
            pass
        # 動くサムネイルは、動きが落ち着いたところで撮る
        page.wait_for_timeout(2500)
        page.screenshot(path=str(out_dir / f"{name}.jpg"), type="jpeg", quality=85,
                        clip={"x": 0, "y": 0, "width": W, "height": H})
        done.append(name)
    browser.close()

print(f"書き出し {len(done)} 件 → public/og/")

コード:src/pages/index.astro(トップのシリーズのカード)

トップのシリーズのカードの並び(日本語を先に)と、7枚の日本語の説明、考察記事のカードの画像の差し替えが入ったトップのページの全文です。

06_sec02.txt11,528字・194行・astro
---
import Base from '../layouts/Base.astro';
import RecordCard from '../components/RecordCard.astro';
import { ymd } from '../lib/format';
import { media } from '../lib/media';
// SNS アイコン(Font Awesome Free 6.7.2・CC BY 4.0。ライセンス表記は各 SVG の中に残している)
import icGithub from '../icons/github.svg?raw';
import icLinkedin from '../icons/linkedin.svg?raw';
別のタブで開く保存

コード:tools/series5_top_card.html(トップの考察記事のカードの元)

トップの考察記事のカードの画像は、このページを 960×640・2倍で撮って public/site/series5-deep-dive.jpg に書き出したものです。

<!doctype html>
<!-- TOP のシリーズのカード(考察記事)の絵の元。直したら、書体が出たあと 960×640・2倍で撮って public/site/series5-deep-dive.jpg に書き出す -->
<html lang="ja" class="wait">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>考察記事 TOP のカード</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+JP:wght@200;300;400;700;900&display=block" rel="stylesheet">
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  html, body { background: #f5f3ee; }
  .wait .wrap > * { visibility: hidden; }
  .wrap { width: 100%; aspect-ratio: 3 / 2; container-type: inline-size; position: relative; overflow: hidden; background: #f5f3ee; color: #22201c;
    font-family: "Noto Serif JP", "Hiragino Mincho ProN", "Yu Mincho", serif; }
  .q { position: absolute; right: 9cqw; top: 6cqw; writing-mode: vertical-rl; white-space: nowrap; letter-spacing: .04em; line-height: 1.55; }
  .s { font-size: 5cqw; font-weight: 300; }
  .b { font-size: 21cqw; font-weight: 900; line-height: 1.1; vertical-align: middle; }
  .m { font-size: 6.2cqw; font-weight: 700; line-height: 1.25; vertical-align: middle; }
</style>
</head>
<body>
<div class="wrap"><p class="q"><span class="s">ロボットに、</span><br><span class="b">何を</span><br><span class="m">任せればいいのか。</span></p></div>
<script>
  (function () {
    var done = false;
    function show() { if (done) return; done = true; var w = document.querySelector('.wrap'); w.style.display = 'none'; void w.offsetHeight; w.style.display = ''; document.documentElement.classList.remove('wait'); }
    if (document.fonts && document.fonts.ready) document.fonts.ready.then(function () { requestAnimationFrame(show); }); else show();
    setTimeout(show, 4000);
  })();
</script>
</body>
</html>

コード:src/pages/preview/deep-dive-thumbs.astro(確認用のページ)

案を比べるのに使った確認用のページです。考察記事のページと同じ組みで、最初に見える12本だけサムネイルを差し替え、ボタンで案を切り替えます。検索には出さない設定です。

07_sec02.txt6,177字・107行・astro
---
// 確認用:考察記事のサムネイルを文字だけで組んだとき、一覧でどう見えるか(2026-10-26)
// /series5 と同じ組み(見出し・考えのページのバナー・記録の一覧)で、最初に見える12本だけサムネイルを差し替える
// 案は上のボタンで切り替える(サムネイルは tools/deep_dive_thumb.py で書き出す)
// 検索に出さない(noindex・サイトマップから外す)。どこからもリンクしない。確かめ終わったら消す
import Base from '../../layouts/Base.astro';
import RecordCard from '../../components/RecordCard.astro';
import ThinkingBanner from '../../components/ThinkingBanner.astro';
別のタブで開く保存

コード:読み込み直しても同じ画面になるかを確かめたスクリプト

同じサムネイルを新しいブラウザで5回ずつ読み込み、画面の画像が何通りあるかを数えました。

from pathlib import Path
import hashlib
from playwright.sync_api import sync_playwright
exe = next((str(p) for p in Path("/opt/pw-browsers").glob("chromium-*/chrome-linux/chrome")), None)
res={}
with sync_playwright() as pw:
    b = pw.chromium.launch(executable_path=exe) if exe else pw.chromium.launch()
    for l in [375,316,319,323,324]:
        hs=set()
        for r in range(5):
            ctx=b.new_context(viewport={'width':340,'height':191}, device_scale_factor=2)
            p=ctx.new_page()
            p.goto(f'file:///home/claude/p2m-site/public/preview/deep-dive/p3b/LOG{l}.html')
            p.wait_for_function("!document.documentElement.classList.contains('wait')", timeout=8000)
            p.wait_for_timeout(200)
            hs.add(hashlib.md5(p.screenshot()).hexdigest()); ctx.close()
        res[l]=len(hs)
    b.close()
print(res)

出力:読み込み直しの確かめ(サムネイルごとの、画面の種類の数)

5本とも、5回の読み込みで画面は1通りでした。

{375: 1, 316: 1, 319: 1, 323: 1, 324: 1}

コード:Gemini の絵と新しいサムネイルの重さを数えたスクリプト

表紙の画像は公開サイトから実際に落として大きさを数え、新しいサムネイルは HTML のファイルの大きさを数えました。

import json,re,glob,subprocess,os
# Gemini の絵(表紙の画像)の大きさ:公開サイトから実際に落として数える
covers={}
for c in json.load(open('src/data/cards.json')):
    if c['series']=='考察記事' and c.get('cover'): covers[int(re.match(r'LOG(\d+)',c['day']).group(1))]=c['cover']
for f in glob.glob('src/content/records/*.md'):
    fm=open(f).read().split('---')[1]
    if re.search(r'^series:\s*"?考察記事',fm,re.M):
        m=re.search(r'^cover:\s*"?([^"\n]+)',fm,re.M); d=int(re.search(r'^day:\s*"?(\d+)',fm,re.M).group(1))
        if m: covers[d]=m.group(1)
size=lambda u:int(subprocess.run(['curl','-s','-o','/dev/null','-w','%{size_download}','https://pixelstomotors.com'+u],capture_output=True,text=True).stdout)
sizes=[size(u) for u in covers.values()]
print(len(sizes), sum(sizes), sum(sizes)//len(sizes), min(sizes), max(sizes))
first=[375,336,329,324,323,320,319,318,316,315,314,312]
print('first12 old', sum(size(covers[l]) for l in first), 'new html', sum(os.path.getsize(f'public/thumbs/LOG{l}.html') for l in first))
new=[os.path.getsize(f'public/thumbs/LOG{i["log"]}.html') for i in json.load(open('tools/deep_dive_thumbs.json'))['items']]
print('new thumbs', len(new), 'total', sum(new), 'avg', sum(new)//len(new))

出力:重さを数えた結果

1行目は Gemini の絵67枚の本数・合計・平均・最小・最大(バイト)です。

67 17333242 258705 89322 2913069
first12 old 10184622 new html 29207
new thumbs 67 total 162961 avg 2432

コード:公開した考察記事の一覧で、実際に読み込まれた量を数えたスクリプト

公開したあとの考察記事の一覧を開き、読み込まれた書体のファイルとサムネイルの HTML と画像の量を数えました。

from pathlib import Path
from playwright.sync_api import sync_playwright
exe = next((str(p) for p in Path("/opt/pw-browsers").glob("chromium-*/chrome-linux/chrome")), None)
with sync_playwright() as pw:
    b=pw.chromium.launch(executable_path=exe) if exe else pw.chromium.launch()
    p=b.new_page(viewport={'width':1280,'height':1000})
    seen={}
    def fin(req):
        r=req.response()
        if not r: return
        try:
            u=req.url
            if 'fonts.gstatic' in u or '/thumbs/' in u or '/media/' in u:
                seen[u]=len(r.body())
        except Exception: pass
    p.on('requestfinished', fin)
    p.goto('https://pixelstomotors.com/series5?p2m-notrack=1', wait_until='networkidle'); p.wait_for_timeout(6000)
    f=[v for u,v in seen.items() if 'gstatic' in u]; t=[v for u,v in seen.items() if '/thumbs/' in u]; m=[v for u,v in seen.items() if '/media/' in u]
    print('unique font files',len(f),sum(f)); print('thumb html',len(t),sum(t)); print('media',len(m),sum(m))
    b.close()

出力:公開した一覧で読み込まれた量

一覧の外で隠れているカードの分も一部読み込まれるため、サムネイルの HTML は36本分でした。Gemini の絵(/media/)は0です。

unique font files 61 2207664
thumb html 36 100784
media 0 0

出力:書き出しにかかった時間

サムネイル67本の書き出しと、SNS 用の画像10本の書き出しを測りました。

$ python3 tools/deep_dive_thumb.py
書き出し 67 件 → public/thumbs/
秒 0.06

$ python3 tools/render_og.py LOG375 LOG336 LOG329 LOG324 LOG323 LOG320 LOG319 LOG318 LOG316 LOG315
og 10本 33.2 秒