# ハコブくんの資料を AI が読めるページに ― Google の埋め込みを表と本文へ

- 記録: LOG458（ひとり研究所の裏側）
- 公開: 2026/11/7
- 記事のページ: https://pixelstomotors.com/record/hakobu-docs-readable-by-ai-without-embeds
- サイト: Pixels to Motors（ADAPT DESIGN / 島津 栄一） https://pixelstomotors.com/
- 要約: pixelstomotors.com のハコブくんのページで、Google の埋め込み5つをサイトの表と本文に置き換え、HTML から読める資料の文字が275字から77,424字になった。デザインノートはサイトの実際の値に替えた。

> AI が読むための記事の全文です（コード・実行ログを含む）。引用するときは上の「記事のページ」の住所を使ってください。

---

**pixelstomotors.com の薬剤搬送ロボットハコブくんのページ（/series1）で、Google スプレッドシートと Google ドキュメントの埋め込み5つをサイトの表と本文に置き換えたところ、ページの HTML から読める資料の欄の文字は275字から77,424字になりました。** 埋め込み（iframe）のままだと、資料の中身は Google のページの側にあり、こちらのページの HTML には見出しの数語しか入っていませんでした。

置き換えて、次のことが変わりました。

- AI と検索エンジンが、4コママンガの進行表（182行）・要件定義書2本・フロー図の部品の一覧を、このページの文字として読める。サイトでは AI に読まれた回数を毎日数えていて（LOG451・LOG453）、読まれる中身がページに入っていることが、その前提になる
- 読む人のブラウザは、Google への読み込み（14件・399KB）をしなくなった。表はサイトの文字と線でそろい、見出しの行を固定したまま枠の中でスクロールする
- デザインノートは、2025年12月に Google で作った一般論のメモから、このサイトで実際に使っている色・文字・形・部品・並びの値に替えた。色と書体の値は、サイトのコードからビルドのたびに読むので、書き写した値がずれることがない
- 時間：最初の依頼（09:33）から、3回目の変更を公開するまで（09:51）が18分。代わりに、Google 側を直したときは、写し直すコマンドを1回動かす手間が増えた（自動では追いかけない）

始まりは、ハコブくんのページの「4コママンガ保管庫」（Figma の埋め込み）を外したいという依頼でした。外したあと、同じページの残りの埋め込みを見ると、進捗管理・デザインノート・ログ & リファレンスの3つが Google スプレッドシート、要件定義書の2つが Google ドキュメントで、どれも Google の「ウェブに公開」した版を iframe で枠ごと借りて見せていました。見た目は Google の表のままで、下に Google のタブの帯が出て、文字の大きさも線もサイトとそろっていません。エイイチの指示は次の一言でした。

> 同じページ内の埋め込みデータをサイトに直製掲載するように変更したい

そこで、公開版の中身を取ってきてサイトのデータに写す道具を作り、サイトの表と本文として描くようにしました。続けて、デザインノートの中身をサイトの実際の値に替え、資料の見出しを「スプレッドシート/進捗管理」のような道具の名前から中身の名前に変えています。Figma の UI デザインのボードと FigJam のフロー図の2つは、拡大して見るボードなので、エイイチの判断で埋め込みのまま残しました。

| やったこと | なぜ | 結果 |
|---|---|---|
| 4コママンガ保管庫（Figma の埋め込み）を外した | エイイチの依頼 | 見出しと iframe の2ブロックを削除 |
| Google の公開版の中身を写す道具 fetch_series_docs.py を作った | 埋め込みのままだと中身がページに入らないため | 表3つ（タブ7枚）とドキュメント2本を src/data/series_docs.json に写した |
| 表と本文を描く部品 SeriesSheet・SeriesDoc を作った | サイトの見た目で読めるようにするため | 見出しの行を固定した表、タブはボタン、本文は見出し・段落・箇条書き・表だけ |
| 前と後を同じ条件で測った | 変わったことを数字で確かめるため | iframe 8→2、Google への読み込み 14件→0件、資料の欄の文字 275字→77,424字 |
| デザインノートをサイトの実際の値に替えた | エイイチ「今のサイトの状況に合わせて整理」 | 色・文字・形・部品・並び・決まりの6タブ。色と書体はコードから読む |
| 資料の並びと見出しを整理した | 道具の名前の見出しが、もう事実と合わないため | 要件定義書 → UI → フロー図 → 部品 → デザインノート → 4コマの進行表 |

[図解: LOG458_embed_vs_inline.html](https://pixelstomotors.com/embeds/LOG458_embed_vs_inline.html)


## 作業ログ ― ハコブくんのページから、4コママンガ保管庫の埋め込みを外す

### 外したのは static.json の2ブロック（見出しと Figma の iframe）

シリーズのページの上にある資料は、p2m-site の src/data/static.json に、見出し（text）と埋め込み（iframe・embed）のブロックとして並んでいます。保管庫は、見出し「Figma/薬剤搬送ロボットハコブくん4コママンガ保管庫」と、Figma のページ（node-id=1791-107604）を幅1280px・高さ1280pxで埋め込む iframe の2つでした。この2つを消し、ビルドしたページに「4コママンガ保管庫」の文字がないこと、前後の見出しが残っていることを確かめて公開しました（09:39）。Figma のマンガのデータそのものには手を入れていません。

![外す前のハコブくんのページ。Figma の4コママンガ保管庫が、ページの幅いっぱいに縮んだ小さなコマの並びとして埋め込まれていた](https://pixelstomotors.com/media/LOG458_01_before_manga_archive.png)

### static.json の差分（保管庫を外した commit 3fd5ee1）

```diff
diff --git a/src/data/static.json b/src/data/static.json
index d6f38c0..fa5b997 100644
--- a/src/data/static.json
+++ b/src/data/static.json
@@ -16,16 +16,6 @@
     "html": "<iframe style=\"border: 1px solid rgba(0, 0, 0, 0.1);\" width=\"100%\" height=\"100%\" src=\"https://embed.figma.com/design/gDAJCTN8VCdVUwJHrQ5N7z/%E3%83%AD%E3%83%9C%E3%83%86%E3%82%A3%E3%82%AF%E3%82%B9%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3?node-id=0-1&embed-host=share\" allowfullscreen></iframe>",
     "height": 800
    },
-   {
-    "type": "text",
-    "html": "Figma/薬剤搬送ロボットハコブくん4コママンガ保管庫",
-    "link": null
-   },
-   {
-    "type": "iframe",
-    "html": "<iframe style=\"border: 1px solid rgba(0, 0, 0, 0.1);\" width=\"1280\" height=\"100%\" src=\"https://embed.figma.com/design/gDAJCTN8VCdVUwJHrQ5N7z/%E3%83%AD%E3%83%9C%E3%83%86%E3%82%A3%E3%82%AF%E3%82%B9%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3?node-id=1791-107604&embed-host=share\" allowfullscreen></iframe>",
-    "height": 1280
-   },
    {
     "type": "text",
     "html": "スプレッドシート/進捗管理",
```

### ビルドして、保管庫の文字が消えたことを確かめたコマンドと出力

```
$ npm ci --silent; npm run build 2>&1 | tail -5; f=$(ls dist/series1/index.html 2>/dev/null || ls dist/series1.html); grep -c "4コママンガ保管庫" $f; grep -o "UIデザイン・イメージボード\|スプレッドシート/進捗管理" $f
09:39:34 ✓ Completed in 11.17s.

09:39:34 [@astrojs/sitemap] `sitemap-index.xml` created at `dist`
09:39:34 [build] 811 page(s) built in 29.05s
09:39:34 [build] Complete!
0
UIデザイン・イメージボード
スプレッドシート/進捗管理
```


## 作業ログ ― Google の埋め込みを、サイトの表と本文に置き換える

### iframe と、Google の「ウェブに公開」

[iframe](https://developer.mozilla.org/ja/docs/Web/HTML/Element/iframe) は、ページの中に別のページを枠ごとはめ込む仕組みです。枠の中に見えている文字は、はめ込んだ先のページ（ここでは docs.google.com）のもので、こちらのページの HTML には入りません。

Google スプレッドシートと Google ドキュメントには、[ウェブに公開](https://support.google.com/docs/answer/183965) という機能があり、ログインしなくても見られる版（/pubhtml・/pub で終わる住所）を作れます。ハコブくんのページは、この公開版を iframe で埋め込んでいました。

| 言葉 | この記事での意味 |
|---|---|
| 埋め込み（iframe） | 別のページを枠ごとはめ込むこと。中身はこちらの HTML に入らない |
| 公開版 | Google の「ウェブに公開」で作られた、ログインなしで見られる版。住所が `2PACX-` で始まる |
| CSV | 表を、値をカンマで区切った文字だけにした形。公開版から1枚ずつ取り出せる（`output=csv`） |
| 写し | 公開版から取ってきて、サイトのデータ（series_docs.json）に置いたもの。Google 側を直しても自動では変わらない |

このプロジェクトでは、公開版を「読むだけ」に使い、取ってきた中身をサイトの側で描きます。Google 側は書き換えません。

### ハコブくんのページの埋め込みは8つで、Google が5つだった

保管庫を外したあとの、/series1 の資料のブロックの一覧です。iframe は Figma と Google スプレッドシート（進捗管理）を直接、embed は、サイトの public/embeds/ に置いた小さな HTML（その中で Google を iframe で埋め込んでいる）を指しています。

```
$ python3 -c "import json;d=json.load(open('src/data/static.json')) ..."
0 text None Drug Delivery Robot |&nbsp;薬剤搬送ロボットハコブくん
1 text None Figma/UIデザイン・イメージボード
2 iframe 800 <iframe style="border: 1px solid rgba(0, 0, 0, 0.1);" width="100%" height="100%" src="https://embed.figma.com/design/gDAJCTN8VCdVUwJHrQ5N7z/%E3%83%AD%E3%83%9C%E3%83%86%E3%82%A3%E3%82%AF%E3%82%B9%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3?node-id=0-1&embed-host=share" allowfullscreen></iframe>
3 text None スプレッドシート/進捗管理
4 iframe 640 <iframe src="https://docs.google.com/spreadsheets/d/e/2PACX-1vQnqeo6YmdjAz9VQiH8-PYR4OzF9RdlgEOBe-SMtNjYcwdeHkkw2gNTOnLw9jSHBcWmIT3gaxnYnTH6/pubhtml?widget=true&amp;headers=false"    width="100%"    height="640px"    frameborder="0">   </iframe>
5 text None スプレッドシート/デザインノート
6 embed 480 <div class="embed"><iframe src="/embeds/series1-1.html" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-presentation" loading="lazy" title="図解" data-embed allowfullscreen></iframe></div>
7 text None ドキュメント/要件定義書
8 embed None <div class="embed"><iframe src="/embeds/series1-2.html" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-presentation" loading="lazy" title="図解" data-embed allowfullscreen></iframe></div>
9 embed None <div class="embed"><iframe src="/embeds/series1-3.html" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-presentation" loading="lazy" title="図解" data-embed allowfullscreen></iframe></div>
10 text None FigJam/フロー図
11 iframe 800 <iframe style="border: 1px solid rgba(0, 0, 0, 0.1);" width="800" height="450" src="https://embed.figma.com/board/fFRdzGMRwQGF7Y36dCmB1m/%E5%9F%BA%E6%9C%AC%E3%83%95%E3%83%AD%E3%83%BC%E5%9B%B3?node-id=0-1&embed-host=share" allowfullscreen></iframe>
12 text None スプレッドシート/ログ &amp; リファレンス
13 embed None <div class="embed"><iframe src="/embeds/series4-1.html" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-presentation" loading="lazy" title="図解" data-embed allowfullscreen></iframe></div>
['series1', 'series2', 'series3', 'series4', 'series5', 'series6', 'series7']
```

public/embeds/ の4つの中身は、どれも Google の公開版を iframe で読むだけの包みでした（高さを親のページに伝える短いスクリプト付き）。series4-1.html は /series4 でも使っているので、ファイルは消さずに残しています。

![置き換える前の4コママンガの進行表。Google スプレッドシートの埋め込みで、列が枠の外へはみ出し、下に Google のタブの帯が出ている](https://pixelstomotors.com/media/LOG458_02_before_google_sheet.png)

### 公開版の中身は、作業環境から取れた

作業環境（クラウド）から Google の公開版に届くかを先に確かめました。届かなければ、Mac で取ってくる手順に変える必要があったためです。結果は 200（届いた）でした。表の中身は、公開版の HTML ではなく、タブごとに CSV で取ります。HTML はスクリプトで組み立てられていて、表（`<table>`）が入っていなかったためです。

```
$ curl -sS -o /dev/null -w "%{http_code}\n" "https://docs.google.com/spreadsheets/d/e/2PACX-1vQnqeo6YmdjAz9VQiH8-PYR4OzF9RdlgEOBe-SMtNjYcwdeHkkw2gNTOnLw9jSHBcWmIT3gaxnYnTH6/pubhtml"
200
```

```
== prog tables 0 rows 0
[('4コママンガプロット', 'https:\\/\\/docs.google.com\\/spreadsheets\\/d\\/e\\/2PACX-1vQnqeo6YmdjAz9VQiH8-PYR4OzF9RdlgEOBe-SMtNjYcwdeHkkw2gNTOnLw9jSHBcWmIT3gaxnYnTH6\\/pubhtml\\/sheet?headers\\x3dfalse&gid=2096872720', '2096872720')]
== note tables 0 rows 0
[('Gemini-光のプロンプト集', '…gid=327170320', '327170320'), ('Figma-スタイル', '…gid=519422369', '519422369'), ('Figma-カラー\\/バリアブルベーシックリスト', '…gid=595743752', '595743752'), ('Figma-カラー\\/バリアブル運用', '…gid=1454063175', '1454063175'), ('Figma-カラー\\/機能', '…gid=0', '0')]
== log tables 0 rows 0
[('FigJam-ライブラリ部品', '…gid=0', '0')]
```

（上の2つ目の出力は、長い住所の繰り返しを「…」で縮めています。タブの名前と gid は出力のままです。）

タブごとに CSV を取ると、行と列の数は次のとおりでした。

```
== prog 183 rows x 10 cols | imgs 0 | bg colors 4
   ['No.', '対応進捗', 'タイトル', 'テーマ', '登場人物', '1コマ目（起）', '2コマ目（承）', '3コマ目（転）']
   ['1', '完了', 'チョコレート差し入れ', 'ロボットが提供する機能以上の「癒やし」の効果。', '夜勤中の疲れた看護師、ハコブくん（ロボット）', '忙しい夜勤の合間、必要な薬剤を運んできたロボットが到着。看護師が操作する。', 'ロボットの下部にある収納トレイが開き、必要な薬剤を取り出す。看護師の表情はまだ業', '薬剤ケースの下に、先輩からの小さなメモ用紙と、個包装のチョコレートが隠れているの']
   ['2', '完了', '言葉なきコミュニケーション', '緊迫した現場での、非言語的な状況伝達デザインの重要性。', 'ベテラン看護師（超多忙）、新人医師（焦っている）、ハコブくん', '救急搬送が重なり、殺気立つナースステーション。新人医師が「あの薬、まだですか！？', 'その背後からハコブくんが到着。しかし、いつもの「到着しました」という音声アナウン', 'その代わり、顔ディスプレイの色を「緊急を示す赤」ではなく、「落ち着きを促す深い青']
   ['3', '完了', '「最短ルート」の罠', 'エンジニアリング的な効率性と、人間的な空間認識のズレ。', '患者（松葉杖でリハビリ中）、ハコブくん', '広い廊下で、患者が一生懸命松葉杖で歩行訓練をしている。', 'ハコブくんが向こうからやってくる。システム上の「最短ルート」は、患者のすぐ横をす', 'ハコブくんは減速せず、計算通り患者のギリギリ横を「シュンッ」と通過する。患者、驚']
== n1 7 rows x 3 cols | imgs 0 | bg colors 4
   ['原則 (Principle)', '指示の内容 (Instruction)', '得られる効果 (Effect)']
   ['表面下散乱 (SSS)', '光が表面で反射するだけでなく、素材（肌・絹・石材）の数ミリ内側に浸透し、内部で拡', 'デジタル特有の硬さを排除し、有機的な「生命感」と内側から溢れるような「色気」を宿']
   ['明暗法と半影の制御', '光と影が交わる境界線（半影）に注目し、その階調をベルベットのように滑らかにするか', '画面全体の「情緒」を支配する。柔らかい階調はエレガンスを、鋭い対比はドラマチック']
   ['鏡面反射の張力', 'ハイライト（鏡面反射）を広範囲に拡散させず、特定の点に凝縮させる。また、反射の強', '物体の「硬度」や「品質」を定義し、ハイブランドのプロダクトのような、研ぎ澄まされ']
== n2 5 rows x 2 cols | imgs 0 | bg colors 4
   ['項目', 'スタイルで管理すべき理由']
   ['テキスト (Text)', 'フォント、サイズ、行間などを一括管理するため。※バリアブルでも数値（サイズ等）は']
   ['色 (Color)', 'グラデーション（線形・放射状など）、画像・動画の塗り、複数の色を重ねるマルチフィ']
   ['エフェクト (Effect)', 'ドロップシャドウやレイヤーぼかしは、スタイルでしか保存できません。']
== n3 33 rows x 3 cols | imgs 0 | bg colors 4
   ['Text（文字・アイコン色）', '', '']
   ['バリアブル名', '役割・用途', 'Primitive参照例 (Light)']
   ['Text / Primary', '主色。見出し、本文など最も重要な情報。', 'Neutral/900 (黒に近い)']
   ['Text / Secondary', '副色。補足説明、メタデータ、ラベル。', 'Neutral/600 (濃いグレー)']
== n4 4 rows x 5 cols | imgs 0 | bg colors 3
   ['階層', '種類', '意味', '命名例', '役割']
   ['Lv.1', 'Primitive', '素材', 'Blue/500', '色そのものの定義。デザインには直接使わない（推奨）。']
   ['Lv.2', 'Semantic', '意味', 'Button/Primary', '「どこで使うか」の定義。Primitiveを参照する。']
   ['(Lv.3)', '(Component)', '(特定部品)', 'Btn-Submit-Bg', '※大規模な場合のみ使用。特定コンポーネント専用の色。']
== n5 6 rows x 3 cols | imgs 0 | bg colors 4
   ['機能', 'バリアブル (Variables)', 'スタイル (Styles)']
   ['主な用途', '単色（Solid Color）、トークン管理', 'グラデーション、画像、エフェクト']
   ['テーマ切り替え', '◎ モード機能で自動切り替え可能', '△ 手動での切り替えが必要']
   ['参照（エイリアス）', '◎ 他のバリアブルを参照可能', '× 他のスタイルを参照不可']
== log 11 rows x 5 cols | imgs 0 | bg colors 4
   ['No', '部品名 (英語/日本語)', '役割・定義', '形状 / 指定色', 'ルール・記載例']
   ['1', 'Start / 開始', 'フローの起点。仕様の開始地点を固定する。', '小さな丸 / グレー or ブルー', '1フローにつき1つのみ配置。']
   ['2', 'State / Idle / 通常', 'UIや機体が安定しており、待機している状態。', '角丸四角 / 白 or 薄グレー', 'ほぼ全てのフローの基準点となる。']
   ['3', 'Command / ユーザー操作', '人が意図的に起こす操作。UIの責務を明確化。', '四角 / 青', '例：「開始ボタンを押す」「停止を指示」']
```

（各セルは、確かめのために先頭40字で切って表示しています。写したデータには全文が入っています。）

画像の入ったセルはありませんでした。背景の色は3〜4種類あるだけで、色に意味を持たせた表ではなかったので、写すのは文字だけにしました。ドキュメントのほうは、公開版の HTML の本文（`doc-content`）に、見出し・段落・箇条書き・表が入っていて、太字は `c17` のような番号の付いた CSS の決まり（`font-weight:700`）で付いていました。

### 写す道具 tools/fetch_series_docs.py（全文）

表はタブごとに CSV で取り、後ろの空の行と列を落とします。ドキュメントは本文から、見出し・段落・箇条書き（何段目の入れ子か）・表・太字・斜体・リンクだけを残し、Google の色・字の大きさ・余白の指定は捨てます。見出しは、ページの見出し（h1・h2）とラベルの下に入るので3段下げ、文書のタイトル（20pt の段落）は h3 にします。Google の転送の住所（google.com/url?q=）になっているリンクは、元の住所に戻します。

下の全文は、デザインノートを Google から写すのをやめたあと（3回目の変更のあと）の版です。最初の版との差分は、後ろの「デザインノート」の節に載せています。

```python
#!/usr/bin/env python3
"""シリーズのページに埋め込んでいた Google スプレッドシート・ドキュメントの中身を取ってきて、
サイトに直接載せるためのデータ（src/data/series_docs.json）に書き出す。

- 元は Google 側。サイトは書き出した時点の写しを載せる。Google 側を直したら、これを動かして push する
    python3 tools/fetch_series_docs.py
- 取ってくるのは「ウェブに公開」した版（/pub・/pubhtml）。ログインはいらない
- 表は CSV で取る。ドキュメントは公開版の HTML から、見出し・段落・箇条書き・表・太字・リンクだけを残す
- デザインノートは Google から写さない（サイトの値が正。src/data/design_note.json と src/lib/sheets.ts）
- 載せる場所は src/data/static.json の {"type": "sheet" | "doc", "key": ...} のブロック
"""
import csv, io, json, re, sys, urllib.request
from urllib.parse import urlparse, parse_qs
from bs4 import BeautifulSoup, NavigableString

OUT = 'src/data/series_docs.json'
SHEET = 'https://docs.google.com/spreadsheets/d/e/{id}'
DOC = 'https://docs.google.com/document/d/e/{id}/pub?embedded=true'

SHEETS = {
    # key: 公開版の id（2PACX-...）。タブは公開版に出ている順にすべて取る
    'hakobu-progress': '2PACX-1vQnqeo6YmdjAz9VQiH8-PYR4OzF9RdlgEOBe-SMtNjYcwdeHkkw2gNTOnLw9jSHBcWmIT3gaxnYnTH6',
    'flow-library': '2PACX-1vRpLxXW9hSt-ediJC6AUIt8peUUbRATWB05SL7fdj14ygbsdQM1Hi9LEondS7phSu43Fv1A5MvGwDCT',
}
DOCS = {
    'hakobu-requirements': '2PACX-1vQ8DnpuXG_E-iyE9U0li_i7iwsneP2VpI7DxK-rOP-HFVhuf2ARqWGgiW0gChVb3QgOPcjAaOqS8l9a',
    'tsukamu-requirements': '2PACX-1vSTXcuWYVsHofVRjwcR3rqTvrzV6mGnOfUe8MdcCsBpKWfVA2zj-HLmDB3rnQ3MW2_OogYNzHhNDCtR',
}


def get(url):
    req = urllib.request.Request(url, headers={'User-Agent': 'p2m-site fetch_series_docs'})
    with urllib.request.urlopen(req, timeout=60) as r:
        return r.read().decode('utf-8')


def trim(rows):
    """後ろの空の行・列を落とす"""
    while rows and not any(c.strip() for c in rows[-1]):
        rows.pop()
    width = max((max((i + 1 for i, c in enumerate(r) if c.strip()), default=0) for r in rows), default=0)
    return [[c.strip() for c in (r + [''] * width)[:width]] for r in rows]


def fetch_sheet(sid):
    base = SHEET.format(id=sid)
    html = get(f'{base}/pubhtml?widget=true&headers=false')
    title = re.search(r'<title>(.*?)</title>', html).group(1)
    title = BeautifulSoup(title, 'html.parser').get_text().removesuffix(' - Google Drive').strip()
    tabs = []
    for name, gid in re.findall(r'items\.push\(\{name: "((?:[^"\\]|\\.)*)", pageUrl: "[^"]*", gid: "(\d+)"', html):
        name = json.loads(f'"{name}"')
        rows = trim(list(csv.reader(io.StringIO(get(f'{base}/pub?gid={gid}&single=true&output=csv')))))
        tabs.append({'name': name, 'rows': rows})
    if not tabs:
        raise SystemExit(f'タブが見つからない: {sid}')
    return {'title': title, 'tabs': tabs}


def clean_doc(html):
    soup = BeautifulSoup(html, 'html.parser')
    css = ' '.join(s.get_text() for s in soup.find_all('style'))
    rules = dict(re.findall(r'\.(c\d+)\{([^}]*)\}', css))

    def has(el, prop):
        return any(prop in rules.get(c, '') for c in (el.get('class') or []))

    def size(el):
        for c in el.get('class') or []:
            m = re.search(r'font-size:(\d+(?:\.\d+)?)pt', rules.get(c, ''))
            if m:
                return float(m.group(1))
        return 0

    body = soup.find(class_='doc-content')
    out = BeautifulSoup('', 'html.parser')

    def inline(src, dst):
        for node in src.children:
            if isinstance(node, NavigableString):
                dst.append(str(node))
                continue
            if node.name == 'br':
                dst.append(out.new_tag('br'))
                continue
            if node.name == 'a' and node.get('href'):
                href = node['href']
                if 'google.com/url' in href:
                    href = parse_qs(urlparse(href).query).get('q', [href])[0]
                a = out.new_tag('a', href=href, target='_blank', rel='noopener')
                inline(node, a)
                dst.append(a)
                continue
            if node.name == 'span':
                target = dst
                if has(node, 'font-weight:700'):
                    b = out.new_tag('strong'); dst.append(b); target = b
                if has(node, 'font-style:italic'):
                    i = out.new_tag('em'); target.append(i); target = i
                inline(node, target)
                continue
            if node.name in ('img', 'sup'):
                continue
            inline(node, dst)

    def empty(el):
        return not el.get_text(strip=True)

    def block(el, parent):
        name = el.name
        if name in ('h1', 'h2', 'h3', 'h4'):
            # ページの見出し（h1・シリーズの見出し h2・ラベル）の下に入るので、3段下げる
            h = out.new_tag(f'h{min(int(name[1]) + 2, 6)}')
            inline(el, h); parent.append(h) if not empty(el) else None
        elif name == 'p':
            if empty(el):
                return
            # 文書のタイトル（大きな文字の段落）は見出しにする。太字の印は見出しには付けない
            if max([size(el)] + [size(s) for s in el.find_all('span')]) >= 20:
                h = out.new_tag('h3'); h.string = el.get_text(strip=True); parent.append(h)
                return
            p = out.new_tag('p')
            inline(el, p); parent.append(p)
        elif name in ('ul', 'ol'):
            m = re.search(r'lst-kix_[\w]+-(\d+)', ' '.join(el.get('class') or []))
            lv = int(m.group(1)) if m else 0
            lst = out.new_tag(name)
            if lv:
                lst['class'] = f'lv{min(lv, 4)}'
            for li in el.find_all('li', recursive=False):
                n = out.new_tag('li'); inline(li, n); lst.append(n)
            parent.append(lst)
        elif name == 'table':
            t = out.new_tag('table')
            for i, tr in enumerate(el.find_all('tr')):
                r = out.new_tag('tr')
                for td in tr.find_all('td', recursive=False):
                    c = out.new_tag('th' if i == 0 else 'td')
                    for k in ('colspan', 'rowspan'):
                        if td.get(k) and td[k] != '1':
                            c[k] = td[k]
                    for j, p in enumerate(td.find_all(['p', 'li'])):
                        if j:
                            c.append(out.new_tag('br'))
                        inline(p, c)
                    r.append(c)
                t.append(r)
            wrap = out.new_tag('div', attrs={'class': 'table'})
            wrap.append(t); parent.append(wrap)
        elif name == 'hr':
            parent.append(out.new_tag('hr'))
        elif name == 'div':
            for c in el.children:
                if getattr(c, 'name', None):
                    block(c, parent)

    for el in body.children:
        if getattr(el, 'name', None):
            block(el, out)
    html = str(out)
    # Google の空白の詰め物を整える
    html = html.replace('\xa0', ' ')
    return html


def main():
    data = {'sheets': {}, 'docs': {}}
    for key, sid in SHEETS.items():
        data['sheets'][key] = fetch_sheet(sid)
        print('sheet', key, [(t['name'], len(t['rows'])) for t in data['sheets'][key]['tabs']])
    for key, did in DOCS.items():
        data['docs'][key] = {'html': clean_doc(get(DOC.format(id=did)))}
        print('doc', key, len(data['docs'][key]['html']), 'chars')
    with open(OUT, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=1)
        f.write('\n')
    print('->', OUT)


if __name__ == '__main__':
    sys.exit(main())
```

### fetch_series_docs.py を動かした出力（最初の版・デザインノートを含む）

```
$ python3 tools/fetch_series_docs.py
sheet hakobu-progress [('4コママンガプロット', 183)]
sheet hakobu-design-note [('Gemini-光のプロンプト集', 7), ('Figma-スタイル', 5), ('Figma-カラー/バリアブルベーシックリスト', 33), ('Figma-カラー/バリアブル運用', 4), ('Figma-カラー/機能', 6)]
sheet flow-library [('FigJam-ライブラリ部品', 11)]
doc hakobu-requirements 12795 chars
doc tsukamu-requirements 1381 chars
-> src/data/series_docs.json
-rw-r--r-- 1 root root 263132 Nov  7 09:43 src/data/series_docs.json
{'hakobu-progress': '薬剤搬送ロボットハコブくん 進捗管理表 2026/10/31', 'hakobu-design-note': 'ナレッジ 2025/12/25', 'flow-library': 'ロボティクスデザイン実務フロー可視化 2026/01/18'}
```

最初の版では、ハコブくんの要件定義書のタイトル（「薬剤搬送ロボット ハコブくん 要件定義書」）が、見出しではなく太字の段落になりました。字の大きさ（20pt）が段落ではなく中の span の CSS に付いていたためで、span の大きさも見るように直しています（上の全文の「文書のタイトル（大きな文字の段落）は見出しにする」の所）。

### 表を描く SeriesSheet.astro（全文）

サイトは [Astro](https://astro.build/) で作っていて、部品（.astro）がビルドのときに HTML になります。表の部品は、1行目を見出しの行にして、枠の中でスクロールしても見出しの行が上に残るようにしました（`position: sticky`）。タブが2つ以上ある表は、表の上にボタンを並べて切り替えます。中身が6字以下の列（番号・「完了」など）は折り返さず、1つ目のセルにだけ文字がある行は、表の中の小見出しとして灰色の行にします。線（#d2d2d2）と見出しの行（#e9e9e9）の色は、記事の中の表と同じにしました。

```astro
---
// シリーズのページに、Google スプレッドシートの中身を表として直接載せる（元は iframe の埋め込み）。
// 中身は src/lib/sheets.ts（Google の写し・サイトのデザインの値）。タブが2つ以上あれば、表の上に切り替えを出す
import { getSheet } from '../lib/sheets';
interface Props { id: string; height?: number }
const { id, height } = Astro.props;
const sheet = getSheet(id);
// 色の値（#d2d2d2 など）には、その色の小さな四角を添える
const swatch = (c: string) => /^#[0-9a-f]{3,8}$/i.test(c);
// 中身が短い列（番号・状態など）は折り返さない
const narrow = (rows: string[][], i: number) => rows.slice(1).every((r) => (r[i] || '').length <= 6);
// 1つ目のセルにだけ文字がある行は、表の中の小見出しとして扱う
const isSection = (r: string[]) => r.length > 1 && r[0] && r.slice(1).every((c) => !c);
---
{sheet && (
  <div class="sh" data-sheet>
    <p class="sh-title">{sheet.title}</p>
    {sheet.tabs.length > 1 && (
      <div class="sh-tabs" role="tablist" aria-label={sheet.title}>
        {sheet.tabs.map((t, i) => (
          <button type="button" role="tab" class="sh-tab" aria-selected={i === 0 ? 'true' : 'false'} data-i={i}>{t.name}</button>
        ))}
      </div>
    )}
    {sheet.tabs.map((t, i) => {
      const [head, ...body] = t.rows;
      return (
        <div class="sh-box" role={sheet.tabs.length > 1 ? 'tabpanel' : undefined} hidden={i > 0} style={height ? `max-height:${height}px` : undefined}>
          <table>
            <thead><tr>{head.map((c, k) => <th class={narrow(t.rows, k) ? 'nw' : undefined}>{c}</th>)}</tr></thead>
            <tbody>
              {body.map((r) => isSection(r)
                ? <tr class="sec-row"><td colspan={r.length}>{r[0]}</td></tr>
                : <tr>{r.map((c, k) => <td class={narrow(t.rows, k) ? 'nw' : undefined}>{swatch(c) && <i class="sw" style={`background:${c}`} />}{c}</td>)}</tr>)}
            </tbody>
          </table>
        </div>
      );
    })}
  </div>
)}
<script>
  for (const box of document.querySelectorAll<HTMLElement>('[data-sheet]')) {
    const tabs = [...box.querySelectorAll<HTMLButtonElement>('.sh-tab')];
    const panels = [...box.querySelectorAll<HTMLElement>('.sh-box')];
    tabs.forEach((t, i) => t.addEventListener('click', () => {
      tabs.forEach((b) => b.setAttribute('aria-selected', String(b === t)));
      panels.forEach((p, k) => { p.hidden = k !== i; });
    }));
  }
</script>
<style>
  .sh-title { margin: 0 0 12px; font: 400 14px/22px var(--sans); color: var(--ink-2); }
  .sh-tabs { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
  .sh-tab { height: 31px; padding: 4px 12px; border: 1px solid var(--dark); border-radius: 4px; background: #fff; color: var(--ink); font: 500 13px/21px var(--sans); cursor: pointer; }
  .sh-tab[aria-selected="true"] { background: var(--dark); color: #fff; }
  .sh-box { overflow: auto; -webkit-overflow-scrolling: touch; border: 1px solid #d2d2d2; }
  .sh-box[hidden] { display: none; }
  table { border-collapse: collapse; width: max-content; min-width: 100%; }
  th, td { border: 1px solid #d2d2d2; vertical-align: top; text-align: left; min-width: 10em; max-width: 26em; white-space: pre-wrap; overflow-wrap: anywhere; }
  th { position: sticky; top: 0; z-index: 1; background: #e9e9e9; padding: 10px 12px; font: 600 14px/22px var(--sans); }
  td { background: #fff; padding: 10px 12px; font: 400 14px/22px var(--sans); color: var(--ink); }
  .nw { min-width: 0; white-space: nowrap; }
  .sw { display: inline-block; width: 14px; height: 14px; margin: 0 8px -2px 0; border: 1px solid rgba(0,0,0,.15); border-radius: 2px; }
  .sec-row td { max-width: none; background: #f7f7f7; font-weight: 600; }
  /* 外枠の線と、端のセルの線が重ならないように */
  tr > :first-child { border-left: 0; }
  tr > :last-child { border-right: 0; }
  thead th { border-top: 0; }
  tbody tr:last-child td { border-bottom: 0; }
</style>
```

### 本文を描く SeriesDoc.astro（全文）

```astro
---
// シリーズのページに、Google ドキュメントの中身を直接載せる（元は iframe の埋め込み）。
// 中身は tools/fetch_series_docs.py が書き出した src/data/series_docs.json（見出し・段落・箇条書き・表・太字・リンクだけ）
import docs from '../data/series_docs.json';
interface Props { id: string; height?: number }
const { id, height } = Astro.props;
const doc = (docs.docs as Record<string, { html: string }>)[id];
---
{doc && <div class="dc" style={height ? `max-height:${height}px` : undefined} set:html={doc.html} />}
<style>
  .dc { overflow: auto; -webkit-overflow-scrolling: touch; border: 1px solid #d2d2d2; padding: 32px 40px; background: #fff; }
  .dc + .dc { margin-top: 24px; }
  .dc > :global(*) { max-width: 760px; }
  .dc :global(h3) { margin: 32px 0 12px; font: 600 22px/32px var(--sans); color: var(--ink); }
  .dc :global(h3:first-child) { margin-top: 0; }
  .dc :global(h4) { margin: 28px 0 8px; font: 600 18px/28px var(--sans); color: var(--ink); }
  .dc :global(h5), .dc :global(h6) { margin: 20px 0 6px; font: 600 16px/26px var(--sans); color: var(--ink); }
  .dc :global(p) { margin: 8px 0; font: 400 15px/27px var(--sans); color: var(--ink); }
  .dc :global(ul), .dc :global(ol) { margin: 8px 0; padding-left: 1.5em; font: 400 15px/27px var(--sans); color: var(--ink); }
  .dc :global(ul.lv1), .dc :global(ol.lv1) { padding-left: 3em; }
  .dc :global(ul.lv2), .dc :global(ol.lv2) { padding-left: 4.5em; }
  .dc :global(ul.lv3), .dc :global(ol.lv3), .dc :global(ul.lv4), .dc :global(ol.lv4) { padding-left: 6em; }
  .dc :global(li) { margin: 2px 0; }
  .dc :global(a) { text-decoration: underline; text-underline-offset: 3px; }
  .dc :global(hr) { border: 0; border-top: 1px solid #d2d2d2; margin: 16px 0; }
  .dc :global(.table) { margin: 12px 0; overflow-x: auto; max-width: none; }
  .dc :global(table) { border-collapse: collapse; min-width: 100%; }
  .dc :global(th), .dc :global(td) { border: 1px solid #d2d2d2; vertical-align: top; text-align: left; padding: 8px 12px; font: 400 14px/22px var(--sans); }
  .dc :global(th) { background: #e9e9e9; font-weight: 600; }
  @media (max-width: 599px) { .dc { padding: 20px 16px; } }
</style>
```

### シリーズのページと static.json の差分（commit 5de80f5）

[series].astro は、ブロックの種類に sheet と doc を足しただけです。static.json は、Google の5つの埋め込みを `{"type": "sheet" | "doc", "key": …}` に置き換えています。height は、前の埋め込みの高さをそのまま枠の高さにしました。

```diff
diff --git a/README.md b/README.md
index 15667e3..5cc7e96 100644
--- a/README.md
+++ b/README.md
@@ -157,3 +157,9 @@ Astro は記事の中身が変わらないと、前の変換結果（`node_modul
 - 同じ記事へのリンクは本文の最初の1か所だけ。書き手が手でリンクを付けていれば、それを1か所目とみなして足さない
 - 自分自身・存在しない番号・リンク／コード／見出し／図の説明の中・ファイル名（`LOG339_01_….png`・`LOG346.md`）・`Log#5` のようなシリーズの中の番号はリンクにしない
 - 英語の記事では、英語版（`LOG201EN`）があればそちらへつなぐ。中のページなので同じタブで移る
+
+## シリーズのページの表・ドキュメント（Google から写す）
+
+- /series1 のスプレッドシートと要件定義書は、iframe で埋め込まず、ページに直接載せている（`src/components/SeriesSheet.astro`・`SeriesDoc.astro`）
+- 中身は Google の「ウェブに公開」版の写し（`src/data/series_docs.json`）。Google 側を直したら `python3 tools/fetch_series_docs.py` を動かして push する。自動では追いかけない
+- 載せる場所は `src/data/static.json` の `{"type": "sheet" | "doc", "key": ...}`。`height` を付けると、その高さで枠の中をスクロールする
diff --git a/src/data/static.json b/src/data/static.json
index fa5b997..82854f4 100644
--- a/src/data/static.json
+++ b/src/data/static.json
@@ -22,8 +22,8 @@
     "link": null
    },
    {
-    "type": "iframe",
-    "html": "<iframe src=\"https://docs.google.com/spreadsheets/d/e/2PACX-1vQnqeo6YmdjAz9VQiH8-PYR4OzF9RdlgEOBe-SMtNjYcwdeHkkw2gNTOnLw9jSHBcWmIT3gaxnYnTH6/pubhtml?widget=true&amp;headers=false\" \n  width=\"100%\" \n  height=\"640px\" \n  frameborder=\"0\">\n  </iframe>",
+    "type": "sheet",
+    "key": "hakobu-progress",
     "height": 640
    },
    {
@@ -32,8 +32,8 @@
     "link": null
    },
    {
-    "type": "embed",
-    "html": "<div class=\"embed\"><iframe src=\"/embeds/series1-1.html\" sandbox=\"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-presentation\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div>",
+    "type": "sheet",
+    "key": "hakobu-design-note",
     "height": 480
    },
    {
@@ -42,14 +42,14 @@
     "link": null
    },
    {
-    "type": "embed",
-    "html": "<div class=\"embed\"><iframe src=\"/embeds/series1-2.html\" sandbox=\"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-presentation\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div>",
-    "height": null
+    "type": "doc",
+    "key": "hakobu-requirements",
+    "height": 800
    },
    {
-    "type": "embed",
-    "html": "<div class=\"embed\"><iframe src=\"/embeds/series1-3.html\" sandbox=\"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-presentation\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div>",
-    "height": null
+    "type": "doc",
+    "key": "tsukamu-requirements",
+    "height": 800
    },
    {
     "type": "text",
@@ -67,9 +67,8 @@
     "link": null
    },
    {
-    "type": "embed",
-    "html": "<div class=\"embed\"><iframe src=\"/embeds/series4-1.html\" sandbox=\"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-presentation\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div>",
-    "height": null
+    "type": "sheet",
+    "key": "flow-library"
    }
   ]
  },
diff --git a/src/pages/[series].astro b/src/pages/[series].astro
index 6541048..286844a 100644
--- a/src/pages/[series].astro
+++ b/src/pages/[series].astro
@@ -6,6 +6,8 @@ import LoadMore from '../components/LoadMore.astro';
 import GuideBanner from '../components/GuideBanner.astro';
 import ThinkingBanner from '../components/ThinkingBanner.astro';
 import ServingBanner from '../components/ServingBanner.astro';
+import SeriesSheet from '../components/SeriesSheet.astro';
+import SeriesDoc from '../components/SeriesDoc.astro';
 import { seriesCards, SERIES_PAGES } from '../lib/records';
 import { media } from '../lib/media';
 import staticPages from '../data/static.json';
@@ -46,6 +48,8 @@ const jsonLd = {
       b.type === 'text' ? (b.link ? <a class="s-label" href={b.link} target={b.link.startsWith('/') ? undefined : '_blank'} rel={b.link.startsWith('/') ? undefined : 'noopener'} set:html={b.html} /> : <p class="s-label" set:html={b.html} />)
       : b.type === 'iframe' ? <div class="s-frame" style={`height:${b.height}px`} set:html={b.html} />
       : b.type === 'embed' ? <div class="s-embed" set:html={b.html} />
+      : b.type === 'sheet' ? <SeriesSheet id={b.key} height={b.height} />
+      : b.type === 'doc' ? <SeriesDoc id={b.key} height={b.height} />
       : b.type === 'img' ? (b.link ? <a class="s-img" href={b.link} target="_blank" rel="noopener"><img src={media(b.src)} alt={b.alt || conf.title} loading="lazy" /></a> : <div class="s-img"><img src={media(b.src)} alt={b.alt || conf.title} loading="lazy" /></div>)
       : null
     ))}
```

ねらい・変わったこと・見込める効果（数字は次の節で測った値）:

- ねらい：資料の中身を、Google のページではなく、このページの文字として置く
- 変わったこと：資料の欄の文字が 275字から77,424字に、表の行（tr）が0から327に、Google への読み込みが14件・399KBから0件になった
- 見込める効果：AI がこのページを読んだときに、ハコブくんの要件や4コマの筋を答えに使える。実際に読まれたかは、サイトの計測（AI の依頼読みの数）で確かめる

![置き換えたあとの4コママンガの進行表。サイトの文字と線で描いた表で、見出しの行を上に固定したまま枠の中でスクロールする](https://pixelstomotors.com/media/LOG458_04_after_progress_table.png)

### 表とドキュメントの見た目を、PC とスマホの幅で確かめた

ビルドしたページを Playwright（ブラウザを動かす道具）で、幅1440px と390px で開いて、表とドキュメントを1つずつ画像にして見ました。表はスマホでも横にスクロールして読め、要件定義書は1列に流れて読めました。確かめで撮った画像は記事には載せていません（上と下の画像は、公開後のページを撮り直したものです）。


## 作業ログ ― 置き換える前と後を、同じ条件で測った

### 測り方：前の commit を別の場所でビルドし、同じ手順で開く

前の版（保管庫を外す前の commit 57f2226）を git worktree で別のフォルダに取り出してビルドし、前と後の2つのページを同じ手元のサーバーで出しました。ブラウザで開いて、30,000px 下まで送り（遅延読み込みも走らせるため）、6秒待ってから、iframe の数と、読み込んだ先ごとの件数と大きさを数えました。

```
$ git worktree add -f /tmp/claude-0/old 57f2226 -q; cd /tmp/claude-0/old && ln -s /home/claude/p2m-site/node_modules node_modules && npm run build 2>&1 | grep -E "Complete!|rror"; ls dist/series1.html
10:00:34 [build] Complete!
dist/series1.html
```

### 測るスクリプト m.cjs（全文）

```javascript
const { chromium } = require('playwright');
(async () => {
  const b = await chromium.launch();
  for (const [name, port] of [['before', 4401], ['after', 4402]]) {
    for (const block of [false]) {
      const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } });
      const p = await ctx.newPage();
      const reqs = []; let bytes = {};
      ctx.on('requestfinished', async (r) => { try { const s = await r.sizes(); const h = new URL(r.url()).hostname; reqs.push(h); bytes[h] = (bytes[h]||0) + s.responseBodySize + s.responseHeadersSize; } catch {} });
      const t0 = Date.now();
      await p.goto(`http://localhost:${port}/series1.html`, { waitUntil: 'load', timeout: 90000 });
      const tLoad = Date.now() - t0;
      // 全部の資料まで下へ送って、遅延読み込みも走らせる
      for (let y = 0; y < 30000; y += 800) { await p.mouse.wheel(0, 800); await p.waitForTimeout(120); }
      await p.waitForTimeout(6000);
      const frames = await p.$$eval('iframe', f => f.map(x => x.src).filter(s => !s.includes('/thumbs/')));
      const by = {}; reqs.forEach(h => by[h] = (by[h]||0)+1);
      const tot = Object.values(bytes).reduce((a,c)=>a+c,0);
      console.log(JSON.stringify({ name, loadMs: tLoad, iframes: frames.length, iframeHosts: frames.map(s=>new URL(s, 'http://x').hostname), requests: reqs.length, totalKB: Math.round(tot/1024), byHost: Object.fromEntries(Object.entries(by).map(([h,n])=>[h, n+' req / '+Math.round((bytes[h]||0)/1024)+' KB'])) }, null, 1));
      await ctx.close();
    }
  }
  await b.close();
})();
```

### 測った結果（全文）

```json
{
 "name": "before",
 "loadMs": 4017,
 "iframes": 8,
 "iframeHosts": [
  "embed.figma.com",
  "embed.figma.com",
  "docs.google.com",
  "localhost",
  "localhost",
  "localhost",
  "embed.figma.com",
  "localhost"
 ],
 "requests": 183,
 "totalKB": 5417,
 "byHost": {
  "localhost": "28 req / 229 KB",
  "fonts.googleapis.com": "14 req / 430 KB",
  "embed.figma.com": "3 req / 12 KB",
  "fonts.gstatic.com": "87 req / 3670 KB",
  "docs.google.com": "14 req / 399 KB",
  "www.figma.com": "28 req / 663 KB",
  "csp.withgoogle.com": "3 req / 0 KB",
  "errors.figma.com": "3 req / 3 KB",
  "themes.googleusercontent.com": "1 req / 1 KB",
  "ssl.gstatic.com": "2 req / 9 KB"
 }
}
{
 "name": "after",
 "loadMs": 3940,
 "iframes": 2,
 "iframeHosts": [
  "embed.figma.com",
  "embed.figma.com"
 ],
 "requests": 154,
 "totalKB": 4332,
 "byHost": {
  "localhost": "25 req / 535 KB",
  "fonts.googleapis.com": "11 req / 427 KB",
  "embed.figma.com": "2 req / 8 KB",
  "fonts.gstatic.com": "95 req / 2791 KB",
  "www.figma.com": "19 req / 569 KB",
  "errors.figma.com": "2 req / 2 KB"
 }
}
```

資料の欄（ページ上部の s-hero の中）の文字の数は、HTML からスクリプトと CSS を除き、空白を抜いて数えました。

```
$ python3 -I -c "… BeautifulSoup(s).select_one('section.s-hero') …"
before html KB 74 資料欄の文字 275 tables 0 rows 0
after html KB 382 資料欄の文字 77424 tables 20 rows 327
```

| 項目 | 前 | 後 |
|---|---|---|
| ページの中の iframe（サムネイルを除く） | 8 | 2（Figma の2つ） |
| docs.google.com への読み込み | 14件・399KB | 0件 |
| www.figma.com への読み込み | 28件・663KB | 19件・569KB |
| 資料の欄の文字（HTML から読める分） | 275字 | 77,424字 |
| 資料の欄の表 | 0 | 20（デザインノートのタブを含む） |
| ページの HTML の大きさ | 74KB | 382KB |

前の版の測り方は、3回目の変更（デザインノートの置き換え）の前ではなく、この日の作業を始める前の版との比べです。後の版は3回目の変更まで入った版です。

**HTML は74KBから382KBに増えました。** 中身をページに入れたぶんで、その代わりに Google の読み込み（399KB）がなくなっています。読み込み全体の大きさ（5,417KB と 4,332KB）には、Google のフォント（fonts.gstatic.com）の読み込みが大きく入っていて、回ごとに件数が揺れたので、比べには使っていません。表示までの時間（4,017ms と 3,940ms）も、手元のサーバーで1回ずつ測っただけなので、差とは言えません。


## 作業ログ ― デザインノートを、このサイトの実際の値に置き換える

### エイイチ「このセクションを今のサイトの状況に合わせて整理することはできますか？」

置き換えたデザインノートの画面写真と一緒に来た依頼です。中身は「ナレッジ 2025/12/25」という Google スプレッドシートで、Gemini で画像を作るときの光のプロンプト集と、Figma の色やスタイルの一般的な決め方の4タブでした。このサイトで実際に使っている色や文字の値は、どこにも書かれていませんでした。

![置き換える前のデザインノート。Figma のカラーバリアブルの一般的な決め方（Text / Primary は Neutral/900 など）で、このサイトの値ではなかった](https://pixelstomotors.com/media/LOG458_03_before_design_note.png)

「整理」の中身が3通りに読めたので、エイイチに選んでもらいました。答えは、3つとも、でした。

| 問い | 選んだ答え |
|---|---|
| 整理の中身 | 中身をサイトの実際の値に・関係ないタブを外す・ページ上部の並びを整理（3つとも） |
| 元の Google スプレッドシート | サイト側を正にする（Google 版はもう写さず、古いまま残る） |

### サイトのコードから、色・文字・角丸の値を拾った

サイト全体の色と書体は、src/layouts/Base.astro の `:root` に [CSS のカスタムプロパティ](https://developer.mozilla.org/ja/docs/Web/CSS/Using_CSS_custom_properties)（`--ink` のような名前の付いた値）として置いてあります。そのほかの色・字の大きさ・角丸は、コードの中で使われている数を数えて、よく使われているものと、使う場所を拾いました。

```
$ grep -rhoE "#[0-9a-fA-F]{6}\b" src/pages/*.astro src/components/*.astro src/layouts/*.astro | sort | uniq -c | sort -rn | head -15
     19 #121418
     18 #1a3fcc
     13 #242424
     10 #f2600c
      9 #6b6f78
      7 #f0f0f0
      7 #eceef1
      7 #c9d6ff
      6 #3a3a3a
      5 #f7f7f7
      5 #d2d2d2
      5 #cfd4de
      5 #c8ccd4
      5 #b9bec7
      5 #0b0c0e
$ grep -rhoE "border-radius: ?[0-9]+px" src | sort | uniq -c | sort -rn | head -5
    103 border-radius: 4px
     60 border-radius: 8px
     10 border-radius: 12px
      9 border-radius:4px
      9 border-radius: 16px
```

```
== 1a3fcc
src/components/OneYearBanner.astro
src/content/records/LOG381.md
src/data/one-year/body.html
src/pages/one-year.astro
== f2600c
src/components/GuideBanner.astro
src/components/GuideCards.astro
src/components/ServingBanner.astro
src/content/records/LOG369.md
src/content/records/LOG399.md
== 121418
src/components/ThinkingBanner.astro
src/components/GuideBanner.astro
src/components/GuideCards.astro
src/components/ServingBanner.astro
src/components/OneYearBanner.astro
== 6b6f78
src/data/one-year/body.html
src/pages/one-year.astro

     10 font: 400 13px/20px var(--sans)
      9 font: 400 12px/18px var(--sans)
      6 font: 600 14px/22px var(--sans)
      4 font: 600 24px/36px var(--sans)
      4 font: 400 14px/22px var(--sans)
      3 font: 700 12px/18px var(--sans)
      3 font: 600 18px/28px var(--sans)
      3 font: 600 16px/24px var(--sans)
      3 font: 600 14px/20px var(--sans)
      3 font: 400 11px/16px var(--sans)
      2 font: 600 16px/26px var(--sans)
      2 font: 500 20px/30px var(--sans)
      2 font: 500 16px/24px var(--sans)
      2 font: 500 14px/22px var(--sans)
```

青（#1a3fcc）と灰（#6b6f78）は、1年の記録の LP（/one-year）だけで使っている色なので、サイト全体の値には入れませんでした。オレンジ（#f2600c）は、トップの案内と記事の中のバナーの、大きな数字と押すところにだけ使っている差し色です。

### デザインノートの中身 design_note.json（全文）

タブは、色・文字・形・部品・並び・決まり、の6つにしました。決まりのタブには、デザインの進め方の決まり（色を増やさない・細い線より面・ボタンは1つ など）の要点を入れています。`{tokens:color}` と `{tokens:font}` の行は、書き写さずにビルドのときに Base.astro から読みます。

```json
{
 "title": "pixelstomotors.com で使っているデザインの値",
 "note": "サイトのコードが正。{tokens:color}・{tokens:font} の行は、ビルドのときに src/layouts/Base.astro の :root から読む（値をここに書き写さない）。それ以外の行は、コードを変えたらここも直す",
 "tokenNotes": {
  "ink": "本文・見出しの文字",
  "ink-2": "説明文・補足の文字",
  "ink-3": "目立たせない文字（日付・アイコン）",
  "line": "区切りの面・メニューを押したときの面",
  "bg": "ページの地",
  "panel": "中身を載せる白い枠",
  "dark": "ボタン・フッターの黒ベタ",
  "sans": "サイト全体の文字（英数字は Inter、日本語は Noto Sans JP）",
  "mono": "コード・ログ"
 },
 "tabs": [
  {
   "name": "色",
   "rows": [
    [
     "名前",
     "値",
     "使う場所"
    ],
    [
     "サイトの基本の色"
    ],
    [
     "{tokens:color}"
    ],
    [
     "表"
    ],
    [
     "表の線",
     "#d2d2d2",
     "表の線と外枠"
    ],
    [
     "表の見出しの行",
     "#e9e9e9",
     "表のいちばん上の行"
    ],
    [
     "札",
     "#f0f0f0",
     "シリーズ名の札の地（文字は #888）"
    ],
    [
     "バナー（トップの案内・記事の中のバナー）"
    ],
    [
     "バナーの地",
     "#121418",
     "濃いバナーの地と、押したときの枠"
    ],
    [
     "バナーの淡い地",
     "#eceef1",
     "淡いバナーの地"
    ],
    [
     "差し色",
     "#f2600c",
     "バナーの大きな数字と、押すところだけに使う"
    ]
   ]
  },
  {
   "name": "文字",
   "rows": [
    [
     "使う場所",
     "太さ・大きさ / 行の高さ",
     "メモ"
    ],
    [
     "書体"
    ],
    [
     "{tokens:font}"
    ],
    [
     "大きさ"
    ],
    [
     "セクションの見出し",
     "600 24px / 36px",
     "一覧ページの「記録 | Record」など（.sec-h）"
    ],
    [
     "記事の小見出し（h2）",
     "600 24px / 36px",
     ""
    ],
    [
     "記事の小見出し（h3）",
     "600 20px / 30px",
     "左に 2px の線"
    ],
    [
     "記事の本文",
     "400 18px / 36px",
     ""
    ],
    [
     "資料の見出し",
     "500 16px / 24px",
     "シリーズのページの資料の名前（.s-label）"
    ],
    [
     "表の中",
     "400 14px / 22px",
     "見出しの行は 600"
    ],
    [
     "ボタン・ヘッダーの案内",
     "500 14px / 21px",
     ""
    ],
    [
     "注記・フッター",
     "400 13px / 20px",
     ""
    ],
    [
     "札・日付",
     "400 11px / 16px",
     ""
    ]
   ]
  },
  {
   "name": "形",
   "rows": [
    [
     "種類",
     "値",
     "使う場所"
    ],
    [
     "角丸"
    ],
    [
     "小",
     "4px",
     "ボタン・札・ヘッダーの案内"
    ],
    [
     "中",
     "8px",
     "カード・メニュー"
    ],
    [
     "大",
     "16px",
     "中身を載せる白い枠（.panel）"
    ],
    [
     "線と影"
    ],
    [
     "表の線",
     "1px #d2d2d2",
     "表"
    ],
    [
     "ヘッダーの影",
     "0 4px 4px rgba(0,0,0,.1)",
     "上に固定したヘッダー（地は白10%＋ぼかし10px）"
    ],
    [
     "メニューの影",
     "0 8px 24px rgba(0,0,0,.12)",
     "シリーズのメニュー"
    ]
   ]
  },
  {
   "name": "部品",
   "rows": [
    [
     "部品",
     "形",
     "メモ"
    ],
    [
     "黒いボタン",
     "高さ 31px・横の余白 16px・黒ベタに白い文字・角丸 4px",
     "ヘッダーの「メッセージ」（.btn-dark）"
    ],
    [
     "もっと見るボタン",
     "282 × 48px・黒ベタ・文字 500 20px",
     "記録の一覧（Load more）"
    ],
    [
     "ヘッダーの案内",
     "高さ 31px・押すと薄い灰の面",
     "いまいるページは下に 2px の線"
    ],
    [
     "札",
     "地 #f0f0f0・文字 #888・11px・角丸 4px",
     "シリーズ名"
    ],
    [
     "表",
     "見出しの行 #e9e9e9・線 #d2d2d2・中は白",
     "はみ出す分は横にスクロール"
    ]
   ]
  },
  {
   "name": "並び",
   "rows": [
    [
     "項目",
     "値",
     "メモ"
    ],
    [
     "中身の幅",
     "最大 1280px",
     "ヘッダー・白い枠・フッターで共通"
    ],
    [
     "ヘッダーの高さ",
     "48px",
     "上に固定"
    ],
    [
     "白い枠の中の余白",
     "32px（599px 以下は 24px / 16px）",
     ""
    ],
    [
     "セクションの外の余白",
     "左右 80px → 1023px 以下 24px → 599px 以下 12px",
     ""
    ],
    [
     "カードの列",
     "3列 → 1199px 以下 2列 → 599px 以下 1列",
     "題名が3行に収まる幅（約270px）を保つ"
    ],
    [
     "スマホに切り替える幅",
     "767px 以下",
     "ヘッダーの案内を ≡ のメニューにまとめる"
    ]
   ]
  },
  {
   "name": "決まり",
   "rows": [
    [
     "決まり",
     "中身"
    ],
    [
     "色を増やさない",
     "差し色は1つ（#f2600c）。意味を決めた場所にだけ使う。青を既定にしない"
    ],
    [
     "細い線より面",
     "押すところ・目立たせるところは黒ベタの面にする"
    ],
    [
     "ボタンは1つ",
     "1つの画面で押させるものは1つにする"
    ],
    [
     "押せるものは押せる形に",
     "リンクだとわかる見た目にする"
    ],
    [
     "そろえる",
     "幅・揃え・角丸・ボタンは、ほかのページとそろえる"
    ],
    [
     "詰め込まない",
     "1つの画面・セクションで伝えることを1つに決め、要らない要素は入れない"
    ]
   ]
  }
 ]
}
```

### 値を組み立てる src/lib/sheets.ts（全文）

Base.astro を文字のまま読み込み（`?raw`）、`:root` の中の `--名前: 値;` を拾います。`#` で始まる値は色のタブに、それ以外（書体）は文字のタブに入ります。色の値のセルには、その色の小さな四角を添えるようにしました（SeriesSheet.astro の `swatch`）。

```typescript
// シリーズのページに載せる表の中身を返す。
// - Google スプレッドシートの写し（src/data/series_docs.json。tools/fetch_series_docs.py で作る）
// - サイトのデザインの値（src/data/design_note.json。サイト側が正）。色と書体の行は Base.astro の :root から読む
import docs from '../data/series_docs.json';
import note from '../data/design_note.json';
import base from '../layouts/Base.astro?raw';

export type Sheet = { title: string; tabs: { name: string; rows: string[][] }[] };

function tokens() {
  const root = base.match(/:root\s*\{([^}]*)\}/)?.[1] || '';
  return [...root.matchAll(/--([\w-]+):\s*([^;]+);/g)].map((m) => [m[1], m[2].trim()]);
}

function designNote(): Sheet {
  const t = tokens();
  const notes = note.tokenNotes as Record<string, string>;
  const expand = (kind: string) => t
    .filter(([, v]) => (kind === 'color') === v.startsWith('#'))
    .map(([k, v]) => [`--${k}`, v, notes[k] || '']);
  return {
    title: note.title,
    tabs: note.tabs.map((tab) => {
      const width = tab.rows[0].length;
      const rows = tab.rows.flatMap((r) => {
        const m = r[0].match(/^\{tokens:(\w+)\}$/);
        return m ? expand(m[1]) : [r];
      });
      return { name: tab.name, rows: rows.map((r) => [...r, ...Array(width).fill('')].slice(0, width)) };
    }),
  };
}

export function getSheet(key: string): Sheet | undefined {
  if (key === 'site-design') return designNote();
  return (docs.sheets as Record<string, Sheet>)[key];
}
```

ねらい・変わったこと・見込める効果:

- ねらい：デザインノートを、このサイトを直すときに実際に見る値の表にする
- 変わったこと：Figma の一般論と光のプロンプト集の5タブが、サイトの値の6タブになった。色7つと書体2つは、コードから毎回読む
- 見込める効果：色や書体を変えたときに、ノートだけが古い値のまま残ることがなくなる（見込み）。角丸・字の大きさ・余白などの手で書いた行は、コードを変えたら手で直す必要が残る

![置き換えたあとのデザインノート。色のタブで、サイトの基本の色7つが小さな色見本付きで並ぶ](https://pixelstomotors.com/media/LOG458_05_after_design_note.png)

### fetch_series_docs.py・SeriesSheet.astro・static.json・README の差分（commit a8f8cdd）

```diff
diff --git a/README.md b/README.md
index 5cc7e96..75be9e2 100644
--- a/README.md
+++ b/README.md
@@ -162,4 +162,5 @@ Astro は記事の中身が変わらないと、前の変換結果（`node_modul
 
 - /series1 のスプレッドシートと要件定義書は、iframe で埋め込まず、ページに直接載せている（`src/components/SeriesSheet.astro`・`SeriesDoc.astro`）
 - 中身は Google の「ウェブに公開」版の写し（`src/data/series_docs.json`）。Google 側を直したら `python3 tools/fetch_series_docs.py` を動かして push する。自動では追いかけない
+- デザインノート（`key: site-design`）は Google から写さず、サイト側が正（`src/data/design_note.json`）。色と書体の行はビルドのときに `src/layouts/Base.astro` の `:root` から読む。それ以外の行は、コードを変えたら json も直す
 - 載せる場所は `src/data/static.json` の `{"type": "sheet" | "doc", "key": ...}`。`height` を付けると、その高さで枠の中をスクロールする
diff --git a/src/components/SeriesSheet.astro b/src/components/SeriesSheet.astro
index 0b77483..5fc884c 100644
--- a/src/components/SeriesSheet.astro
+++ b/src/components/SeriesSheet.astro
@@ -1,10 +1,12 @@
 ---
 // シリーズのページに、Google スプレッドシートの中身を表として直接載せる（元は iframe の埋め込み）。
-// 中身は tools/fetch_series_docs.py が書き出した src/data/series_docs.json。タブが2つ以上あれば、表の上に切り替えを出す
-import docs from '../data/series_docs.json';
+// 中身は src/lib/sheets.ts（Google の写し・サイトのデザインの値）。タブが2つ以上あれば、表の上に切り替えを出す
+import { getSheet } from '../lib/sheets';
 interface Props { id: string; height?: number }
 const { id, height } = Astro.props;
-const sheet = (docs.sheets as Record<string, { title: string; tabs: { name: string; rows: string[][] }[] }>)[id];
+const sheet = getSheet(id);
+// 色の値（#d2d2d2 など）には、その色の小さな四角を添える
+const swatch = (c: string) => /^#[0-9a-f]{3,8}$/i.test(c);
 // 中身が短い列（番号・状態など）は折り返さない
 const narrow = (rows: string[][], i: number) => rows.slice(1).every((r) => (r[i] || '').length <= 6);
 // 1つ目のセルにだけ文字がある行は、表の中の小見出しとして扱う
@@ -29,7 +31,7 @@ const isSection = (r: string[]) => r.length > 1 && r[0] && r.slice(1).every((c)
             <tbody>
               {body.map((r) => isSection(r)
                 ? <tr class="sec-row"><td colspan={r.length}>{r[0]}</td></tr>
-                : <tr>{r.map((c, k) => <td class={narrow(t.rows, k) ? 'nw' : undefined}>{c}</td>)}</tr>)}
+                : <tr>{r.map((c, k) => <td class={narrow(t.rows, k) ? 'nw' : undefined}>{swatch(c) && <i class="sw" style={`background:${c}`} />}{c}</td>)}</tr>)}
             </tbody>
           </table>
         </div>
@@ -59,6 +61,7 @@ const isSection = (r: string[]) => r.length > 1 && r[0] && r.slice(1).every((c)
   th { position: sticky; top: 0; z-index: 1; background: #e9e9e9; padding: 10px 12px; font: 600 14px/22px var(--sans); }
   td { background: #fff; padding: 10px 12px; font: 400 14px/22px var(--sans); color: var(--ink); }
   .nw { min-width: 0; white-space: nowrap; }
+  .sw { display: inline-block; width: 14px; height: 14px; margin: 0 8px -2px 0; border: 1px solid rgba(0,0,0,.15); border-radius: 2px; }
   .sec-row td { max-width: none; background: #f7f7f7; font-weight: 600; }
   /* 外枠の線と、端のセルの線が重ならないように */
   tr > :first-child { border-left: 0; }
diff --git a/src/data/static.json b/src/data/static.json
index 82854f4..39cffc2 100644
--- a/src/data/static.json
+++ b/src/data/static.json
@@ -8,67 +8,67 @@
    },
    {
     "type": "text",
-    "html": "Figma/UIデザイン・イメージボード",
+    "html": "要件定義書",
     "link": null
    },
    {
-    "type": "iframe",
-    "html": "<iframe style=\"border: 1px solid rgba(0, 0, 0, 0.1);\" width=\"100%\" height=\"100%\" src=\"https://embed.figma.com/design/gDAJCTN8VCdVUwJHrQ5N7z/%E3%83%AD%E3%83%9C%E3%83%86%E3%82%A3%E3%82%AF%E3%82%B9%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3?node-id=0-1&embed-host=share\" allowfullscreen></iframe>",
+    "type": "doc",
+    "key": "hakobu-requirements",
+    "height": 800
+   },
+   {
+    "type": "doc",
+    "key": "tsukamu-requirements",
     "height": 800
    },
    {
     "type": "text",
-    "html": "スプレッドシート/進捗管理",
+    "html": "UIデザイン・イメージボード（Figma）",
     "link": null
    },
    {
-    "type": "sheet",
-    "key": "hakobu-progress",
-    "height": 640
+    "type": "iframe",
+    "html": "<iframe style=\"border: 1px solid rgba(0, 0, 0, 0.1);\" width=\"100%\" height=\"100%\" src=\"https://embed.figma.com/design/gDAJCTN8VCdVUwJHrQ5N7z/%E3%83%AD%E3%83%9C%E3%83%86%E3%82%A3%E3%82%AF%E3%82%B9%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3?node-id=0-1&embed-host=share\" allowfullscreen></iframe>",
+    "height": 800
    },
    {
     "type": "text",
-    "html": "スプレッドシート/デザインノート",
+    "html": "フロー図（FigJam）",
     "link": null
    },
    {
-    "type": "sheet",
-    "key": "hakobu-design-note",
-    "height": 480
+    "type": "iframe",
+    "html": "<iframe style=\"border: 1px solid rgba(0, 0, 0, 0.1);\" width=\"800\" height=\"450\" src=\"https://embed.figma.com/board/fFRdzGMRwQGF7Y36dCmB1m/%E5%9F%BA%E6%9C%AC%E3%83%95%E3%83%AD%E3%83%BC%E5%9B%B3?node-id=0-1&embed-host=share\" allowfullscreen></iframe>",
+    "height": 800
    },
    {
     "type": "text",
-    "html": "ドキュメント/要件定義書",
+    "html": "フロー図の部品",
     "link": null
    },
    {
-    "type": "doc",
-    "key": "hakobu-requirements",
-    "height": 800
-   },
-   {
-    "type": "doc",
-    "key": "tsukamu-requirements",
-    "height": 800
+    "type": "sheet",
+    "key": "flow-library"
    },
    {
     "type": "text",
-    "html": "FigJam/フロー図",
+    "html": "デザインノート",
     "link": null
    },
    {
-    "type": "iframe",
-    "html": "<iframe style=\"border: 1px solid rgba(0, 0, 0, 0.1);\" width=\"800\" height=\"450\" src=\"https://embed.figma.com/board/fFRdzGMRwQGF7Y36dCmB1m/%E5%9F%BA%E6%9C%AC%E3%83%95%E3%83%AD%E3%83%BC%E5%9B%B3?node-id=0-1&embed-host=share\" allowfullscreen></iframe>",
-    "height": 800
+    "type": "sheet",
+    "key": "site-design",
+    "height": 560
    },
    {
     "type": "text",
-    "html": "スプレッドシート/ログ &amp; リファレンス",
+    "html": "4コママンガの進行表",
     "link": null
    },
    {
     "type": "sheet",
-    "key": "flow-library"
+    "key": "hakobu-progress",
+    "height": 640
    }
   ]
  },
diff --git a/tools/fetch_series_docs.py b/tools/fetch_series_docs.py
index cb3df22..a5904f2 100644
--- a/tools/fetch_series_docs.py
+++ b/tools/fetch_series_docs.py
@@ -6,6 +6,7 @@
     python3 tools/fetch_series_docs.py
 - 取ってくるのは「ウェブに公開」した版（/pub・/pubhtml）。ログインはいらない
 - 表は CSV で取る。ドキュメントは公開版の HTML から、見出し・段落・箇条書き・表・太字・リンクだけを残す
+- デザインノートは Google から写さない（サイトの値が正。src/data/design_note.json と src/lib/sheets.ts）
 - 載せる場所は src/data/static.json の {"type": "sheet" | "doc", "key": ...} のブロック
 """
 import csv, io, json, re, sys, urllib.request
@@ -19,7 +20,6 @@ DOC = 'https://docs.google.com/document/d/e/{id}/pub?embedded=true'
 SHEETS = {
     # key: 公開版の id（2PACX-...）。タブは公開版に出ている順にすべて取る
     'hakobu-progress': '2PACX-1vQnqeo6YmdjAz9VQiH8-PYR4OzF9RdlgEOBe-SMtNjYcwdeHkkw2gNTOnLw9jSHBcWmIT3gaxnYnTH6',
-    'hakobu-design-note': '2PACX-1vThIVjUIwNFufI1_XPHkqj5Elkz1SL6gx9Dig_UTk-On1SUYWMWu9Yra2gLgk8tMo_5kFByxoUfZ7wo',
     'flow-library': '2PACX-1vRpLxXW9hSt-ediJC6AUIt8peUUbRATWB05SL7fdj14ygbsdQM1Hi9LEondS7phSu43Fv1A5MvGwDCT',
 }
 DOCS = {
```


## 作業ログ ― ハコブくんのページの資料の並びと見出しを、中身の名前にした

見出しが「スプレッドシート/進捗管理」のような道具の名前だったのは、Google と Figma を埋め込んでいたころの名残です。中身をサイトに載せた今は、事実と合いません。見出しを中身の名前にし、並びを、ハコブくんが何か（要件定義書）→ どう見えるか（UI）→ どう動くか（フロー図と部品）→ サイトの値 → 4コマの進行、の順にしました。

| 前の並び | 後の並び |
|---|---|
| Figma/UIデザイン・イメージボード | 要件定義書 |
| スプレッドシート/進捗管理 | UIデザイン・イメージボード（Figma） |
| スプレッドシート/デザインノート | フロー図（FigJam） |
| ドキュメント/要件定義書 | フロー図の部品 |
| FigJam/フロー図 | デザインノート |
| スプレッドシート/ログ & リファレンス | 4コママンガの進行表 |

公開したページから、見出しを順に読み出した結果です。

```
[
  '要件定義書',
  'UIデザイン・イメージボード（Figma）',
  'フロー図（FigJam）',
  'フロー図の部品',
  'デザインノート',
  '4コママンガの進行表'
]
```

![並べ直したあと、いちばん上に来た要件定義書。Google ドキュメントの見出し・段落・表を、サイトの文字で描いている](https://pixelstomotors.com/media/LOG458_06_after_requirements.png)

Figma の UI デザインのボードと FigJam のフロー図の2つは、ページに直接載せるなら画像に書き出すことになり、拡大や移動ができなくなります。エイイチに聞いて、埋め込みのままにしました。

> Figma と FigJam の2つは埋め込みのままでOK


## 作業ログ ― ビルドと公開の記録

### 3回の commit と時刻

```
$ git log --format='%h %ad %s' --date=format:'%H:%M:%S' 57f2226..a8f8cdd
a8f8cdd 09:51:19 series1: デザインノートをサイトの実際の値に置き換え、資料の並びと見出しを整理
a029a65 09:45:09 params: unity_rules に standby_queue（待機点を列で使う）と staff_moves_robot.repeat_stop を足す（どちらも既定は切）
5de80f5 09:44:58 series1: Google スプレッドシート・ドキュメントの埋め込みを、ページに直接載せる表と本文に置き換え
3fd5ee1 09:39:42 series1: ハコブくん4コママンガ保管庫（Figma埋め込み）のセクションを削除
```

a029a65 は、同じ時間に別の作業（サービスロボット導入シミュレーションの値）で入った commit で、この記事の作業ではありません。

### 2回目の変更のビルドと、Google の埋め込みが消えたことの確かめ

```
$ npm run build 2>&1 | grep -i "error\|warn\|Complete" | head; f=dist/series1.html; grep -c "<iframe" $f; grep -o "docs.google.com" $f | wc -l; grep -o "唐揚げは何グラム\|ロボットアームツカムくん - 要件定義書\|表面下散乱\|FigJam-ライブラリ部品" $f | sort | uniq -c
09:44:02 [build] ✓ Completed in 1.42s.
09:44:09 [build] ✓ Completed in 6.53s.
09:44:13   ├─ /record/handover-control-code-complete.md (+9ms) 
09:44:18   ├─ /record/handover-control-code-complete.html (+13ms) 
09:44:21 ✓ Completed in 11.30s.
09:44:21 [build] Complete!
1
0
      1 ロボットアームツカムくん - 要件定義書
      1 唐揚げは何グラム
      1 表面下散乱
```

`<iframe` の行が1、docs.google.com が0です。iframe の行の数え方は「iframe を含む行の数」で、Figma の2つは同じ行に入っているため1になっています。FigJam-ライブラリ部品はタブが1枚だけなので、タブの名前を出さない作りで、ページに出ないのが正しい動きです。

### 公開されたページの確かめ

```
$ curl -sS https://pixelstomotors.com/series1 | grep -o 'data-sheet\|docs.google.com/spreadsheets' | sort | uniq -c
      4 data-sheet
```

公開後にエイイチが送ってきた画面写真には、まだ前の Google の埋め込みが写っていました。ブラウザに前のページが残っていただけで、公開されているページは置き換わっていました。

### Google 側を直したときの手順（README に書いたもの）

ページに載っているのは、写した時点の中身です。Google のスプレッドシートやドキュメントを直したら、p2m-site で次の1行を動かして push します。

```
python3 tools/fetch_series_docs.py
```

写し直しは、Claude に「シリーズ1の表を更新して」と頼んでも行います。デザインノートは Google から写さないので、この手順の対象外です。
