learning
/section 学習ログ
/section(TERASU Labセクション追加)で得た知見・大串FB・うまくいったパターンを溜める。
同じFBが2回出たら section.md 本体に昇格。
構築メモ(2026-06-29・初版)
- 目的: 参考HPの好きな箇所をTERASUオリジナル化して、HP制作の有無に関係なく都度Labセクションに足せる体制。
- 体制の土台(lab-terasu内):
tools/add_section.py… メタentries/<id>.jsonを規約通り生成(img/media/snippet自動補完・新種別は警告のみで許可)tools/shoot.sh… Chrome headlessでimg/sections/<id>.pngを自動生成(手撮り撲滅)。--virtual-time-budget=3000で数秒。playwright不要。build.py(既存)… entries/*.json → sections-data.json- スクショ確定コマンド:
Chrome --headless=new --hide-scrollbars --virtual-time-budget=3000 --window-size=1440,H --screenshot=OUT file://... - プロファイル分離(
--user-data-dir新規)は初回起動でハングするので使わない。 - macに
timeoutコマンドは無い(Bashツール側のtimeoutで囲む)。ImageMagick無し(余白トリム不可・固定高で運用)。 - データ単位: 1セクション=
entries/<id>.json+code-previews/<id>.html+img/sections/<id>.png(+任意.mp4)。1件1ファイルで同時編集競合ゼロ。 - snippetパスは拡張子なし(
code-previews/<id>)が正。Cloudflare Pagesが.htmlを自動補完して200。.html付きは308。
見つかった既存の問題(2026-06-29・要対応メモ)
code-previews/cafe-fv.htmlの中身がLabトップ(index)のコピーになっている=データ破損(head は petomo導線カードのコメント・bodyはHP Production Reference/TERASU LAB/section.sites)。entriescafe-fvのプレビューが別物になる。後日要修正。
重大な失敗と教訓(2026-06-29・コマンド設計を決定づけた)
失敗 = go-spiral.ai のFVを「動画から寄せて自作」して2回却下された
- やったこと(NG): 大串が動画+URLを送ったのに、私は動画を見てそれっぽいコードを自作(位置依存モーフィングのマーキーをCSS/JSで実装)。1回目「全然違う」→ 2回目も手描き枠等で寄せたが「全く同じか?」「寄せるじゃ全く意味がない」と却下。最終的にこの自作セクション(flowmorph-fv)は削除。
- 大串FB(恒久・最重要):
- 「完全再現じゃなくて寄せるじゃ全く意味がない」
- 「元のHP送ってるんだから、そこからコードを抜き取ってオリジナル化しないと」
- 「一回コードをそのまま完全再現、問題ないってなってからTERASUオリジナルコード化の流れが良い」(= hp-flowと同じ2段階+確認)
- 教訓 → コマンドをv2に全面改訂:
1. 「寄せる」自作は禁止。必ず該当箇所の実コード(レンダリング後DOM+CSS+アセット)を抽出して土台にする。
2. 完全再現を先・確認①を取ってからオリジナル化(hp-flow Step1→Step4と同じ)。
3. 抽出できない実装は最初に弾く:今回の go-spiral.ai は Studio Design(ノーコード)製で、流れるFVは<canvas>にJS描画=HTML/CSSとして抜き出せない=完全再現不可。こういう箇所はStep0で判定し「canvas描画で完全再現できない・別箇所にするか相談」と正直に止める。勝手に近似で進めない。
抽出の技術メモ
- レンダリング後DOM:
Chrome --headless=new --virtual-time-budget=6000 --dump-dom URL > dom.html - 実装方式の見分け: DOMに該当の構造があり対応CSSがある→抽出可。
<canvas>/WebGLしか無い→抽出不可(go-spiral.aiが該当)。 - Nuxt/Next製はCSSが
_nuxt/*.css_next/*.cssに集約。curlで取得して該当セレクタを拾う。 - 動画は補助(動きの確認用)。
ffmpeg -i mov -vf fps=2,scale=1000:-1 frames/g_%02d.png。ただし動画だけで自作するのは禁止(上記失敗の通り)。
大串FB / 学び
2026-07-15 chillnn 横スクロールカードカルーセル追加で得た知見(重要4点)
1. section.md の手順書が古い(tools/add_section.py・shoot.sh は現存しない)
- 現行運用の正本=~/dev/lab-terasu/README_運用.md(2026-06-15 git移行)。
- 現行フロー: entries/<id>.json を新規作成 + img/sections/<id>.png(motionは.mp4も)+ code-previews/<id>.html を配置 → git add/commit/push → GitHub Actions が自動で build.py 実行→Cloudflare Pages デプロイ(約30-60秒)。ローカルで build.py を回す必要なし。tools/add_section.py tools/shoot.sh python3 build.py(手動)は使わない。
- entry schema: { id, cats[], img, url, caption, snippet, author, created_at }(contributorはビルド未参照=入れても無害だが評価軸は現状authorで見る)。snippetは"code-previews/<id>"(拡張子なし)。
- push衝突時: 未ステージ変更(.labserve.log/index.html等・自分の変更でない)があると pull --rebase が弾かれる → git stash push -u で退避してrebase→push→pop。remoteに.claude/launch.jsonがあってuntracked衝突する場合は一旦mv退避。
2. 実サイトが「SSR版」と「ハイドレーション版」で別実装のことがある(最重要)
- chillnnは Astro + Svelte islands。chrome --headless --dump-dom で取れたDOMは古いSSR版(svelte-9ml990スコープ・CSSがJS注入で静的ファイルに無い)だった。
- だがユーザーが実際に見ているライブ版は Tailwind v4 ベース(text-zinc-900 w-(--width-card-featured) text-2xs等)で完全に別物。スクショはライブ版と一致。
- 教訓: headless dump-dom を鵜呑みにしない。in-appブラウザ(Claude Browser)でライブDOM+getComputedStyle+document.styleSheetsを正とする。スクショと照合してどちらの実装か必ず確認。
- Tailwind製サイトの忠実再現: ①実HTML(utilityクラス付き)をverbatim取得 ②getComputedStyleで全要素の実算出値を採取(カード幅/gap/font/color/radius/shadow等)③カスタムクラス(.card-img-hover=scale(1.03)/cubic-bezier(.23,1,.32,1) 等)は外部CSS(/_astro/*.css)から実ルールを抜く ④それらをt-<prefix>-*スコープCSSに実値で書く(=コンパイル済みTailwindと同値=寄せではない)。
3. font-family:inherit は Lab本番で明朝化する(貼り先ラッパーが明朝base)→ゴシック原本は明示指定
- Labの既存例は font-family:inherit(貼り先に馴染ませる思想)だが、code-preview単体表示だとLabラッパーの明朝を継承して原本と別物になる。
- chillnnはNoto Sans JP(ゴシック)→ font-family:"Noto Sans JP","Hiragino Kaku Gothic ProN",...,sans-serif を明示。原本が明確にゴシック/明朝の時はfont-familyを固定する(inheritにしない)。ローカル確認では自分のラッパーが正しいフォントを当てるので気づけない→必ず本番URLでも目視する。
4. JS駆動カルーセルは実サイトと同一ライブラリをCDN自己完結で使う
- chillnnのカルーセル=Embla Carousel(SectionCarousel.E8hT4uz3.js内。options: align:'start', containScroll:'trimSnaps', slidesToScroll:1 + WheelGesturesプラグイン、slides=[data-carousel-slide])。矢印はcanScrollPrev/Nextがtrueの時だけ描画(先頭では「前へ」非表示)、lucide chevron、size-9(36px)丸ボタン。
- 再現=Embla UMDをjsdelivr CDNで読み、同一optionsで初期化。矢印はemblaApi.scrollPrev/Next+canScrollPrev/Nextで--offトグル。GSAP/Splideと同じ「JS-heavyは自己完結CDN」パターン(creo-section-jsheavy-techniqueと同型)。
5. /tmp は途中で消える環境がある → 作業ファイルはスクラッチパッド(session固有dir)に置く。minified DOMは改行ゼロの巨大1行なので grep -o/cat が壊れる→Pythonでパースしてファイル出力→TaskOutput --filter か Read で読む。
2026-07-19 大串FB: ダミー化しすぎで「内容が違う」(skewbanner/isomurass案件)
- 事象: isomurass.jp の斜めバナーを再現。デザイン/レイアウト/アニメは実物と完全一致していたが、タグ・英字装飾・見出しをダミー語(サービス情報/SERVICE 等)に変えたら「再現できてない・内容が違う」とFB。
- 学び: 斜めバナー等「英字大型装飾+タグラベル」が意匠の核のセクションは、タグ(技術・製品情報/保有設備)や英字装飾(TECHNICAL/FACILITY)、ボタンラベル、見出しの行構成を元のまま/元に沿って残す。逐語コピーの本文だけダミー化。ラベルまで別語に変えると「別物」に見える。
- 切り分け手法: 実サイトがWAF(NinjaFirewall)やBrowserペイン白飛びで撮れない時は、元テキストそのままの忠実版をローカルに置きChrome headlessで撮影→動画フレーム(実物)と照合。デザイン一致を確認できれば「違い=文言」と特定でき、ラリー最小化。
- 付随Tips: isomurassは --dump-dom が403、curl -A "Mozilla/5.0" は通る。GSAP scroll_view は is-show 付与で発火。CSSインライン時は相対 url(../images/) を絶対URLへ sed 補正必須。scroll_view初期opacity0→ html.reveal-all で最終状態即表示にしサムネ薄飛び防止。
2026-07-19 斜めバナー(skewbanner)の高さ制御・コンパクト化の型
- 問題: 斜めバナーで「画像が縦長アスペクト側だと、画像が grid-row:1/3 で高さを支配し、テキスト下に青い余白が大量に残って高く見える」。max-height/固定remでは、ルートフォントが可変(vwで変化)のため幅により文字が見切れる/余白が出るで破綻。
- 正解の型: 画像を
position:absolute; top/bottom:0; width:56%にして高さ計算から外す→カード高さはテキスト(inner)依存になり、余白ゼロ・文字切れゼロで全幅安定。画像はobject-fit:coverで斜めPNGごと収まる(斜めは画像PNGの透明カットが正体)。 - 2枚を同じ高さに揃える: テキスト行数を合わせる(本文長を近づける+狭い側の列 padding-left を広げて行数を減らす)。高さ調整は
.tsbanner--innerの padding-top/bottom で均等に増減(例:2→3.6→5rem)。 - 付随: Labプレビューの登場アニメは scroll_view+GSAP、キャプチャ薄飛び防止に 2.5s で最終状態を setProperty(...'important') で確定。装飾(縦英字 deco--item / 大英字 deco_en / タグ tsbanner--tag)は要素ごと削除で対応。
2026-07-22 モードB(画像から100%再現)追加 — 実測で判明した知見
大串の要望「写真を送って、それと全く同じものをオリジナルコードで作りたい」に対応。
実装前に採点エンジンを実データで検証し、設計の前提が3つ覆ったので記録する。
実測1|フォント違いが再現度の最大の支配要因
同一レイアウトで書体だけ変えて比較したところ、輪郭スコア(edge_match)が
1.0 → 0.16 まで落ちた。他の指標(SSIM 0.97 / 一致率 0.99)はほぼ落ちないため、
SSIMだけ見ていると「フォントが違う」ことに気づけない。
→ 輪郭スコアは書体一致のセンサーとして使えると判明。これを利用して
reproduce_loop.sh --font-sweep で候補10種を総当たりする仕組みにした。
検証: 正解Antonを 輪郭1.0で一発特定(他候補は全て0.04〜0.09。判別力に10倍以上の差)。
フォントは目で見て決めない。機械に当てさせる。
実測2|SSIM単独では4pxのレイアウトズレを見逃す
画像を4px下にずらして採点したら SSIM 0.9637 でPASSしてしまった。
余白(単色領域)が多い画像ほどSSIMが薄まるため。
→ 対策として位置ズレを直接測る指標を追加(行/列の輝度プロファイルの相互相関で
グローバルオフセットを算出)。再検証で 4pxズレ・2pxズレとも確実にFAILするようになった。
採点は必ず複合基準(SSIM+輪郭+位置ズレ+色差+高さ差)で行う。
実測3|色は測れば当たる。目測は外れる
measure_ref.py を既知のセクション画像にかけたところ、
背景 #242424・マーク #1f1f1f を実際のCSS値どおりに的中。
ロゴの帯高さ151px(実際のfont-size 155.6px)も精度良く逆算できた。
「たぶん#333」で書かない。測ってから書く。
設計上の判断(大串決定 2026-07-22)
- 写真素材 → 参考画像から切り出して使う(見た目が完全一致し、採点スコアも最大化)
- 合格ライン → SSIM 0.95(実用重視)。厳格0.97はフォント未所持時に到達不能になり頭打ちが増える
- 入口 →
/sectionのまま自動分岐(URLの有無で判定・コマンドを増やさない)
落とし穴
.reproduce/が過去コミット(b10996c)で追跡対象に混入していた →.gitignoreに追加して除外済み- 参考画像と再現画像の高さを揃えないと採点が無意味になる →
reproduce_loop.shが参考画像の縦横比から撮影高さを自動算出するようにした
2026-07-22 モードB(画像から再現)は廃止
大串FB: 「精度微妙すぎてダメ。コマンドから消して。URL送ってもらう前のやり方に戻す」
(同時に「今のURL送ってもらうやり方はめっちゃ完成度高いから残して」)
何をやって、なぜダメだったか
URLなし・スクショだけで再現する方式を実装し、採点ツール3本(measure_ref.py /
visual_diff.py / reproduce_loop.sh)で自動採点ループを回した。
HBA採用サイトFVで実地テストした結果:
| 指標 | 到達値 | 合格基準 |
|---|---|---|
| SSIM | 0.888 | 0.95 |
| 輪郭 | 0.42 | 0.75 |
| 一致率 | 0.52 | 0.90 |
位置・色・写真は実測でほぼ完全一致させられた(写真は髪の位置が1px以内、背景色は実値的中)。
それでも大串の目には「パッと見で全然違う」だった。
根本原因=フォント
- 参考サイトは商用日本語フォント(モリサワ系・My Galano Grotesque 等)を使っており、
Google Fonts に同等品が無い。候補7種を総当たりしても最良0.586止まり。 - 書体が1種違うだけで輪郭スコアは 1.0 → 0.16 まで落ちる(実測)。
→ フォントが手に入らない以上、画像だけからの完全一致は原理的に不可能。 - 見出しを画像化すれば見た目は一致するが、それはテキストではなくなり
セクションとしての使い回しが落ちる(本末転倒)。
残った教訓(モードAでも有効)
- 数値と「パッと見」は乖離する。SSIM 0.79でも人間には「全然違う」。
面積の大きい要素(背景・写真)が合っていると数値は上がるが、
人が見るのは書体の印象と文字サイズ。数値だけを信じない。 calc(100% / 144)は font-size に効かない。セクション幅基準にしたいなら
cqw(container-type: inline-size+calc(100cqw / 144)) を使う。
vwはホストHPに貼ると画面幅を拾うのでNG。- 切り出した画像は「切り出した範囲の実寸」で配置する。
範囲862pxの画像を947px幅に置いて横に引き伸ばしていた(SSIM 0.63→0.78 の最大要因)。 - 数値が悪化したら即戻す。実測のつもりがテキストのノイズを拾っていて、
「実測で直したら逆に悪化」が起きた。採点はこういう自己修正に効く。
結論
URLをもらってから着手する。URLさえあれば実コード抽出で完成度は非常に高い(大串評価)。
URLが無いときはURLを求めて待つ。ゼロから作らない。
ツール3本(
tools/measure_ref.py/visual_diff.py/reproduce_loop.sh/
grab_chat_image.py)は lab-terasu に残置。/section からは呼ばない。
2026-07-22 tsuklio.com「Brand Concept」= Astro+Tailwind v4・画像クロスフェード(成功)
成果物: concept-gallery-ogushi-0722(フード宅配コンセプト|左2列マスク画像+右テキスト2カラム)。
抽出(Astro + Tailwind v4 サイト)
- Astroは静的HTML出力なので
curl -L <URL>で完成HTMLがそのまま取れる(JS描画不要)。CSSは1本/_astro/index.<hash>.css、アニメJSは/_astro/<Component>.astro_astro_type_script_*.js。全部curlで取得可。 - Tailwind v4は「コンパイル済みCSS全量をインライン」で完全再現できる。カスタムトークン(
--spacing-more-*--text-more-*--radius-more-*・.flex-2/3・.aspect-7/12・色--color-*-green等)も同一ファイルに入っているので、全量入れれば全ユーティリティが解決する。個別抽出は不要・むしろ危険。 - lg: は
@media(min-width:64rem)。
「左画像の選択できるアニメーション」の正体(再利用可能パターン)
- 1スロットに写真2枚を
absolute inset-0で重ね、GSAPclipPath:"inset(0 0 100% 0)"のワイプで交互に入れ替える無限ループ(ScrollTriggerで画面内のみ再生)。opacityではなくclip-path。 - 実値(そのまま移植): duration1・stagger2.2・delay1.5・ease"power2.out"・y±15・順序を配列で並べ替え(0,4,2,3,1,5)して斜めに波及。
- テキストは別アニメ:
gsap.from(opacity:0,y:15,duration:.8)のフェードアップ(Layout側)、見出し小ラベルはSplitTextの文字リビール。GSAP+ScrollTrigger(+SplitText)が要る。 - ライブラリはcdnjs
gsap/3.13.0/{gsap,ScrollTrigger,SplitText}.min.js(3.13でSplitTextも無料化・cdnjsに有り)。既存preview規約もcdnjs GSAP。
フォント
- 見出しは FontPlus の TazuganeGothic(
webfont.fontplus.jpのkeyがドメインロックで外部では読めない)。CSS変数のフォールバック連鎖に Noto Sans JP が入っているので、Google Fontsで Figtree + Noto Sans JP を読むだけで代替表示できる(見た目ほぼ同等)。
ハマった落とし穴(恒久対策)
- 画像URL絶対化は「CMS module scriptタグ除去」の後にやる。先に
src="/_astro/→src="https://.../_astro/すると、除去用正規表現(/_astro/始まり前提)に一致せず<script type="module" src="/_astro/...js">が生き残る。順序を逆にするか、除去regexを絶対URLにも対応させる。 - サムネはGSAPのopacity登場アニメで非決定になる。
--virtual-time-budgetだけだと撮影タイミングでテキストが半透明のまま写る(feedback_preview_must_openの通り)。対策: サムネ撮影は--disable-javascriptで撮る(レイアウトは純CSSなのでJS無しでも崩れない・テキストは常時可視・画像は上レイヤ静止=クリーン)。アニメ本体はライブpreviewで動くので問題なし。 - Chrome headlessがBashで終了せず居座る(screenshot/dump-domとも)。PNG/DOM生成後もプロセスが残り120sタイムアウトになる。→ バックグラウンド実行にして、成果物ファイルが出たら
pkill -f <user-data-dir>で落とす運用が安定。--user-data-dirを毎回別名にすると競合しない。
2026-07-22 recruit.sunbelx.com = STUDIO製カルーセル(成功・STUDIOは抽出できる)
成果物: pjtstory-carousel-ogushi-0722(採用サイト|プロジェクトストーリー自動スライダー)。
まずSTUDIO=抽出不可、ではない
- 2026-06-29 の go-spiral.ai は「STUDIO製+FVが
<canvas>」だったから不可だっただけ。STUDIOでも通常のコンポーネントはHTML/CSS/JSで出力され抽出できる。Step0では「STUDIOか」ではなく<canvas>/WebGLがあるか で判定する。 - STUDIO公開HTMLの構造: CSSもJSも全部インライン(外部cssファイル無し)。カルーセルは
<sd-carousel>カスタム要素+<template>スライド、56KBのインラインランタイム(script#2)がcustomElements.define("sd-carousel")して駆動。ランタイムをそのまま同梱すれば動く。
STUDIO固有の3大ハマりどころ(全部これで詰まった)
<style media="(max-width: 840px)">を落とすな(「画像デカすぎ」の原因)
STUDIOはレスポンシブCSSを<style media="...">のタグ属性で出し分ける(840/1278/540px)。<style>の中身だけ連結すると制約が消え、モバイル用ルールがPCでも効いて全幅になる。→<style>タグを属性ごとそのまま出力すること。img[data-sd-img-src]{display:none}(画像が出ない原因)
STUDIOはsdImg()が「srcを入れる+data-sd-img-src属性を削除」して初めて表示する。ビルド時にsrcを直書きするなら属性も必ず削除する。srcだけ入れてもdisplay:noneのまま。- 祖先チェーンを省略するな(サイズがズレる原因)
カルーセルの幅・余白は親が持つ(.sd-101{padding:60px 32px}等)。実サイトの祖先を全部再現する:div.sd-1 > main.section.sd-14 > div.section-inner.sd-15 > section.section.sd-99 > div.section-inner.sd-100 > div.sd-101 > div.sd-113 > sd-carousel。
「完全一致」を目視でなく数値で証明する方法(今後これを標準にする)
保存した実サイトHTMLと自分の再現HTMLの両方に同じ診断スクリプトを差し込み、headless --dump-dom で getBoundingClientRect() を比較する。
// <html data-diag="..."> に書き出して dump-dom で回収
var r=el.getBoundingClientRect(); // カルーセル/スライド/imgの w,h,x を比較
今回これで「car 1376x631 x32 / slide 1144x631」が実サイトと完全一致まで詰められた。目視では絶対に気付けないズレが取れる。
サムネ撮影は「JS要否」をセクションごとに判断する
- 今回(STUDIO): スライドが
<template>内でJSがcloneして描画 → JS必須。かつ--virtual-time-budget=12000くらい取らないと初期化前(高さ90pxに潰れた状態)を撮る。shoot.sh既定の3000msでは失敗する。 - tsuklio(GSAP)の時: 逆に
--disable-javascriptが正解だった(レイアウトは純CSS・JSはopacity登場アニメだけ)。
→ 「JSが無いと描画されない構造か?」で決める。テンプレclone/カスタム要素系はJS有効+長バジェット、CSSレイアウト+登場アニメ系はJS無効。
STUDIOのオリジナル化(どこまでやるか)
- 除去する: 記事CMSパス(
href="/pjt-story/..."→#)、sendAnalytics("<projectId>")とその公開、data-scope-NN(CSS未参照なので安全)。 - 汎用化する:
sd-carousel→story-carousel(HTML/CSS/JS横断で一括置換。ランタイム内は全て完全一致の文字列リテラルで連結が無いので安全。カスタム要素名はハイフン必須)。 - 触らない: 自動採番の
sd-Nクラス群。ブランド情報を含まない匿名トークンで、370KB全置換はランタイム破壊リスクだけ大きく originalization の意味が無い。 - 画像URLはルール通り維持(
storage.googleapis.com/studio-design-asset-files/...)。
プレビュー提示(大串が「みれない」と言った件)
python3 -m http.server の裏起動はターン間で落ちる。大串に出すときは
サーバー不要の file:// パス(自己完結HTMLなのでそのまま開ける)を第一候補で渡すか、Bashツールのrun_in_backgroundで起動する。localhostだけ渡すのは事故る。
環境メモ(2026-07-22時点)
- Bashはネット疎通あり(curlでcdnjs/対象サイト取得OK)。ただし ugokko.jp はサーバー側が接続拒否で全経路(curl/headless/WebFetch)不通だった(サイト側要因・時間をおくか実コードを直接もらう)。
- in-appブラウザ(mcp__Claude_Browser__)は外部navが拒否されがち。claude-in-chrome拡張は断続的にDisconnectする。確実なのは Bashのcurl + Chrome headless(ローカルfile:// レンダリング)。
2026-07-22 berg-onishi.co.jp「メニューバー」= 幅で別機構・2往復した失敗(最重要)
成果物: dropdownnav-navbar-ogushi-0722(グローバルナビ|メニュー選択でナビが下にheight展開+ハンバーガー×モーフ)。追加者=細川。
最大の失敗:同じサイトでも「幅」で全く別のメニューが動く
berg-onishi は1つのサイトに2機構あり、動画だけ見て取り違えた(デスクトップ版→誤ってモバイル版に作り替え→また戻す、で2往復)。
- デスクトップ(≥960px) = .lHeaderNav のインライン展開。height:0 → calc(201*px) を .4s var(--ease-out-expo)。細い右カラム(183px)がメニューボタンとお問い合わせの間にスッと伸びる。
- モバイル(<960px) = .cMenu のフルスクリーン。clip-path:inset(0 0 100% 0) → inset(0) を .6s expo+opacity .4s。円形矢印アイコン・子リンク・SNS・住所付き。
- JSも分岐:vendor-CommonMenu.min.js は matchMedia("(min-width:960px)") が真のときだけハンドラを張る=ロード時の幅で決まる。リサイズしただけでは切り替わらない(幅を変えたら必ずリロードしてから検証)。
- 恒久対策(Step0に追加):動画/スクショを受け取ったら、まずブラウザ幅を変えて両方の状態を開き、どちらが素材と一致するか照合してから抽出する*。「メニューバー」等あいまいな語は幅で意味が変わる。
GSAPにCSS変数を渡すと移植先で崩れる(×が「➤」になる)
- 元コード:
gsap.set(o,{y:"var(--line-top-y)",rotation:25,transformOrigin:"left center"})。実サイトではGSAPがinlineにtranslate(0px,var(--line-top-y)) rotate(25deg)と書き、ブラウザ側がvar()を解決して効いている。 - 移植先ではこれが解決されず移動量ゼロ=線が交差せず「➤」状に崩れる。
- →
--pxをJSで実測して数値で渡す:parseFloat(getComputedStyle(inner).width)/16。GSAPは関数値を受けるのでy:function(){return -2*u();}にするとリサイズにも追従。
参考元自体が「粗い」ことがある → 完全再現の後に整える
- 実サイトの×は交点が左から約58%にあり非対称(top/bottomのmatrix・transform-origin・描画boxまで完全一致させても、大串FB「左側が埋まってる」は解消しない)。
- → 両線とも
transform-origin:center centerにしてy=±(H/2-0.5)で箱の中心に重ね、±25°回転=交点50%の対称な×に整えた。角度・線長・線幅は元のままなのでトーンは崩れない。 - 学び:まず座標レベルで完全再現→実測で「一致している」ことを示した上で、明らかな粗は整える。「合わせたのに変」なときは参考元を疑って実測する。
Labの code-previews は「登録」しないとURLで見られない
- HTMLをpushしただけでは
https://lab.terasu.scale-group.co.jp/code-previews/<id>はSPAトップにフォールバックする(.html付きでも同じ)。 - → 確認URLを求められたら
add_section.py→shoot.sh→build.py→ push まで通す。登録後は同URLでプレビューが出る。 add_section.pyは既存entryがあると上書き拒否(--force必要)。contributorだけ後から入れるならentry JSONに"contributor"キーを直接追加→build.pyが安全(既存内容を失わない)。
サムネはアニメ途中で写る → 撮影時だけ最終状態を固定する
shoot.sh(virtual-time-budget=3000)でも height/clip-path のCSSトランジションは描き切らない(1項目だけ開いた途中で写る)。setTimeoutの自動オープンも発火しないことがある。- → cpで退避 → 撮影用に「開いた最終状態」をCSSで固定(
is-open付与+transition:none+自動オープン無効)→ shoot → cpで復元。×の形もtransform:translate(...) rotate(...)!importantでCSS固定すると綺麗に写る。配信ファイルは触らずに済む。
in-appブラウザペインは非フォーカス時にアニメを間引く(検証が嘘になる)
- GSAPタイムラインが途中で凍る/
getComputedStyleが更新されない/element.styleに!importantで入れてもGSAPが毎フレーム上書きする。「目視で崩れている」が誤判定になる。 - → 検証は次のどれかで確定させる:
1. アニメ最終状態を数値で直接適用してgetBoundingClientRectで計測(交点や対称性は座標で判定する)
2. headless Chrome + CSSで最終状態固定してPNG化し目視
3. 実サイト側も同じ手順で計測し、数値で突き合わせる(今回これで「完全一致」を確定できた) - ref指定クリックが当たらないことがある。
document.querySelector('.js-menu-open').click()の方が確実。
併走セッションがある前提でgitを扱う
- 作業中に他セッションのWIP(concept-gallery/footerset/pjtstory等)がworking treeに現れる。
git add -Aは使わず自分のファイルだけをstage。 git pull --rebaseは未staged変更があると失敗するのでgit -c rebase.autoStash=true pull --rebaseで退避しつつrebase(他人のWIPを壊さない)。
2026-07-23 同じSTUDIOサイトから2本目(座談会・動画CTA)= zadankai-movie-ogushi-0723
追加で分かったSTUDIOの仕様
- セクションの中に
<style>が直接埋まっていることがある(div.frameの中)。今回の回転アニメの正体はそれ:
css #loop_rotate{animation:rotation 5s linear infinite;transform-origin:center} @keyframes rotation{from{transform:rotate(0)}to{transform:rotate(360deg)}}
→ 円形「WATCH MOVIE」SVG(140x141)を5秒で1回転。セクションHTMLをverbatim切り出せば一緒に付いてくるので、body内の<style>を絶対に捨てないこと。 - STUDIOのカスタム要素は3種(全部 script#2 が
customElements.define):
sd-carousel(スライダー)/sd-loop-box(マーキー。speed="60" reverse属性)/sd-video(muted loop autoplay playsinlineの動画)。ランタイムを同梱すれば全部そのまま動く。 - 動画セクションのheadless撮影は
--autoplay-policy=no-user-gesture-requiredを付けると1フレーム目でなく再生状態が撮れる。
.appear は「クラスが外れる=表示」実装(見出しが消える原因)
- STUDIOのランタイムはIntersectionObserverで
.appearクラスを外すことで表示する。CSS側は.sd-164.appear{opacity:0}のようにクラスが付いている間は非表示。 - headless/キャプチャではIOが発火せず、見出し「座談会」と副文が丸ごと消えた状態で撮れてしまった(背景と動画は出るので気付きにくい)。
- 恒久対策(毎回入れる): 末尾に表示保証のフォールバックを足す。実ブラウザでは通常どおり登場アニメが先に走る。
js setTimeout(function(){document.querySelectorAll('.appear').forEach(e=>e.classList.remove('appear'));},2500);
※ feedback_preview_must_open の「opacity依存の登場アニメで非表示のまま終わらせない」の具体形。
数値照合は2本目でも効いた(ダミー文の文字数ミスを検出)
実サイトと再現の getBoundingClientRect() を突き合わせた結果、
sec 1440x868 / bg 1264x640 x88 / title 274x104 x276 / btn 912x512 / badge 140x141 x711 / marquee 1440x48 は一致したが
副文だけ 274 → 292px(+18px) ズレた。原因はダミー文が元より1文字多かったこと。
→ ダミー化ルール「元と同じ文字数」は感覚でなく実測で検証できる。文字数を合わせたら274pxで完全一致。
同一サイトから2本目を取るのは安い
/tmp/belx_raw.html(1本目で取得済み)を再利用でき、Step0の取得をやり直す必要がない。
手順も確立済み(styleのmedia属性保持 → 祖先チェーン再現 → ランタイム同梱 → 画像のdata-sd-img-src削除 → appearフォールバック → 数値照合)。
2026-07-31(細川)正本リポは ~/dev/terasu-lab。section.md の「lab-terasu が正本」は古い
- 事故った:section.md の記述どおり
~/dev/lab-terasu(scale-group-jp/lab-terasu)で作業→push したが本番に出なかった。
このリポの.github/workflows/deploy.ymlは 2026-07-23 に push トリガーを外され(on: workflow_dispatchのみ)、
「TERASU Lab は scale-group-jp/terasu-lab へ移設」とコメントされている。lab-terasu へ push しても本番は動かない。 - 本番の正本=
~/dev/terasu-lab(GitHub: scale-group-jp/terasu-lab)。deploy.ymlはon: push [main]で
wrangler pages deploy _lab_dist --project-name=lab-terasu --branch=main→ lab.terasu.scale-group.co.jp に配信(2026-07-23 大串承認で本番切替済)。
→ /section は今後~/dev/terasu-labで作業する(section.md も直すべき)。
terasu-lab の追加フロー(lab-terasu と違う点・実測)
git pull時にunable to update local refが出たらgit gc --prune=nowで直る。- core/ は submodule(terasu-web-core)。未取得だと
add_section.pyがModuleNotFoundError: contributors。
.gitmodulesが https URL で認証失敗するので SSH で入れる:
git config submodule.core.url git@github.com:scale-group-jp/terasu-web-core.git && git submodule update --init
(またはgit clone git@github.com:scale-group-jp/terasu-web-core.git core)。 - エントリ生成は
python3 tools/add_section.py --id … --kinds … --cats … --caption … --url … --contributor …。
---kindsが必須(30分類。絞り込みの正。無いと一覧に出ない)。旧--catsは互換で併記。
- caption の「分類|会社名」部分(:の前)は18字以内(超えると「カードで切れる」と弾かれる)。--urlは注記なしの純URL。 - サムネ:
tools/shoot_section.mjsは playwright 依存で未インストールだと動かない。
→ Chrome headless で撮ってtools/normalize_thumbs.py <id>(4:3・2880x2160化)でOK。
- 小要素(今回=バッジ単体)は normalize の自動トリムで中央がズレることがある。
対策:4:3ぴったりの小窓(例--window-size=440,330)で撮り、sips -z 2160 2880で拡大(normalizeを通さない)と中央維持で大きく写る。 - push:他セッションの未コミット(例 maskcard)や
node_modulesの差分が混ざるので 自分のファイルだけをgit add明示。
sections-data.jsonは CI がbuild.pyで全件再生成するのでコンフリクトは無視でよい。 - 反映確認は sections-data.json / api/meta を見ても0件で当てにならない(一覧は別経路)。
確実なのは①code-previews/<id>が 200+中身反映、②本番/sections/?kind=<kind>をブラウザで開きbody.innerHTMLに<id>が入っているかを見る。CI反映は数分かかる。
「セクションの一部だけ」を抜く指示への対応(今回:中央のバッジのみ)
- 大串「後ろの動画と背景はいらない、真ん中のアニメだけ」→ セクションから 該当コンポーネントのDOMだけ切り出す。
- このWATCH MOVIEバッジは 2枚重ねのホバークロスフェード:通常=
.sd-177(白リング/赤文字/赤丸再生)、ホバー=.sd-182(赤リング/白文字が#loop_rotateで回転/白丸赤三角)。
.sd-172:hover .sd-177{opacity:0}/.sd-172:hover .sd-182{opacity:1}で切替。回転は常時走っていてホバーで見える設計。 - 単体化:
sd-172を 160x160・position:relative の器に上書きし、動画フレーム(sd-173)を捨てて sd-177/sd-182 だけ残す。sd-→tb-にリネームしてオリジナル化。
2026-07-23 「デプロイしたのに反映されない」の誤認 — 原因は旧デプロイの残骸
何が起きたか
セクション追加後、https://lab.terasu.scale-group.co.jp/sections-data.json を見て
「95件のまま・今回分が無い=反映されていない」と判断した。これが誤りだった。
本番ページ /sections は正しく最新(今回分がNEWバッジ付きで掲載)だった。
真因(2階建て)
- Cloudflare Pages は過去デプロイの実体を残す。リポ分離(2026-07-23)前の旧デプロイが置いた
*-data.jsonがURL上に生き続けていた。現在の配信物_lab_distには含まれていないのに 200 が返る。 - それが
cache-control: public, s-maxage=604800(1週間)でCDNにキャッシュされ、
no-cacheヘッダを付けても剥がれず、13時間前の内容を返し続けていた。
対処(恒久)
core/tools/worker.js の RETIRED に [a-z0-9-]+-data\.json を追加して 404 で塞いだ。
- これらは ビルドの入力(build.py が生成 → build_lab.py が読む)であって配信物ではない。
- 塞いでもページ表示に影響しないことを、正規表現の単体テスト17件(塞ぐ7・通す10)と
デプロイ後の実HTTP確認(JSON=404 / 各ページ=200)で検証済み。
教訓(他コマンドにも効く)
- 反映確認は「人が見るページ」で行う。中間生成物のURLを見て判断しない。
- 200が返る=最新とは限らない。Pagesは旧デプロイの実体を残すので、消したはずのURLが生き続ける。
- 同じ構造の事故が過去にもあった(worker のコメントに
reference-sites-sakilayer-decompositionを
塞いだ記録あり)。URLを廃止したら RETIRED に足すのがこのリポの作法。
同時作業で気づいたこと
git status に自分が触っていない変更(別セッションの作業中ファイル)があった。
git commit <path> で自分の変更だけを指定してコミットし、pull --rebase は -c rebase.autoStash=true
で退避して実行した。git add -A を癖でやると他人の作業途中を巻き込むので、
同時作業日は必ず git status を見てからコミット対象を絞る。
2026-07-25 報告リンクは詳細ページ /section//
- FB: 「①②送るときはそれぞれのセクションの詳細ページのリンクにしてほしい」
- 対応: Step5報告の第一リンクを code-previews から
/section/<id>/に変更(section.md反映済)。
2026-07-26 並行セッションのID衝突=汎用名は担当プレフィックスで守る(ksart案件)
- 起きたこと: 同じ大串アカウントで2つのセッションが同時に/sectionを実行。片方がUMICAHI案件、
片方がksart案件。両方が汎用idのcontact-form-ogushi-0726を使い、後発が先発の
code-previews/contact-form-ogushi-0726.htmlを上書き。大串がプレビューを開いた時に
別案件のフォームが出て「⑥仕様が全然違う」となった(実際は上書き事故)。 - 真因: idの
<担当>-<日付>は別人前提で衝突回避しているが、同一担当が同日に並行すると
<主題>(contact-form等の汎用名)が衝突する。担当名では守れない。 - 対策(恒久): 同時作業日/並行の可能性がある日は、参考元を表す短いプレフィックスをidに付ける
(例:ksart-contact-ogushi-0726)。social/社名をidに入れない原則より衝突回避を優先(実害=上書き事故)。
着手時と push直前にgit statusで他セッションのファイルを確認し、git add <自分のpathだけ>で
コミット対象を絞る(-A禁止は既存学び)。集約(sections-data)はCIが再生成するので気にしない。 - 付随の技術メモ:
- スライダーの左右「線」矢印はサイト独自の slick-theme.css の
.slick-prev/next::before(160×1pxの線)由来。
CDNのslick-themeだと文字矢印になるので、実slick+slick-themeをスコープ化して同梱する(JSだけCDNでOK)。 - stickyフローナビは
side_columnをalign-items:stretchで全高に伸ばして中の.fixside{position:sticky}。
汎用.side_column{display:none}@max-width:1280(アーカイブ用)がフローにも波及するのでデスクトップで表示に上書き。 - EFO等の外部フォームは実フィールドは静的DOMに入っている。
<script>/<link>/hiddenを除去し action=# に。
intro+TELだけの上部は淡くてshoot_sectionの内容判定に落ちる→フォーム項目が写る位置でカードを撮る。 - CSSスコープ化ツールの落とし穴: f-string で
@media{...}の閉じを}}(=}のつもりが2つ)にすると
直後のルールを飲む。.single-worksが.single\bにマッチしてセレクタが壊れる((?=[\s>+~]|$)で回避)。
2026-07-26 KAUNIS CLINIC 12→8件 一括抽出(大串・並列ワークフロー)
- 複数セクション一括は並列ワークフローが有効。まず全セクションのHTML/CSS/JS設定を機械抽出→
extract_css.py(アンカー方式で使用ルールだけ抽出)+build_section.py(sec-スコープ化・reveal/swiper/accordion JS注入)で"崩れないベース"を機械生成→各エージェントは検証・ダミー化・レスポンシブに集中。品質・速度とも良好。 - CSS抽出の文脈除去は body/html/.home だけでなく
.clinic/.clinic2 等のページbodyクラスも外す。下層ページ(/clinic/等)はレイアウトCSSが.clinic .p-xxx ...とbody文脈前置で書かれ、cc⊆classset判定で落ちる→レイアウト総崩れ。CTXCLASSに追加で解決。 - keyframe名リネームの罠:
\bfadeInDown\b一括置換はクラス選択子.fadeInDownまで壊す→JS付与クラスに対応ルールが消え全要素opacity:0で真っ白。keyframeは同一定義なら共有安全なので原則リネームしない。 - 下層ページの画像は相対パス
/wp/...→https://kaunis-clinic.com前置しないと壊れる(トップは絶対URL)。 - webp画像でheadless Chromeがハング→レンダは必ずサブプロセスtimeout付き。ブラウザペインは file:// も localhost もブロック→headless撮影 or
openで実ブラウザに出す。 - caption「分類|会社名」18字制限: 会社名が長い時(KAUNIS CLINIC=13字)は主要部分(KAUNIS)に統一すると分類が入る。
- 固定幅(.t-cont:1280px 等)がタブレットで縮まない共通崩れ→
@media(max-width:1279px){max-width:100%;min-width:0}+swiper/marquee器にoverflow:hidden/clip。PC(>=1280)は不変。 - ワークフローagentが構造化出力に失敗してもファイル編集は完了していることがある→レポートを鵜呑みにせずファイル実体(mtime+grep+render)で検証。今回2/12が出力失敗だったが1件は編集完了・1件は未着手だった。
- 元DOMで複数セクションが同一ブロックに同居(p-index_doctorblog=医師紹介+ブログ)→重複を避け、大串が送った素材(動画)に合わせて単体sectionを切り出す。
- 他セッション同時進行時:
git add -A禁止。自分のidのファイルだけ明示add(zshは$VARを単語分割しないのでループはリテラル列挙)。CIがentries/*.jsonから全再生成デプロイなのでsections-data.jsonは非commitでOK。
2026-07-26 tomatec 6セクション一括抽出 — 抽出ツールの汎用知見(大串・恒久)
tomatec.co.jp トップ6セクション(ヘッダー/FV/製品Swiper/海外数字/研究開発/3カラム・id *-ogushi-0726)。minified CSS 310KB からの抽出で確立した、次回も効く型:
- サイト全体が親ラッパー付きセレクタ(.top .xxx{})のことが多い→ラッパー(.top等)をスコープルートとして読み替え(先頭から除去)しないと抽出が丸ごと空になる。tomatecは全ルール.top付きで、最初これに気づかず研究開発が真っ黒になった。css_extract側にWRAP=['top']で先頭除去を実装。
- 採用判定は「名前空間の部分一致」でなく「HTMLの使用クラス集合に、そのルールの全要求クラスが含まれるか」(KAUNISのcc⊆classsetと同じ)。部分一致だとfooter/recruit等の無関係ルールを大量に拾い肥大(30KB→本来5KB)。動的状態クラス(is_/slick/swiper/inview)だけ許可接頭辞で通す。
- CSS内の相対 url(../images/…) は必ず絶対URL化(CSS基準/css/でurljoin)。背景画像・矢印SVGが404で消える(⑤研究開発の矢印欠けはこれが原因、大串指摘で発覚。CSSを絶対URL化前に生成していた)。
- font-sizeは継承モデルにする(最重要)。reset で .sec-<id> *{font-size:14px} を敷くと継承チェーンを切る。「親要素にfont-size・テキストは子<p>」構造(FVの巨大英字.top_mv_txten{160px}>子p)でpが14pxに潰れ、slick中間要素も切られてvertical高さ計算まで壊れる。正解=ルートにだけfont-size(.sec-<id>{font-size:14px})・子孫は継承。tomatecのhtml{62.5%}+*{1.4rem}の実挙動と一致。remはpx併記が多いのでNrem→N*10 px変換でhtml宣言を残さず崩さない。
- 単独プレビュー用の“打ち消し”(FV上に重なる/ロードでフェードイン前提のCSSが単独だと崩れる): ヘッダーopacity:0(#wrapper.is_loaded前提)→opacity:1/position:absolute(FVに重なる前提)→relative+min-height確保/.sp要素のPC表示は@media(min-width:768){display:none}。FVtop:-100px(ヘッダー潜り込み)→top:0。hover大背景(lgbg)はスコープ化でz-index順が変わり手前に被る→row{z-index:1} lgbg{z-index:0}明示。
- アニメ確認: 背景画像が巨大(tomatec FVは8.9MB)→ロード待ち必須・slick fadeの現slideは遷移中opacity:0(撮影/確認は数秒待つ)。countUpはプレビューのrAF間引きで途中停止(11で止まった)→setTimeout(dur+α)で最終値保証の自前カウントが確実。hover(mouseover)はブラウザペインのcomputer hoverで発火しない→jQueryで強制addClassして見た目確認・実ブラウザ側で動く(メガメニュー確認はこれ)。
- nodeがPATHに無い→/Applications/Codex.app/Contents/Resources/cua_node/bin(Codex同梱)をPATHに。timeoutコマンドも無い(macOS)。
- 大串FB: ヘッダーは「項目にカーソルでメガメニュー(ドロワー)展開」まで含めて再現(js_headerMegaOpenホバー→data-pageで製品/企業を切替)。ナビバー単体で終わらせない。矢印/アイコンSVGが相対URLで欠けやすい→絶対URL化を必ず確認。
2026-07-27・ACOT(ヘアサロン)5セクション一括追加で得た知見(大串「プレビューで見せてから格納」)
参考: https://www.acot-hairsalon.link/ (WordPress・制作リーピー・NinjaFirewall有)。実コード抽出を機械化する build_section.py(cssutilsでCSSをパースしsec-
抽出の技術(ここが肝・再利用価値大)
- cssutils は CSS変数(--var)の定義を全部落とす。
var(--x)参照は値として残るが--x:#2C2C2C定義が消える →var(--clr-main,#1D7FB7)のフォールバック(青)が出て色が全崩壊。対策=:root{…}ブロックを正規表現で生テキスト抽出し.sec-<id>{…}にして先頭に足す。 - CSSの型判定は
isinstance(rule, CSSStyleRule/CSSMediaRule/CSSKeyframesRule)。rule.type==CSSRule.KEYFRAMES_RULEはcssutilsのバージョンで属性名が無く落ちる。 - rem基準は要確認。standard_sass(リーピー系)は
html{font-size:1rem}=16px基準(62.5%/10pxではない)。.sec-id{font-size:10px}を足すとremが0.625倍で全崩壊。html/body/:rootルールはスコープ化して採用(is_base)し、font-size:10pxは入れない。 - レイアウトがbody class依存(
.home .header--logo,.archive .posts-style{display:grid})だと抽出時にそのクラスが無くグリッド定義ごと漏れる→ルート要素に--root-class archive等を付けて抽出。styleギャラリーの3列gridは.archive依存だった(ヘッダー大ロゴは.home依存)。 - CSS採否はキーセレクタ+セレクタ内の全クラスがセクション内に存在するかで判定すると、
.flex-col2 > *{width:50%}のような> *終わりも拾えて貼っても崩れない粒度になる。
レスポンシブ(Step3.5)
- 5件全部が同じ⛔「タブレットで横はみ出し(1088>768)」。原因は元サイトが
body{min-width:1080px}(→.sec-id{min-width:1080px})でPC版をタブレットでも横スクロールさせる設計。min-width:1080px→0に打ち消しで5/5合格(コマンド定義Step3.5の表どおり)。
ヘッダーの状態依存(判断ポイント)
- ACOTヘッダーは最上部=丸い予約ボタン(
.header:not(.header-sm))/スクロール後=四角(.header-sm)。ユーザーが見せたスクショは四角版だったが、四角版(header-sm付与)はロゴがflexで幅0に潰れる(状態遷移JS前提のCSSのため)。→破綻なく完全再現できる最上部(丸)版を採用し、大串に選択肢提示して「丸でOK」を確定。状態で見た目が変わる要素は、静的に完結する状態を選ぶ。
id衝突・他人の作業保護(重大・事故一歩手前)
- 同日・同名idが別案件で既に存在(私の
faq-accordion-ogushi-0726がKAUNIS案件の同idと衝突)。add_sectionが⛔停止=事故防止が効いた→ユニークなqa-blackbar-ogushi-0726に変更。 - 配置の
cp code-previews/<id>.htmlで既存(KAUNIS)ファイルを上書きしていた(git statusでM)→git checkoutで復元。教訓=(1)配置前に既存id衝突を確認 (2)git add -Aせず自分の-ogushi-<MMDD>だけ明示add(同日に走る他セッションの作業を巻き込まない)。
テキストダミー化の落とし穴
el.textだけ抽出すると<br>後・複数<p>の後半(el.tail)を見落とす。FAQ回答の続き(「洗い流さないトリートメント」「ブリーチ」等の実コピー)が残った→抽出はel.textとel.tail両方。固有語(白髪ぼかしハイライト)だけでなく実コピー全文をダミー化。- 一般的なヘア用語(「ハイライト」「白髪が気になる方」等の髪型説明)は店舗特定情報でないので残してよい。ロゴ画像は実物のまま(コマンド定義:画像は元URL)=テキスト社名はダミーでもロゴはACOTのまま、は許容。
画像・プレビュー・環境
- lazyload/フル解像度画像でClaude Browserのプレビューは初回グレー→数秒後に描画。
shoot_section.mjs(Playwright)は画像ロードを待つのでサムネは問題なし。慌てて「画像が出ない」と誤判断しない。 - ローカルプレビューのサーバーは空きポートをOSに選ばせる(
socketserver.TCPServer(('',0))で採番)。8080/8899/8911等は他プロセスが埋めていてAddress already in useになる。 - Claude Browserはスクロール操作でペインがhidden化しタイムアウトしやすい。個別navigate+screenshotの方が安定。
- nodeはPATHに無い→
/Applications/Codex.app/Contents/Resources/cua_node/bin/nodeを使う(check_responsive.mjs/shoot_section.mjs)。python3にbs4/lxml/cssutils/tinycss2は有るがcssselectは無い(lxmlの.cssselect()不可→xpath/iterで自作)。ffmpeg/swift(SDK不整合)/PyObjCも無く、動画フレーム抽出はqlmanage(ポスター1枚)止まり=URL必須(実サイトを直接見て確認)が正解。
2026-07-27 ギークリー corp.geekly.co.jp 6セクション一括(大串・FVあり)
うまくいった型
- CSS抽出はブラウザ非依存のPythonツール(extract_section.py を /tmp に自作): セクションHTMLの使用クラス/id/タグを集め、minified CSSからセレクタがマッチするルールだけ抽出→
sec-<id>スコープ化。@media/@keyframes/@font-face対応。CSSOM(ブラウザ)経由は大串のタブ操作と競合するので却下した。 - fade-in の発火クラスは
is-visible(base.min.css:.fade-in.is-visible{opacity:1;filter:grayscale(0)})。IntersectionObserverで付与して再現。extractは初期HTMLにis-visibleが無く落とすので、発火スタイルは手動追記が要る。 - CSS内 url(/...) の絶対化を忘れない(HTMLのsrc/hrefだけでなく)。sedで
url((["']?)/([^/])→ 絶対化。下層はurl(images/...)→/about/images/に別途変換。 - 素タグのルート要素(footer{})はextract漏れ(クラス/idが無くneed空→除外)→ 背景等を手動補完。id付き(section#news, #hero)は拾える。
FV(canvas/動画blob)の扱い=抽出不可を正直に告げる
- 実際のトップFVは3シーン(scene1文字アニメ / scene2動画 / scene3 canvas物理演算=HTML/CSS抽出不可)。大串了承のうえcanvas省略。
- 動画はblob(元mp4取れず)→ playwrightで #hero を element.screenshotし幾何学完成フレームをキャプチャ(currentTime=3.75が完成形・ヘッダーとCookieバナーはpage.evaluateで非表示・deviceScaleFactor2)。data URIを自分で受け取らずnodeが直接png保存=コンテキスト軽量。
- 文字アニメ(scene1)は実サイトのchar span(--enter-delay/--exit-delay付き)をそのまま流用+keyframes slideInFromRight/slideOutToLeftを抽出。JSでscene1→scene2切替ループ。
- 送られた画像=目的のFVと誤認しやすい。集合写真スクショをFVと判断したが、実際のFVは幾何学動画で集合写真は別ページだった。URLで実物を必ず確認してから作る。
複数まとめフロー(6件)実地メモ
- DOM/CSS取得・push・CI待ちは各1回、抽出/検品/サムネは各件。品質は1個ずつと同じ。
- ファイル名は
<id>.html(sec-なし)、scope classがsec-<id>。最初sec-付きファイルにして直した。 - node PATHはCodex同梱(/Applications/Codex.app/Contents/Resources/cua_node/bin)。check_responsive.mjs/shoot_section.mjsもこれで動く。
- 「見送り」と早合点しない:大串の番号FB「1 完璧じゃない」はFV除外でなく"直して入れて"の意だった(6件目として追加)。
2026-08-10 STUDIO製サイト(growth-next.com)からのフッター抽出で得た知見
対象: Fukuoka Growth Next のフッター → dark-linkgrid-footer-saki-0810。STUDIO(Nuxt)製サイトの抽出は通常と勘所が違う。次回同型サイトで再利用する。
- headless
--dump-domでは本文が出ない(Nuxtハイドレーション前のシェルのみ)。playwright(terasu-labのnode_modules同梱)でレンダリング後に抽出する。実スクロールを挟むと安定。 - CSSはdata-s-UUID属性セレクタ+
:where(.render-canvas)スコープ済みリセットの構成。.render-canvas→sec-<id>に置換すればリセットがそのままスコープ版になる(html/body直指定は元から無い)。 - **
el.matches()で使用ルールを判定すると:before/:hover付きセレクタが全部落ちる**(疑似要素はmatchesに掛からない)。判定前にセレクタから疑似要素/疑似クラスを剥がすこと。今回これで写真・ホバーアニメが丸ごと欠けた。 - ** @mediaを条件ごとにグループ化すると元のカスケード順が壊れる。STUDIOはdesktop-first(1160→840→480→320の順で後勝ち)。media出力は必ず幅の広い順**に並べ直す。順序を間違えると「1160のdisplay:noneが768でも勝つ」型の崩れ+横スクロールが出る。
- サイト共通トークン
--s-color-*/--s-font-*は:root相当にあり抽出網に掛からない。原本ページでgetComputedStyle().getPropertyValue()により解決し、sec-<id>{ --…: 値 }で埋め込む(欠けると「白文字が白背景に溶ける」全崩れに見える)。 - 遅延ロードで placeholder(data:svg) のままの img の実URLは、公開されている symbol-views/*.json(storage.googleapis.com/studio-publish/...)から uuid→content.src で引ける。ホバー用二枚目のimgはスクロールでもホバーでもロードされないのでこれが確実。
- PC/タブレット/スマホでカード等の要素が複製されており、非表示変種の
<style></style>(:before背景の遅延注入枠)は空のまま。symbol JSON の src から.sd[data-s-UUID]:before{background-image:…}を補完しないと、モバイル幅で写真だけ消える。 - 検証は目視より「原本と抽出版を同幅で開き全data-s要素のcomputedスタイルをdiff」が速くて確実(今回88差分→0構造差まで機械で収束確認)。残るのはダミー文の文字数由来のテキスト幅のみ。
- フォントは fonts.studio.design のCSS(130KB×2)をインライン化せず
<link>参照で軽く保つ(画像と同じ遠隔参照扱い・表示は同一)。
2026-08-16 大串FB「実際にサイトに入れ込んだら精度が低い」→ 実サイト埋め込み耐性の型(カクトクFV)
対象: photo-mantra-fv-saki-0816(kakutoku.co.jp corporate FV)。単独プレビューは完璧でも、実HPに貼った時の再現度は別問題。次回から下記を標準装備する。
- ** 出現アニメをJS必須にしない(最重要)。元サイトが「初期 opacity:0 → JSで .is-in-view 付与 → 表示」型だと、scriptを入れずに貼った/CMSがscriptを剥がした環境で要素が永久に消えたまま(今回は写真だけ表示=「精度が低い」の最有力原因)。→
.js-animゲート方式**に組み替える: 素のCSSは全表示・scriptが最初に.js-animを付けた時だけ初期非表示になり、スクロール到達で元と同じアニメ再生。JSあり=原本と同一・JSなし=静的だが完全表示。 - フォントは
<style>先頭の@importでも読み込む。Google Fontsの<link>はhead側なので、styleブロックだけコピーされると書体が変わる。@import併記ならstyleコピーだけで追従する。 - リセットはホストCSSに負けない特異性で。
:where()は特異性0なのでホストのp{margin-bottom:1.6em}等に負ける。sec-<id>.sec-<id> :is(p,img){margin:0;padding:0}(二重クラス)で上書きされにくくする。※sectionを強リセットに含めるとセクション自身のmargin指定(例margin:0 0 65px)を潰すのでp/imgに限定する。 - 検品に「ホスト埋め込みシミュレーション」を足すと事前に捕まえられる: 典型的グローバルCSS(
*{margin:0;padding:0}・img{max-width:100%;display:block}・p{margin-bottom:1.6em}・section{padding:80px 0}・明朝body)を持つページに style+body を埋め込み、①script有り ②script剥がしの2通りをplaywrightで数値検証(photoX/opacity/margin等が単独版と一致するか)。今回この方法で修正版の一致を機械確認した。 - 小ネタ: ブラウザペインは非表示状態だと innerWidth=0 になり IntersectionObserver が発火しない=「アニメが動かない」ように見える。埋め込み検証はplaywright(ビューポート確定)でやる方が確実。
2026-08-17 Vivoo(大塚製薬 otsuka-plus1.com/vivoo/about) 6箇所指定→5件抽出(Astro製サイトの型)
- ** 大串FB「ダミーテキストじゃなくて本文そのままにして」(確認①提示後)。本コマンドv2.2の「テキストは同文字数ダミーに差し替え」を大串指示で上書きし原文のまま格納**。同じFBが2回来たらコマンド定義本体のダミー化手順を書き換えること(今回は1回目)。
- AstroサイトはCSSが3層に分散: ①
<link>の@import先4ファイル(セクション本体CSS)②JSが後から注入する<style>(CircleArrow/SectionTitle/SecondaryPageMv等・headless --dump-domでは取れない)③断片内のインライン<style>(:where(.img-hash)のサイズ指定)。→ ②は実ブラウザでdocument.styleSheetsから cssRules を収集する(@import先シートは document.styleSheets に出ない=ローカルDLで補完)。 - astro-iconのsymbol定義は「最初に使った箇所」にインライン埋め込みされる。
href="#ai:local:*"の参照だけでなくid="ai:local:*"の定義側もリネームしないと grep 残存する。断片に定義が無いアイコンはページ先頭側にあるので、live DOMから<symbol>を回収して同梱(断片内定義と重複させない)。 - rem基準が
html{font-size:calc(.0111*100vw)}(PC)/4.10vw(SP)型の流体設計。sec-<id>{font-size:10px}移植ではremは直らない(remは常にdocument rootを見る)。→ 全CSS/インラインstyleのNremをcalc(N * var(--sec-rem))に機械変換し、--sec-remを sec-上でPC/SPの@media付きで定義する方式が完全再現になる。 - フォントCORS: 元サーバーにAccess-Control-Allow-Originが無い → Lab別ドメインでは@font-face遠隔参照が読めない。.woffをbase64埋め込み(今回 .woff2 の実体はPostScriptの壊れファイル・format()表記も逆だった。
fileコマンドで実体確認してから埋める)。 - マーキーの設定値はDOMにあった:
data-loopflow='{"duration":30,"direction":"left"}'→ CSS keyframes(30s linear infinite / translateX -50%・トラックは複製wrapper×2)に転写。inner/track/wrapperのflexはJS由来なのでスコープCSSで補完。 - 出現アニメ(SlideInText)はCSSで初期非表示+GSAPで表示型 → IntersectionObserver+transition-delay(35ms/文字)のscoped JSで再現(カクトクFVの教訓通りJS無しでも全表示になるゲートは今回
.SectionTitle.-secShow方式)。 - ブラウザペインが非表示だと visibilityState=hidden でスクショが古いフレームのまま・innerWidth=0。視覚照合は headless Chrome
--screenshot --window-size=1440,11500 --virtual-time-budgetで原本フルページを撮り、帯分割(sips -c)して同幅比較が確実。 - 添付動画はDesktopから消えることがある(今回4本とも作業中に消えた)。受領直後に
qlmanage -t -s 1400で先頭フレームをscratchpadへ保存しておくと特定作業が継続できる。ffmpeg/swiftc/pyobjcはこのPCでは全滅なのでqlmanage一択。 - FV背景が three.js WebGL canvas(LowerKvGL)=抽出不可 → 2026-07-25ルール通りその件だけ外して他5件は続行。
2026-08-17・Maison Orchidée(b-exgroup.com/orchidee/)2セクション(咲輝PC・saki)
スクショ=FVとは限らない。まずDOMと照合してから抽出(今回の最重要)
- 大串/咲輝から届いた「FVっぽいスクショ」(ネイビー地に写真が散り、中央に大きな
Orchidée)は、現在の本番FVとは別物だった。サイトが「2nd line 2026.4.8 debut」でリニューアル済みで、今のFVは白背景+大判写真スライド。 - スクショの中身(写真6枚・不規則配置・中央の白い大ロゴ・ヘッダーが白文字=
mix-blend-mode:exclusion)をDOMと突き合わせたら、ページ下部のギャラリー.p-index__gallery(photo--a〜f+__logo)と完全一致。→ そこを抽出して正解だった。 - 教訓: 「FVを」と言われてもDOM照合を省略しない。今回は写真の枚数と配置が決め手。リニューアル済みサイトでは古いスクショが別セクションと一致することがある。
CSS mask-image のクロスオリジンはChromeがブロックする(再利用価値・大)
<img src>の画像は他ドメインでも普通に出るが、mask-image:url(https://別ドメイン/x.svg)は描画されない(今回ロゴが消え、ボタンの矢印も消えた)。b-exgroup は CORSヘッダ無し。- → maskに使うSVGだけは実ファイルを落として
data:image/svg+xml;base64,に埋める。画像そのものは参考元URLのまま(コマンド定義の原則は維持)。ロゴ5.6KB/矢印295Bなので容量影響はほぼ無い。 - 判定の速い見分け方: 「
<img>は出るのに、mask由来の図形(ロゴ・アイコン)だけ出ない」=これ。
切り出すと margin-top が背景の外へ抜ける(margin collapse)
- 元は
.p-index__inversion(ネイビー背景)の中に複数セクションが入っており、gallery{margin-top:200px}は帯の内側の余白として見えていた。単独に切り出すとマージンが帯の外へ出て上に白帯が出る。 - → 背景を持つ親に
display:flow-rootを足すだけで元と同じ見え方に戻る(overflow:hiddenと違い中の sticky を壊さない)。
「前セクションとの間隔」はセクション単体では空きすぎになる(大串FB)
- 元CSSどおり
margin-top:200pxで出したら 「上余白空きすぎ」(2026-08-17 大串FB)。元値はあくまで“前のセクションとの間隔”で、単体セクションの上下余白ではない。 - → PC 60px / SP 40px 程度に圧縮して合格。恒久ルール化: 抽出物の先頭に来る
margin-top(前セクション間隔)は、セクション単体用に詰めてから見せる。理由をコメントで残す。
同PCに他セッションの未コミット entries があると build.py が巻き込む
build.pyはentries/*.jsonを全部集約するので、別セッションが作業中の未コミット4件までsections-data.jsonに入った(=本体が無いのに参照だけ入る)。- → 他人の entries を
/tmpに一時退避 → build.py → 自分の7ファイルだけgit add→ commit → 退避を戻す。git add -Aは厳禁(今回も_afloat_work/等が大量にあった)。 git pull --rebaseは他作業の未ステージ変更で失敗するが、リモートに新コミットが無ければgit pushはそのまま通る。
環境・道具(このPC=咲輝PC)
- node は PATHに無い。
/Applications/ChatGPT.app/Contents/Resources/cua_node/bin/node(旧 Codex.app は2026-08-11の改名で消滅)。check_responsive.mjs/shoot_section.mjsはこれで動く。 - minify された1行CSSは
grep -nが役に立たない → ブレース追跡の自作パーサ(@media/@supportsは再帰、@font-face/@keyframesは生保持)でルール単位に分解し、セレクタ正規表現で必要分だけ抜くと速い。 - GSAP実装はバンドル(webpack 151KB)に埋まっていても、
js-フック名で検索すればタイムライン定義がそのまま読める。今回はgsap.fromTo(mask,{scale:.6},{scale:1,maskPosition:'0% 0%'})+ ScrollTrigger scrub をそのまま移植(CDNは cdn.jsdelivr の gsap 3.12.5)。 - ScrollTriggerの
start:'top '+innerHeightは単独プレビューだと開いた時点で progress=1=完成状態で出る。サムネ撮影も正しく撮れるので、そのままでよい。
2026-08-17 glänta(グランピング)|背景固定クロスフェード
ページ級の position:fixed 背景レイヤーは sticky に置き換える
- 元サイトは
.section-back{position:fixed;z-index:-1}をページ全体で共有し、各セクションのスペーサーが ScrollTrigger でimg1..img6の.on(opacity 1s)を切り替える設計。 - セクション単体に切り出すと fixed 背景がセクションの外まで出て居座る(=貼ると崩れる)。
- → 背景レイヤーをセクション内の先頭に置き、
position:sticky; top:0; height:100vh; margin-bottom:-100vhに変換すると、見た目は完全に同じまま器の中で収まる。margin-bottom:-100vhを忘れるとセクション高さが1画面ぶん余分に伸びる。 - このとき器に
overflow:hiddenは使わない(sticky が死ぬ)。overflow-x:clipを使う。
GSAP ScrollSmoother はセクション単体では外す
- PC版は
ScrollSmoother.create({smooth:1,effects:true})=ページ全体を#smooth-wrapper/#smooth-contentで包む前提の有料プラグイン。セクション単位では成立せず、CDNでも配れない。 - → 慣性スクロールだけ落とし、背景切替とフェードインは GSAP core + ScrollTrigger(cdnjs 3.12.5) で再現すれば見た目は一致する。
Browser paneのスクショは sticky+will-change で真っ黒に写ることがある
position:sticky×will-change:opacityの合成レイヤーで、Browser pane の screenshot が全面黒を返した(DOM上は.onが正しく付いていた)。pane 自体もその後 30s タイムアウトで固まった。- → Playwright(
terasu-lab/node_modules)で直接スクロール撮影に切り替えると正しく撮れる。判断が割れたら Playwright を正とする。
完全再現の確認は「computed値の機械比較」が速い
- 実サイトと自作プレビューを同じ 1440×810 で開き、対応要素の
fontSize/lineHeight/letterSpacing/getBoundingClientRectを突き合わせて ALL MATCH を確認。目視だけより速くて確実。 - 実サイトを headless で開くと読み込み演出の途中が撮れることがある(別画像に見えて焦る)。
waitForTimeoutを2.5秒以上取る。
動画から箇所を特定する(ffmpeg が無いPC)
- このPCに ffmpeg は無いが、Swift + AVFoundation の10行スクリプト(
AVAssetImageGenerator)で .mov から任意間隔のフレームを書き出せる。swift script.swift <mov> <outdir> <秒間隔>。 - Chrome headless の
--dump-domが0バイトで固まるサイトがあった(--user-data-dirを分けても再発)。WordPress等のサーバーレンダリング型ならcurlの生HTMLで足りる(今回はインライン<style>7個+ style.min.css で全部揃った)。
2026-08-17 JR西日本コミュニケーションズ 新卒採用サイト|1サイトから6セクション
include.js 型サイトは assets/includes/*.html を直接取りにいく
- ヘッダー/フッター/スライダーを
include.jsが後から差し込む作り。レンダリング後DOMを使うと Swiper の loop 複製スライドが混入して二重になる。 - →
curlでassets/includes/header.htmlfooter.htmlinterview-swiper.htmlを直接取得すると初期状態の綺麗なマークアップが手に入る。素のindex.html(curl)+ includes の組み合わせが最短。
元サイトが ScrollTrigger.matchMedia('(min-width:768px)') に入れているアニメは、その条件ごと再現する
- 横スクロール(pin)や拡大アニメが PC幅だけの実装だったのに、無条件で移植してSPで真っ白になった(pinがSPレイアウトを潰す)。
- →
ScrollTrigger.matchMedia({ '(min-width: 768px)': function(){ ... } })ごと持ってくる。元JSの matchMedia は「見た目の一部」なので削らない。
GSAP の pin は window.load 後に張る(幅0で固定される)
- スクリプト実行時に画像未ロードだと pin-spacer が
width:0を記録し、セクションが潰れて真っ黒になる。 - →
window.addEventListener('load', ...)で張り、最後にsetTimeout(()=>ScrollTrigger.refresh(), 100)。
js-fadeup はスクロール計算でなく IntersectionObserver に置換する
- 元サイトの「scroll位置で is-fadeUp を付け外し」方式をそのまま移植すると、GSAP の
ScrollTrigger.refresh()が一瞬スクロールを動かした拍子に else 側でクラスが剥がれ、見出しが opacity 0 のまま残る。 - →
IntersectionObserver(rootMargin:'0px 0px -10% 0px'・付けたら unobserve)にすると挙動は同じでタイミングに強い。
参考元の自前フォント(.ttf)は CORS で読めない → Google Fonts の同名に差し替える
@font-faceの src が参考元ドメインの.ttfでaccess-control-allow-originが無い=別オリジンの Lab からは読めない(画像はCORS不要なのでそのままでOK)。- → Google Fonts にある書体(今回 Hanken Grotesk / Noto Sans JP / Outfit)は fonts.googleapis.com のリンクに置換すれば同じ見た目。未使用の商用フォント(DIN Next 等)は宣言ごと捨てる。
ヘッダー単体はサムネ検品に落ちる(余白が多すぎ)
min-height:100vhの白い器+細いナビ=「中身が写らない(輪郭0.8%)」でshoot_section.mjsが2回とも不採用。- → 器を
position:relative; min-height:150px程度に詰めると一発で通る(既存の header 系セクションもmin-height:var(--sec-h,120px)で同じ考え方)。
caption 18字制限は「サイト自称の略称」で回避し、正式名称は説明側へ
ヘッダー|JR西日本コミュニケーションズは20字でadd_section.pyが停止。- → サイト自身が使っている略称(今回「Jコミ」)を会社名に採用し、正式名称は「:」以降の説明に入れると、カードも詳細ページも成立する。
他セッションが同じリポで作業中は git add -A 厳禁・集約JSONも staged しない
code-previews/に他セッションの未コミット作業が200件以上あった。- → 自分の id のファイルだけを個別
git add(entries / code-previews / img×3)。sections-data.json等の集約は CI がbuild_core.py+build.pyで再生成するので staged 不要=他人のエントリを巻き込まない。
動画のフレーム抽出は環境で壊れることがある → qlmanage で足りる
- このPCでは swiftc も JXA の AVFoundation も使えなかった(
SwiftBridgingの redefinition エラー/$.AVURLAssetが undefined)。ffmpeg も無し。 - →
qlmanage -t -s 1400 -o <dir> <mov>でポスターフレームが1枚取れる。「どのセクションの動画か」を特定するだけならこれで十分。
2026-08-18 KALPA(kalpa-wajima.com/wajimanuri・WordPress)制作工程セクション
対象: craftflow-steps-saki-0817。素材=スクショ1枚+画面収録+URL。静止画と動画で指している箇所が別だった(静止画=イントロ、動画=03 制作工程)ので両方作って一括提示→大串が②だけ採用。素材が複数ある時は「同じ箇所を指しているか」を先に疑う。
抽出ツールの落とし穴(今回すべて実地で踏んだ・次回は最初から回避する)
- セレクタはカンマ単位で採否を判定する。
.js-scroll__anime .js-text__anime, .js-visual__anime .js-text__anime{opacity:0}のようなリストを「1つでも断片外なら不採用」にすると、初期非表示だけ残って表示側が落ちる(=要素が永久に消える)。パートごとに残す。 - 全称セレクタ
*を.sec-<id>に変換してはいけない。reset由来の*{font-size:inherit;line-height:inherit}がスコープルート自身に当たり、こちらが書いたベース値(font-size/line-height)を丸ごと潰す。*は.sec-<id>, .sec-<id> *に展開する。 html/body/:root由来のページ級リセットは採用しない(:rootは疑似を剥がすと空文字になり除外判定をすり抜けるので、剥がす前に文字列で弾く)。必要なベース値は原本の computed を実測して自前で明示する(今回 root=10px / body=6.25px。body{font-size:62.5%}が二重に効いていた)。- スコープ内リセットは
:where()で特異性0にする。:is()や二重クラス(.sec-x.sec-x)だと元CSSのmargin-bottom:50px等を潰して余白が消える。ホスト耐性より先に「元の見た目」を壊さないこと。 - rem は
calc(N * var(--sec-rem))に機械変換(remはdocument root基準なので.sec-x{font-size:…}では直らない)。Vivoo で得た型が今回もそのまま効いた。 .js-animゲートは「初期非表示側」と「.active 側」の両方に付ける。片側だけに付けると特異性が変わってアニメの終状態が原本とズレる(今回クレジットだけ translateY(30px) のまま残った)。
クラス名リネーム(オリジナル化)の事故
- URL とスコープ名を保護してから置換する。
wjmnr-flow→flowsecの一括置換が 画像パス.../label/wjmnr-flow.pngまで書き換えて404(高さ48px縮んで発覚)。https?://…とsec-[\w-]+をプレースホルダに退避→置換→復元。 - 置換対象は参考元固有のプレフィックスだけに絞る(今回
wjmnr-*のみ)。js-scroll__anime/flow-step/f-amg/bg-bなどは汎用命名なので触らない。広く置換するほど衝突・巻き込みで壊れる。リネーム後は必ず「リネーム前後で computed 差ゼロ」を機械確認する。
書体(Adobe Fonts の有料書体を使っているサイト)
- 参考元CSSの日本語フォント名が文字化けしていることがある(
"貂ク繧エ繧キ繝ック Medium"=游ゴシック)。=原本でも効いていないので、正しい日本語名を書くとこちらだけ字幅が変わる。computed を見て実効している英字名だけに揃える。 - Typekit(use.typekit.net)の書体は他ドメインでは読めない前提で設計する。見出しゴMB31は字幅0.94em、代替の Noto Sans JP は 1em で、そのままだと見出しが1行→2行に折り返す。→
font-feature-settings:"palt"(0.968emまで詰まる)+ letter-spacing を実測から逆算して補正(今回 .27em→.245em で原本と同じ3行組みに一致)。
検証の型(これが決定打)
- 原本と抽出版を同幅で開き、全要素の computed を index 順に diff する。今回 msg 30ノード→差ゼロ / flow 150ノード→差1件(書体由来の8px)まで機械で収束させた。目視より速く確実。PROPS に transform を必ず入れる(アニメ終状態のズレは transform にしか出ない)。
- 画像の遅延ロード(
data-src)は src に変換。img.lazyloadedのクラス差はlazyload JS由来なので無視してよい。
運用
- id 衝突は entries だけでなく
code-previews/も見る。flow-steps-saki-0817は entries 未登録だが code-previews に他の人の確認①待ちファイルが実在し、上書きしてしまった(git statusのMで気づきgit checkout --で復元)。ビルド先を書いたら必ず git status を見る。 git pull --rebaseは他の人の未コミット差分があると失敗する。git fetch+git rev-list --count HEAD..origin/mainで差0を確認してから push すれば、他人の作業に触れずに済む。git addは必ず自分のファイルだけを個別指定(-A厳禁は既知だが、sections-data.jsonも他人の未登録entriesを巻き込むのでコミットしない=CIが生成する)。- 確認①は code-previews だけ先に push して本番URLで見せる(entries 未登録なので一覧には出ない=「Labに載せる前に確認」を守れる)。採用されなかった方は
git rm(CDNキャッシュで数日200が返るが実体は削除済み)。 - 動画フレーム抽出はこのPCではやはり不可(ffmpeg無し・playwright/Chrome は headful でも HEVC の .mov を loadedmetadata すら発火させない)。qlmanage のポスター1枚で「どのセクションか」の当たりを付け、URLの実物で確定する。
2026-08-17〜18 work kit(hitoba-office.com)FV→実績一覧の連動:GSAP pin を sticky で完全再現
成果: kv-shrink-works-saki-0817(導入事例|work kit)/fv-slider-auto6-saki-0817(FV|work kit)。
Nuxt風WordPress+Tailwind任意値+Alpine+GSAP ScrollTrigger の重い構成だが、実測値が全部リニアだったので数値一致で再現できた。
- 大串FB「画像の下に左下のテキストが来るように」=z順の話。参考実装は一覧が
z-index:1、
ロゴがz-autoで、カード画像がワードマークの上を通る。自作でz-index:2にしていて手前に出ていた。
→ 重なり物(sticky ロゴ・固定ナビ)は必ず原本の z 順を確認してから決める。 - 大串FB「もう一つのセクションも同じ動きだとしてもやって」。動画が2本来たら、
同じ一連の動きに見えても動画の本数ぶんセクションを作るのが期待値(1本にまとめない)。
今回は ①FV→縮小→一覧の連動 ②FV単体スライダー、の2本立てにした。 - スムーススクロール(lenis等)のサイトは
window.scrollToが効かない。
computed値は動くのにスクショだけ古いままという食い違いが出て、比較を丸ごと誤る。
→page.mouse.wheelで小刻みに送る(今回120pxずつ+待ち)。撮影と同時にscrollYを読んで突き合わせる。 - GSAP pin は
pin-spacerがposition:absoluteなので通常フローに寄与しない。
自作では.track{height:100svh+PIN}+.pin{position:sticky;top:0;height:100svh}に置き換えれば
ホストHPに貼っても壊れない(fixed を使わずに済む)。 - ピン距離は「縮み切ったFVの上端=一覧1件目の上端」で決まる(実測 1475 = 60svh+1000+60−125)。
この式で組むとどの画面サイズでも受け渡しがズレない。 transform: scale() translateY()は順序で意味が変わる。原本の matrix は translate 成分が等倍
(matrix(0.7986,0,0,0.7986,0,125))=translateY() scale()の順が正。逆にすると 125→99.9 にズレる。- 受け渡しの正体: 一覧は
orderで回転し、FVに出ている物件のカードだけopacity:0。
縮んだFVがちょうどその空きスロットに収まり、FVがそのままカードになる。
これを知らずに作ると「画像が2枚重なる/情報行が二重に出る」。スクロール後は自動送りが止まるのもセット。 - 文字は
1/var(--fv-scale)で逆補正されている(縮んでも級数が変わらない)。移植時も同じ手当てが要る。 - ロゴ色はピン進行に対する #fff→#000 の線形補間(実測 y=300→203 / 700→134 / 1100→65 / 1475→0)。
「重なり判定で白黒を切り替える」と推測すると外れる。まず数値を取る。 - FONTPLUS配信のフォント(
fpbf_xxxx)は再利用不可 → Inter / Noto Sans JP 等で代替する(@font-face を持ち出さない)。 - このPCの動画フレーム抽出は決定打が無い(ffmpeg/swiftc/pyobjc 全滅、
qlmanageは先頭1枚のみ、
ブラウザcanvasはペイン非表示だとseekしてもフレームが更新されない=全コマ同一の罠)。
→ 先頭フレーム(qlmanage)で箇所を特定 → 以降は playwright で実サイトを直接計測が最短。動画の解析に時間を使わない。 - scratchpad(/private/tmp/claude-501/...) はセッション中に消えることがある(今回2回消失)。
作業ファイルは~/dev/.section-work-<案件>/に置く。 - 同時作業中は push が弾かれる(
cannot lock ref)。ただし相手の push に自分のコミットが取り込まれている
ことがあるので、git merge-base --is-ancestor <自分のcommit> origin/mainで先に確認する(無駄なrebase防止)。 - CI(deploy.yml)が
build_core.py→build.py→build_lab.pyを自前で回すので、
集約JSONはコミットしない。entries と img だけ add すれば良い(他セッションの未コミット分を巻き込まない)。
2026-08-18 大串FB 7点 → コマンド本体 v2.4 に恒久ルール化(最重要)
大串から同一セッションで2通。「これルールとして保存しておいて/他のセッションでも同じ指摘ないように」。
①テキストをダミーテキストじゃなくて、本文をそのまま使う
②コマンドとURL、スクショ、動画を送った後に「これで合っていますか?」などの確認はいらない。すぐに作り始める。
③スクショ、動画を送っても動画だけ作られてないことが多いからスクショと動画、送ったやつは全て作る。
④作り終わったら一つ一つChromeで開けるプレビューのURLを送る。
⑤勝手にLabに格納しない
⑥精度が低いのが多い。修正しないで一発で格納できるのは10%〜20%ほど。修正に時間はかけたくないから精度をあげて。
⑦いろんなHPを一気にセクションコマンド使った時に違うHPのプレビューが送られてくることが結構あった
真因分析(ここが本質・"気をつける"では直らないものだった)
-
⑥精度が低い真因=「学びが本体に昇格していなかった」。崩れる原因は毎回ほぼ同じ10パターンで、
この learning.md に実証済みで全部書いてあった(CSS変数落ち/rem基準取り違え/*にfont-sizeで継承断/
相対url絶対化漏れ/bodyクラス文脈依存の取りこぼし/登場アニメ発火クラス/keyframeリネーム事故/
単独プレビュー用の打ち消し/素タグのルート漏れ/ライブラリのテーマCSS差異)。
learning.md は"読まれる保証がない" ので、毎回同じ所を踏んでいた。→ v2.4 で本体に「崩れる原因トップ10」表として昇格。
同様に、2026-07-22 に「今後これを標準にする」と書いたまま未昇格だったgetBoundingClientRect()数値照合も、登録前ゲートに昇格。
教訓=learning.md に「今後これを標準にする」と書いたら、その場で本体 .md にも反映する(書きっぱなしは効かない)。 -
⑦違うHPのプレビューが混ざる真因=2つとも本体の書き方が原因の構造バグ(注意力の問題ではなかった):
1. 旧 Step0 が取得先を/tmp/dom.html//tmp/site.cssに固定。2サイト目を取得した瞬間に上書きされ、
前のサイトのDOMを見て作る状態になっていた。→/tmp/section_<site>/にサイト別分離。
2. 旧 id 規則が<主題>-<担当>-<MMDD>で「参考元社名は使わない」と明記していた。汎用主題(contact-form等)が
別サイト間で衝突して既存プレビューを上書き(2026-07-26 ksart×UMICAHI で実際に発生し大串が別案件のフォームを見た)。
この事故の対策は当時「恒久」と learning.md に書かれたが本体は旧規則のままだった。
→ id は<site>-<主題>-<担当>-<MMDD>でサイト識別スラッグ必須に本体を変更(衝突回避 > 社名を出さない原則)。 -
④と⑤は両立する(一見矛盾するが構造で解ける)。
build_lab.py:3055が("img","assets","code-previews")を
丸ごと_lab_distにコピーするため、entries/<id>.jsonを作らなくてもcode-previews/<id>.htmlは公開URLで開ける。
実測: entries 205件に対し code-previews 465件(=未登録プレビューの運用は既に一般的。コミットa9fcb30も「一覧登録はまだ」と明記)。
→ 確認①はhttps://lab.terasu.scale-group.co.jp/code-previews/<id>.htmlを1件ずつ送る(Chromeで開ける・localhost禁止)。
/sections一覧には出ないので「勝手に格納した」ことにならない。格納=entries登録は大串のOK後。 -
③動画だけ作られない真因=動画を「動きの参考資料」と解釈し、作るべき1件として数えていなかった。
→ 受領直後に素材を番号付きで棚卸しし、動画にも id を割り当てる。完了時に受領件数と作成件数を照合してから報告。 -
②着手前確認の禁止は、canvas等で抽出不可の時も同じ(返事を待たず、その件だけ外して残りを作り切る)。
反映先
section.mdv2.4(冒頭に「2026-08-18 大串FB(7点・最優先ルール)」表を新設+各Stepを改定)- memory
feedback-section-command-7rules.md(他セッション用)/Vault Claude_学びストック
2026-08-18 STUDIO(studio.design/ja)4セクション = Studio製サイトの抽出ノウハウ
参考元=ノーコードCMS「STUDIO」で作られたサイト。自動生成クラス(sd-NNN / symbol-N__sd-NNN)+<style>22枚という特殊構造で、素直に抜くと必ず崩れる。次に Studio 製サイトを見たらここを最初に読む。
抽出時に踏んだ4つの罠(全部「見た目が壊れる」形で出る)
- ブレークポイントが
<style media="(max-width: …)">のシート単位(@mediaブロックではない)。
document.styleSheets[i].media.mediaTextを見ずに集めると、モバイル用CSSがPC用を後勝ちで上書きし、2カラムが縦積みに崩れる。
→ 抽出時にシートの media を読み、空でなければ@media <cond>{…}で包み直す。これでレスポンシブも本物になりcheck_responsive.mjsが素で合格する。 - アイコンが
[data-material-symbols-loaded="true"]ゲートで消える。
Studio は「アイコンフォント読込完了」を html/body の data 属性で示し、CSSが[class*="material-symbols-"]{visibility:hidden}→ 属性が付いて初めてvisible。
→ html/body のdata-*状態属性を「祖先ラッパー」へ移植する。.sec-<id>自身に付けても、スコープ化後は.sec-<id> [data-…] …なので効かない(ここで1回ハマった)。 - 遅延画像が
data:image/svg+xmlのプレースホルダーのまま抜ける。
→ 抽出前に対象をscrollIntoViewし、srcがdata:image/svgでなくなるまで待つ(最大10秒くらいリトライ)。 - 開閉は
<sd-toggle open>属性1つで切り替わる(CSSはsd-toggle:not([open]) .x{height:0}等)。
→ 参考元のランタイムJSは持ち込めないが、属性を付け外しする20行のJSを自分で書けば、CSSは実コードのまま動きが完全再現できる。FAQのアコーディオンもヘッダーのメガメニューも同じ仕組み。
抽出の型(今回うまくいった手順・他サイトでも使える)
playwright で参考ページを開き、CSSOMを走査して「対象サブツリーの要素に実際にマッチするルールだけ」を集めるのが速くて正確(el.matches() で判定・:hover 等の擬似クラスは判定時だけ除去し、出力セレクタは元のまま残す)。
- 祖先はクラス名だけのラッパーとして再現し、祖先自身のCSSは持ち込まない(.sd-1 .x のような子孫セレクタを効かせるため)
- getComputedStyle(root) から --* を総取りして .sec-<id>{} に載せる(変数の解決漏れ防止)
- 使う @font-face / @keyframes は、集めたルール本文に名前が出るものだけ残す
→ この方式だとルール数がフッター747/ヘッダー684と多くても、外来グローバルCSSはゼロ(html/body/* の機械検証も0件)。
撮影・運用
- ヘッダー単体はサムネ検品に落ちる(
shoot_section.mjsが「中身が写らない=色14種/輪郭1.5%」で弾く)。
ヘッダーは閉じているとバーだけ=ほぼ空白のため。→ 見どころ(メガメニュー展開)を開いた状態で自前撮影してimg/sections/に置き、normalize_thumbs.pyで4:3化する(main 1440×1080@2x /__detail1440×1000@2x /__sp390×760@2x の3枚必須)。 position:fixedのセクションは単独プレビューだと高さが出ない → COPY範囲の外に<div style="background:#fff;min-height:620px">の下地を置く(body{}を書くと「スコープ漏れ」機械検証に引っかかるのでインラインstyleの外側ラッパーでやる)。
事故:ロック未確認による id 衝突(同じことを繰り返さない)
- 着手前に
ai_session_lock.sh listを必ず見る。この日は同じMacの別Claude Codeセッションが terasu-lab のロックを保持していた(_afloat_work/と*-saki-0817が多数未コミット)。 - 見ずに書き始めた結果、同じ id
header-megamenu-saki-0817を同時に使い、自分の抽出物が上書きされて消えた。<内容>-<担当>-<日付>は同担当・同日・同種だと普通に衝突する(→header-floating-mega-…のように内容を具体化)。着手時にcode-previews/<id>.htmlの存在チェックを入れる。 - さらに向こうの
git add -Aに自分の作りかけHTMLが巻き込まれて push された。→ リポの外(~/dev/_section_work_0817/)で作り、完成後に自分のファイルだけコピー。作業スクリプトもリポ直下に置かない。 - 集約は
entries/全部を舐めるので、他人の未コミット entries は一時退避してからbuild_core.py && build.py(一覧に他人の未完成分が出るのを防ぐ)。add も1ファイルずつ明示。
確認用の実URL
Cloudflare Pages のscratch project に上げて渡す(大串は毎回Chromeで開ける実URLを求める)。
CLOUDFLARE_API_TOKEN=$(cat ~/.creo_cf_token) CLOUDFLARE_ACCOUNT_ID=$(cat ~/.creo_cf_account) npx wrangler pages project create <name> → pages deploy <dir>。今回は lab-section-preview.pages.dev。
node は ChatGPT.app 同梱(/Applications/ChatGPT.app/Contents/Resources/cua_node/bin。Codex.app は改名で消滅)。
2026-08-18 日テレアート(ntvart)about から3件(Reasons / Flow / Q&A)
動画で指定されたとき、ffmpeg 無しでフレームを抜く
このMacに ffmpeg / pyobjc は無い。Playwright(chromium) で <video> を再生してフレームを撮るのが最速。
- file:// の mov は about:blank から src 指定すると読めない(readyState が上がらずタイムアウト)。
→ mov を作業ディレクトリにコピーし、同じフォルダに置いた html を file:// で開いて相対srcで読むと通る。
- v.currentTime = t → onseeked を待って locator('#v').screenshot() を等間隔で数枚。これで「どのセクションか」の特定は十分できる。
- スクリプトを /tmp に置くと playwright が解決できない(node_modules はリポ側)。リポ直下に置いて実行する。
実サイトから「そのセクションが実際に使うCSSだけ」を機械抽出する
document.styleSheets を全走査 → セレクタから疑似クラス/疑似要素と状態クラス(.is-* / .--*)を剥がして、対象サブツリー内の要素に matches() するものだけ採用。@media は条件文字列ごと保持。
- これで 289KB の app.css から 1セクション分 50〜70ルール(80〜110行)まで落ちる=「外来グローバルCSS混入で数千行」を構造的に防げる。
- :root / html / body にマッチしたルールは別枠に集め、必要な物だけ sec-<id> に寄せる。
- クロスオリジンのCSSは cssRules が読めない(今回モリサワ morisawafonts.net は 0件)。日本語は Noto Sans JP、英字は Google Fonts の Ubuntu で代替(既存 ntvart セクションと同じ方針)。
- rem はホストの html{font-size} 依存。抽出値を 16px基準で px に直して書くとホストHPに貼っても崩れない(sec-<id>{font-size:10px} を置いても rem は解決しない=過去の void 系の弱点)。
minified バンドルからでも「実装のロジック」は取れる
webpack のモジュール索引("./section-stack-cards.js":"kqu8" のような map)を grep → その id の function(t,e,n){…} を波括弧の対応を数えて切り出すと、Alpine コンポーネントの中身がそのまま読める。
- 今回の section-stack-cards:introProgress = clamp(0,(vh - top - vh*i)/vh,1) を毎フレーム delta += .3*(target - delta) で追従させ、--stack-intro-progress としてCSS変数に流すだけ。CSS側が scale / border-radius / 写真の translateY・rotate を全部やる。GSAP無しの素JSで完全再現できた。
- decoration-marque:original の scrollWidth ぶんを 20秒 linear で無限ループ。CSS keyframes + --marque-shift(JSで実測値を代入)で等価。フォント読み込み後に測り直す(document.fonts.ready.then(layout))のを忘れると幅がズレる。
- 外部SVGスプライト(<use xlink:href="…sprite.svg#id">)は該当 symbol を丸ごとインライン化。path を途中で切ると別の字形になる(Qが黒丸になった)=必ず </symbol> まで正規表現で取り切る。
モバイル専用の1行を取りこぼしやすい
.page-about .section-stack-cards__indices{top:-17%} のような body クラス起点のモバイル専用ルールは、サブツリーへの matches() では拾えるが見落としやすい。
→ PCだけでなくモバイル幅でも実サイトと並べて撮る。今回これでカウンター位置のズレ1件を登録前に発見(check_responsive.mjs は通っていた=検品は横スクロールしか見ない)。
同じMacで別セッションが同じリポを触っている前提で動く(8/17の事故の続き)
今回も着手時点で 127ファイルが未コミット(別セッションの作業)。
- git add -A は絶対にしない。自分の code-previews/ / entries/ / img/sections/ をフルパス列挙で add。
- sections-data.json は CIが build_core.py → build.py → build_lab.py を再実行して作り直すので、コミットしなくていい(他人の未コミット entries を巻き込むリスクの方が高い)。
- 逆に向こうの git add -A に自分の HTML が巻き込まれて先にコミットされていることがある(今回 366e597 に3件とも入っていた)。git add しても git status に出ない時は事故ではなくこれなので、git log -- <file> で確認してから残り(entries / img)だけコミットする。
- git pull --rebase は他人の未ステージ変更があると失敗する(cannot pull with rebase)。push 自体は通るので、git fetch && git log origin/main で載ったことを確認する。
2026-08-18 groxi.jp/company(会社概要・アクセス・沿革の3件)
- 動画/スクショからの箇所特定:
ffmpegもpyobjc(AVFoundation)も無い環境ではqlmanage -t -s 1200 -o <dir> <mov>で先頭フレームのサムネが撮れる。動画から「どのセクションか」を特定するにはこれで十分だった(swift の AVAssetImageGenerator まで書く必要なし)。 - node が PATH に無い:
/Applications/ChatGPT.app/Contents/Resources/cua_node/binを PATH に足す(旧 Codex.app のパスは 2026-08-11 改名で消滅)。check_responsive.mjs/shoot_section.mjsはこれで動く。 - PC前提の固定幅サイト(min-width:768px で 954px 固定)はタブレットで必ず検品に落ちる。元サイト自身が横スクロールする設計のため。直し方=ブレークポイントを 768→1024 に上げ、元サイトのSPレイアウトを1023px以下に適用する(PC1440の見た目は完全に不変・元サイトに無いCSSを足さずに済む)。3件ともこれ一発で ✅ になった。
- 罫線テーブル系の長いセクションはサムネ検品に落ちやすい(白地+細い罫線=輪郭率が低い)。
MAX_H=2600で切られたうえ4:3に縮むと「中身が写らない(輪郭2.8%)」判定になる。実物は問題なく読めるので、目視で確認してから--forceで保存してよい(今回 info-table が該当・history 2271px は通過)。 - 有料ブランドフォントの代替: groxi は英字見出しが Campton(自社サーバー直リンク不可)。Poppins Bold で置換して大串OK。幾何学サンセリフ系はこれが第一候補。
- 並行作業への配慮: この日 terasu-lab は別セッションが数十件を同時編集していた。
git add -Aは絶対に使わず自分の15ファイル(html/json/png×3種)だけを個別add。ルートsections-data.jsonは CIが再生成する残骸なのでコミットしない(他人の未コミットentriesを巻き込まないため)。git pull --rebaseは他セッションの unstaged 変更で失敗するが、リモートが進んでいなければ push はそのまま通る。
2026-08-18 表参道高柳矯正歯科(j-ortho.jp/flow/)3箇所一括:WordPress自作テーマ+bodyクラス出し分けの型
対象: photo-hero-flow-saki-0817 / step-cards-arrow-saki-0817 / sticky-step-detail-saki-0817。動画1本+画像2枚+URLで来た3箇所。
動画フレーム抽出はこれで解決(このPCの正解)
- 送られた
.mov(画面収録・H.264)は playwright のchromium.launch({channel:'chrome'})=システムChromeなら再生できる。file://に<video src="rec.mov">の小さなHTMLを置いてgoto→currentTimeを進めて canvas に drawImage →toDataURLで任意枚数のフレームが取れる。 - 全滅だった手段: playwright同梱chromium(H.264非対応)/playwright同梱ffmpeg(movを demux できず "Invalid data")/JXAの
ObjC.import('AVFoundation')($.AVURLAssetが undefined)/qlmanage(ポスター1枚だけ)。 setContentの about:blank からfile://の動画は読めない(gotoでHTMLを開くこと)。onloadedmetadataはイベント登録前に発火済みでタイムアウトするのでwaitForFunction(readyState>=2)で待つ。- scratchpad(
/private/tmp/claude-501/...)は作業中に消える。素材と中間ファイルは~/work-<案件>/に置く。
.pc / .tablet / .sp を body に付けるサイトは @media に変換して抽出する
- このサイトは
matchMedia("min-width:1025px")等で body クラスを差し替える方式(@media をほぼ使っていない)。素直に抜くとPC固定になりレスポンシブ検品で落ちる。 - 抽出時に
.sp X→@media screen and (max-width:767px){ .sec-<id> X }、.tablet X→768〜1024px、.pc X→ 無条件(PC既定)に機械変換する。これで検品3件とも一発PASS・SP実機も元と同じ縦積みになった。
セレクタから「ページ級文脈」を剥がすときの2大事故
- 祖先ラッパーの直下(
>)ルールを剥がして誤爆させない。.sticky_container>.left{flex-direction:column;height:calc(100vh-16px)}を.leftとして拾い、セクション内の別の.leftに当たって高さが倍(4318px→8553px)になった。→CTX >で始まるルールは丸ごと捨てる(ページ枠専用のため)。剥がしてよいのは子孫(空白)だけ。 - 文脈クラスを正規表現で広く取らない。
\.wp-[\w-]+で剥がしたら WordPress ブロックの.wp-block-button__link{color:#fff}が「残り空=sec自身」に化けて全テキストが白になった。→ CTXは body/html の実クラス列+実在する祖先ラッパー名だけを列挙する。残り空になった時に sec 自身へ寄せてよいのはhtml/body/:root由来のときのみ。
日本語フォント代替は「paltが効くか」で選ぶ(測って決める)
- 元は Adobe Fonts の秀英ゴシック銀(ドメイン紐付けで持ち出し不可)+商用 Canela(CORSヘッダ無しで @font-face 不可)。
- 同一文言・同一 font-size/letter-spacing で幅を実測: 元743px / Zen Kaku Gothic New 827px(palt非対応)/ Noto Sans JP 773px(palt対応)。見た目の「字間が広い」は palt の有無が主因。→ 和文代替は Noto Sans JP、Canela 代替は Cormorant Garamond。
- 測り方: 抽出版ページで
document.createElement('span')に対象要素の computed(fontSize/fontWeight/letterSpacing/fontFeatureSettings)をコピーしてwhite-space:nowrapで幅比較。
Labリポは他セッションが並走している前提で commit する
- 今回
git statusに別作業(アフロート案件)の未コミット62ファイル+ステージ済みentriesがあった。git add -Aはもちろん、素のgit commitも index 全体を巻き込む。 - →
git add <自分のファイル>の後git commit -m "..." -- <同じパス列>(pathspec付きcommit)で自分の分だけ確定する。zshでは変数展開が1語になるのでパスは直接列挙する。 - 集約JSON(sections-data.json 等)はコミットしない。CI(deploy.yml)が
build_core.py → build.py → build_lab.pyを回して再生成するので、他人の未コミットentriesを巻き込むリスクだけが残る。 - CIが
cancelledになるのは後続pushのconcurrency。後続CIがmain全体をビルドするので自分の分も入る(失敗と誤認しない)。
確認①は「code-previewsだけ先にpush」で本番URLで見せられる
entries/を登録しなければ/sections一覧には出ないまま、https://lab.terasu.scale-group.co.jp/code-previews/<id>が200で開ける。localhostでなく実URLで確認①を出せる(2026-08-01 大串FB「プレビューは毎回Chromeで開ける実URLで送る」に適合)。OK後にadd_section.py+サムネ+pushで一覧へ。
その他の小ネタ
- headless
--dump-domはUA判定で body にspが付いた状態のDOMになる。PC版の構造・CSSが要るので playwright(1440幅・実UA)でdocument.styleSheetsから収集するのが正。 - FVが
position:fixed+ 次セクションが重なる設計は、単体化時にsec-<id>{position:relative;padding:8px;background:#e6eef0}+.fv{position:absolute;top:8px;left:8px;width:calc(100% - 16px);height:640px}に置き換えると元の縁(8px余白+角丸8px)ごと再現できる。 - JSが書いた inline style(
transform: translate3d(...)・z-index:-1)が outerHTML に残る。断片からstyle="..."を機械的に落とす(残すと画像が背景に潜って消える)。パララックスは data-y を読む自前JSで再現し、JS無しでも表示は壊れないようにする。
2026-08-18 hitoba-office(work kit)Contact = Alpine.js製フォームの抽出
新規ファイルの commit は「add → pathspec付き commit」まで通す(今回やらかした)
- 既存の学び通り pathspec commit を狙ったが、新規ファイルは untracked なので
git commit -m ... -- <path>がdid not match any file(s) known to gitで弾かれる。 - そこで
git add <3ファイル>→ 素のgit commitにしたら、別セッションが同時に index を触っていて他人の4ファイル(entryと画像だけ・プレビューHTML無し)が混入して push された(=一覧にカードが出て詳細404になる事故)。git add直前のgit diff --cachedは0件だったのに、commit時には増えていた=並走セッションがある前提で「直前確認」は当てにならない。 - 正:
git add <自分のパス列>→git commit -m "..." -- <同じパス列>(add済みならpathspecが効く)。そして commit直後に必ずgit show --name-only --format="" HEADで中身を検証する。混入したらgit rm --cached <混入分>で本番から外す(手元のファイルは消えないので他セッションの作業は壊さない)。
Alpine.js(x-show/x-data)駆動サイトの抽出手順
- 除去してよい:
<template>、x-show="confirming"(確認画面用マークアップ・今回105KB→16KBに激減)、x-show="errors.*"、_formrun_gotcha(ハニーポット。CSSで消えているだけなので残すと「If you are a human…」が表示される)、空のform-file-name。 style="display:none"の一括除去は危険。タブパネル(x-show="activeId===…")まで消えて2枚目以降が空になる。→ パネル系クラスは除去対象から除外し、非表示は自前で持たせる。- 動きは Alpine を持ち込まず元のtransitionクラス(is-enter/is-enter-start/is-enter-end)を付け外すだけの素のJSで再現できる(クラスもCSSも元のまま=見た目完全一致)。
- 初回アニメの発火条件が
html.is-ready .xxx/html:not(.is-ready) .xxxの形。→sec-<id>.is-ready …に寄せ、rAF2回でスコープ根にis-readyを付ける。
Tailwind製サイトのCSS機械抽出(使用クラス照合方式)
- CSSをトップレベルからトークナイズ→ (メディア文脈, セレクタ, 本文) に分解 → セレクタ中の全class(
\.((?:\\.|[^\s>+~,:\[\]()#.])+)で拾ってバックスラッシュを除去)が使用クラス集合に含まれる時だけ採用。Tailwindのエスケープ(sm\:\[--text-fluid-var\:18\])もこれで一致する。 - 落とし穴3つ(全部これで崩れた):
1.:root{--…}は class を含まないので素直に書くと落ちる → カスタムプロパティ定義だけは root系セレクタでも拾う(rem基準・fluid計算の土台)。
2.<main>自身のクラス(l-wrapper等)はfind_all(True)に入らない → 使用クラス集合に足す。忘れるとラッパー余白が消える。
3.is-ready/is-enterなどJSが付ける状態クラスは使用集合に無い → 「使用クラスと同居している時だけ許可」(単独.is-enter{}はページ遷移用で別物なので拾わない)。 - preflightを捨てる時、
:before,:after{--tw-content:""}を移植し忘れるとcontent:var(--tw-content)の疑似要素が全部消える(selectの矢印が消えた)。select{appearance:none}・button,input,select,textarea{font:inherit}も同様にスコープ版で残す。
「完全再現」は目視でなく実測で証明する
- playwrightで元サイトと抽出版を同じ幅で開き、主要要素の rect(root相対)とページ高さを突き合わせるのが速くて確実。今回は Δy 全項目0.0px・高さ差 −1.0〜0.0px(4タブ全部)まで詰めた。
- 単体プレビューは body の既定margin 8px 分だけ幅が狭くなる(1380→1364)。既存セクションも同じなので許容。ホストに貼れば差は出ない。
- 契約フォント(FontPlus=TWK Lausanne / FOT-ニューセザンヌ)はドメイン紐付けで持ち出し不可 → Inter+Noto Sans JP に差し替え(オリジナル化の一環)。
2026-08-18 groxi株式会社 お問い合わせフォーム(contact-form-progress-saki-0817)
白基調セクションは必ずサムネ採用ゲートに落ちる(誤検知・原因の見分け方)
shoot_section.mjsの不採用条件はcore/tools/shot_common.mjsのisPoorShot()。3本目の
flatRatio >= 0.92 && edgeRatio < 0.03(画面の9割超が同一色) が白フォーム系を必ず巻き込む。- 今回の実測: 色24種 / 輪郭1.89% / 最頻色97.6% → 不採用。
上余白を104→48→0pxに変えても flatRatio は 97.6→97.6→97.5% とほぼ不変=トリミングでは絶対に抜けられない。
白地に1px罫線のフォームは、忠実に再現している限り数値が下がらない(参考元のgroxi自体が同じ白基調)。 - 「落ちた=壊れている」ではない。切り分け手順を先にやる:
1. 候補クロップ(1440×1080)を自前で撮って目視する。中身が写っていれば描画は成功している。
2.measureShot()を直接呼んで colors / edgeRatio / flatRatio を出す(measureShot(page, buf)の
第1引数はディレクトリではなく Playwright の page。ここを間違えるとscratch.evaluate is not a function)。
3. flatRatio が余白調整で動かない=デザインが白基調と確定 →--forceを使ってよい。使った事実は必ず報告する。 - 逆に flatRatio が余白で大きく動く/文字数0なら、それは本物の撮影失敗なので --force してはいけない。
並行セッションの git add -A に成果物を巻き込まれる(実害あり・対策必須)
- 同じ
~/dev/terasu-labで別セッションが走っていると、向こうのgit add -Aがこちらの未コミットの
code-previews / entries / img を丸ごと巻き込んでcommit+pushする。今回は161ファイルの一括コミットに
自分のHTMLが入り、続く別コミットで entry+サムネ3枚も入った。 - さらに悪いことに、向こうが「プレビューHTMLが未コミットだから本番404になる」と誤解して
Revert accidental inclusion of …でこちらの entry とサムネだけを消した(HTMLは既にコミット済みだった)。 - 対策(次回から必ず):
add_section.pyを打った直後に、自分の4〜5ファイルだけを個別git add→ 即 commitする(放置しない)。git add -Aは絶対に使わない。パスを列挙して add する。git addが何もステージしない時は gitignore を疑う前にgit ls-files <path>で「既に他人にコミットされていないか」を見る。- revert された時は
git log --oneline -- <ファイル>で前提が正しいか裏取りしてから入れ直し、
commit message に「なぜ revert が誤りか」を書いて再発を止める。 sections-data.jsonは CI(.github/workflows/deploy.yml)がbuild_core.py→build.py→build_lab.pyで
再生成するので、コミットしないのが正解。ローカルで build すると他セッションの未コミットentryまで混ざる。
抽出メモ(groxi.jp/contact のフォーム)
- 見た目の要はCSSではなく配置トリック: 同意チェックの枠は
table tr:first-childを
position:absoluteで浮かせ、.inner{margin-top:280px}(SPは300px)で空けた隙間に置いている。
PCは#vue-form、SPは.innerが基準(SPだけ.innerにposition:relativeが付く)ため、
PC=top:125px/ SP=top:-272pxと符号が逆になる。ここを読み違えると上下がまるごとズレる。 - ステップ表示の数字は契約フォント Campton(自社配信・持ち出し不可)→ Poppins 700 に差し替え。
この差でだけ実測が ±1px ズレる(01の字幅 85px→84px)。他は全項目0px一致。 - チェックの動きはVueが
label.isCheckを付けていただけ → 素のJSでis-checkedをトグルすれば同じ挙動。
label:afterのtransform:scale(0)→scale(1)は元CSSのまま。
STUDIO製サイトの抽出手順(NewsPicks corp / 2026-08-18・再現性高い)
STUDIO(studio.design)製サイトは他CMSと構造が違うので、この型どおりに進めれば1発で完全再現できる。
見分け方=DOMに class="sd appear" と data-s-<UUID> 属性が並んでいたらSTUDIO。
- CSSは2か所に分かれている。両方を拾わないと崩れる。
- ① ページ内
<style>(今回85KB)= 各要素の実スタイル。セレクタは.sd[data-s-<UUID>]の形。 - ②
/_nuxt/entry.*.css(今回25KB)=.sdのベース挙動(flex/縦並び/transition)。
ここを抜くと全要素が横並びになって崩壊するので必ず拾う。ただし:where(.render-canvas) …の
巨大リセットが含まれるため、union セレクタは「,」で分解して1本ずつ判定して要る物だけ残す
(セレクタ全体をまとめて判定すると、テーブル/input用まで巻き込むか、丸ごと落として崩れる)。 - DOM構造を器ごと再現する。
div.render-canvas > div.StudioCanvas > div[ページルートUUID]の3層が
無いと ① のCSSが効かない。セクションだけ抜いても再現しない。 - フォントはCSS変数経由(
font-family: var(--s-font-298b8a52)→:rootでMontserrat等に解決)。
:rootの変数定義ブロックを拾ってsec-<id>{…}に寄せる。拾い忘れるとフォントだけ別物になる。 - 画像は lazy-load でDOMダンプからは絶対に取れない(
srcが空SVGのプレースホルダのまま・
data-srcも無い=JSが差し替える方式)。実ブラウザで開き、各<img>を1枚ずつ
scrollIntoViewしてからcurrentSrcを回収する(まとめてスクロールでは発火しない画像が残る)。
UUID→実URLの対応表を作り、抽出後のHTMLに差し込む。 appearアニメは class 付け替え方式。.appear{opacity:0}→ 表示時にappear-activeへ。
IntersectionObserver 5行で同じ挙動を再現できる(元のJSは不要)。- UUIDのリネームは2段階。
data-s-<UUID>だけでなく、CSS変数名(--gap-h-<UUID>等)にも
裸のUUIDが埋まっているので、属性を置換したあと「裸のUUID」も同じトークンへ置換する。
なお画像URLに含まれるUUIDは残ってよい(アセット名=参考元URLのまま使うルール)。
見出しだけのセクションはサムネ検品に落ちる(誤検知・--force が正解)
shoot_section.mjs は「色数/輪郭量」で中身が写っているかを見るが、白地に見出し1行+バーだけの
セクションは本物でも "中身が写らない" 判定になる(今回 services-heading が該当)。
実際に撮れているかを目視で確かめてから node tools/shoot_section.mjs <id> --force で通す。
中身が本当に空の時との区別は人がやる(forceを常用しない)。
2026-08-18 音更電設(otoden.co.jp)— sns-info / intro-poem
「ずれる」の真因は属性セレクタ [class*=…] の抽出漏れ(最重要・再発必至)
このサイトの共通レイアウトは .c-inner-md{} ではなく [class*=c-inner].-contents-xl{} の形で書かれていた。
クラス名(\.([\w-]+))だけでセレクタ判定しているとこの種のルールが丸ごと落ちて、中身が左寄せ・
1カラムになる(=大串の「ずれてる」FB)。単独プレビューでも一見それらしく見えるので気づきにくい。
- 対策: 抽出フィルタに \[class[*^|~]?=["']?([\w-]+) を足し、部分一致トークンを USED/MODS へ前方一致で展開する。
- 検証は目視でなく機械で: 本番と自作を同じビューポート幅で開き、主要要素の
getBoundingClientRect() と fontSize を JSON で並べて比較する。今回これで
「内側コンテナ x=120 / 幅1200 / カード577.5×278」まで一致を確認できた。目視だと絶対に取りこぼす。
rem基準は --rem 変数化して calc() へ機械変換する
html{font-size:62.5%}+画面幅連動(calc((10/1440)*100vw) 等)のサイトは、remがルート基準なので
sec-<id> にスコープ化できない。そのまま貼るとホストHPのルート値で全サイズが狂う。
- 型: html の font-size を sec-<id>{--rem:…} に変換 → CSS全体の \d+rem を
calc(var(--rem) * N) に一括置換。メディアクエリごとの --rem 再定義もそのまま効く。
動画FBの扱い(ffmpeg無し環境)
- 動画は Desktop 直下ではなく
~/Desktop/名称未設定フォルダ/に入っていることがある。
見つからなければmdfind -name "画面収録 <日付>"で探す(「ファイルが無い」で止まらない)。 - ffmpegが無いので、
python3 -m http.serverで配信 → ブラウザで再生。ただし
python http.server は Range 非対応でシークできない(currentTimeを代入しても 0 に戻る)。
→fetch(url).then(r=>r.blob())で blob化してURL.createObjectURLを src に入れ直すとシークできる。 - 動画は最初に必ず見る。今回は見ないまま進めて「動画のは?」と指摘された。
さらに大串の指す箇所が1つ上のセクションだった(波カーブは境界装飾で、本命はhome-introの
「背景stickyで詩が流れる」演出)。動画=別セクションの可能性を最初に潰す。
ブラウザペインのスクショが真っ白になる時は headless Chrome で確認する
ページは正常なのにペインのキャプチャだけ白飛びすることがある(DOM計測では要素が正しく存在)。
「壊れた」と早合点せず --headless=new --screenshot で実レンダリングを撮って確かめる。
他セッションの未コミット作業がある時の格納(データ事故の防止・恒久)
リポに他セッションの未コミット作業(今回は entries 16件・img 56件・tmp スクリプト多数)が
残っていることがある。ここで git add -A はもちろん、build.py の集約JSONをそのまま commit するのも危険。
sections-data.json は entries ディレクトリ全件から再生成されるので、他人の未完成分(画像が未追跡)まで
一覧に載り、本番でサムネ404になる(過去に caafb4c で同じ理由の revert 実績あり)。
- 型: 集約後に 「git管理済みの entries」+「自分の新規id」だけに絞ってJSONを書き戻す →
git add は自作ファイルだけを明示列挙 → commit。他人のファイルには一切触らない。
今回はこれで12件を除外して公開した。
- git pull --rebase は他セッションの unstaged 変更があると失敗する。
git fetch して origin/main...HEAD の差を見て、origin側が0なら pull 不要(無理に autostash しない=他人の作業を触らない)。
- もっと簡単で確実な型(2026-08-18 実地)=集約JSONは commit しない。
.github/workflows/deploy.yml が CI側で core/build_core.py → build.py → build_lab.py を毎回実行するので、
sections-data.json はローカルで commit する必要が無い(=他人の未完成分が混ざる経路をそもそも作らない)。
ローカルでの build は「登録できたかの確認用」と割り切って、作業ツリーに置いたままにする。
- git add すら競合する(他セッションが同時に add していて、自分の6ファイルだけ add したのに
ステージに他人の29ファイルが乗っていた)。→ ステージを介さない git commit -m "..." -- <パス列挙> を使う。
パス指定 commit はインデックスの状態を一切変えずに指定パスの作業ツリー内容だけを確定できるので、
他セッションのステージング作業を壊さない。git reset で剥がそうとするのは逆に危険。
- 同時進行だと push も他セッションが先に済ませていることがある(git push が Everything up-to-date)。
慌てず git merge-base --is-ancestor <自分のSHA> origin/main で自分のコミットが載ったかだけを機械判定する。
CI run も同様に自分の分が cancelled になり後続 run に引き継がれるので、最新 run を watch すればよい。
抽出したHTML/JSでハマる2つの「元サイトでは成立していた」罠(2026-08-18)
<p>の中に<div>は静的HTMLでは書けない(パーサが p を自動で閉じ、中身が外に出る)。
元サイトは GSAP SplitText 等が JSでDOM挿入しているので成立していただけ。DOMダンプをそのまま貼ると
文字が全部はみ出して二重表示になる。→ char入れ物の<p>を<div>に変換して解決(見た目は不変)。- CSSの初期非表示(
transform:translateY(-100%))を残すとGSAPと二重管理になる。
gsap がアニメ終了時にインライン transform を消した瞬間、CSS側が再適用されて要素が消える。
→ 初期状態は CSSから外し、JS側のgsap.set(el,{y:'-100%'})に一本化する。
node が PATH に無い(このPC)
node 未インストール。export PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH" で
check_responsive.mjs / shoot_section.mjs が動く(v24)。毎回これを付ける。
reninc.jp(株式会社ren)で確定した5つの罠(2026-08-18・fv-video-scroll / impact-cards-stack)
-
CSS mask画像とWebフォントは CORS で落ちる(最重要・見た目が消える)
curlは 200 なのに、ブラウザからはrequestfailed。丸ボタン・矢印・波形が全部透明になり
「要素はある(visible / サイズあり)のに何も描かれない」状態になる。通常の<img>/background-imageは
CORS 不要なのでそのまま参考元URLでよい(=ルール5は守れる)。mask と @font-face だけ data URI に埋め込む。
→ 切り分けは playwright のpage.on('requestfailed')が一発。目視で悩まない。 -
body.is-xxxのような状態クラス付きルールをスコープ化してはいけない
body{opacity:0}(ローディング演出)やbody{position:fixed;overflow:hidden}(スクロールロック)が
無条件で.sec-<id>に適用され、セクションが真っ白・スクロール不能になる。
→ 抽出器は 素のhtml/body/:rootだけ採用し、直後に.#[:が続くものは捨てる。
保険として、ルート自身に効くルールからは position/overflow/opacity/height/width 等を落とす。 -
rem基準がビューポート連動のサイトは
--sec-remに写して rem を絶対値化する
ren はhtml{font-size:calc(var(--vw)*16/1440*100)}。スコープ内に font-size を書いても
rem は root 基準のままなので効かない。Nrem→calc(var(--sec-rem) * N)に機械変換し、
--sec-remを元と同じメディアクエリで定義する。@media の条件式内の rem は変換しない(var不可)。 -
sticky FV は「後続コンテンツ」が無いと動かない(大串FB「動きが違う」の真因)
position:stickyの可動域=親の高さ−自身の高さ。セクション単体では 0 になり、
動画が画面に貼り付かず上へ流れて出ていく。→ 元サイトでFVの次に来るセクション(renなら.p-home-lead)を
同じ実装のまま置く+元のパララックス(.js-parallax/ num=0.3)も入れると実サイトと完全一致する。 -
同じ変数がSPとPCで別値になっている(大串FB「下の形が違う」の真因)
波形の高さ--_topは SP2.375rem/ PC6.4375rem。SP値のまま出すと
画像(2880×154)の幅が711pxにしかならず波が小さく中央に寄る。メディアクエリ違いの値を必ず拾う。
動き系セクションの検証は「実サイトと数値で突き合わせ」が最速・最確実(2026-08-18)
スクショ比較より、playwright で実サイトと再現版を同時に開き、scrollY 別に computed 値を並べる方が速く確実。
ren では width / border-radius / padding-top / コピーの opacity / 動画の画面Y位置を 6 点で比較し、全一致で確定した。
「動きが違う」と言われたら、まずこの表を作る(画像を何枚も撮るより安い)。
作業ファイルの置き場所とローカルプレビュー(2026-08-18)
- スクラッチパッド(/private/tmp/...)は作業途中で消えることがある(抽出器もCSSも全部消えた)。
→ 作業ファイルは~/dev/<案件>_work/に置く。消えて困るものを /tmp に置かない。 - 提示前にローカルサーバーを立て直す。古い
http.serverプロセスが残っていると大串の画面で「見れない」になる。
lsof -ti:8080 | xargs kill -9→ 起動し直し →?v=<時刻>を付けたURLで渡す(キャッシュ回避)。 - 大串が送ってくる .mov は Chromium で seek できないことがある(画面収録のコーデック)。
フレーム抽出に時間を使わず、実サイトを直接操作して比較する方が速い。
Tailwind+Alpine+GSAP サイトからの抽出は「ブラウザで実マッチCSSを機械収集」が正解(2026-08-18・hitoba-office/work kit)
Tailwind の任意値クラス(sm:[--text-fluid-var:49] 等)はCSSをテキスト検索しても拾いきれない(\[ エスケープだらけ)。
実サイトを開いて document.styleSheets を全走査し、対象要素に matches() するルールだけ集めるのが最速・漏れゼロ。
- @media / @supports は再帰で辿り、conditionText を保持したまま出す
- :hover / ::before は擬似部分を剥がしてから matches() に掛ける(そのままだと一致しない)
- cssText はグラデーション等の値が欠ける(background-image: ; になる)。該当ルールは元CSSファイルから正規表現で拾い直す
- :root 変数(--base-content-w 等)は @media 別に定義が変わるので、PC幅にリサイズしてから computed を取る
74ルールまで絞れて、完成品は331行に収まった(外来CSS混入ゼロ)。
有料フォントは外部から読めない=代替に切り替える(2026-08-18)
TWK Lausanne は 200 で落とせるが Access-Control-Allow-Origin が無いため他オリジンからは適用されない。
curl -H "Origin: https://lab.terasu..." -D - でCORSヘッダを先に確認し、無ければ近い系統の代替に置換する
(TWK Lausanne / Helvetica系ネオグロテスク → "Helvetica Neue", Helvetica, Arial)。画像と違いフォントはCORS必須。
GSAP ScrollTrigger は素のJSで数値一致まで再現できる(2026-08-18)
scrollTrigger:{start:"top 90%", end:"bottom 10%", scrub:0} の進行度は
p = (innerHeight*0.9 - rect.top) / (rect.height + innerHeight*0.8) を 0..1 にクランプするだけで一致する。
CDN依存を持ち込まずに済む(ホストHPのGSAPと衝突しない)=貼っても安全。実測で期待値と完全一致を確認してから出す。
ブラウザペインが隠れていると rAF が止まる=「動かない」と誤診する(2026-08-18)
requestAnimationFrame を使った視差は、ペイン非表示だとコールバックが走らず transform が固まる。
スクショを1枚挟むとフレームが進むので、scrollTo → screenshot → transform を読む の順で測ること。
同じ理由で 1枚目のスクショは前フレーム(画像未描画)が返ることがある。もう1枚撮れば正しい絵になる。
並行セッションの git add -A に自分のファイルが巻き込まれる(2026-08-18・実害あり)
別セッション(同じPCの別作業)が git add -A すると、まだ確認①前の code-previews/<id>.html が勝手にコミット・pushされる。
今回は中身が完成版だったので実害なしだったが、未完成版が本番に出る事故になり得る。
- 症状:git add しても git status に出ない=既にHEADに入っている(git log -- <file> で犯人のコミットが分かる)
- 対策:terasu-lab では自分のファイルだけを明示的に git addする。git add -A は絶対に使わない
- sections-data.json は CIが build.py で作り直す(.github/workflows/deploy.yml)のでコミットしない。
ローカルで build.py を回すと他セッションの未push entries が38件混入する(=実体の無いカードが本番一覧に出る)
11. ** 同一PCで別セッションが並行作業していると git add -A が事故になる(2026-08-18 実地)。今回 git status が179件差分・他セッションの未コミット作業(既存セクションのpadding調整・未完成entry6件)が混在していた。→ 自分のファイルだけを明示 git add(entries/code-previews/img の3点セット×件数)。また集約 sections-data.json はコミットしないのが安全(CIの deploy.yml が core/build_core.py→build.py→build_lab.py を毎回実行して再生成するため。ローカル版を入れると他セッションの未コミットentryを巻き込み、preview不在で本番404になる=過去commit caafb4c と同じ事故)。
12. CI runが cancelled でも失敗とは限らない。並行pushがあるとconcurrencyで古いrunがキャンセルされる(今回10分間で7回入れ替わった)。gh run watch の X 表示に釣られず、conclusion を API で見る+最終判定は本番URLの200と一覧掲載で行う。
13. 自分の作業ファイルが別セッションのコミットに巻き込まれることもある(今回5件のpreviewが先に他コミットへ入っていた=git log -- <file>で「既存」に見えてid衝突と誤認しかけた)。中身の<title>とsecクラスを見れば自分の物か判別できる**。
2026-08-17 AI model(ai-model.jp)CASE STUDY
「傾いたカードが流れて巨大文字が重なる」は WebGL=抽出不可(判定手順)
ai-model.jp トップの #comparison。DOMを見ても文字が無いので探し回りがちだが、3点を測れば即断できる。
1. document.querySelector('.comparison__canvas canvas').getContext('webgl') が返る=WebGL描画
2. 見えている画像の元 .comparison__images が visibility: hidden(canvasに渡す下敷き素材)
3. 画面に見える文字(例「TYPE 001」)が DOM・CSS content・画像ファイル名のどこにも無い
→ この3点が揃ったら その箇所は諦めて大串に3択(別の箇所/近似/やめる)を出す。全ページ grep に時間を溶かさない。
流体rem サイト(html{font-size:○vw})の移植は --sec-rem 方式
ai-model.jp は html{font-size:0.0694444vw}(PC=1440基準)/ 599px以下 0.2777778vw(SP=360基準)。
remは常にdocument rootを見るので sec-<id>{font-size:10px} では直らない。
→ 全 Nrem を calc(N * var(--sec-rem)) に機械変換し、--sec-rem をPC/SPで定義する。
JS側の '…rem' 文字列も同罪(ホストのroot依存で移動量が16倍になる)。今回 -360rem は 1440px基準で 25vw なので
window.innerWidth * 0.25 の px に直した。文字列長を変えても壊れない利点もある。
font-feature-settings:"palt" が抜けると日本語が1行増える
body側の指定は「#how配下にマッチするセレクタ」だけを拾う抽出網から漏れる。
本文の高さが 108px→129px(5行→6行)になり、連鎖でセクション全高が67pxズレた。
→ 数値照合で ffs の差として出る。ルートに font-feature-settings:"palt" を補完して一致。
同様に body 由来の font-size:1.4rem / line-height:1 / color:#000 もルートに移す(img高さ500→506px の犯人)。
リセットをスコープ化すると本体CSSに勝ってしまう
.sec-x.sec-x :is(p,img,…){margin:0}(0,2,1)は .sec-x .caseMedia__ja(0,2,0)より強いので、
セクション本体の margin-left が消えて幅がズレる。
→ 本体セレクタも二重クラス .sec-x.sec-x <sel> に統一する(ホストCSSにも勝てて一石二鳥)。
置換は「長い順」に当てる(短い社名が長文を殺す)
("三越伊勢丹","ハルカ百貨店") を先に当てると、後段の本文まるごと置換がマッチしなくなり、
本文中の AI model ISETAN STUDIO MIオンラインストア が残ったまま公開されかけた。
→ sorted(TEXTS, key=lambda x: -len(x[0])) で長い順に適用。検証grepも社名だけでなく本文内の固有名詞を対象に含める。
並行セッションの一括処理に @font-face を破壊された(実害あり・要警戒)
code-previews/ に置いた自分のファイルが、別セッションの一括処理で
base64埋め込みフォント4つ→同一データの3つに差し替えられた(書体もレイアウトも崩れ、サムネが素のHTMLに見えた)。
- 気づき方:grep -c '@font-face' とファイルサイズが自分のビルド結果と合わない/git diff がフォント行だけ±
- 対策:サムネを撮る直前に自分のビルド版をコピーし直す。撮影後もサイズとフォント数を確認してからcommitする
2026-08-18 NewsPicks Brand Design(ページネーション / イベント一覧カード)
参考元のSVGを url() で直リンクするとCORSで消える(矢印が出ない)
mask-image/background-image に参考元ドメインのSVGを絶対URLで指定すると、
localhost から見たとき CORSブロックで画像だけ描画されない(要素自体は存在するので気づきにくい)。
- 気づき方:疑似要素に background-color:red を一時的に当てると赤は出るのに画像が出ない/
console に blocked by CORS policy: No 'Access-Control-Allow-Origin'
- 対策:そのSVGだけ data URI にインライン化する(urllib.parse.quote でエンコード)。
ラスタ画像(png/jpg)は <img> なのでCORS無関係=ルール通り参考元URLのままでよい。
影響を受けるのは「CSSから読むSVGアイコン」だけ。
疎なセクション(ページネーション等)はサムネ検品を誤検知で落ちる
shoot_section.mjs は「色の種類×輪郭の量」で中身を測るので、
白背景+小さい要素が両端に散る構成は 中身が写らない(色8種/輪郭1.5%) で落ちる。
- 先に効くのは上下余白の適正化(元サイトの余白は"一覧との間隔"で、単体セクションでは過大)。
ただしそれでも輪郭量は足りないことがある。
- 最終判断は --force で撮って実物を目視する(FORCE=1 は効かない。引数 --force が正)。
既存の circle-pagination 等も同じくらい疎で通っているので、写っていれば採用でよい。
- 逆に「落ちた=必ず不良」ではないので、目視せずに諦めないこと。
他セッションの未コミットが大量にあるPCでは git add -A が致命的
このPCには常時 45 modified + 129 untracked(他セッションの作業途中)がある状態。
- 自分のファイルだけをフルパス列挙して add する(code-previews / entries / img/sections の3種×各id)。
- sections-data.json は add しない。CIが core/build_core.py → build.py → build_lab.py を
最新mainから実行して生成するため、ローカル版をコミットすると他人の未コミットentryを巻き込む。
- git pull --rebase は unstaged changes で失敗する。git fetch して behind 0 なら rebase 不要。
同時作業が多い日はCIが cancelled になる(失敗ではない)
concurrency deploy-terasu-lab(cancel-in-progress:false)のキューに複数溜まると、
待機中の自分のrunがキャンセルされる。ただし後続runは自分のcommitを含むmainからビルドするので実害なし。
- 確認手順:git merge-base --is-ancestor <自分のsha> origin/main で含まれていることを確認し、
最新runの成功を待つ。自分のrunがXでも慌てて再pushしない。
大串の指示で背景を変えたら、サムネの見え方まで確認する
「背景白にして」→ 一覧カードでほぼ真っ白になりやすい(セクションは画像で選ぶので体験に直結)。
今回は実物が視認できる範囲だったのでそのまま採用したが、白背景×疎な構成は要注意。
動画しか無いときは ffmpeg で全フレームをコンタクトシート化して特定する
ffmpeg は未インストールでも python3 -c "import imageio_ffmpeg; print(imageio_ffmpeg.get_ffmpeg_exe())" で同梱版が使える。
fps=5 で抜き→12枚ずつ連結して一覧化すると、どのセクションを指しているか一発で確定できる。
(今回:1本目の判断では「スクショと同じページネーション」と誤読み。全フレーム化で
スクロールで通過するイベントカード一覧が本命だと分かった。動画は必ず全フレームを見る)
2026-08-18 JILLION(jillion.co.jp/business) 4件抽出(WordPress自社テーマ=素直な型・ただし置換事故が多発)
対象: mission-parallax / mission-circlegraph / businessfield-cards / roadmap-timeline。
サイト構造は素直(headless --dump-dom でDOMが取れ、CSSは common.css + business.css の2本)。
にもかかわらず 「置換」と「詳細度」で4回踏んだ。次回は下記を最初から組み込む。
置換事故(3つとも1回のビルドで同時発生した)
- クラス改名は "スコープ接頭辞を付ける前" にやる。後からCSS全文に
.replace()を掛けると
scope名そのもの(.sec-roadmap-timeline-…)が置換されて全ルールが死ぬ。しかもse+c-roadmapのような
部分一致まで拾う(.sec-roadmapの中にc-roadmapが含まれる)。→ セレクタ単位で改名→その後スコープ付与。 - 改名置換からURLを退避する。
image_roadmap_01.jpg→image_steproad_01.jpgになり画像が全滅した。
src|srcset|data-src="…"と CSS のurl(…)をプレースホルダに逃がしてから置換し、最後に戻す。 - 検証greps も scope名を除いてから数える(
sed "s/sec-<id>//g"してから grep)。しないと
「旧クラス名97件残存」のような偽陽性で無駄に直しに行く。
詳細度(カクトクの教訓をそのまま適用すると逆に壊れる)
- スコープ用リセットを
.sec-x.sec-x :is(h1..h6,p,…){font:inherit}にしてはいけない。(0,2,1) が
セクション本体の.sec-x .sectitle-en{font-family:Bebas Neue}(0,2,0) に勝ってしまい、見出しが本文フォントになり
レイアウトごと崩れた(元サイトは:where(:not(...))=特異性0なので絶対に勝たない設計)。
→ リセットは.sec-x :where(...)の (0,1,0) に固定。ホストHPの素の要素指定 (0,0,1) には勝ち、
セクション本体の.クラス指定には負ける=元のカスケードが保たれる。
※2026-08-16の「二重クラスで強く」は p/img など限定的な打ち消しの話。要素リセット全体に広げない。 - 出現アニメのJSゲートは「初期状態」と「.is-shown」の両方を同じ
.sec-x.js-animに寄せる。
片方だけゲートすると詳細度が逆転して「JS無しで永久に消える」or「JSありでも出ない」になる。
両方同じ (+1クラス) にすれば、JS無し=どちらも当たらず完成形/JSあり=原本通り。実測でclip-path:noneを確認する。
抽出漏れ
- 状態クラス(
.is-hover等)のCSSはel.matches()では絶対に拾えない(今のDOMに付いていないため)。
疑似要素/疑似クラスを剥がすのと同じ場所で\.is-[\w-]+も剥がしてから判定する。
これを忘れてリンクの「1語ずつ入れ替わるホバー」が丸ごと落ちていた(静止画では気づけない)。 - 親グリッドから切り出した要素は、親の幅制約も一緒に持ってくる。
.mission-graphは親.mission{width:118.4rem}
前提の設計で、単体にすると横に広がり 中心ロゴが背景画像の同心円からズレる(背景の点線円は画像に焼かれている)。
その他
- rem基準は Vivoo と同型の流動設計(
html{font-size:calc(vw/144)}PC //40SP)→ 全rem値を
calc(N * var(--sec-rem))に機械変換。--sec-vwをJSでclientWidthにすると原本と完全一致(無ければ100vw)。 - Morisawa等ライセンスフォントは Noto Sans JP に差し替え(0720のjillion既存2件と同じ扱い)。
family変数を差し替えるだけでは太さが出ないので、--font-family-*-db/-b/-r/baseを使う宣言に
font-weight を機械注入する(db/b→700, r→400, base→500)。 - 外部スプライトSVG(
<use xlink:href="別ドメイン#id">)は別ドメインでは描画されない。使うsymbolだけ
インライン化し、idもico-*にリネームする(参照側だけ直すと定義が衝突する)。 - 検証は「原本と抽出版の全要素を getBoundingClientRect で総当たりdiff」が最速。ツリー階層を揃えるため
抽出版は.sec-<id> > *から測る(wrapper分ずれて全要素が差分に見える)。
最終的に残る差分が font-weight だけになれば完全再現とみなせる。
同時作業まわり(他セッションが100件級の未コミット作業を抱えていた日)
sections-data.jsonはコミットしない。CIがbuild_core.py→build.pyで再生成する。
他セッションの未コミットentriesを含んだ集約JSONを push すると本番に実体の無いカードが出る。
git addは自分のidのファイルだけを列挙する(git add -Aは当然禁止)。- CIが
cancelledでも失敗ではない。他セッションの連続pushで concurrency により中断されるだけで、
自分のcommitは後続runでデプロイされる。→ runを追いかけず本番URLの200と/sections掲載で確認するのが正。
環境
- scratchpad がセッション途中で消えることがある(今回2回消えた)。中間スクリプト・抽出JSONは消える前提で、
成果物(code-previews/.html)は早めに~/dev/terasu-lab側へ書き出しておく。消えた後は
生成済みHTMLを直接編集する方が速い*(余白追加はこの方式で対応)。 - .mov はこのPCでは中身を読めない。ffmpegなし/qlmanageは先頭1フレームのみ/Playwright+実Chromeでも
file://・http:// どちらもMediaError code 4。受領直後にqlmanage -t -s 1400でフレーム退避が必須
(Desktopの元動画も作業中に消える)。先頭フレームだけでも「どのセクションか」の特定には足りた。
2026-08-18 jillion.co.jp から5セクション(news / header / business / recruit / stairnav)
15の訂正 — .mov は Playwright で読める
- 前回「.movは読めない」と記録したが読める。手順は3つだけ:
① 動画とplayer.html(<video id=v>だけのページ)を同じディレクトリに置きpython3 -m http.serverで配信
②page.gotoでその player.html を開く(動画と同一オリジンにするのが肝)
③v.currentTime = t→onseekedを待ってelement.screenshot()
canvas.drawImage→toDataURLは Tainted canvas で必ず失敗する。要素スクショなら通る。
これで6フレーム×4本を1分で抜けて、どのセクションかを正確に特定できた(BUSINESSカード/RECRUIT/CONTACT/SPのFVナビ)。
rem が可変なサイト(jillion系)の移植
html{font-size:calc(var(--vw)/144)}(SPは/40、1440px以上は 1440/144=10px 固定)という設計。
rem は html を参照するのでスコープ化できない →--uに置き換え、全数値をcalc(var(--u)*N)にする。
3分岐(--u:calc(100vw/144)/@min1440{10px}/@max767{calc(100vw/40)})を必ず書く。--border-radiusや--font-size-baseは@media内の:rootで定義されている。
トップレベル:rootだけ拾うと角丸が消える(実際に消えた)。@media内の :root も集めてスコープに再現する。- 本文の継承値(PC
1.4rem/行間2、SP1.3rem/行間1.75)は body から来る。sec ルートに書かないと
全要素が 16px/normal になって全体が狂う。
文字分割アニメ(GSAP SplitText)は静的に再現できる
- app.js の
qi({...})呼び出しを grep すれば設定が全部わかる。jillion の場合:
.c-ttl→words /.c-link→words(disableOnMobile) /l-header-navi・mv-navi→chars(baseClass指定)。
blockClassは lineClass の-lineを-blockに置換した名前。clone は-block-clone。
→ 同じ入れ子(block > line > char)を静的HTMLで書けば JS 無しで動く。hover はis-hoverを残しつつ
:hoverを併記(元はJSがclassを付けるので、そのままだと静的HTMLで動かない)。 - headless の
--dump-domでは hover系の分割は走っていない(data-split-hover-initialized="true"だけ付く)。
実DOMが欲しければ Playwright でpage.hover()してから outerHTML を取る。
「完全再現」を機械で言い切る方法
- 元サイトと抽出版を同条件(1440/390・Playwright)で computed style 総当たり比較が効く。
今回ヘッダーは 差分 0/105。残差はダミー化した英字の字形幅と 0.4px 以下のサブピクセルだけ、と説明できる。 - ホバーの一致は静止画では判定できない。
is-hover付与後にtransformを時系列サンプリング
(100/200/300/450/650/900ms)してカーブごと突き合わせる。元-8.2→-24.2→-29.1→-30.9→-31.2に対し
自作-4→-22.6→-28.6→-30.8→-31.2で一致と判定できた。 - 比較セレクタは必ずセクション配下に限定する。
document.querySelector('.c-btn')はページ先頭の別ボタンを拾い、
「色が違う」と誤検出した(実際は一致していた)。
サムネ検品に構造的に落ちるセクションがある
- ヘッダー/ナビのようなミニマルな帯は
色数・輪郭率の閾値を超えられない(黒地に白文字だと色6種)。
既存の header 系サムネも同じ見え方なので、高さを詰めて密度を上げる→それでも落ちるなら--force+目視でよい。
ただし詰めてよいのは単独表示用に自分が足した余白だけ。元サイト由来の余白は動かさない。
貼っても崩れない形にするための追加ポイント
- FV系は
.mv-cover.mv-logo.mv-naviがposition:fixed。そのまま貼るとホストHP全体に居座るので
セクション内absoluteに変える(見え方は同じ)。変更点は必ずコメントに残す。 - 元サイトは FV(固定背景)の中に次のブロックを内包していることがある(jillion は
.mvの中に.philosophy)。
</section>の対応だけで切り出すと後続セクションが丸ごと混入する。切り出し後に不要な子ブロックを削る。 - SVGアウトライン化されたタグライン・社名は文字を差し替えられない。ダミー化できない以上そのセクションからは外す
(今回 FV のタグラインを外し「階段ナビだけ」にした ← 大串の指示とも一致)。
反映確認
- push直後の
/sectionsは前の版が出る。今回 283件(未反映)→ 数分後 291件。
gh run watchが success でもデプロイ反映にラグがあるので、件数か自分のidで判定する。
2026-08-18 表参道髙柳矯正歯科(j-ortho.jp)4セクション
FVに重なるヘッダーは「暗幕レイヤー」も一緒に抜く(大串FB「メニューがない」)
- 透過ヘッダーを抜いたのにメニューが読めない=暗幕の抜き漏れ。j-ortho の FV は
<video>の上に<div class="layer">(black.svg=全体30%の黒+上部240pxのグラデ)が乗っていて、
白いナビ文字はこの1枚で読ませている。動画だけ下地にすると明るいフレームで文字が完全に溶けて
「メニューがない」と見える。→ ヘッダー/FV系を抜くときは、下地の動画・画像だけでなく
重なっている layer / overlay / ::after の暗幕を必ずセットで持ってくる。
チェック法: 明るいフレームで止めて白文字が読めるかを実機スクショで見る(測定値だけで判断しない)。
position:fixed の背景動画をセクション単体に落とすとき
- 元サイトが背景動画を
position:fixedでページ全体に敷いている場合、セクション単体では
position:absolute(親にposition:relative; overflow:hidden)に置き換える。このとき、
兄弟のコンテンツ帯にposition:relative; z-index:1を付け忘れると動画の裏に隠れる。
厄介なのは「静的な要素だけ消えて、position:relativeを持つ子(ボタン等)は見えている」ため
崩れに気付きにくいこと。今回はフッターのロゴとサブリンクだけが消えていた。
→ 背景メディアを absolute 化したら、同階層のコンテンツ帯すべてに position+z-index を明示。
.sp / .pc の body クラスで出し分けるサイトの抽出
- j-ortho はメディアクエリではなく JS が body に
sp/tablet pc/pcを付け替える方式
(max-width:767px→sp/768〜1024px→tablet pcかつ viewport meta をwidth=1100にしてPC版を出す / それ以上→pc)。
→ 抽出時は.sp Xを@media (max-width:767px)に、.pc/.tablet Xを@media (min-width:768px)に変換する。
注意: 元は.spの1クラス分だけ詳細度が高くて順序に依存しなかったが、メディアに移すと同詳細度になるので
.sp由来のブロックは base の後ろに置く(前に置くと効かない)。
有料フォント/Typekit は持ち込めない(今回の実測)
Canela Web(参考元の自前ホスト woff2)はaccess-control-allow-originが無く hotlink 不可
(@font-face は CORS 必須・画像と違って落ちる)。Typekit の和文(dnp-shuei-gothic-gin-std)も
https://use.typekit.net/<kit>.cssがドメインロックでprecondition failed。
→ 英字見出しは Google Fonts で代替(Canela にはCormorant Garamondweight300 が近い)、
和文は元CSSのフォールバック(游ゴシック/ヒラギノ)をそのまま残すのが素直。代替したことは報告に書く。
CSS抽出はブラウザで「サブツリーにマッチするルールだけ」を機械収集する(型として推奨)
- playwright で対象URLを開き、
document.styleSheetsを全走査して
「セクションのサブツリーのどれかに matches するセレクタ」だけを残すと、
reset.css・サイト全体CSS・未使用@font-face が構造的に混入しない(今回 40〜103ルール/実測99〜248行)。:hover::after等は判定前に外す(マッチしないので落ちてしまう)- 1440 / 768 / 390 の3幅で回して和集合を取る(
.spクラスやJS分岐で出る指定を取りこぼさない) - CSSOM の
cssTextは相対URLを解決しないまま返ってくる →url(img/…)をテーマURL基準で絶対化する。
(今回、絶対化の正規表現で\./?と書いて「.必須」になりurl("img/…")を素通りさせた。(?:\./)?が正)
他セッションと並行しているときの格納(terasu-lab)
sections-data.jsonはコミットしない。CI(deploy.yml)がcore/build_core.py→build.py→
build_lab.pyを回してコミット済みの entries から作り直すので、ローカルの集約結果を混ぜると
他セッションの未コミット entries が一覧に載ってカードだけ生きて中身が無い状態になる。
→ 格納時に add するのはentries/<id>.json+img/sections/<id>.png+code-previews/<id>.htmlだけ。- 他セッションが作業中だと
git add -Aは当然アウトだが、git pull --rebaseも submodulecoreの
ポインタ差分で止まる。git -c rebase.autostash=true pull --rebaseで回避できる。 - 反映確認は
/sections/(末尾スラッシュ必須)。/sectionsはゼロバイトが返る。
- 他セッションが作業中だと
作業ファイルは scratchpad に置かない
/private/tmp/claude-501/.../scratchpadはセッションを跨ぐと消える(今回2回消えて抽出をやり直した)。
DOM・CSS・比較スクショなど数十分かけて作る中間物は~/dev/_<案件>_work/に置く。
確認①は「Chromeで開ける実URL」で出す
- /section の手順は localhost だが、大串の恒久FBは「毎回Chromeで開ける実URLで送る」。
Lab は push で本番自動デプロイなので、code-previews/<id>.htmlだけを先にコミット&pushして
実URLを出し、add_section.py(一覧への登録)はOKが出てからにすると両立する。
id は「空きを機械確認してから」使う(2026-08-18)
- 0817 だけで 220件超の既存セクションがあり、
report-cards-saki-0817/news-list-saki-0817のような
ありがちな id は既に埋まっている。気づかず同名で書くと他メンバーの完成済みHTMLを上書きする
(今回2件やってしまい、HEADから復元された=自分の作業が消えた)。
→ 着手前に必ず空き確認する。埋まっていたら接頭辞を足す(例sus-= そのページ由来):
bash for id in <id1> <id2>; do [ -e code-previews/$id.html ] || [ -e entries/$id.json ] && echo "$id 衝突" || echo "$id 空き"; done
add_section.pyは既存 id で停止する(--forceを付けない限り安全)が、code-previews/<id>.htmlを
自分で書く工程には止める仕組みが無い。危ないのはHTMLを書く方。
合わせるのは「CSSの宣言」ではなく「実ページの computed 値」(2026-08-18)
- CARTA の
.sustainability__news-titleは CSS上font-size:1.125rem(=18px)なのに、実ページの
computed は 16px / line-height 28px / gap 20px(=768px以下ブロックの値)だった。宣言をそのまま
移すと元より文字が大きくなり、行数が増えてパネル高が 594→627px にズレる。
→ 抽出したら必ず元サイトと自分のプレビューで同じ要素を getComputedStyle + getBoundingClientRect し、
数値で突き合わせる。食い違ったら実測値の方に合わせる(見た目一致が正)。
ダミー文も元と同じ文字数にすると折り返し行数まで一致し、箱の高さがピタリと合う。
slick 等のスライダーはサムネ検品に落ちることがある(2026-08-18)
shoot_section.mjsの前処理oneSlide()は「2枚目以降を display:none」にするが、slick は clone を
含む8枚構成+track の transform でレイアウトを組むため、これに当たると絵が崩れて
「検品不合格(輪郭0.9%)」になる。--forceで撮った画像は中身が正常だった。
(判定は1回目の測定値richを使うので、2回目の撮り直しで改善しても不合格のまま=ツール側の仕様)
→--forceで撮り、生成画像を必ず目視確認してから採用する。SP版・詳細版も同時に生成される。
Studio(studio.design)製サイトは <style media="..."> でブレークポイントを分ける(2026-08-18)
- Studio は CSS を
<link>ではなく 複数の<style>タグに出し、レスポンシブは
<style media="(max-width: 1280px)">のように style タグのmedia属性で切り替える
(@mediaブロックが CSS 内に無い)。
→re.findall(r'<style[^>]*>(.*?)</style>')のように属性を捨てて中身だけ抜くと、
全ブレークポイントの宣言が「素のルール」として一列に並び、後勝ちで最小幅の値が全部に効いてしまう
(42px見出しが28pxになる等)。必ずmedia属性も取り、@media <値>{ … }に包み直してから使う。
※ 同じ理由で、ライブページを devtools/JS で計測すると 480px 相当の computed 値が返る環境がある。
計測が明らかにおかしい時は、自分の抽出物に元テキストを流し込んだ「参照版」をローカルで描画して
元スクショと突き合わせるのが確実(折り返し位置まで1行ずつ照合できる)。
単独プレビューの body 標準マージン8pxで折り返しが1文字ズレる(2026-08-18)
width:1440px; max-width:100%のセクションを<body>(既定 margin 8px)直下に置くと、
実効幅が 1424px になる。86.6%コンテナ→4カラムに割るとカラム幅が約3px縮み、
1行18文字が17文字になって行数が増える(元サイトと折り返しがズレる)。
→ 他セクションと同じ<body style="margin:0">(インライン属性) を付ける。
CSS にbody{}を書くとスコープ漏れ検証(grep)に落ちるのでインライン属性で書くのが正。
同じリポで別セッションが git add -A していると巻き込まれる(2026-08-18)
- terasu-lab を複数セッションで同時に触ると、片方の
git add -Aが自分の作業中ファイルまで
commit/push してしまう(今回は entry/サムネを作る前の HTML が先に commit された)。
さらにその sweep コミットが 242ファイル削除・15,346行削除(他人の作業中ファイルの消滅)を
含んでいることがあり、そのまま push すると本番から実セクションが消える。
→ push 前に必ずgit diff --stat origin/main..HEADと
git diff --diff-filter=D --name-only origin/main..HEAD | wc -lで削除件数を確認する。
危険なら push せず、plumbing で origin/main の上に自分のファイルだけ載せたツリーを作って
git push origin <commit>:main(作業ツリー・インデックスに触らず安全):
GIT_INDEX_FILE=/tmp/idx git read-tree origin/main→git hash-object -w <file>→
git update-index --add --cacheinfo 100644,<sha>,<path>→git write-tree→git commit-tree。
※ 今回は結果的に相手の push に自分の5ファイルが同内容で含まれていたので追加pushは不要だった。
git diff --stat origin/main <tree>が空=もう反映済み、の判定に使える。
※ 集約sections-data.jsonは CI がcore/build_core.py→build.pyで再生成するので
ローカルで commit しなくてよい(他人の未完成 entry を巻き込まないためむしろ staging しない)。
このPCは node が PATH に無い / headless Chrome が固まる(2026-08-18)
check_responsive.mjs/shoot_section.mjsは node が要るが、このPCは PATH に node が無い。
export PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH"で v24 が使える。
またGoogle Chrome --headless=new --screenshotが無応答で固まることがある
(2回目以降・プロファイル指定を変えても再発)。ブラウザペイン(preview_start)で代替すれば止まらない。
2026-08-18 あかつき証券 4件(会社概要/沿革/代表メッセージ/経営方針)
同一リポで他セッションが並行作業中は「別ワークツリーからpush」が唯一安全
- 症状:
git pull --rebase --autostashがcould not detach HEADで失敗。原因は他セッションがstage済み・未コミットのファイル(cases-slider/footer-panel/header-nav等)が、リモートの新コミットと同じパスで衝突していたこと。git mergeでも同じ理由で拒否される。 - ここで
reset --hard/checkout -f/ stash を使うと他セッションの作業を破壊する。絶対にやらない。 - →
git worktree add --detach <tmp> origin/mainで別ワークツリーを作り、そこへ自分のコミットだけcherry-pickしてgit push origin HEAD:main。メインの作業ツリーには一切触れずにpushできる(作業後git worktree remove --force)。
git worktree add --detach /tmp/lab-push-wt origin/main
cd /tmp/lab-push-wt && git cherry-pick <自分のcommit> && git push origin HEAD:main
cd - && git worktree remove --force /tmp/lab-push-wt
git add の前に index.lock を確認する(他セッションのgitと衝突する)
fatal: Unable to create '.git/index.lock': File existsが実際に出た。他セッションがまさにgit操作中という意味なので、rmせず数秒待って再試行する(今回は3秒で解放)。
git commit -- <paths> のパス指定コミットで他人のstagedを巻き込まない
- 着手時点で他セッションのファイルが既にstagedされていた。
git commitすると巻き込んでしまう。 - →
git commit -m "..." -- "${PATHS[@]}"で自分のパスだけコミット(他人のstagedはindexに残る=妨害しない)。 - zshでは
PATHS="$PATHS a b"の文字列連結だと1つのpathspecとして渡りdid not match any file(s)になる。必ず配列PATHS+=(...)+"${PATHS[@]}"で渡す。
font-feature-settings の引用符が全角なCSSはコピーしない(元サイト側のバグ)
- 元CSSが
font-feature-settings: “palt”;(全角ダブルクォート)で実際には無効だった。これをそのまま持ってくると、こちらでは有効化されてしまい字送りが元と変わる(=再現ミス)。 - → 無効な宣言だと分かったら入れないのが「視覚一致」として正しい。
プレビューHTMLの body は CSSルールでなくインラインstyleでリセットする
body{margin:8px}のUA既定が残ると、元サイトとの左端位置が8pxずれる。- → 既存セクションの家風どおり
<body style="margin:0;background:#fff;">。<style>内にbody{}を書くと "貼ると崩れる" 検証(スコープ漏れgrep)に引っかかるのでインラインが正解。
ページ本文カラムの余白は「ラッパーCSS」ではなく sec- の padding に寄せる
- 元は
.contentsArea_side{width:18%}+.contentsArea_main{padding-right:7vw}の2カラム構造。ラッパーごと持ち込むと外来CSSになる。 - →
sec-<id>{padding: 5vw 7vw 6vw 18%}としてスコープ内で同じ余白を再現(実測1440pxで左259px=元と一致)。SPは@mediaで24pxに戻す。
2026-08-18 スタディサプリ ブランドサイト(Gatsby製)フッター+ドロワー 2件
- ** スコープ用リセットを二重クラス(
.sec-X.sec-X :is(...))にすると本体CSSを潰して全崩れする。.sec-X.sec-X :is(footer)は特異性(0,2,1)で、本体の.sec-X .module(0,2,0) に勝ってしまいpadding:400px等が0になった(フッターが239pxに潰れた)。→ リセットは元サイトと同じ弱さ(.sec-X :is(...)=(0,1,1))に保つ**。2026-08-16の「二重クラスで強くする」はp/img限定の話であり、レイアウトを持つ要素へ広げてはいけない。 - Gatsbyのハッシュclass(
styles-module--<base>--<hash>)はリネームが機械的にできる。ファイル内の全トークンを収集→base名へ一括置換(show→is-show、open→is-open、社名由来recruit→corpmarkだけ手当て)。base名の衝突を事前チェックしてから置換すれば安全。 - rem基準
html{font-size:62.5%}はsec-X{font-size:10px}では直らない(remは常にdocument root)。全CSSのNrem→calc(N * var(--sec-rem))に機械変換し--sec-rem:10pxを sec 上で定義するのが正解(2026-08-17 Vivooの知見がそのまま効いた)。 html,body{margin:0}をプレビュー用に足すのは禁止(スコープ漏れ検査に引っかかる)。既存97件も html/body ルールは0件。body既定マージン8pxはnormalize_thumbs.pyが余白トリミングするので実害なし。- 原本との照合は「同幅でcomputed styleをdiff」が速い。ただし
[class*=logo--]等の緩いセレクタは原本ページのヘッダー側を拾って偽の差分を出す。セクションのルート要素の高さ(985/1076/1091px)が3幅で一致すれば実質OK。 - ** terasu-lab は同時セッションが多く
git pull --rebaseがほぼ通らない(未コミットの他人ファイル+リモート側に同名untrackedが来てcould not detach HEAD)。→git worktree add /tmp/<一意名> origin/main --detachで隔離し、そこに自分の6ファイルをコピー→build.py→commit→push が確実。/tmp/lab-push等は他セッションが既に使っているので名前を一意にする**。 - worktreeでは push前に必ず
build.pyを撮り直して「消失0件」を機械確認する。リモートのsections-data.jsonは集約漏れ(entryはあるのにデータに無い/逆に実体の無いidが残る)が発生しており、rebase後に再ビルドすると自動で是正される。 - ローカルプレビューのサーバーは
nohupで切り離して起動する。&だけだとツール呼び出し終了やセッション終了で死に、大串が開こうとした時に落ちている(今回2回やり直しになった)。 - 同じidを過去セッションが既にコミットしている場合がある(今回
bluepanel-drawer-saki-0817.htmlが生プレビューだけコミット済みだった)。リネーム前にgit statusでDが出ていないか確認し、出たらgit checkout --で戻す。
2026-08-18 LENZ DX 4件( id衝突で他サイトに上書きされた事故)
- **
<主題>-<種別>-<担当>-<MMDD>は同日・同担当だと衝突する。flow-steps-saki-0817/contact-cta-saki-0817で作った2件が、同じ日の別セッション(AI model・青木茂建築工房)に同じidで上書きされ、大串の確認時に「①と②違うHPのじゃない?」となった。担当名が入っていてもid衝突しない保証にはならない(同じ担当が1日に何十件も足す運用のため)。
→ id は主題を具体的にして一意性を稼ぐ(flow-steps→flowsteps-arrow、contact-cta→contact-darkcardのように見た目の特徴を主題に含める**)。作る前にls code-previews/ entries/ | grep <候補id>で衝突チェックする。 - 大串の確認待ちの間に上書きされる(確認①〜格納の間が最も危険)。プレビューを作ったら放置せず、OKが出たら速やかに entries 登録まで進める。entries に登録されていない生プレビューは"誰のものでもないファイル"扱いで消えやすい。
- 他セッションが同リポで作業中なら
git add -Aもgit stashも絶対にしない(今回は未コミット230件が並走していた)。→ 自分のファイルだけをパス指定でgit add。gitはリネームも自動検出してくれる(R code-previews/旧.html -> 新.html)。 - CIが
build_core.py→build.py→build_lab.pyを自前で回すので、集約JSON(sections-data.json 等)はコミットしなくてよい。むしろコミットすると他セッションの未コミット分を巻き込んだ集約結果が載って事故る。コミットするのは entries / code-previews / img/sections の自分の分だけ。 - 自分のCI実行が
cancelledになっても失敗ではない。後続pushがconcurrencyで前の実行を打ち切るだけ。→git merge-base --is-ancestor <自分のsha> origin/mainで最新mainに含まれているかを見て、最新の実行の完了を待つのが正しい判定。 - 動画からのフレーム抽出は ffmpeg / swift / qlmanage がどれも使えない環境がある。→
<video>を貼った確認用HTMLをローカルサーバーに置き、ブラウザで再生してスクショが確実(python http.server は Range 非対応でシークが効かないので、play()して待ってから撮る)。 - ブラウザペインは非表示になるとSwiper等のautoplayが止まる。「動いていない」と誤判定しかけた。→ 動作確認は headless の
--dump-domでtranslate3d(...)が進んでいるかを機械判定するのが速くて確実。
2026-08-18 groxi株式会社(groxi.jp)円形サービスカード/パートナーロゴ/ニュース 3件
- ** 参考元が自社ホストしている独自フォントは CORS 未許可でまず読めない。groxi の
Campton(/assets/fonts/3969AE_1_0.woff2)はcurl -sI -H "Origin: …"にaccess-control-allow-originが出ない=クロスオリジンでは適用されない。→ 着手前に curl でCORSを確認し、無ければ同系のGoogle Fonts(ジオメトリックサンセリフなら Poppins、日本語 NotoSansCJKjp は Noto Sans JP**)に置換する。フォント以外(DOM・CSS値・JS設定)は完全再現のままにできるので、置換対象を「フォントだけ」に閉じ込めるのがコツ。 - 巨大CSS(209KB/6294行)からの抽出は自作パーサが速い。ブレース深度を追ってトップレベルのルールを切り出し、セレクタ文字列に
.s-service等が含まれるものだけ残す。@mediaは中を再帰して同じ判定をかければ、PC/SP両方の指定を取りこぼさない。(落とし穴=while … else: breakを書くと最初の{で外側ループを抜けて出力が空になる) .index .s-service …のようなページ限定プレフィックスはsec-<id>に置き換えるだけでスコープ化が済む。元が「ページclass配下」で書かれているサイトは、外来グローバルCSSを持ち込む必要がほぼ無く、100〜250行に収まる。- ** headless Chrome の
--window-size=390,700は効かない(最小幅500でレンダリングされ、画像だけ390にクロップされる)。SPの「進捗バーが画面外に出ている」はこの見かけ上の切れで、実測すると 500px 幅での座標だった。→ SPの判定はtools/check_responsive.mjs(playwright・390で正しく測る)を信用する**。どうしても位置を知りたい時は、一時コピーにgetBoundingClientRectをdocument.titleへ書き出すスクリプトを差し込み--dump-domで読むのが軽い。 - 白基調・低密度のセクション(余白多め+12px級の細字)は
shoot_section.mjsの検品で誤検知落ちする(今回 news が「色14種/輪郭3.0%」で不合格)。→--forceで撮ってから必ず自分で目視し、見出し・リスト・ボタンが読めていれば採用してよい。判定を緩めるのではなく「force+目視」で運用する。 - このPC(咲輝PC)は node が PATH に無い。
export PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH"を付ければcheck_responsive.mjs/shoot_section.mjsが動く。 - ** 別セッションが同一ワークツリーで並行作業していると、自分の
code-previews/*.htmlが相手の一括コミットに巻き込まれる(今回、書いた直後に相手が commit → 自分の3ファイルが追跡済みになっていた)。慌ててgit add -Aや stash をしないこと。→git worktree addした隔離環境でgit checkout -B <一意名> origin/main→ 自分のコミットだけgit cherry-pick→git push origin <一意名>:main**。これなら相手の未push分を巻き込まずに自分の分だけ本番へ出せる。git addが一時的にdid not match any filesで落ちるのも相手のgit操作との競合なので、数秒おいて再試行すればよい。 - Swiperの初期化値はバンドルJSから grep で取れる。
slidesPerView:e,spaceBetween:r,centeredSlides:iの実装を辿ると既定slidesPerView:1、進捗は(activeIndex+1)/3で.progress-valueをscaleX()していた。→ CDN の swiper-bundle JSだけ読み、CSSは使う分(.swiper-container/.swiper-wrapper/.swiper-slide)をsec-<id>配下に自前で書けば、ライブラリCSSをグローバルに撒かずに動きを完全再現できる。
2026-08-18|NewsPicks Brand Design 6セクション(実地の学び4件)
1. セクション単体で「スクロールできない」バグ=ページ級CSSの畳み込み
- 症状: プレビューを開くとホイールが効かない(大串FB「4スクロールできない/6スクロールできない」)。見た目は完璧なので目視では気づけない。
- 真因: 抽出時に
html{overflow-y:scroll}body{overscroll-behavior:none}をsec-<id>ルートへ畳み込んでいた。スコープルートがスクロールコンテナ化してホイールがページへ伝播しない。 - 対策(恒久): スコープルートに集約する宣言から
overflow/overflow-x/overflow-y/overscroll-behaviorを必ず除去する(opacity:0の除去と同じ扱い)。これらはページ級の設定でセクションには不要。 - 機械検証: 目視では絶対に落ちる。Playwrightの実ホイールイベントで検証する(
page.mouse.wheel()を数回→window.scrollYが増えるか)。window.scrollTo()は効いてしまうのでテストにならない(ここが罠)。
2. body{opacity:0}(ロード演出ゲート)で真っ白になる
- 参考元は
body{opacity:0}→ JSでjs-loadedを付けて表示する型が多い。そのままスコープ化するとプレビューが真っ白。 - 対策: ロード後状態を正として
opacity:0を除去。同型で.js-loaded.s-topなど body常時クラスは先頭から剥がして常時適用にする。
3. 他ドメイン参照はCORSで死ぬ(画像は生きるがマスク/JSONは死ぬ)
<img src>は他ドメインでも表示されるが、CSSmask-imageのSVGと Lottieのpath:JSON は CORS で読めない(元サーバーにAccess-Control-Allow-Originなし)。矢印アイコンが消える・イラストが出ない。- 対策: CSS参照アセット(svg/png/woff)は data URI 化、Lottieは
animationData:で JSON を埋め込み(path:を使わない)。画像URLは従来どおり参考元のまま。 - 併せて CSSの相対
url(../img/...)は絶対URL化が必須(CSSは/asset/css/配下配信なので../の基準が変わる)。
4. --font-device-width-unit 系のJS注入変数を拾い忘れると文字サイズが全崩れ
- NewsPicks系は
font-size: calc(... var(--font-device-width-unit) ...)を多用。この変数は startup.js がdocumentElementに注入しており、CSSだけコピーすると全部16pxに落ちて巨大見出しが消える。 - 対策: 変数注入をスコープ版で移植(
Math.min(window.innerWidth, 1440)をsec-<id>にsetProperty)。html{font-size:10px}と同じ「ベース設定は拾ってスコープ化」の一種。 mac.cssの存在に注意: Mac/iOS UAのときだけJSが後から注入する調整CSS。拾わないとMacでの見た目が僅かにズレる。sec-<id>.is-macuaとしてスコープ化し、UA判定でクラス付与。
5. 並行セッションと作業ツリーを共有しているときのpush
- 同じPCの別セッションが同一リポで数分おきにcommit/pushしていると、
git pull --rebaseが「unstaged changes」で止まり、git stashは他セッションの作業を壊すので使えない。 - 対策:
git worktree add --detach <tmp> origin/main→ 自分のコミットだけcherry-pick→build.py再生成 →push origin HEAD:main。他人の未pushコミットを巻き込まずに自分の分だけ出せる。終わったらgit worktree remove。 - CIが
cancelledになっても問題ない(後続runが最新mainからビルドするため)。慌てて再pushしない。確認は本番/sectionsを実際に開いて掲載を見る。
2026-08-18 あかつき証券トップ 5件(大串FB2回=どちらも「左に寄りすぎ」)
6. セクションの左余白を作っている「隣の列」を一緒に持ってこないと左に寄る(同じ指摘を2回もらった)
- 症状: 抜き出したセクションが元サイトより左に詰まる。大串FB「③左につまりすぎ」→直した直後に「1のタイトル左によりすぎ」=同型の見落としを連発した。
- 真因: あかつき証券トップは
<div class="contentsArea">の中がcontentsArea_side(幅13%の追従ナビ列)+contentsArea_mainの2カラム。本文の左余白は padding ではなく隣の列の存在で作られている。contentsArea_mainだけ包んで持ってくると、その13%が丸ごと消えて左に寄る。 - 対策(着手時のチェックに入れる): 本文セクションを切り出す前に、そのセクションの左右の余白が「自分のpadding」か「兄弟要素(サイド列/グリッドの空きカラム)」かを必ずDOMで確認する。兄弟要素由来なら空の
<div class="サイド列">を一緒に入れてCSSも拾う(中身は空でよい・幅指定だけ効けばよい)。 - 見分け方: 親が
display:flex/gridで、セクション側に大きなpadding-leftが無いのに元サイトでは寄っている → 兄弟列パターン。
7. 「追従サイドナビ」等の小さいセクションはサムネ検品(shoot_section.mjs)に落ちる
- 症状:
⛔ 中身が写らない(色7種/輪郭0.2%・2回試行)で保存されない。ナビ自体は正しく描画できている。 - 真因: デモを「ナビ+高さ稼ぎの空div」で作るとページのほぼ全部が白になり、色数・輪郭量の検品ゲートに引っかかる。セクションが小さいほど起きる(サイドバー・パンくず・ページネーション等)。
- 対策:
--forceで通さない。元ページと同じ文脈を実コードで足す=そのナビが指している本文セクション(今回は Mission と News の実抽出コード)を隣の列に入れる。捏造ではなく同一ページからの実コード流用なので品質基準を下げずに済み、追従の動き自体もデモとして成立する(スクロールでナビの現在地が移るのを見せられる)。 - 併せて
IntersectionObserverで-currentを移す実装を入れると、静的プレビューでも元サイトと同じ挙動になる。
8. このPCは node が素のPATHに無い(毎回ハマる)
node tools/check_responsive.mjsがcommand not found。ChatGPT.app同梱nodeを使う:
export PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH"(v24系)。/sectionの検品・撮影コマンドは全部これを前置きしてから実行する。
9. 全画面ドロワーはタブレット(768)で必ず検品に落ちる(元サイトのPC固定幅が原因)
.hamMenu{ left:18%; width:calc(100% - 18%); min-width:1000px }のようにPC版を横スクロール前提で組んでいるサイトが多い。元サイトのSP用メディアクエリはmax-width:767pxなので、768pxちょうどの帯だけPC指定が生き残ってはみ出す。- 対策:
@media (min-width:768px) and (max-width:1023px)でその帯だけmin-width:0、必要なら列数を4→2に落とす+器にoverflow-x:clip。PC1440の見た目は1mmも変えない(撮り直したPNGがバイト単位で同一になることを確認すると確実)。
10. 他セッションが同じリポで並行作業していると push できない → worktree で回避する(2026-08-18 実地)
- 症状:
git pull --rebaseがcannot rebase: You have unstaged changes/could not detach HEADで必ず失敗。git stashも exit 1。作業ツリーに他セッションの未コミット変更が200件超あり、触ると相手の作業を壊す。 - やってはいけない:
git add -A/他セッションの変更を stash・reset する(同時作業日は特に危険)。 - 正解=現在の作業ツリーに一切触れずに push する:
bash cd ~/dev/terasu-lab git worktree add --detach /tmp/lab-push-<日付> origin/main # クリーンな別ツリー cp <自分の成果物> /tmp/lab-push-<日付>/{code-previews,entries,img/sections}/ cd /tmp/lab-push-<日付> && python3 build.py # 集約JSONはここで再生成 git add <自分のファイルだけ> && git commit && git push origin HEAD:main git worktree remove /tmp/lab-push-<日付> --force - push は必ずリトライループにする(相手が数十秒おきに push してくるので1回目は
non-fast-forwardで弾かれる):fetch → rebase origin/main → build.py → amend → pushを最大5回。 - CI が
cancelledになっても慌てない:concurrency: deploy-terasu-labで後続 push の run に集約されるだけ。自分のコミットが origin/main の祖先か(git merge-base --is-ancestor <sha> origin/main)を見て、あとは本番URLの200と実表示で確認する。run の成否だけで判断しない。
11. 確認①用のローカルサーバーはセッションが切れると落ちる(大串が「リンク開けない」と言う真因)
python3 -m http.serverを run_in_background で立てても、Claude Code のセッション再起動でプロセスごと消える。大串側からは「さっきのURLが開けない」に見える。- 対策: ①
nohup python3 -m http.server 8518 > /tmp/x.log 2>&1 < /dev/null & disownでデタッチする(setsidは macOS に無い=使うとエラー)。②同時に file 直開きの手段も渡す=セクションは1ファイル自己完結なのでopen -a "Google Chrome" "<絶対パス>"で全機能動く。③再送を求められたらまずcurl -o /dev/null -w "%{http_code}"で生死を確認してから送り直す。
10. 同じ日に複数セッションが走ると id衝突でファイルが相互上書きされる(2026-08-17 実地・重大)
- 症状: 一度「完全再現できました」と確認まで通したプレビューが、あとで開くと CSSが1つも効いていない素のHTML(リンクが青い下線、レイアウト崩壊)になる。別の箇所は無事なので「一部だけ崩れた」ように見える。フッターに至ってはまったく別のサイト(証券会社)のフッターに置き換わっていた。
- 真因:
<内容>-<担当>-<日付>のidは、同じ担当(saki)・同じ日(0817)に別セッションが同じ内容名(footer-darknews-inforecruit-ctaなど汎用語)を使うと衝突する。さらに他セッションがgit add -Aでcommitするため、自分の作業中ファイルが他人のコミットに巻き込まれ、あとから別内容で上書きされる。 - 対策(恒久):
1. 内容名を具体的にする(footer-darkではなくfooter-3col-sns、news-infoではなくnewslist-thumb)。汎用語ほど衝突する。
2. 着手前にls code-previews/ | grep <候補id>で空きを確認してからidを決める。git ls-filesでcommit済みかも見る(既にcommitされていたら他人の作品=別名にする)。
3. 生成直後にCSSパースを機械検証する(下記11)。目視や1440pxのスクショだけでは「壊れていない」と誤判定する。
4. push は自分のファイルだけgit add <path...>で明示し、git add -Aは使わない。作業ツリーが他セッションの変更で汚れているときはgit worktree add <tmp> origin/main --detach→cherry-pick→push origin HEAD:mainで、他セッションのローカル作業に一切触れずに出せる。
11. 「崩れていないか」は cssRules 数で機械判定する(目視では見抜けない)
- 壊れたファイルは
document.styleSheets[n].cssRules.lengthが 1(=先頭ルールだけ読まれて以降が全部無視)になる。正常なセクションは 50〜180 程度。 - 検証コード(Playwright・生成直後とデプロイ後の両方で回す):
js const r = await page.evaluate(() => { const sheets = [...document.styleSheets].map(s => { try { return s.cssRules.length } catch(e) { return 'X' } }); const root = document.querySelector('[class^="sec-"]'); const f = root && root.firstElementChild; return { rules: sheets.filter(x => x!=='X').reduce((a,b)=>a+b,0), display: f && getComputedStyle(f).display }; }); displayがblock(本来flex)なら CSS が効いていない。rules<10かdisplay:blockなら即アウトとして扱う。
12. STUDIO製サイト(class="sd appear" / data-s-<uuid>)の抽出手順
- 見分け方: DOM に
.sd appearが大量、<style>内が.sd[data-s-<uuid>]{...}形式、__NUXT_DATA__あり。CSSは 要素1つ=uuid1つに対応するので、セクション内のdata-s-/data-r-属性名を集めて、その属性名を含むルールだけ拾えば過不足なく抽出できる(reset.cssを丸ごと持ってくる必要がない)。 - ルート要素自身の
data-s-を拾い忘れるとセクションのpadding/justify-contentが丸ごと落ちて高さが変わる。for el in [node] + node.find_all(True)のようにルートを明示的に含めること(find_all は子孫のみ)。 - 画像は lazy-load で
srcが空SVGのプレースホルダ。--dump-domでは実URLが取れないので Playwrightで全域スクロールしてからpage.content()を取る(#service等のアンカー付きURLで開くだけでは発火しない)。 - 元サイトはページ最上位の
.sd{overflow:clip}ではみ出しを切っている。抽出後は器にsec-<id>{ overflow-x: clip }を付けると、タブレット幅の検品に落ちずPC表示も元のまま保てる。 - ベース設定は
:where(.render-canvas) …に入っている。:where(.render-canvas)をsec-<id>に置換するだけでスコープ化が完了する(html/body/*へのページ級リセットが元から無いので安全)。
13. id衝突の事前チェック(2026-08-18 実地で事故りかけた・恒久)
- 同じPC・同じ担当だと一般的なid(
about-2colnews-slidercontact-band等)は既に使われている。今回 goinc.co.jp の7件を-saki-0817で作ったところ、6件が既存の別サイトのセクションと同名で、code-previews/*.htmlを上書きしてしまった(add_section.pyはentries/しか衝突判定しないので素通りする)。 - 着手前に必ず両方チェックする:
bash cd ~/dev/terasu-lab for id in <候補id...>; do ls code-previews/$id.html entries/$id.json 2>/dev/null && echo "⛔ $id は既存" || echo "OK $id" done - 上書きしてしまったら
git checkout -- code-previews/<id>.htmlで復元し、日付部分を当日に変える(-0817→-0818)だけで衝突を回避できる。ファイル内のsec-<id>参照もsed 's/-saki-0817/-saki-0818/g'で一括置換すること(クラス名にidが入っているので置換漏れると無スタイルになる)。 - idの日付は「作業した実日付」にする。推測で前日の日付を付けると、その日の既存idと当たりやすい。
14. push は worktree 経由(他の人の未コミット作業を巻き込まない)
- terasu-lab は複数セッションが同じローカルリポで同時に作業していることがあり、
git statusに他人の未コミット変更(_afloat_work/や別セクション)が大量に出る。 git add -Aは厳禁(既知)だが、それに加えてgit pull --rebaseも "unstaged changes" で失敗する。git stashは他セッションが編集中のファイルを一時的に消すので危険。- 安全な型=自分のコミットだけ worktree で cherry-pick して push(作業ツリーに一切触らない):
bash git fetch origin git worktree add --detach ~/dev/.section-work/lab-push origin/main cd ~/dev/.section-work/lab-push && git cherry-pick <自分のsha> && git push origin HEAD:main cd - && git worktree remove ~/dev/.section-work/lab-push --force sections-data.jsonはbuild.pyが entries/ 全部(他人の未コミット分も含む) から再生成するので、コミットに含めない(含めると他人の作業中セクションが混入する)。本番はCIが最新mainからビルドするので影響なし。
15. 動画(画面収録)は実Chromeでフレーム抽出して必ず中身を見る
- 2026-08-18、動画に映っていた7つ目のセクション(WORKS)を見落として6件で完成報告してしまった(大串「7セクションなかった?」)。ffmpegが無い=動画を見ない、は不可。
- このMacには ffmpeg が無いが、Playwright の
channel:'chrome'(実Chrome=H.264対応)で<video>を読み、currentTimeを送って canvas に描きtoDataURLで抜ける。Playwright同梱Chromiumはコーデック非対応なのでchannel:'chrome'が必須。
js const b = await chromium.launch({ channel: 'chrome' }); // 実Chrome await p.evaluate(async t => { const v=document.querySelector('video'); await new Promise(r=>{v.onseeked=r; v.currentTime=t;}); /* canvasに描いてtoDataURL */ }, t); - 抜いたフレームは PILで4×4のコンタクトシート1枚に連結してから見る(画像を何枚も読むとコンテキストが膨らむ)。
16. 「JSが表示時に切り替えるスタイル」はCSS抽出では絶対に再現できない(GO/goinc.co.jp で全部踏んだ)
CSSルールを正しく抜いても、元サイトがGSAP等で実行時に書き換えている値は初期値のままになる。単体で開くと「見えない/色が違う」になる。
- 症状と正体(goinc WORKS の実例):
| 見え方 | 正体 |
|---|---|
| 白背景に黒文字(本当は黒背景に白文字) | .home-works{color:#fff} だが背景の黒は body 側。切り出すと白地になる → セクション自身に黒背景を持たせる |
| 見出しだけ黒いまま | .home-works-title{color:#000; transition:color 1s} をスクロール時にJSが白へ変える設計 → 切替後の色を初期値として焼く |
| 大画像が出ない | data-src の遅延読み込み+GSAPが付けた インライン visibility:hidden |
| 全面が黒く覆われる | 出現アニメ前の .cover{opacity:.5} と transform:translateY(-…) が残っている |
- 対処の型=「元サイトを表示状態にして computed style を全要素比較し、差分を焼き込む」。色だけでなく opacity / transform / visibility も必ず比較項目に入れる(色だけ見て「差分なし」と誤判定した)。
- インラインstyleは background-image だけ残して他は捨てるのが安全:
python
keep = [d for d in style.split(';') if d.split(':')[0].strip() in ('background-image','background')]
- 比較の落とし穴: 元サイト側を scrollTo(0,0) に戻して測ると演出前の値を拾う。そのセクションが画面に入った状態で測ること。
17. ダミー文は「文字数」でなく「表示幅」で合わせる
- 原文が英数混じり(
The Breakthrough Company GO、…)だと、同じ文字数の全角主体ダミーは1.5〜2倍の幅になり、狭い幅(768)で行数が増えてレイアウトが変わる。 - 全角=1.0 / 半角=0.5 で表示幅を計算して合わせ、PC(1440)とタブレット(768)の両方で行数一致を機械確認する:
python w = lambda s: sum(1.0 if unicodedata.east_asian_width(c) in 'WFA' else 0.5 for c in s) - 行数の確認は
el.getBoundingClientRect().height / parseFloat(getComputedStyle(el).lineHeight)。
18. サムネ検品の誤判定(白・グレー基調のセクション)
shoot_section.mjsの「中身が写らない(色N種/輪郭N%)」は、CONTACT帯やニュース一覧のような白地+文字だけのセクションで誤判定する(実際は正しく描画されている)。- 自分でスクショを撮って中身を目視してから
--forceで保存する。目視せずに--forceを使わない。
19. 作業ファイルはスクラッチパッドに置かない
/private/tmp/claude-501/.../scratchpadはセッション中に消えることがある(実際に2回消え、抽出済みJSONとビルドスクリプトを失って作り直しになった)。/sectionの中間生成物(DOM/CSS/matched.json/build.py)は~/dev/.section-work/<サイト名>/に置く。
20. rem基準は「最後に書かれた指定」が正(拡大バグの真因・2026-08-18 neie)
- neie.jp の
:rootは10px→2001px:14px→1601px:12px→768px:10px→767px以下:10pxの順。
CSSは後勝ちなのでmin-width:768pxの 10px が拡大指定を全部打ち消していて、実効値は全幅で 10px 固定(12/14pxは死んだコード)。 - ここで 1601/2001 の指定だけを拾うと、広いディスプレイで1.2〜1.4倍に拡大する。大串の画面(1600px超)で
「大きく表示されすぎてる」と即バレた。狭い画面で確認していると気付けない。 - rem基準を移植するときは
:root/htmlの font-size 指定を出現順に全部列挙して、後勝ち後の実効値を確定させる:
bash python3 -c "import re;css=open('site.css').read();[print(m.start(),m.group(0)[:110]) for m in re.finditer(r'@media[^{]*\{:root\{[^}]*font-size[^}]*\}\}|(?<![-\w]):root\{[^}]*font-size[^}]*\}',css)]" - 検証は 1440だけでなく 1700 / 1920 / 2100 でも元サイトと computed値を機械比較する(Playwrightで両方開いて差分を出す)。
1440だけ見ていると今回のバグは通過してしまう。 - スコープ化の書き方:
--u:10pxをsec-<id>に置き、CSS中のNremをcalc(N * var(--u))に機械置換する。
sec-<id>{font-size:10px}だけでは rem はルート要素基準なので効かない(既存セクションにこの取りこぼしがある)。 - ついでに
sec-<id>{font-size:calc(1 * var(--u))}も入れる(元は body が root の 10px を継承しているため、
入れないと font-size 未指定の器が 16px になり継承値がズレる)。
21. push は git add -A しない(他セッションの作業を巻き込む・2026-08-18 実害)
/sectionの手順どおりgit add -A && git commit && git pushをやったら、同じMacで並行動作していた別セッションの
未コミット作業(別セクション25件・ルート直下の_tmp_*.mjs4本・node_modulesのgitlink削除)まで一緒に本番へ公開された。- 幸いCIは success・全ページ200で壊れてはいないが、他人の作業を勝手に公開したのは事故。
- push前に必ず
git status --shortを見る。汚れていたらgit add -Aを使わず、自分のidのファイルだけを列挙して add する:
bash git add entries/<id>.json code-previews/<id>.html "img/sections/<id>"*.png sections-data.json - 集約JSON(
sections-data.json)は全件再生成なので、他セッションの分が混ざるのは避けられない。
同時作業の日は、集約とpushを片方に寄せるか、相手のセッションが止まってから流す。
22. shoot_section.mjs が落ちたら Playwright のブラウザを入れ直す
browserType.launch: Executable doesn't exist at .../chromium_headless_shell-XXXX/は Playwright更新でブラウザが未取得なだけ。
bash cd ~/dev/terasu-lab && npx playwright install chromiumnpm i相当は不要。入れ直せばそのまま撮影できる。
23. 日本語の改行位置がズレる=html{line-break:strict;overflow-wrap:anywhere} と p{word-break:auto-phrase} の拾い漏れ(2026-08-18)
- NOT A HOTEL ARCHITECTS の Project セクションで、CSSは全項目一致なのに本文だけ 5行→4行になった。
原因は元サイトのベース設定html{line-break:strict; overflow-wrap:anywhere}とp{font-feature-settings:"kern" off; font-kerning:none; word-break:auto-phrase}の拾い漏れ。
word-break:auto-phrase(文節で折り返す日本語向け指定)が無いと改行位置が変わり行数が増減する=見た目が変わる。 - 対処=スコープ化して残す(
html/bodyを素で書かない):
css .sec-<id>{line-break:strict;overflow-wrap:anywhere} .sec-<id> p{font-feature-settings:"kern" off;font-kerning:none;word-break:auto-phrase} - font-size や line-height の比較だけでは気づけない。抽出の検証では
height(=行数)も必ず比較対象に入れる。
行数差が出たらword-break/line-break/overflow-wrap/font-feature-settingsを computed 値で突き合わせる。
24. 動画(画面収録)は「どのセクションか」を必ずフレーム抽出して確かめる(2026-08-18)
- スクショ2枚(Process・FAQ)+動画1本で依頼が来たとき、動画を見ずにスクショ2件だけ作って提示 →「動画のは?」と指摘された。
動画は3つ目の別セクション(Project)を写していた。 素材が複数種類あるときは、全部に目を通してから箇所を確定する。 - このMacに ffmpeg は無い。Playwright(terasu-lab の node_modules)でフレームを抜くのが速い:
file://の video を canvas に描くとTainted canvases may not be exportedで失敗 → ローカルHTTPで配信する(python3 -m http.server 8090 --directory /tmp)。currentTimeを代入してseekedを待つ方式は全フレーム同じ画になることがある。
play()しながらrequestVideoFrameCallbackで連続キャプチャするのが確実(meta.mediaTimeで間引く)。
25. /tmp は他セッションと共有=上書きされる(2026-08-18)
/tmp/dom.htmlに参考サイトのDOMを置いて作業していたら、並行セッションが別サイトのDOMで上書きしていて、
終盤に og:site_name を読んだら別の会社名(StockSun)が出た。危うく誤った社名で登録するところだった。- 中間ファイルは id を含む名前にする(
/tmp/dom_<id>.html)か、セッション専用のスクラッチパッドに置く。
社名はcore/data/reference-sites/<slug>.jsonのnameから取るのが最も安全(参考HP集に登録済みなら確実)。
26. caption の18字制限に掛かる社名は、既存の同サイト登録に合わせる(2026-08-18)
NOT A HOTEL ARCHITECTSは「分類|会社名」が26字になりadd_section.pyが弾く。- 勝手に略さず、同じサイトの既存セクションの caption を見て表記を揃える:
bash python3 -c "import json,glob;[print(json.load(open(f))['caption']) for f in glob.glob('entries/*.json') if '<ドメイン>' in (json.load(open(f)).get('url') or '')]"
実例=社名はNOT A HOTELに留め、説明の側で「ARCHITECTSの〜」と補う(既存3件がこの形で統一されている)。
27. pinしてクロスフェードするセクションは「単体だと開いた瞬間が真っ白」(2026-08-18)
- 参考元(日鉄興和不動産
sumai.nskre.co.jp/brand/の.brand-v2-values-blocks)は
tl.to(items[0],{autoAlpha:1,duration:.4},0)で1枚目をフェードインさせる作り。
実サイトは前セクションから入ってくるので成立するが、単体プレビューはセクション先頭=progress 0 なので
1枚目が autoAlpha 0 のまま=全面真っ白で開く。大串から「おかしいよ」。 - 初期化に
gsap.set(items[0], { autoAlpha: 1 })を足す(参考元のヒーロー側 IIFE が使っている同じ手当てなので
"寄せる"ではなく実コード由来)。scrub の.toは開始値1として記録されるので以降の挙動は不変。 - 抽出したら必ず「scrollY=0 のスクショ」を撮って確認する。opacity/visibility を機械で見るのが速い:
js Array.from(document.querySelectorAll('<スタック> > article')).map(a => getComputedStyle(a).opacity)
28. 「同じstart/endのScrollTriggerを2本立てる」書き方は単体だとstartがズレる(2026-08-18)
- 参考元は ①pin付きtimeline ②scale制御用の
ScrollTrigger.createを同じ trigger / start / end で2本立てていた。
実サイトでは両方 start=5771 で一致するが、単体ページに移すと2本目が pin 後の座標で測られて start が 0 → 4500 にズレる
(=背景のゆっくりズームが一生発火しない)。ScrollTrigger.refresh()では直らない。 - pin している側の
scrollTrigger.onUpdateに同じ処理を載せる(1本化)。実測 scale 1.0884 対 1.0892 で実サイトと一致。 - 見つけ方=
ScrollTrigger.getAll().map(s => [Math.round(s.start), Math.round(s.end), !!s.pin])を実サイトと単体で見比べる。
29. モリサワWebフォントは持ち出せない/白一色のリード面はセクションに含めない(2026-08-18)
- モリサワ(
morisawafonts.netのMFW-*)は配信ドメイン限定+EULAで他ドメインへの持ち出し不可。
CSSの font-family は実コードのまま残し、mf.css は取り込まない(游明朝/ヒラギノ明朝にフォールバック)。
実測の残差は「VALUE」の字幅が 112px→108px の4pxだけ。ここは詰められないので報告に明記する。 - 参考元の当該ブロックには「白背景+緑の一行」だけのリード面が含まれていたが、
Labのセクションとしては先頭がほぼ白紙になり、大串が送ってきた動画の範囲とも違った → 外してビジュアル本体だけにした。
「セクションの範囲」は参考元のDOM区切りではなく、大串が動画/写真で指した範囲に合わせる。
30. プレビューは open <URL> で既定ブラウザに出す(2026-08-18)
http://localhost:8080/<id>.htmlをリンクとして送っても大串は開けない(「開けない」)。
open "http://localhost:8080/<id>.html"を実行したら即「開けた」。リンクを貼るだけで終わらせない。
31. 並行セッションの git add -A に自分の分が巻き込まれる(2026-08-18)
- 別セッションが同じ
~/dev/terasu-labで作業しており、向こうのgit add -Aで今回の entry/HTML/サムネごとコミット+pushされていた
(Add inquiry-form-saki-0817 sectionに混入)。こちらが commit する前に既に本番反映済みだった。 - 格納前に
git log --oneline -3 -- entries/<id>.jsonとgit status --porcelain -- <自分のファイル>を見る。
空=もうコミット済み。二重コミットしにいかない。中身はgit diff HEAD -- <自分のファイル>で一致確認するだけでよい。 - 自分が commit するときは
git add -Aを使わず自分のファイルだけ明示 add(向こうの作業中ファイル27件を巻き込まない)。
27. 「ホバーが無い」=グローバル a:hover を剥がしたせい(2026-08-18 大串FB)
- リビオ(sumai.nskre.co.jp)の4カラム価値カードで 「カーソル合わせた時のホバーがない」 と指摘された。
- 真因:カードのホバーはカード固有CSSではなく、サイト全体の素の
aルールだった。
css @media screen and (min-width:769px){ a{transition:700ms ease-in-out} } @media screen and (min-width:769px){ a:hover{opacity:.5} } a,a img{...;transition-property:opacity,background,color}
「貼っても崩れない形で抽出する」工程で素の要素セレクタとして丸ごと削除していた。 - リンク・ボタンを含むセクションを抽出したら、素の
a{}/a:hover{}を必ず確認してスコープ版で残す。
css sec-<id> a, sec-<id> a img{ ...元のtransition... } @media screen and (min-width:769px){ sec-<id> a:hover{opacity:.5} }
「未使用CSSは入れない」と「ページ級リセットは剥がす」を機械的にやると、ホバーだけが静かに消える。 - 検証は目視でなくcomputed値の機械比較が速い。Playwrightで実サイトとローカルの両方を
page.mouse.move()でホバーさせ、opacity / backgroundColor / borderColor / transitionDurationを突き合わせる。
今回これで「カード=0.5/0.7s・View more=0.9/0.2s・CTAボタン=0.5+rgba(0,0,0,.4)/0.3s」の完全一致を確認した。
28. モリサワ等のライセンス明朝は Google Fonts で寄せる(2026-08-18)
- 参考元が
MFW-A1MinchoStd-Bold(モリサワ・ドメインロックのCDN)だと、フォールバックの游明朝で明らかに太く出る。 - Shippori Mincho(Google Fonts)に差し替えるとA1明朝にかなり近い。オリジナル化の表でも
「固有ブランドフォント → 代替フォント」は許可されている。参考元のフォントCDNのURL(ライセンスキー入り)は貼らない。
29. 並行セッションの git add -A に自分の作業が巻き込まれる(2026-08-18)
- 3件を作り終えて commit しようとしたら、別セッションが
git add -Aで私の未コミットファイルごと commit & push 済みだった
(commit message は相手のセクション名)。成果物は本番に出たが、履歴上は他人のコミットに混ざる。 - 同時に
npm install/npx playwright installが走るとERR_MODULE_NOT_FOUND: playwrightや
browserType.launch: spawn Unknown system error -88(新しいバイナリの展開途中)で一時的に落ちる。数十秒待って再実行すれば通る。 - 同時作業日は、格納前に
git log --oneline -3とgit statusを見て自分のファイルが既に取り込まれていないか確認する。
取り込まれていたら二重コミットせず、本番URLで反映だけ確認すればよい。
32. 動画のフレーム抽出は「Range対応サーバー+ブラウザ」で確実に刻める(2026-08-18・貴瞬案件)
- 前提: このMacに ffmpeg なし / pyobjc なし / swift はSDK不一致でコンパイル不可(AVFoundationをJXA・Swiftで叩く方式は両方とも失敗)。
- 効いた手順(動画9秒・22MBで実証):
1..movをスクラッチパッドにコピーし、<video>だけの HTML を置く
2.python3 -m http.serverは Range 要求に未対応 →currentTimeを代入してもシークできず、全部同じ画になる。
206 Partial Content を返す簡易サーバー(SimpleHTTPRequestHandlerを継承してRangeを処理)を自作して配信する
3. ブラウザペインで開き、currentTime代入 →onseekedを待ってからcomputer{action:"screenshot"} - ポートは 必ず空きを確認する。8901 が別プロジェクトのサーバーに使われていて、
自分のサーバーは起動失敗しているのに「404が返る=ファイルが無い」と誤読して時間を溶かした(curl http://localhost:<port>/で中身を見れば一発で分かる)。
33. ブラウザペインが hidden だと rAF が止まる=GSAP/ScrollTriggerの検証はヘッドレスでやる(2026-08-18)
- 抽出したセクションをペインで開いてスクロールさせても 画像が切り替わらない。
ScrollTrigger.update()を手で呼ぶと動く
→ 原因はペインが hidden で requestAnimationFrame が停止していた(コードの不具合ではない)。
さらにcomputer{action:"scroll"}やjavascript_toolが 30秒タイムアウト・スクショが真っ黒になる症状も同時に出る。 - スクロール連動の検証は Playwright(terasu-lab の node_modules)でやる。段階的にスクロールして
各時点の.is-active/visibilityを実測すると「item1〜5 で1〜5枚目が出る」と機械的に確認できる。
nodeは PATH に無い →export PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH"。
34. 作業ツリーが他セッションの未コミットで汚れている時は worktree から push する(2026-08-18)
- 並行セッションが
code-previews/*.htmlを編集中&未追跡ファイル多数で、git pull --rebaseが
「unstaged changes」「untracked files would be overwritten」で止まった。他人の作業を stash/削除するのは厳禁。 - 一時 worktree を切って、そこへ自分のファイルだけコピーして push する(本体の作業ツリーに一切触らない):
bash git worktree add /tmp/<scratch>/wt origin/main cp <自分のファイル> /tmp/<scratch>/wt/<同じ場所>/ cd /tmp/<scratch>/wt && git checkout -b tmp-push && git add <自分のファイルだけ> && git commit -m "…" && git push origin tmp-push:main git worktree remove --force /tmp/<scratch>/wt - 集約JSON(sections-data.json 等)はコミットしない。CI が
core/build_core.py && build.pyを回すので、
entries と img だけ push すれば足りる。手元で集約すると他セッションの未承認 entries まで巻き込む危険がある。
35. 「プレビュー送って」と言われたら公開URLで出す=Labは未登録ファイルだけ先に上げる(2026-08-18 大串FB)
- ローカル(
localhost:8080)とスクショで確認①を出したら「プレビューで送って」と2回言われた。
このPCには Cloudflare の認証も cloudflared 等のトンネルも無いため、公開URLの手段は Lab の CI デプロイのみ。 - 折衷案=
code-previews/<id>.htmlだけを main に push(entries/は作らない)。
https://lab.terasu.scale-group.co.jp/code-previews/<id>で開けるが/sectionsの一覧には出ない=未承認のものが並ばない。
OKが出てから entries+サムネを push して一覧に載せる。取り消しはファイル削除だけで済む。
2026-08-18・桔梗が丘整骨院(kikyougaoka.com)3セクション(咲輝PC・saki)
Google Fonts の日本語で palt が効くのは Noto Sans JP だけ(明朝は全滅・再利用価値大)
- 元サイトが Adobe Fonts(
ryo-gothic-plusn/shippori-mincho/azo-sans-web/shabby-chic)=他ドメインでは読めないので代替が要る。 - 候補を機械計測して選ぶのが正解。同一文字列を
font-feature-settings:"palt" 1とnormalで計測し、両者の幅が同じなら palt 非対応。
実測(13.7px・letter-spacing 1.476px・句読点/括弧の多い和文):
| フォント | palt有 | palt無 | 判定 |
|---|---|---|---|
| ryo-gothic-plusn(元・Adobe) | 592.8 | 657.2 | 基準 |
| shippori-mincho(元・Adobe) | 537.0 | 660.4 | 基準 |
| Noto Sans JP | 590.5 | 667 | ✅ paltが効く・元と +1.0% |
| Noto Serif JP / Shippori Mincho / Zen Old Mincho / Shippori Mincho B1 / Hina Mincho | 367.4〜368.7 | 同値 | ⛔ 全部 palt 非対応 | - → 本文ゴシックは Noto Sans JP 一択。明朝は palt が無い前提なので、どうせ幅が同じなら元と同一書体の
Shippori Minchoを選ぶ(字形が完全一致するぶん得)。 - 副作用: 明朝は全角括弧が詰まらず 句読点だらけの見出しが約19%広くなる。今回は「体験プラン(カウンセリング込み)」が2行に折れたので その見出しだけ
white-space:nowrapで元どおり1行に(はみ出しはカードのpadding内に収まる)。text-spacing-trimは効果ゼロだった。 - フォント計測は必ず実ページで。
page.setContent()+<link>だと読み込まれずシステムフォント(Hiragino=paltが効く)に落ちて全候補が同じ数字=誤った合格判定になる。document.fonts.load()→document.fonts.check()で読めたか毎回確認する。
流体rem(html{62.5%} + vw)は --sec-rem 変換+帯の統一で完全一致した
- 元:
html{62.5%}/≤767px:25.641vw/≥768px:503.2942898975%/≥1100px:7.3206442167vw((min-width:1366)は px 抜けの無効指定なので効いていない)。 - 全
Nremをcalc(N * var(--sec-rem))に機械変換(data:URI はプレースホルダで保護してから置換/base64中の "3rem" 等を壊さない)。 - 768〜1099px の帯は
body{min-width:1100px}で横スクロールさせる設計。セクション単体では出せないので、その帯も7.3206442167vwに統一した。7.3206442167vw は 1100px 時に元の 80.53px と一致するので、見た目は「1100px設計の比例縮小」=崩さずに横スクロールだけ消える。 - 結果、PC1440pxの実測高さが お知らせ 511.89px / 料金 834.27px(元と完全一致)・フッター 809.22px(元 808.97・差0.25px)。
サムネ検品は「白基調でスカスカな設計」を誤検知する
shoot_section.mjsの採用ゲートflatRatio>=0.92 && edgeRatio<0.03(画面の9割超が一色)に、白地+細いグレー文字の上品な設計が引っかかる(今回3件とも「中身が写らない」で不採用)。- 撮れた画像自体は正常。file:// でも Google Fonts も参考元画像も普通に読めることを
requestfailedとdocument.fonts.statusで確認したうえで--forceを使うのが正解。原因を潰さず force するのは禁止だが、「本当に白い設計」は force が正しい。
環境(このPC=咲輝PC・2026-08-18時点)
- node は
~/.nvm/versions/node/v24.18.0/bin/node。2026-08-17に書いたChatGPT.app/Contents/Resources/cua_node/bin/nodeはもう存在しない(Codex.app側には残っている)。nvm を見るのが確実。 - playwright は
chromiumはあるのにchrome-headless-shellが未DL でshoot_section.mjsだけ落ちる →npx playwright install chromium(2〜3分かかるのでバックグラウンド実行)。 - 動画のフレーム抽出は
swiftスクリプト実行(コンパイルではなくswift file.swift)で通った。ffmpeg/pyobjc は無いが/usr/bin/swiftがあり、AVFoundation のAVAssetImageGeneratorを使う30行で任意の秒数のフレームが取れる。※同日の別記録(項32)では「swiftはSDK不一致で不可」とあるので、まずswift frames.swiftを試し、落ちたら項32のvideo+Rangeサーバー方式に切り替えるのが早い。今回これで「動画の箇所=料金表の体験プランカードが6秒周期で±1度ゆれるanimation:rotate」を特定できた。 - ESM スクリプトは
~/dev/terasu-lab直下に置いてから実行する(scratchpadに置くとplaywrightを解決できない=ESMは cwd ではなくファイル位置から探す)。
同リポの別セッションの git add -A に自分のファイルが巻き込まれて先にpushされる
- 作業中に別セッション(arflex/spiralai)が走っており、こちらが
add_section.pyを回した直後の 12ファイル(entries 3・code-previews 3・img 9)が向こうのコミットに丸ごと入って本番反映済みになっていた。 - → commit する前に必ず
git ls-files <自分のファイル>とgit status --porcelainを見る。既にコミット済みなら二重コミットせず、git log origin/main..HEADで push 済みかだけ確認すればよい(今回は push も済んでいて、本番の実体と手元のsha256が3件とも一致)。 - 反映確認は
/sectionsの HTML を取って id を grep(ルートsections-data.jsonはSPAフォールバックのHTMLが返るので JSON パースできない=ここでも「見るな」が正しい)。
そのほか(今回効いた小ネタ)
- グラデ文字は
background-image:url(mask.png)+-webkit-background-clip:text+color:transparent。背景画像はクロスオリジンでも描画されるので参考元URLのままでよい(mask-imageは不可なのでarrow.svgだけ data URI 化=2026-08-17の教訓どおり)。 @keyframesの中に@mediaがネストされた無効CSSがあった(SP用 ±0.5deg)。ブラウザは無視して ±1deg で動くので、実挙動どおりに書き直すのが「視覚不変」。- caption の会社名は参考HP集
core/data/reference-sites/<slug>.jsonのnameをそのまま使う(今回桔梗が丘整骨院が既に登録済みだった)。社名・住所・TEL・SNS ID・コピーライトはダミー化し、ナビ文言や料金プランなど設計の実体である文言は原文のまま(2026-08-17 大串FBの運用を踏襲)。
2026-08-18 株式会社貴瞬 採用ページ(福利厚生カード12枚/選考フロー矢羽根)
clamp(...calc(N * var(--mw) / var(--bw))) は JSで書き換わる変数を疑う(今回最大の落とし穴)
- CSS の
:root{--mw:1920px}をそのまま持ってくると、全部の文字が本家より大きくなる(title 24px vs 本家 20.57px)。 - 実体は
main.jsのdocument.documentElement.style.setProperty("--mw", Math.floor(window.innerWidth)+"px")+ resize 再設定。 - → 抽出時は「CSS変数がJSで上書きされていないか」を必ずJS側でgrepする。対処は
--mw:100vwをCSSに置き、同じ setter をsec-<id>要素に対して入れる(documentElementに入れると漏れる)。
ベース設定は html/body だけでなく :where(:root) も見る
- 今回のreset は
:where(:root){line-height:1.5; overflow-wrap:anywhere; line-break:strict; text-autospace:normal; text-spacing-trim:trim-start}。 line-height:1.5を落とすと見出しブロックが13pxずれる(en見出しの line-height が normal になるため)。日本語の折り返し系(overflow-wrap/line-break)も行分割に効くので拾う。- 拾い方は
sec-<id>{ … }へ寄せる。*側のmin-inline-size:0; border-style:solid; border-width:0も忘れずスコープ化。
「完全再現」の判定は目視でなく数値でやると速い(今回これで一発)
- Playwright で 本家と自分の両方を開き、
getBoundingClientRect()と テキスト行数Range.getClientRects().lengthを並べて出す。 - 今回は カード12枚の
w319×h232/265/282と タイトル/本文の行数、フロー側のsecH 642.83 / leadTop 173.81 / listTop 323.98が全部同値になるまで詰めた。スクショ比較より圧倒的に速く、ダミー文の文字数調整(08を短く・12を長く)も機械的に決まる。
Claude Code のブラウザペインでは GSAP が動かない(環境要因・バグではない)
- ペイン内は
document.visibilityState==='hidden'で requestAnimationFrame が1フレームも発火しない →gsap.globalTimeline.time()が進まず「アニメが固まっている」ように見える。 - → 動きの検証はブラウザペインでやらない。playwright(chromium)でスクロール→スクショが正解。
別セッションの未push分を巻き込まずに push する(隔離worktree)
- 着手時にローカルが ローカル17 / リモート21 で乖離(別セッションの未push 16件が混在)。この状態で
git pull --rebaseは他人のWIPを巻き込むうえ衝突しやすい。 - →
git worktree add --detach <tmp> origin/main→git cherry-pick <自分のcommit>→git push origin HEAD:mainが安全。作業ツリーに一切触れない。/private/tmp/lab-push*に他セッションの同型worktreeが残っていたのでこの型は既に共通運用とみてよい。 - 併せて
git add -Aは使わず、自分のファイルをフルパスで add。sections-data.jsonはコミットしない(CIがbuild.pyで再生成するため。ローカルには他人の未コミットentriesが混ざる)。
環境メモ(2026-08-18・咲輝PC)
- この日は node が
/Applications/ChatGPT.app/Contents/Resources/cua_node/binにあった(nvm には無し)。日によって置き場が変わるのでcommand -v node→ 無ければ ChatGPT.app / nvm の順で探す。 - 参考元がフォント自ホストのとき、General Sans は Fontshare CDN(
https://api.fontshare.com/v2/css?f[]=general-sans@400,500)で同じ字形が出る。Noto Sans JP は Google Fonts で可。 - 本家の実装バグ(SPで4番目の矢羽根が
flow03-mb.pngを指す)も完全再現の原則どおりそのままにした。
2026-08-18(Jコミ新卒サイト・3カラム/斜めカット2件・咲輝PC)
確認①は「localhost」でなく Lab本番の code-previews を先にpush して公開URLで出す
- ローカル
python3 -m http.serverはターン跨ぎで落ちるうえ、そもそも大串の環境からは開けない(「開けないからもう一回送って」)。 - → entries を登録する前に、
code-previews/<id>.htmlだけを commit→push(CIで約1分)。https://lab.terasu.scale-group.co.jp/code-previews/<id>が200になったらそのURLで確認①。一覧(/sections)には出ないので登録前に見せるというフローは崩れない。 - 併せて、確認①の前にオリジナル化(クラス名の汎用化)まで済ませてから公開した。見た目は変わらない工程なので確認の意味は落ちず、参考元クラス名を本番に一瞬でも出さずに済む。
macOSの画面収録(.mov/HEVC)は Chromium でシークできない
video.currentTime=を代入しても 0 のまま・seekableが[0,0]・play()も進まない(1フレーム目だけは描画される)。ffmpeg も pyobjc も無い環境だった。qlmanage -t -s 1400 <mov> -o <dir>が中盤あたりのポスターフレームを1枚出すので、「1フレーム目(ブラウザ)」+「ポスター(qlmanage)」の2枚で当たりを付け、あとは実サイトのDOMで該当セクションを特定した方が速い。- 学び: 動画は「どのセクションか」の特定に使えれば十分。動きの検証は実サイトを開いて computed style を見る方が確実。
画像と動画で別々のセクションを指していることがある
- 今回、添付画像=
.sec-faces__list(01/02/03の3カラム)、動画=.sec-future(斜めカット2ブロック)と別物だった。 - → Step0で両方を特定して「A: 両方 / B: ①だけ / C: ②だけ」の3択で1問だけ聞く。勝手に片方だけ作らない・勝手に両方作らない。
別セッションが同じリポで作業中のときの安全な push
- 共有ツリーが他セッションの未コミット変更だらけだと
git pull --rebaseは "cannot pull with rebase: You have unstaged changes" で必ず失敗する。--autostashは相手の編集中ファイルを触るので避ける。 - 正解は一時worktree:
bash git worktree add -q --detach /tmp/lab-push origin/main cd /tmp/lab-push && git cherry-pick <自分のsha> && git push origin HEAD:main cd - && git worktree remove /tmp/lab-push --force
相手の作業ツリーに一切触らず、最新 origin/main の上に自分の1コミットだけ載る。 - CI は
core/build_core.py→build.pyを自分で回すので、sections-data.jsonはコミットしなくてよい(他セッションの未コミットentryを巻き込まないためにも、コミット対象はentries/<id>.jsonとimg/sections/<id>*.pngだけに絞る)。
node_modules は ~/dev/mikke/node_modules へのシンボリックリンク(terasu-lab)
- gitに mode 120000 で入っているリンク先は旧
Codex.appのまま。git checkout -- node_modulesすると壊れたリンクに戻り、playwright が全滅する(ERR_MODULE_NOT_FOUND→ 一見「別セッションがnpm操作中」に見える)。 - 復旧は
rm -f node_modules && ln -s /Users/oogushisaki/dev/mikke/node_modules node_modules。ChatGPT.app 同梱の playwright(1.57) はブラウザ(1200)が未DLで使えないので、mikke側(1.62.1=chromium-1234あり)を指すのが正。この差分はコミットしない。
抽出まわりで効いた型
- rem基準が無く px 主体のサイトでも、reset由来の
*{box-sizing}とfont:inheritをスコープ版で再現しないと余白が崩れる。sec-<id> :where(div,span,h1..h6,p,ul,ol,li,img){margin:0;padding:0;border:0;font:inherit;vertical-align:baseline}が効いた。 - 単独プレビューで上下が黒帯(=白背景が効かない)のは、中身の
marginが root の外へ抜ける margin collapsing。root にdisplay:flow-rootを足すと、余白の値は元のまま白背景で覆える(overflow-x:clipではBFCにならないので直らない)。 - jQuery
heightLineの高さ揃えは、jQueryを持ち込まず 15行の素のJS(innerWidth>768のとき最大高さを揃える)で挙動まで同じにできる。 - 参考元フォントが自サーバーの
.ttf(CORS不可の恐れ)でも、Hanken Grotesk / Noto Sans JP は Google Fonts にあるのでfonts.googleapis.comで代替すれば見た目は一致する。 - ダミー文は原文の文字数を python で測って ±0 に合わせる(今回12箇所すべて一致)。文字数が同じでも改行位置は1行ずれることがあるが、これは preview の
body既定マージン16px分で、既存セクションも同条件。
2026-08-17 neie(お知らせ一覧+ページネーション)
同じidを別セッションが使うとプレビューが別サイトの中身にすり替わる(2026-08-17 実地・大串が気づいた)
- neie のページネーションを
pagination-arrow-saki-0817で作ってhttp://localhost:8080/...を渡したら、大串の画面には白地・丸ボタン・赤矢印の別サイトのページネーションが出た。「このサイトのページネーションはこれじゃないの?」と2往復した。 - 原因は2つ重なっていた:
1. 別セッションが同じidpagination-arrow-saki-0817でcode-previews/を上書きしていた(-saki-0817は今日70件超あり、<主題>-<種別>-<author>-<MMDD>だけでは衝突する)。
2. ポート8080も別セッションが先に専有しており、こちらのpython3 -m http.server 8080はAddress already in useで起動失敗していた(&でバックグラウンド起動+ログを捨てていたので気づかない。curlは相手のサーバーが 200 を返すので疎通確認も素通りする)。 - → 恒久対策(着手時に必ずやる)
- id は着手前に
ls code-previews/ | grep <主題>で空きを確認してから決める。衝突しそうなら主題を具体化する(pagination-arrow→pagination-dotsarrow)。 - ローカルサーバーは空きポートを確保してから起動する。
lsof -nP -iTCP:<port> -sTCP:LISTENで確認し、起動後にログを必ず読む(cat /tmp/xxx.log)。8080 は共用前提で使わない。 --bind 127.0.0.1にすると Browser pane から見えないことがある(localhost が ::1 に解決される)。--bindは付けない。新しいポートはpreview_start {url}で開くと承認が通る(navigateだけだと denied になる)。- 渡したリンクが疑われたら、同じ画角・倍率で自分のプレビューを撮って画像で並べると一発で切り分けできる(
--force-device-scale-factor=2+sips -c+sips --resampleWidth)。
薄い線画セクション(ページネーション等)はサムネ検品に落ちる
shoot_section.mjsの採用ゲートは色<=10種 && 輪郭<2%で落とす。白地に細いグレー線+小さな数字のページネーションは実際に色2種/輪郭0.3%で不合格になる(描画は正しい)。- 既存の合格例
pagination-dots-saki-0817は現在ページが黒ベタなのでゲートを通っていた=中身が薄いセクションは構造的に落ちる。 - → 描画が正しいことを実測(computed値・スクショ)で確認済みなら
--forceで保存してよい。保存後に必ず PNG を目視すること。
id が既存とかぶると既存セクションのプレビューHTMLを上書きして本番を壊す(2026-08-18・実際に起こした)
- neie のフッターに
footer-sitemap-saki-0817を使ったが、同じ日に千代田工販で同じidが既に登録済みだった。code-previews/<id>.htmlを書いた時点で既存ファイルを上書きし、そこへ別セッションのgit add -Aが走って本番の千代田工販フッターが neie のフッターに差し替わった状態でpushされた。 add_section.pyはentries/<id>.jsonの重複だけは止めてくれるが、その前に書くcode-previews/<id>.htmlは素通りする。つまり「add_section.py で気づく」では遅い。- → HTMLを書く前に必ず空きを確認する。id は
<主題>-<種別>-<author>-<MMDD>だが、footer-sitemapworks-gridのような汎用的な主題名は同じ日に衝突しやすいので、主題側を具体的にする(今回はfooter-darksitemapに変更)。
bash cd ~/dev/terasu-lab test -e code-previews/<id>.html -o -e entries/<id>.json && echo "⛔ 衝突" || echo "✅ 空き" - 壊してしまったら
git log --oneline -- code-previews/<id>.htmlで元のコミットを特定 →git show <sha>:code-previews/<id>.html > code-previews/<id>.htmlで復元できる。復元後はgrep -c <参考元ドメイン>で中身が戻ったことを機械確認する。
rem基準(:root{font-size:10px})は calc(N * var(--nr)) に変換する
- neie は
:root{font-size:10px}を前提に全寸法が rem。コマンドの手順どおりsec-<id>{font-size:10px}に寄せても、rem は常に root を見るので効かない(ホストHPの root=16px で 1.6倍に膨らむ)。 - → セクションに
--nr: 10pxを定義し、抽出CSSのNremを機械置換でcalc(N * var(--nr))にする。clamp(1.6rem,1.25vw,2.4rem)もclamp(calc(1.6 * var(--nr)),1.25vw,calc(2.4 * var(--nr)))で成立する。 - あわせて
sec-<id>{ font-size: calc(1 * var(--nr)) }を入れる(入れないと font-size を持たない要素の継承値が実サイト10px/抽出16pxでズレ、em指定が狂う)。 - 検証は実サイトと抽出側の computed 値を機械比較するのが速い(今回フッターは 1440/768/390 の3幅で471項目すべて一致まで詰められた)。
モリサワのWebフォント(MFW-* / morisawafonts.net)は持ってこない
mf.cssの冒頭に複製・改変の禁止が明記されており、かつドメイン制限で他サイトからは読めない。Noto Sans JP / Noto Serif JP に置換する(neie 自身も併用しているので違和感が出にくい)。- 置換時は font-weight を元の値のまま残す(
font-weight:300の明朝を 400 で代用すると computed 比較で差が出る)。Google Fonts の URL 側でwght@300を要求しておくこと。
サムネの「残像」は表示時の縮小によるもののことがある
- 生成したサムネを見て別カラムの文字が薄く重なって見えたが、PNGを等倍で切り出すと何も無かった(ピクセル走査で背景より明るい画素ゼロ)。撮り直す前に
PILで該当座標を等倍クロップ+走査して確かめる。
2026-08-18 SpiralAI(go-spiral.ai)=STUDIO製サイトからの抽出
STUDIO製サイト(data-s-<uuid> 属性がCSSセレクタ)の抽出手順
- STUDIO は class ではなく
[data-s-<uuid>]属性セレクタでスタイルを当てる。抽出後は uuid→意味のある汎用名に機械置換する(data-s-30b9ce21-…→data-s-cards)。 - 置換時の落とし穴が2つ。どちらもURLと変数をマスクしてから置換すれば防げる:
1. 画像URL(storage.googleapis.com/.../s-480x480_<uuid>.webp)に uuid が入っており、素直に置換するとアセットが404になる。
2.data-r-…パスや--gap-h-<uuid>は親のuuidを含む。マップに無い=抽出漏れではないので、ctx1等の汎用トークンへ自動割当てにする。 - CSSは
:where(.render-canvas) …で始まるものが素のリセット相当。.render-canvasをsec-<id>に置換すればそのままスコープ化できる(html/body/*を持ち込まずに済む)。
「スマホ用に別DOMを丸ごと用意している」サイトがある
- SpiralAI は 540px以下でPC版DOMを
display:noneにし、別に用意したSP専用DOMを出す(CONTACT/RECRUITカード)。PC版だけ抜くとスマホで何も出ないセクションになる。 - 見つけ方: 390px幅で対象テキスト(
CONTACT等)を全部拾い、offsetWidth||offsetHeightで可視な方を探す。可視ノードが別uuidならSP専用DOMがある。 - 対処: 両方を同梱し、切替のブレークポイント(元サイトの
@mediaを読む=今回は540px)ごと再現する。
器(親の余白・中央揃え)は"セクション本体の外"にあるので抜けやすい
- 抽出対象を
sec-<id>で包むと、元サイトで親が持っていた「中央揃え」「左右余白」「上下余白」が消える(今回①が左寄り・②が上に詰まる、と大串FB)。 - → 親チェーンの computed(
margin/max-width/align-items)を確認し、器sec-<id>側に再現する。今回は①にdisplay:flex;align-items:center、②に元footerのmargin:76px 48px(990px以下57px/24px、540px以下16px)を移した。
全画面の"極薄テクスチャ背景"はサムネ検品に落ちる(--forceが妥当なケース)
- 紙テクスチャ(ほぼ白+微細な粒のGIF)は
shoot_section.mjsの「中身が写らない(色2種/輪郭0.0%)」で弾かれる。描画は正常で、被写体が本当に薄いだけ。 - 撮影幅を狭めても粒は大きくならない(
background-size:coverなので常に画面いっぱいに伸縮する=粒の相対サイズは不変)。ズームでの回避は不可能。 - → 実物PNGを目視確認したうえで
--forceで保存する。ただし一覧カードではほぼ白に見えることは、登録時に大串へ明示する。
同時セッション中の rm _tmp_* と git add -A は事故る
_tmp_*.mjsは別セッションの作業中スクリプトで、しかもリポにコミット済みのことがある。まとめてrmすると他人の作業を消す(今回発生・git checkout -- <files>で全復元できた)。- 逆に他セッションの
git add -AがこちらのWIPを巻き込んでコミットすることも起きる(今回、私の3セクションは相手のコミットに同梱されて push 済みだった)。 - → 片付けは自分が作ったファイル名を明示して消す。コミットも
git add -Aではなく自分の対象ファイルだけ明示ステージする。sections-data.jsonは CIが再生成するので、他セッションのWIPが混ざる状況ではコミットしない。 - CIは連続pushで前のrunがcancelledになる(
X表示=失敗ではない)。successが出た run の sha に自分のコミットが含まれるかはgit merge-base --is-ancestor <自分のsha> <runのsha>で機械確認する。
node_modules が壊れて Playwright 系ツールが全部落ちることがある
- 症状:
Cannot find package 'playwright'。原因はnode_modulesが別Mac用の絶対パス(/Users/<別ユーザー>/…)へのシンボリックリンクに差し替わっていたこと。 - 復旧:
rm node_modules && npm install→ 続けてnpx playwright install chromium(パッケージが上がるとブラウザのビルド番号も変わるため。1200→1228 で必要になった)。
2026-08-18 StockSun(stock-sun.com/about/)3セクション(saki)
元サイトがwebfontを読み込んでいないなら、こちらも足さない(今回の最重要)
- StockSunは
font-family:"Noto Sans JP", sans-serifを指定するだけで @font-face もGoogle Fontsリンクも無い(=閲覧環境のOSフォントで出るのが"元サイトの見え方")。 - 良かれと思って Google Fonts の Noto Sans JP を足したら、
70名の「名」が h30→44px・x-13.8px ズレて不一致になった。webfontとOSフォントで ascent/descent が違うため。 - → 元のfont-family指定をそのまま写すのが最も忠実。webfontを足すのは「元サイトが実際に読み込んでいる時」だけ(2026-08-16の@import併記ルールはその場合の話)。
ホスト埋め込み対策リセットは :where() を付ける(2026-08-16の型の修正)
.sec-X.sec-X p{margin:0}は特異性(0,2,1)で、セクション本体の.sec-X .c-client__ttl{margin-bottom:2rem}(0,2,0) を潰す(見出し下の余白32px→0で詰まった)。- →
.sec-X.sec-X :where(p, img){margin:0;padding:0}(0,2,0)にすると、ホストのp{margin-bottom:1.6em}(0,0,1)には勝ちつつ、本体CSSには先に書いてある分だけ負ける=狙いどおり。
「マッチするCSSルールだけ抜く」抽出スクリプトが速くて確実
- playwrightで実ページを開き、
document.styleSheetsを順番どおりに列挙 → 各ルールのセレクタを対象サブツリーの全要素にmatches()で当てて、ヒットしたものだけ@media付きで回収する方式。minifyされた448KBのCSSでも 36〜50ルールまで絞れた(=121〜268行の自己完結HTML)。 - CORSで
cssRulesが読めないシートは、Node側で fetch → ページ内でnew CSSStyleSheet().replaceSync(text)して再パースする(ページ内fetchはCORSで落ちる)。 - 素の要素セレクタ(
div,img,li,p,span,ul)・全称・::-webkit-scrollbarのルールは捨てて、スコープ版リセットに手で書き直すとそのまま「貼っても崩れない形」になる。
検証は「元サイトと抽出版のcomputedを全要素diff」(今回も有効)
- 結果: ロゴ一覧=差分ゼロ、数字カード=サブピクセル(278→277.984px)のみ、理由リスト=アイコンだけ差分。
- 遅延ロード画像は元サイト側が未ロード(h=24等)のまま計測されるので、
data-src→srcを入れたこちらの方が正しい。この差分は「不一致」ではないと判断してよい。
同じMacで複数セッションが同時に走っている時の事故と回避
- 作った3ファイルが、別セッションの
git add -Aに巻き込まれて先にコミットされていた(中身は同一だったので実害なし)。着手前にgit ls-files | grep <id>で同名idの既存を確認すると早い。 - 自分の分だけ確実に入れるには
git commit -m "..." -- <明示パス...>(indexに他人のstagedがあっても巻き込まない)。git add -Aは厳禁のまま。 - CIは後続pushで次々 cancelled になる(concurrency group)。runの成否を追うより、本番URLの200と
/sections一覧で確認する方が速い。 build.py前に他人の未コミットentries/*.jsonを/tmpへ退避 → build → 自分の分だけ add → commit → 戻す、は今回も有効。
playwrightが「Executable doesn't exist … chromium_headless_shell-1228」で落ちたら
- 誰かが node_modules を更新してplaywright本体のバージョンが上がると、旧ブラウザ(1200)しか無くて全ツールが落ちる。→
cd ~/dev/terasu-lab && npx playwright install chromiumで復旧(既存1200は残るので他は壊れない)。
2026-08-17 エイトデザイン /about(Next.js+CSS Modules+Lenis)から2件
Next.js(CSS Modules)サイトの抽出の型
- マークアップは SSR HTML(
curl)から取る。レンダリング後DOM(headless --dump-dom)は文字分割スパンが1文字=4要素に膨らんでいて読めない。SSRなら分割前の素の構造が出る。 - CSSはチャンクを総当たりで落としてハッシュで特定する:
<link ... /_next/static/chunks/*.css>を全部curl→grep -l "<モジュールハッシュ>__" *.css。1モジュール=1ファイルとは限らないので、コンポーネント(ボタン/見出し切替/目玉)ごとに探す。 - CSSの切り出しはブレース走査のパーサで。ざっくり正規表現だと
@media (hover:hover)内のホバー規則を丸ごと取りこぼす(実際に落として、ボタンのホバー反転が消えた)。@media文脈ごと拾う。
液体レイアウト(html{font-size:calc(100vw/1440)})は rem→calc(var(--u)*N) に機械変換する
- この手のサイトは
96remのような値が全編に出る。sec-<id>{font-size:...}に寄せても rem は root 基準のままなので効かない(スコープ化の定石が唯一通用しないケース)。 - 正解は
sec-<id>{--u:calc(100vw/1440)}を置いてNrem→calc(var(--u)*N)に一括変換。SP側は@media(max-width:960px)で--u:calc(100vw/375)に差し替え(参考元の--liquid-htmlrootと同じ設計)。 - 落とし穴=負のrem。
(?<![\w.#-])(\d...)remのような否定先読みだとright:-90remが変換されず、root既定16px換算で 要素が1440px右へ吹き飛ぶ(目玉が画面外に消えた)。(?<![\w.#])(-?\d*\.?\d+)rem\bで符号ごと拾う。calc(100% - 128rem)は空白があるので誤爆しない。
動きは「推測せず Playwright で数値を実測」してから実装する
- Lenis を使うサイトは
window.scrollToが効かない(body.scrollHeightが画面高と同じ・実体は wrapper の transform)。page.mouse.wheel()で動かし、2秒以上待って落ち着かせてから読む。0.6秒待ちだと慣性の途中を読んでしまい、進捗が非線形に見えて解析を誤る。 - 今回取れた値をそのまま実装に使った: インデックス切替=
round(-main.top / vh)/見出しドラムの回転=90deg + 270deg × index(-90ではなく+270。前回りに回る)/写真パララックス=リード上端が画面上端+170pxから約1050pxの間を power2.out、y −130/−100/−60vh → 100/200/300vh・scale .2 → .8/.7/.6・x 15%→0。 - 登場アニメも同じ手で計測(100msごとにinline styleを読む)。文字ロールは stagger 0.022s / duration 1.1s / power3.out、行は
y:1em→0+opacity、和文は0.7s、目玉は0.95s遅れてback.out(1.7)。 - 合否は目視でなく座標で判定する。参考元と自作を同じviewportで開いて
getBoundingClientRectを突き合わせ、画像枠[270,200,900,500]/見出し[64,170,1312,90]/ボタン[1088,578,288,72]まで一致させた。ズレたら原因が必ずある(今回は分割spanに入れたline-height:1が行高91.2→96pxにしていた。参考元のline-height:1.は不正値で無効=入れないのが正解)。
文字分割は「単語単位でくくる」
- 1文字ずつ
inline-flexに入れると、SPで単語の途中で改行する(SERVIC / ES)。単語ごとにdisplay:inline-block;white-space:nowrapの箱でくくると語中改行が消え、参考元のSP(3行)と同じ見え方になる。 - 参考元がSPでDOMごと行を出し分けている場合、単一DOMでは行幅が画面幅いっぱいになり装飾(目玉)の位置がズレる。
@media(max-width:960px){ .row{width:min-content} }で「一番長い単語の幅」に縮めると同じ配置になる(fit-contentは利用可能幅を返すので効かない)。
スクロール連動セクションのサムネは自前で撮る
tools/shoot_section.mjsはセクション先頭から4:3で切り出すので、100vh×5ブロックのスクロール連動ものは導入部だけが写る(真っ暗+小さい写真=何のセクションか分からない)。- viewport 1440×1080・
deviceScaleFactor:2で 見せ場までスクロールしてから撮り、img/sections/<id>.pngへ直接保存すれば規格(2880×2160)は同じ。normalize_thumbs.pyも不要。
terasu-lab が他セッションで汚れている時は 一時worktreeでpushする
- 実際に遭遇した状態: 未pushのローカルcommitが10件以上+作業ツリーに36ファイルの未コミット変更(全部他セッションの進行中作業)。この状態で
git pull --rebaseすると autostash が衝突し、git addしたファイルが他セッションのcommitに吸われる。 - 安全な型(他セッションも同じ方式で運用していた):
git worktree add --detach /private/tmp/lab-push-<自分の名前> origin/main→ 自分のファイルだけ cp →git add <明示パス>→ commit →git rebase origin/main→git push origin HEAD:main→git worktree remove。共有の作業ツリーに一切触らない。 - 未追跡ファイルがリモートと衝突して pull が止まる時は、消さずに
_local_backup_<日付>/へ退避してから pull(他人の未コミット作業を消さない)。 - CIはpushのたびに走り、後続pushで前のrunがcancelされる。自分のrunがcancelでも中身は後続runに含まれるので、runの成否ではなく本番URLで反映を確認する(
/section/<id>/が200になるまでポーリング)。
2026-08-18 株式会社プレックス(plex.co.jp)から2件(ニュース/記事カルーセル)
巨大CSS(467KB)からの抽出は「ブラウザで拾う」より「ローカルで機械抽出」が速くて確実
bs4 + soupsieve + tinycss2はこのMacに入っている。DOMダンプ(Chrome headless--dump-dom)とCSSをローカルに落として、セレクタが実際にセクション内の要素にマッチするルールだけ抜くスクリプトを書くのが最短(news 136ルール / articles 221ルールまで自動で絞れた・全部で数秒)。- マッチ判定は2段構え。① soupsieve で実DOMにマッチするか、② セレクタ中のクラスが全部そのセクションで使われているか(
is-activeなど実行時に付くクラスを拾うために必須。これが無いと出現アニメのCSSが丸ごと落ちて「アニメだけ消えた」状態になる)。 html{}/body{}/:root{}は自動でスキップし、body由来の font-family / color / font-feature-settings はsec-<id>{}に手で寄せる。plexはhtml{font-size:1rem}(=16px・ブラウザ既定と同じ)なので rem はそのままで一致した。
Splide の splide.length は 0 を返す(進捗バーが壊れる真犯人)
- 進捗ドットは
left: calc(var(--current) / var(--total) * 100%)。元サイトはthis.total = ee.lengthで入れているが、実サイトでも--total: -1のまま=本番の進捗ドットが動いていない(元コード側のバグ)。 - 正しくは
splide.Components.Controller.getEnd()(今回11)。Lab版はこちらで動くようにした(大串OK済み)。 - 併せて:Splideは深くネストした
.splide__arrowsもちゃんと拾う(自前で click→go()を張ると二重送りになるので張らない)。
ブラウザペインが非表示だと innerWidth = 0 → max-width ブレークポイントが誤発火する
breakpoints:{767:{destroy:true}}のカルーセルが「mountされない・矢印が効かない」ように見えるが、コードは正しい。ペインが隠れた瞬間に幅0=SP判定でdestroyされていただけ。- 動作検証はブラウザペインでやらない。リポの playwright(
node_modulesは mikke 側を指すリンク)で実ビューポート1440で開いてクリックまで機械検証するのが正解。
bash /Applications/ChatGPT.app/Contents/Resources/cua_node/bin/node <リポ直下に置いた検証.mjs> # /tmp に置くと playwright を解決できない - ペインは「見た目の最終確認」だけに使う。数値・動作は playwright の evaluate で取る。
今回の実測一致(完全再現の確認に使える型)
- 元サイトと抽出版を同じ幅で開いて
getBoundingClientRect().height/padding/font-sizeを突き合わせると一発で判定できる(news: 高さ894px・上下160px・タイトル16px・日付12pxが完全一致)。 - 単独プレビューはスクロールバー16px分だけ container が細くなるが、これは実HPに貼れば消える差なので不一致扱いしない。
Alpine の小さな挙動は素のJSで再実装できる
x-intersect.full.once→IntersectionObserver({threshold:1})でis-activeを付与。x-data="hover"のロール(rollEnter/rollLeave)は、is-hoverを enterTime だけ付け、離れたらno-hoverを leaveTime 付けて外すという10行程度の再実装で同じ動きになる(元実装を main.js から読んで写すのが速い)。
15. cssRules=1 のもう一つの原因=@font-face の base64 が多重化して url("data:…; が欠落(2026-08-18 実地)
- 既知の 11「cssRules 数で機械判定」と同じ症状(
cssRules.length === 1・CSS全滅)だが、原因が id衝突ではないケース。フォントをbase64インライン化する処理が重複適用され、1つのsrcがこうなっていた:
css src:url("data:font/woff2;base64,XXX") format("woff2");base64,XXX") format("woff2");base64,XXX") format("woff2");… /* ^^^^^^^ url("data:font/woff2; が欠けた不正トークン */ - CSSトークナイザはここで復帰できず、1つ目の @font-face だけ読んで残り全部(50〜180ルール)を捨てる。目視では「フォントが違うだけ」に見えるので気づけない。
- 検出(base64の妥当性まで見る):
bash # 1行の @font-face に base64 が2個以上入っていたらアウト grep -o 'base64,' code-previews/<id>.html | wc -l # @font-face の数と一致するのが正 - 修復(重複を落として最初の1つだけ残す):
python pat = re.compile(r'(src:url\("data:font/woff2;base64,[A-Za-z0-9+/=]+"\) format\("woff2"\);)' r'(?:base64,[A-Za-z0-9+/=]+"\) format\("woff2"\);)+') s2, n = pat.subn(r'\1', open(f).read()) - 切り分けの型:
sed '<@font-face行>d'した版でcssRulesを数え、急に 1→54 に戻れば犯人は @font-faceと確定できる。
16. rem基準が vw連動(html{font-size:1.14vw})のサイトは sec-<id>{font-size:…} では移植できない
- j-security.co.jp は
@media(min-width:768px){html{font-size:max(9px,1.1428571429vw)}}/ SP は4.2666666667vw。rem は常にルート基準なので、sec-<id>に font-size を書いても中のremは 16px のまま=コマンド定義の「sec-<id>{font-size:10px}へ寄せる」が効かない唯一のパターン。 --rtを置いてNrem → calc(N * var(--rt))に機械変換する(完全等価・レスポンシブも元のまま):
css .sec-<id>{ --rt: max(9px,1.1428571429vw); font-size: var(--rt); } @media (max-width:767px){ .sec-<id>{ --rt: max(9px,4.2666666667vw) } } /* .title{font-size:clamp(12px,1rem,1.5rem)} → clamp(12px,calc(1 * var(--rt)),calc(1.5 * var(--rt))) */
変換はre.sub(r'(-?\d*\.?\d+)rem', r'calc(\1 * var(--rt))', css)の1行で足りる。sec-<id>自身にもfont-size:var(--rt)を付けるのを忘れない。付け忘れると font-size 未指定の要素だけ 16px を継承して、実サイトと 0.46px ずれる(行送りが変わり、テキストの折り返し行数が変わって高さが29px単位でずれる)。
17. 「完全再現できたか」は実サイトとの computed値の機械比較で確定させる(目視では詰め切れない)
- 実サイトと自作プレビューを Playwright で同時に開き、対応セレクタの computed を突き合わせる。今回これで3つの取りこぼしを検出した:
| 検出された差 | 真因 |
|---|---|
|fontSize: 16.4571 / 16|sec-<id>に html 相当の font-size 未設定(上記16) |
|lineHeight: 24.68 / 28|body{line-height:1.75}の後に reset のbody{line-height:1.5}があり後勝ちで1.5。1行に畳むとき先に書いてある方を採ると間違える |
|width: 1440 / 1424| プレビュー単体では body の既定 margin 8px が残る →<body style="margin:0">(既存作法・<style>内ではないのでスコープ検証に引っかからない) | - 比較する props:
fontSize/fontWeight/lineHeight/letterSpacing/color/margin*/padding*/width/gap/borderTop*/backgroundColor/display/position/top+getBoundingClientRect()の w/h。 - 残ってよい差: ダミー化した文字列・数値の字幅差だけ(例
70,000→60,000で 5px)。それ以外が残っているうちは Step3 に進まない。 - 実サイト側は
body:not(:has(.loader))等でローディング中だけoverflow:hiddenになっていることがある。スクロールバーの有無で 16px ずれるので、差がその値のときはdocument.documentElement.clientWidthを両方で測って確認する。
2026-08-18 お問い合わせフォーム(j-security.co.jp)で得た知見
成果: contactform-split-saki-0818。PC1440でセクション高さ 2059.046875px が実サイトとピクセル一致、
リネーム前後は 146要素×6287項目が3幅すべて完全一致。フォーム系は動きの検証項目が多いので、下記の型が効いた。
:LiFlaskConical Typekit配信フォントは他ドメインでは使えない(412)→ 実フォントは推測せずCDPで確定する
use.typekit.net/<kitId>.cssは 412 precondition failed(ドメイン制限)。参考元がAdobe Fontsを使っていたら
その欧文は再現できないと最初に確定させ、確認①で正直に伝える。- ただし諦める前に 「実際に何が描画に使われたか」をCDPで測る。今回それで
「日本語はTypekitではなく元サイト自前の woff2(Kinto Sans)が当たっている」と分かり、
そのwoff2を絶対URLで読むだけで日本語は完全一致した(=差は欧文だけに限定できた)。
js const client = await page.context().newCDPSession(page); await client.send('DOM.enable'); await client.send('CSS.enable'); const {root} = await client.send('DOM.getDocument', {depth:-1}); const {nodeId} = await client.send('DOM.querySelector', {nodeId: root.nodeId, selector: '<sel>'}); const {fonts} = await client.send('CSS.getPlatformFontsForNode', {nodeId}); // familyName + glyphCount - 参考元の woff2 / mask用svg は CORSが
access-control-allow-origin: *なら絶対URLでそのまま使える(先に curl -I で確認)。 - 残差は「欧文グリフ幅0.3px」「SPでラベル行が1pxずつ低い」程度に収まる。これは避けようがないので確認①で明示する。
:LiRuler rem基準が vw のサイト(html{font-size:max(9px,1.14286vw)})のスコープ化
rem は常に html を見るので sec-<id>{font-size:…} では効かない。--rem 変数+機械変換が正解(既存の
accesscontact-info-saki-0817 と同型)。Nrem → calc(var(--rem)*N) を正規表現で一括変換し、clamp() の中も同時に置換する。
re.sub(r"(-?\d*\.?\d+)rem\b", lambda m: "calc(var(--rem)*%s)" % m.group(1), body)
ブレークポイントごとに --rem を差し替える(SP max(9px,4.26667vw) / PC max(9px,1.14286vw))。変換後に生remが0件であることを機械検証する。
:LiTriangleAlert 「中身が空のdiv」を消すと margin collapse で高さがズレる
元HTMLにあった Turnstile用の空 .contact__row(コメントだけ)を「意味がないから」と削ったら、
margin-bottom:43.2px と直前の 37.03px の相殺が変わり ちょうど6.17px低くなった。
→ DOM上で空に見える要素も「余白の一部」。抽出時に消さない。高さが数px合わない時は真っ先にここを疑う。
:LiShieldCheck オリジナル化(リネーム)の崩れゼロを機械証明する型
class名を一括置換した後、git HEAD版を code-previews/_tmp_before.html として横に置き、
DOM順のindex対応で全要素×全computed値を突き合わせる。クラス名が変わっても構造は同じなので完全比較できる。
git show HEAD:code-previews/<id>.html > code-previews/_tmp_before.html
今回 1440/768/390 の3幅すべてで 6287/6287 一致(差分0)。目視より速くて確実。確認②はこれで代替できる。
リネームは page-contact__main が contact__ を含むように部分一致で壊れる順序があるので、長い名前から先に適用する。
:LiUsers 複数セッション同時作業の地雷(今回3つ踏んだ)
- 他セッションの
git add -Aに作業中ファイルを巻き込まれてコミットされた。
→ 自分はgit add <自分のファイルだけ>にする。Labは1件1ファイルなので全く問題ない。 node_modulesを作り直され、pixelmatch/pngjsが消えた(package.jsonにはplaywrightしかない)。
→ ピクセル比較に依存せず computed値比較を主にする(そもそもこちらの方が原因が特定できる)。- リポ内に置いた
_tmp_*.mjsが消される。
→ 一時スクリプトはスクラッチパッドに置き、絶対パスでplaywrightを読む:
import { chromium } from '/Users/MUGI/dev/terasu-lab/node_modules/playwright/index.mjs'
:LiGitBranch CIの cancelled は失敗ではない
並行pushが続くと concurrency で前のrunが次々キャンセルされる(今回5連続)。
git merge-base --is-ancestor <自分のsha> origin/main で自分のコミットがmainに載っていることを確認し、
その後の成功したrunでデプロイされたかを見る。反映確認は /sections をPlaywrightでレンダリングしてカードの有無で判定する
(curl+grepでは出ない=一覧はJS描画)。
:LiUserCheck 追加者の恒久ルールを id に反映し忘れない
このPCからの追加は --author saki --contributor saki/id も -saki-(2026-08-13 大串指示)。
今回いったん -ogushi-0817 で作ってしまい、id・ファイル名・スコープクラス sec-<id>・JS内のセレクタを一括で揃え直した。
先に id を確定させてから Step1 に入ると手戻りがない。
フォーム系セクションで再現すべき「動き」チェックリスト
実サイトのJSから参考元CMSへの送信だけ除去し、以下は残すと実例として価値が高い(今回すべて動作確認済み)。
- 同意チェック → 送信ボタン解禁(disabled/opacity/cursor)
- 空送信 → 各項目に .…__error 生成+フォーム全体メッセージ(success/error)
- 入力/変更でエラーとメッセージをクリア
- チェックボックスの :has(input:checked) による塗り+mask SVGのチェックマーク
- 郵便番号7桁 → zipcloud(公開API) で都道府県・住所を自動補完(外部APIなのでそのまま動く)
- 送信ボタンhover → 背景色+矢印とラベルの入れ替えスライド
2026-08-18 ポジティブドリームパーソンズ(3件同時)で得た知見
- CSSの
mask-imageはクロスオリジンだと問答無用でブロックされる(=マスク全面が黒=中身が全部消える)。
元サイトでは同一オリジンなので動いていたものが、Labに持ってくると「斜線が出ない」形で壊れる。
→ マスク用SVGだけ data URI に埋め込む(数百バイトなので軽い・形状は元データと1バイト同じ)。
background-imageは CORS 不要なのでそのまま参考元URLでよい=画像は元URL維持のルールと両立できる。
切り分け方: 「背景だけの箱/単一マスクの箱/2枚マスクの箱」を並べたテストHTMLを1枚作って撮ると一発で分かる。 - rem基準(
html{font-size:1.1111111111vw})はsec-<id>{font-size:...}に移しても効かない(remは常にhtml基準)。
コマンド本文の「rem基準はスコープ内に寄せる」を素直にやると崩れる。
→--r変数をsec-<id>に持たせ、1.25rem→calc(1.25 * var(--r))に機械置換する。
ブレークポイントごとに--r:16px/--r:1.1111111111vwを切り替えれば元サイトと完全一致。 - ローカルmainが他セッションの未pushコミットを抱えている時(
ahead 21 / behind 31等)は、
git pull --rebaseが他人の未コミット変更で失敗し、素直にpushすると他人の未完了分まで公開してしまう。
→git worktree add --detach <tmp> origin/main→ 自分のコミットだけcherry-pick→push origin HEAD:main。
他セッションのツリー・コミットに一切触らずに自分の分だけ本番へ出せる。終わったらworktree remove。 build.pyが作るsections-data.jsonは他セッションの未コミットentriesまで巻き込む。
CIが本番ビルド時に再生成するので、集約JSONはコミットしない(git checkout -- sections-data.json)のが安全。- 動画から箇所を特定するとき、このPCには ffmpeg が無いが
~/Library/Python/3.9/lib/python/site-packages/imageio_ffmpeg/binaries/ffmpeg-macos-aarch64-v7.1が使える。
(swiftはCLTのモジュール重複エラーで動かない=AVFoundationスクリプトは諦めてよい) - 同名idのcode-previewsが既にあることがある(前日の未登録プレビューが
git logに居る)。
add_section.pyは entries/ しか見ないのでHTMLの上書きは検知されない。着手前に
ls code-previews/<id>.htmlとgit log -1 -- code-previews/<id>.htmlを確認する。
2026-08-18 野村不動産PMO(setupoffice)FV / FAQ 追加で得た学び
素材が複数あるとき「全部が同じセクションの説明」と決めつけない(最重要)
静止画1枚+動画2本+URLをもらい、動画2本も静止画と同じセクション(Three Answers)の動きを写したものと決めつけて着手した。実際は動画①=FV、動画②=FAQ の別セクションで、素材は3箇所を指していた。
→ 着手前に、素材1つずつが「どのセクションか」を先に特定して一覧提示する(Step0の一覧提示を素材単位でやる)。
ffmpeg が無い環境での動画フレーム抽出(実地で確立)
ffmpeg も pyobjc(AVFoundation) も無い。Chrome に <video> で読ませて canvas に描くのが唯一動いた方法。
- ローカルHTTPサーバに .mov を置き、<video>+canvas.drawImage でフレーム化
- seeked 発火直後に描くと前のフレームが描かれ、全フレーム差分0=「動きなし」と誤判定する。setTimeout(120ms) を挟むこと。これで最初「動画に動きなし」と誤報告した
- --headless + --virtual-time-budget では動画デコードが進まない(フィルムストリップが全部同じ絵になる)。動画がらみはブラウザペインで実行する
ブラウザペインのバックグラウンドタブは「見えない」
computer screenshotが真っ白/IntersectionObserverのコールバックが配信されない、という現象が出る- DOM計測(getBoundingClientRect / getComputedStyle)は正常に効く
→ 数値検証はブラウザペイン、見た目とIO/アニメの発火確認は実Chrome(Google Chrome.app --headless=new --screenshot)と使い分ける
CSS抽出は「クラス名 grep」だけでは必ず取りこぼす
.faq__* を拾っただけでは、親側の汎用ルール .setupoffice-caption ul{margin-top;display:flex;gap:4px} と ... ul li{font-size:12px;color:#00031a99} を取りこぼしていた(設問の間隔44pxと文字の不透明度60%が変わる)。
→ 抽出後に必ず「実サイト vs 自分の再現」で算出スタイル+幾何を機械比較する。要素×プロパティのJSONを両方で吐いて突き合わせると、目視では絶対に気づけない差が出る。今回はこれで2件検出。
(一致確認の例:総高さ1322.82px・設問高さ77px・回答パネル164.8px までピタリ一致すれば完全再現と言い切れる)
terasu-lab は他セッションと並行作業になる前提で push する
着手時 git pull 済みでも、作業中に ahead 21 / behind 31 まで乖離し、git pull --rebase は「他セッションの未コミット200ファイル」で失敗、push も連続で non-fast-forward / cannot lock ref。
→ 隔離ワークツリーで自分の分だけ積むのが安全(ローカルの他人作業に一切触れない):
git worktree add --detach /tmp/lab-push origin/main
# 自分のファイルだけ cp → git add <明示パス> → commit
cd /tmp/lab-push && for i in 1 2 3 4 5; do
git fetch -q origin && git rebase -q origin/main >/dev/null 2>&1
git push -q origin HEAD:main 2>/dev/null && break
done
git worktree remove --force /tmp/lab-push
git add -Aは厳禁(既知ルール)。CI がbuild_core.py→build.py→build_lab.pyを回すので、生成物(sections-data.json)はコミット不要=他セッションの未コミットentryを巻き込まずに済む- CI run が
cancelledになっても失敗ではない(後続pushに追い越されただけ)。run を追うより本番URLをポーリングして反映確認する方が速く確実
id 衝突は起こる(同じサイトを別セッションが同時にやる)
faq-accordion-saki-0817 が既に別セッション登録済みだった(同じPMOだが /faq/ ページ・回答内にSTEP縦線フロー図の別物)。
→ add_section.py が「既に存在します」で止めてくれる設計に助けられた。--force で潰さず、id を変えて共存させる(今回 faq-accordion-setup-saki-0817)。
淡色ミニマルなセクションはサムネの採用ゲートに誤って落ちる
FAQ(地色#ddddd9+細い罫線と文字だけ)が「中身が写らない(色24種/輪郭1.8%)」で不合格。実物は正しく描画されていた。
→ --force で保存し、必ず画像を目視してから採用する。リポの過去ログにも「白基調ミニマルは機械だけでは正常/異常を区別できない」と同じ記録あり。
2026-08-17 エイトデザイン(eightdesign.co.jp・Next.js + three.js + GSAP + Lenis)
WebGL描画かどうかは「文字がDOMにあるか」で機械判定できる
FVスライダー(湾曲画像・番号・カテゴリのピル)は一見HTMLだが document.body.innerText に「BINO」「エイトトレーラー」が1つも出てこない=WebGL描画。逆に巨大見出し「EIGHT JOURNAL」やフッターの「KEEP IN TOUCH ON...」は1文字ずつspanに割られているため生HTMLのgrepでは0件でも innerText には出る。
→ --dump-dom の生HTMLをgrepして「無い」と判断しない。ブラウザで innerText を見る。加えて document.querySelectorAll('canvas') の実寸を見れば全面canvas(1440x900)が一発で分かる。
→ 画像が湾曲している=シェーダー。CSSでは作れないので抽出不可の合図。
rem基準が html{font-size:calc(100vw / 1440)} の"リキッド設計"はスコープ化で必ず壊れる
remは常にhtml基準なので、sec-<id>{font-size:…} に寄せても効かない(ボタン文字が0.89pxになった)。
→ --sec-root を定義し、抽出CSS中の Nrem を機械的に calc(N * var(--sec-root)) へ置換する。--sec-root: var(--liquid-htmlroot) にすると、元サイトのメディアクエリ(1440基準→375基準)もそのまま効く。
→ 併せて 抽出CSSに残る html{font-size:var(--liquid-htmlroot)} は落とす(二重適用になる)。
next/font の --font-xxx は別ルールで定義されている=拾い漏れると別フォントで描画される
font-family:var(--font-unbounded) だけコピーしても変数定義が来ないので Zen Kaku Gothic New にフォールバックしていた。
→ セクションのCSSに --font-unbounded:"Unbounded",sans-serif; 等を明示的に足す(Google Fontsから読む)。数値検証で getComputedStyle(el).fontFamily を必ず確認する。
サムネは「ページ先頭・スクロール無し・透明要素は全部表示」で撮られる
shoot_section.mjs は先頭を撮り、reveal() が opacity<0.15 の要素を強制表示する。そのため
- スクロール用の先頭余白(100vh等)を入れるとサムネが空白になる → 余白は入れず、スクロール余地が無い時は演出を一度だけ再生する分岐にする。
- ホバー時だけ出るレイヤ(目・手など)が全部出る → 装飾要素に aria-hidden="true" を付ける(reveal は aria-hidden 配下を触らない仕様)。
terasu-lab は同時セッションが多く「共有インデックス」で他人のファイルを巻き込む
git add から git commit までの間に別セッションが stage したファイル(他案件のentry/preview)が自分のコミットに混入した。さらに他セッションの未コミット変更があるため git pull --rebase が実行できない。
→ 別worktree(git worktree add /tmp/... origin/main)を作り、自分のパスだけ git checkout <commit> -- <path> で持ち込んでコミット→push。作業ツリーに一切触らないので他セッションを壊さない。終わったら git worktree remove。
WebGLの画像帯を近似するときの素材の取り方
フッターの写真帯は fv_videos.mp4(3×3タイル・35MB)を貼ったWebGL。近似のためにハイライト画像を探すより、元の動画から ffmpeg で3×3を切り出して9枚の静止画にするのが最も元に近い(crop=1280:720:x:y)。ffmpegが無い環境は npm i ffmpeg-static で即用意できる。
クラス名の一括リネームで sec-<id> スコープ名を壊す(2026-08-18・The Day Osaka)
c-news → newsslide の単純置換が sec-news-swiper-fullbleed-saki-0817 の中の c-news("se|c-news|-swiper…")まで置換し、
スコープ名が senewsslide-swiper-… に化けた。CSSもHTMLも同じ文字列なので見た目は崩れず、気づかないまま登録されるのが怖い。
→ リネーム後は必ず grep -o 'class="sec-[a-z0-9-]*"' で ルートが sec-<id> のままかを機械確認する。
→ そもそも id に news header footer 等の一般語が入ると旧クラス名と衝突しやすい。置換は長い名前から順に、かつ置換対象から sec- 始まりを除外するのが安全。
ヘッダー単体はサムネ検品に必ず落ちる → 「メガメニューを開いた状態」で撮る
白いヘッダー帯だけのセクションは 中身が写らない(色11種/輪郭0.7%) で撮影が2回とも不合格になる(正しい判定=カードが真っ白になる)。
--force で通しても上に帯・下8割が白の使えないカードになる。
→ tools/shoot_section.mjs を一時コピーし、prepare(page) の直後に ①閉じているドロワーを display:none ②メガメニューの is-megaOpen/is-show を付与 する処理を挟んで撮ると自然に合格する(1107KB・中身558px)。
→ このとき メガメニュー下の暗幕(megaBg02 = rgba(51,51,51,.3))も外す。元サイトでは下のFVを暗くする正しい演出だが、
セクション単体では「下半分が灰色一色」の絵になり、原因を暗幕と気づくまで2回撮り直した。撮り終えたら一時コピーは削除する(リポに残さない)。
動画で「どの箇所か」を渡された時、このPC(咲輝機)ではフレーム抽出が両方死んでいる
ffmpeg 未インストール、かつ /usr/bin/swift も CommandLineTools の SwiftBridging 再定義エラーでビルド不可、
システムpython3にPyObjC(AVFoundation)も無い。→ フレーム抽出に時間を使わない。参考URLがあるなら実サイトを直接触って動きを確認するのが最短。
どうしても必要なら npm i ffmpeg-static(上の項目と同じ手)。
2026-08-17 kishun.co.jp(株式会社貴瞬)から7セクション追加で得た知見
① 参考元のフォントは他ドメインから読めない(CORS)=全セクションが代替フォントで描画される
kishun.co.jp のフォント(woff2)は access-control-allow-origin を返さないため、@font-face をそのまま持ってきても読み込み失敗して黙って代替フォントになる。見出しが1行に収まらない等の微妙なズレの正体がこれ。
→ 参考元ドメインの @font-face は入れず、同じ書体を配布元CDNから読む(Noto Sans JP=Google Fonts / General Sans=Fontshare https://api.fontshare.com/v2/css?f[]=general-sans@300,400,500,600,700)。
→ 検証は document.fonts.check() だけだと false でも実際は効いていることがある。canvasで同じ文字列の描画幅を実サイトと突き合わせるのが確実(今回 "Statement" 123px・本文226px で一致を確認)。
② スケール基準の CSS変数を JS で入れているサイトがある
このサイトは clamp(14px, calc(20 * var(--mw) / var(--bw)), 24px) の --mw に JSで window.innerWidth を毎回代入していた(--mw の初期値 1920px のままだと文字が最大値に張り付いて別物になる)。
→ 抽出時は --mw: 100vw に置換すればCSSだけで同じ挙動になる。「clampなのに元とサイズが違う」ときは変数の出所をJSまで追う。
③ :where(:root) のベース指定を拾い忘れると全体の行送りがズレる
:where(:root){line-height:1.5; text-spacing-trim; text-autospace; line-break; overflow-wrap} が効いていた。line-height が normal になると見出しの高さが 149px→119px のように変わる。
→ scoped base に line-height:1.5 +日本語まわりの既定値まで移植する(html{font-size} だけ見ていると漏れる)。
④ レスポンシブ検品の⛔が「プレビューHTMLの body 既定マージン8px」由来のことがある
position:fixed で left 未指定の要素(メガメニュー)は静的位置=8px から始まり width:100%(=viewport) で右に8px はみ出す。カルーセルも同様に4pxオーバーになる。
→ 直し方は2つとも1行: left:0 を明示/セクションの器に overflow-x: clip(参考元が body{overflow-x:clip} でやっていることのスコープ版)。見た目は変わらない。
⑤ 同じ日に他メンバーが登録していてIDが衝突する(今回2件)
footer-sitemap-saki-0817 header-globalnav-saki-0817 が既存だった。add_section.py は entries の重複は止めてくれるが、code-previews/<id>.html は先に書くと黙って上書きされる(=他人の作業を消す)。
→ 書き込む前に ls code-previews/<id>.html entries/<id>.json で空き確認。衝突したら git checkout -- <file> で戻してからID変更(今回 footer-sitemapsns / header-frostmenu に変更)。
⑥ terasu-lab が他セッションの作業中で分岐しているときは worktree で回避
git pull --ff-only が ahead 15/behind 20 で失敗し、作業ツリーには他人の未コミット削除が残っていた。この状態で git add -A すると他人の作業を消す。
→ git worktree add <別ディレクトリ> -b <作業ブランチ> origin/main+ git submodule update --init でクリーンな場所を作り、そこで登録→git push origin HEAD:main。本体リポには一切触らない。node_modules は本体からシンボリックリンクで共有できる。
→ push は他メンバーと競合するので fetch→pull --rebase→build.py 再実行→push を数回まわす。自分のrunが cancelled になっても後続runが最新mainを配信するので、本番URLと /sections の掲載で確認する(今回 908カード中に7件掲載を確認)。
⑦ 動画だけ渡されたときのフレーム確認
このMacに ffmpeg は無く、swiftc も SDK不整合で使えない。Playwright(Chrome) で <video> を seek → canvas に並べてコンタクトシート化が実用的(file:// はcanvasが汚染されるのでローカルHTTPで同一オリジンにする/player HTMLはキャッシュされるので id ごとに別ファイル名にする)。
2026-08-18 kishun.co.jp /contact/ のフォームを追加して得た知見(contact-form-split-saki-0817)
Contact Form 7 の「見えないスピナー」が送信ボタンの中心を36pxずらす
CF7 の送信ボタンには <span class="wpcf7-spinner">(visibility:hidden / width:24px / margin:0 24px)が必ず並んでいる。
justify-content:center の中で幅72pxの不可視要素が場所を取るため、実サイトのボタンは中心より 36px 左に寄る。
→ これを省くとボタンだけ右にズレる(今回それで37px差が出た)。不可視スピナーごと再現するのが正解。
sec-<id> ルートの背景が上に抜けるのはマージン相殺
中の .pContact{margin-top:25.7vw} がルート外へ抜けて、背景色がその分だけ塗られない(上が白く残る)。
→ ルートに display:flow-root を1行足すだけで元と同じ塗りになる(レイアウトは1pxも動かない)。
あわせて <body style="margin:0;background:<セクション背景色>"> にする(body既定8pxで全体が8px下・末尾の空白行ボックスで下に白帯が出るのも同時に消える)。
リセットに textarea{vertical-align:top} を勝手に足すと5pxズレる
元サイトの reset には無い指定を「良かれと思って」足すと、インラインブロックの行ボックスが変わり以降が5px上がる。
→ ベースは元サイトにある物だけをスコープ化して移植する。迷ったら足さない。
同じ理由で、元DOMのラッパー(CF7の span.wpcf7-form-control-wrap 等)も外すと行ボックスが変わるので残す(クラス名だけ改名)。
共有インデックス経由で「他セッションのステージ済みファイル」を巻き込む(実害を確認)
git add <自分のファイル> → git commit としても、別セッションが先に git add していたファイルが同じコミットに入る
(今回 servicecards-dark/light の entries+画像 6件が混入。共有リポなのでインデックスは1つ)。
→ 対策は2段: ① コミット後に git show --stat で中身を必ず確認する。
② push は一時 worktree(git worktree add --detach <tmp> origin/main → git checkout <自分のcommit> -- <自分のファイルだけ> → commit → git push origin HEAD:main)。
本体の作業ツリー(他人の未コミット変更)に一切触らずに自分の分だけ本番へ出せる。
デプロイrunが cancelled でも慌てない
セクション追加が同時多発している日は、後続pushが前のrunをキャンセルする(concurrency)。
自分のrunがキャンセルでも後続runが最新mainを配信するので、判定は run ではなく 本番URLで行う
(/section/<id>/ が200+/sections にidが出る+プレビュー実描画)。
このMacで動画のフレームを見る最終手段
ffmpeg 無し・PyObjC 無し・swiftc はSDK不整合・headless Chrome は .mov を canvas に描けない、で全滅した時は
qlmanage -t -s 1400 -o <dir> <file.mov> でポスターフレーム1枚だけ取れる(今回はこれで箇所を特定できた)。
複数フレームが要るときは既出の Playwright+ローカルHTTP方式を使う。
2026-08-18 野村不動産PMO スペック(左テキスト+フェード自動スライダー)で得た学び
「全部崩れてる」= <style> の中で文字列が閉じていないを最初に疑う
症状: 背景色とフォントサイズだけ効いて、レイアウト指定(flex/幅/高さ)が丸ごと無視され画像が縦積みになる。
真因: @font-face の src:url("data:...base64,XXXX") の base64 が重複して書き込まれ、引用符が閉じないまま
以降のCSSをパーサが全部捨てていた(18,855字 → 正しくは 2,307字)。
- 見つけ方: {/} の数は合うので括弧チェックでは出ない。@font-face を丸ごとCDNの正しい物と差し替えるのが最短。
- 予防: 生成後に「(a) 先頭の .sec-<id>{} が効くか (b) 末尾側のセレクタも効くか」を両端で1つずつ実測する
(getComputedStyle(該当要素).display を見るだけ。片方だけ効いていたら途中で死んでいる)。
mask-image は別オリジンだと CORS で読めない=アイコンが消える
一時停止ボタン(-webkit-mask-image:url(参考元/pause.svg))が無言で消える。画像<img>は別オリジンでも出るので気づきにくい。
→ mask/背景に使うSVGだけは data URI で埋める(元ファイルの中身そのままをbase64化=再現度は落ちない)。写真はURLのまま。
ベースに img{max-width:100%} を「良かれと思って」足すと画像高さが変わる
元サイトは #pxBase img{max-width:100%;width:100%;height:auto} のように祖先ID配下のグローバルで画像を決めていた。
これを拾わず自作リセットを当てると、height:100%!important と喧嘩して画像が 500px → 494px になる(=下に隙間)。
→ 抽出時は #<主要コンテナID> 〜 のルールも必ず拾って sec-<id> 根に寄せる。逆に元に無い指定は足さない。
shoot_section.mjs の1枚化前処理が「重なり型スライダー」を消す
oneSlide は器の子のうち slide|item に当たる先頭要素だけ残して他をdisplay:noneにする。
Swiper fade + 先頭に swiper-slide-behind のような非スライド要素がある構造だと、残るのが実体のない要素になり
右カラムごと消える(サムネが見出しだけ・中身605px)。
→ その場合は自前で撮る: 1440幅・4:3・deviceScaleFactor 2 で file:// を開き、DOMは触らず自動送りだけ止めて
img/sections/<id>.png に保存 → python3 tools/normalize_thumbs.py <id> で正規化(採用ゲートは通る)。
ブラウザペインが非表示だと <video> のフレームが進まない
currentTime を動かしても requestVideoFrameCallback が発火せず、何枚撮っても1枚目と同じ絵になる(md5が全部同一で気づける)。
→ 動画の箇所特定は Playwright(node_modules/playwright をそのまま import)+ローカルHTTP配信で撮るか、qlmanage -t。
作業途中の code-previews/<id>.html は他セッションの git add -A に巻き込まれて本番へ出る
今回、完成前(壊れた状態)のHTMLが別セッションのコミットに入って origin/main に載っていた。
→ 気づき方: publish用の一時worktreeに自分のファイルをcopyした時、新規のはずが M(modified) で出たら「既に上流にある」。
→ 直し方はそのまま完成版で上書きコミット(退化ではなく修復)。完成までは code-previews/ に置かない運用が本来は安全。
2026-08-18 The Day Osaka(3箇所まとめて追加)で踏んだ落とし穴
同じサイトの同じ箇所を別セッションが並行で作っていた(二重作業)
theday.osaka の News / リンク帯 / フッターを3件作り切った直後に、別セッション(同じ「さき」名義)が
news-swiper-fullbleed linkband-dotted footer-sitemap-shape として同じ3箇所を完成させていたことが判明。
→ 着手前に必ず既存・作業中を確認する(entries は未コミットでもディスクにある):
grep -l 'theday.osaka' ~/dev/terasu-lab/entries/*.json
→ 2026-08-18 大串判断は「両方とも載せる」。消し合わない。
id衝突は add_section.py が止めるが、code-previews/<id>.html は先に上書きされている
footer-sitemap-saki-0817 が既存(千代田工販)と衝突。add_section.py は ⛔ 既に存在します で止めてくれるが、
その前のビルドで他人のプレビューHTMLを上書き済みだった(実害は出る前に気づけたが危険)。
→ HTMLを書く前に存在チェックする: ls entries/<id>.json code-previews/<id>.html。
→ idは <主題>-<種別>-<担当>-<MMDD> でも衝突する。主題を具体的にする(footer-sitemap → footer-hotelinfo)。
一時ディレクトリ(/private/tmp のscratchpad)はセッション中に消える
DOM・CSS・抽出スクリプトが作業途中で全部消えた。
→ 中間成果物が消えた前提で1コマンド再生成できるスクリプトにしておく(curl→抽出→ビルドを1本にまとめる)。
IntersectionObserver はブラウザペインで発火しないことがある
スクロールフェード(is-animeActive)が付かず、セクションが opacity:0 のまま真っ白に見えた。
→ 元 script.js と同じ scroll ハンドラ方式にすると確実に動く(しかも元挙動に忠実):
function scrollTrigger(){ trg.forEach(el=>{ const top=el.getBoundingClientRect().top+scrollY;
if (scrollY+innerHeight > top) el.classList.add('is-animeActive'); }); }
scrollTrigger(); addEventListener('scroll', scrollTrigger, {passive:true});
100vwフルブリードのスライダーは「外側の器」が切っていた
元サイトは .l-outer{overflow-x:hidden} がセクションの外ではみ出しを切っており、抜き出すとその器が無くなって
check_responsive.mjs が PC/タブレットで横はみ出しを検出(3735>1440)。
→ sec-<id>{ overflow-x: clip; } を足す(見た目は不変・stickyも壊さない)。抜き出し前に
「どの祖先が overflow を持っているか」を実測しておくと早い:
let el=target; while(el){ console.log(el.className, getComputedStyle(el).overflowX); el=el.parentElement; }
shoot_section.mjs の検品は「細い白帯セクション」で誤検知する
guide-shapelink(上余白240px+帯37px)が 中身が写らない(色12種/輪郭2.4%) で不合格。
実際は Facility/Q&A/Pamphlet も点線区切りも緑シェイプもちゃんと写っていた=描画失敗ではなく元デザインが淡いだけ。
→ 絵を目視して中身が写っていれば --force で採用してよい(単色・真っ白なら本物の失敗なので直す)。
ローカルとリモートが分岐したまま作業が進むことがある
着手時は Already up to date だったのに、push直前には ローカル25 / リモート40 コミットの分岐(うち21件は同内容の重複)。
→ 解消手順(2026-08-18 大串「今すぐ分岐を解消してpush」で実施・無事完了):
git branch backup-local-<日付> HEAD # ① 退避(必須)
tar czf <backup>.tar.gz entries code-previews # ② 作業ツリーも退避
git add <自分のファイル>; git commit -- <自分のファイル> # ③ 自分の分だけ部分コミット(他人のstaged分を巻き込まない)
git stash push -u -m "other-session-wip" # ④ 他セッションの作業を退避
git rebase origin/main # ⑤ 重複21件は "already upstream" で自動スキップされる
# 競合が `sections-data.json` だけなら --ours で潰してよい(CIが build.py で再生成する)
# プレビューHTMLの add/add 競合は「idとファイル名が一致し、html/body 指定が無い方」が正
git push origin main && git stash pop # ⑥ 戻す(競合したら HEAD 版に寄せる。stashは消えないので復旧可)
→ CIが core/build_core.py と build.py を回すので、生成データ(sections-data.json / data/*.json)はコミット不要。
これを知っていると「他人の未完了分を巻き込まないと整合が取れない」というジレンマが消える。
2026-08-18 野村不動産PMO(pmo-web.com)4件 — 事故と対策4つ
参考: https://www.pmo-web.com/ → fv-slidein-kv-saki-0817 / photowall-autoslide-saki-0817 / value-sticky-split-saki-0817 / lineup-cardslider-saki-0817
1. id衝突を「ファイルを書く前」に確認する(今回いちばんの事故)
fv-fadeslider-saki-0817 は 既存のLIVIOのFVで使用済みだった。先に code-previews/<id>.html を Write してしまい既存を上書き(=退化)。大串の「マルイチ開くとLIVIOのFVが出る」で発覚した。
- add_section.py には --force ガードがあるが、HTMLを先に書く手順だとガードより前に壊れる。
- 着手時に必ず:git ls-files --error-unmatch code-previews/<id>.html entries/<id>.json(ヒットしたら別idにする)。
- 既存が壊れていないかは git diff --quiet HEAD -- <path> で確認できる(今回は無傷だった)。
2. base64(フォント等)は絶対に手で書かない
swiperの矢印グリフを埋める際、base64を手打ち/記憶から書くと壊れる。さらに壊れたdata URIは
引用符が閉じず、@font-face以降のCSSが丸ごと無効になる(=素のHTMLが縦に伸びる「崩れ」)。1440幅では偶然気付けず1280幅で発覚。
- 手順:curl で実CSSを落とす → 正規表現で @font-face{...} を機械抽出 → python で置換 → <style>内の " と ' が偶数か検査。
- 崩れの機械判定:ページを開いて document.styleSheets の cssRules 総数が0でないか/scrollHeight が想定内か。
3. サムネ撮影ツール(shot_common.oneSlide)の前処理に当たらない作りにする
oneSlide は クラス名に slide|item を含む子を1枚だけ残して他を display:none にする。スライダー系はここで真っ白/1枚だけになる。
| 症状 | 対策(実表示は不変) |
|---|---|
| FVが真っ白(残った1枚が translate(-100%) のまま) | .swiper-slide:first-child .item{transform:translate(0)} を足す |
| グリッドのセルが空で写る(wrapperのtransformが残る) | 各セルに1枚目と同じ写真を background: url() center/cover で敷く |
| カード横並びが1枚しか写らない | Swiperの slideClass/slideActiveClass/... を slideもitemも含まない独自名にする(lc-card等)。lc-slide は "slide" を含むので当たる |
4. 同じリポを別セッションと同時に触らない
別セッションが git add -A を使っていたため、こちらの作りかけHTMLが相手のコミットに巻き込まれ、逆にこちらの未追跡ファイル(entries/サムネ/node_modules)が消えた。マージ競合が未解決のまま放置される時間帯もあった。
- 着手前に git log --since="30 minutes ago" --oneline | wc -l で他セッションの活動量を見る。多ければ待つ。
- 自分は必ずパス指定コミット(git commit -- <自分のファイル>)。git add -A は使わない。
- 相手のステージを外す必要がある時は git restore --staged(内容は消えない)。git checkout/clean は使わない。
その他(今回の抽出で効いたもの)
- 別オリジンの
mask-imageはCORSで落ちる → 同じ図形を data URI 化する(pause/play アイコン)。 - 元サイトが
body{overflow:clip}で切っていた分は、器側にoverflow:clipを付ける(overflow:clipはスクロールコンテナを作らないのでposition:stickyを壊さない)。 html{font-size:62.5%}(1rem=10px)の site は、rem を px に換算して持ってくるとホストHPのルート指定に依存しない。
2026-08-20 LUMA(luma-pet.com)5件 — 抽出パイプラインで踏んだ地雷4つ
参考元: https://luma-pet.com/ ・ https://luma-pet.com/about/ / 追加者=さき
① 疑似要素セレクタを querySelector で判定すると必ず落ちる(影響が一番大きい)
セクションが使うCSSを「そのセレクタが配下にマッチするか」で選別する方式だと、
.about_greeting_img::after のような疑似要素つきセレクタは querySelector が例外を投げて false になり、丸ごと捨てられる。
実害=写真の上に重なる線画アイコンが消えた(見た目でしか気づけない)。
→ 判定前に ::before/::after と :hover/:focus/:active/:visited 等を外した文字列で照合する。
const norm = s => s.replace(/::[\w-]+(\([^)]*\))?/g,'')
.replace(/:(hover|focus|active|visited|target|checked)\b/g,'').trim() || '*';
② CSSOM の rule.style.cssText は var() 入りショートハンドを壊す
padding: var(--inner-padding) が padding-right: ; padding-left: ;(値が空)になって出力され、
セクション左右の余白が丸ごと消えた(元 x=128px → 再現 x=0)。rule.cssText でも同じ。
→ CSSOMは「どのルールを採用するか」の判定にだけ使い、本文は生CSSを自前パースしてそのまま使う。
(@import を再帰解決 → url() を絶対化 → ブレース走査でルール分解 → ブラウザ側は採否判定だけ返す)
③ 親コンテナの制約が抜けて幅が合わない
#about_greeting{ padding: var(--inner-padding) } のようにルート自身、あるいは #contents など祖先が
幅・余白を決めている。セクション単体を切り出すと当然消える。
→ ルートの祖先チェーン(body直下〜親)をそのままラッパーとして再現し、祖先にマッチするCSSも採用対象に含める。
④ 背景が position:fixed のセクションはそのまま持ってくると貼り先HPを覆う
LUMAのTOP ABOUTは背景を持たず、FV(#idx_main .img-back)の position:fixed レイヤーが透けているだけだった
(elementsFromPoint で確認できる)。これを気づかず抜くと背景なしの灰色になる。
かといって fixed のまま持ってくると、貼り先HPでビューポート全面を覆って事故る。
→ 背景レイヤーも一緒に抜き、セクション内に閉じた absolute(全高) + 中身を sticky(100vh) に置き換える。
見え方は元と同じまま、外へ漏れない。
数値照合は「出現アニメ後」に取らないと全部ズレて見える
.inv-item 系は初期状態で translateY(20px)。元サイトを読み込んだだけで測ると全要素がy+20になり、
再現側が間違っているように見える。→ 比較スクリプトは両方 scrollIntoView して2.5秒待ってから測る。
<source> <br> など幅0の非描画要素は比較から除外する(永遠に一致しないが見た目に無関係)。
同じPCで別セッションが同じリポを触っている前提で動く(今回いちばん危なかった)
~/dev/terasu-lab は作業ツリーを共有する。今回、別セッションが並行して同じLUMAを作っており、
luma-top-fv-saki-0820 / luma-top-about-saki-0820 が完全に同じidになって、
cp で相手の未コミットファイルを上書きしてしまった(相手のビルドスクリプト _make_luma.mjs を再実行して復旧)。
- 着手直前だけでなく、ファイルを置く直前にもう一度 ls code-previews/<id>.html で衝突確認する
(今回は着手時0件→作業中に相手が作成、という時間差だった)
- リポ直下の _*.mjs など他セッションの作業ファイルからidを拾って照合する:
grep -ohE "luma-[a-z0-9-]+-saki-0820" _*.mjs | sort -u
- 未コミットの他人ファイルは git で戻せない。上書きしたら相手のビルドスクリプトで再生成できないか探す
- git pull が submodule core のダーティで Aborting すると、HEADだけ進んで作業ツリーにファイルが無い状態が残る
(ctsap-* の2件が消えていた)。git status で D が出たら git checkout -- <path> で戻す
2026-08-20 ANA新千歳空港(ctsap.ana-g.com /about)から2件(下層FV/追従目次2カラム)
mask-image はクロスオリジンだと無音で消える(CORS)
- 参考元の
-webkit-mask:url(.../icon-plane-tail.svg)を絶対URLのまま持ってくると、404も出さずにアイコンだけ消える(サブタイトル横の飛行機マークが丸ごと消えていた)。background-imageはクロスオリジンで出るので「画像は出るのにマスクだけ出ない」という形で現れる。 - → CSS内の
.svgは全部 data URI 化する(既存のctsap-pagehead-saki-0820も base64 で入っていた=これが正解の型)。検出は目視でしかできないので、マスクを使うルールがあったら data URI 化を機械的にやるのが速い。
自ホストのwebfontは Access-Control-Allow-Origin が無くLabからは読めない
- fonts.css の
@font-faceを絶対URLで持ってきても、フォントはCORS必須なのでdocument.fonts.load()が NetworkError(=フォールバック描画)。curlは200なのでURL確認だけでは気づけない。 - 症状は数値照合にだけ出る:
No.1の幅が 147px(元は139px)。レイアウト箱は一致するので目視では絶対に分からない。 - → 同一書体を Google Fonts の
@importに置き換える("Inter Variable"→"Inter")。fontsource=Google Fonts なので書体は同一・メトリクスも一致(置換後に 139px で完全一致)。Lab既存515件中400件が Google Fonts 方式。 - 検証は
document.fonts.check('bold 68px "Inter"')を見る。falseなら読めていない。
body.xxx .foo{} をスコープ化すると常時適用になって高さがズレる
body.is-section-upper-mg .l-two-column{padding-top:8.75rem}を.sec-x .two-column{...}に変換したせいで +20px。- → 前置きが
body.クラス/html.クラスのルールは捨てる(そのページ限定の状態指定なので、単体セクションでは成立しない)。素のbody{}は寄せる、body.xxx{}は捨てる、と分けるのが正解。
素タグリセットの写し漏れは「高さの丸い差」で出る
pをmargin-block:1em(ブラウザ既定)のまま出したら +96px(32px×3段落)。元サイトはp{margin:0}相当だった。- → リセットは推測で書かない。元サイトで
getComputedStyle(p).marginTopを実測してから書く(今回0pxを確認して修正)。
/tmp/section_<site>/ は担当名まで入れないと別セッションと衝突する
- 同じPCの別セッション(さき)が同じ
ctsapを同時に処理していて、/tmp/section_ctsap/を共有していた(相手のpagehead.htmlservice.html_ph.css等が同居)。DOMダンプを取り直されていたら別サイトを見て作る事故になっていた。 - →
/tmp/section_<site>_<担当>/にする。コマンド本体の「サイト別」だけでは足りない。 - 併せて 同じサイトを別担当が同時に触っている可能性を前提に、id は
<site>-<箇所>-<担当>-<MMDD>の担当名で必ず分ける(今回ctsap-pagehead-saki-0820とctsap-about-fv-ogushi-0820が別物として共存)。
Labは未デプロイでも200を返す(フォールバックHTML)
curl -o /dev/null -w %{http_code}だけ見て「反映OK」と誤判定した。実体は mikke のindex(1.35MB)が返っていた。- → 反映確認は
curl | grep "sec-<id>"(中身で見る)。ステータスコードは反映の証拠にならない。
このMacには ffmpeg が無い/swift もSDK不整合で動かない → 動画のフレーム抽出は Playwright+実Chrome
ffprobe/ffmpeg無し、swiftは「SDK is not supported by the compiler」で落ちる、python3に pyobjc 無し。- → 動画をローカルHTTPで配信 →
chromium.launch({channel:'chrome'})の<video>に読ませてcurrentTimeを送りながら要素スクショ。 playwright同梱のchromiumはH.264非対応なのでchannel:'chrome'(実Chrome)必須。file://はsetContentのページから読めないのでHTTP配信にする。
抽出スクリプトの型(今回も数分で完了・再利用推奨)
tinycss2で全ルールを走査 →soupsieveでセクションのサブツリーに実際にマッチするものだけ回収(@media/@container文脈ごと)。style.css 195KB → FV 35ルール / 2カラム 118ルールまで自動で絞れた(131行 / 219行の自己完結HTML)。- 実行時クラス(
is-active等)はDOMダンプに無いので、RUNTIME集合を作って「セレクタ内のクラスが(実在クラス ∪ RUNTIME)に収まり、かつ1つは実在」なら拾う。これが無いと目次のアクティブ色が丸ごと落ちる。 - スクロールスパイは元JSを読んでから移植する(
IntersectionObserver/rootMargin:'-50% 0px'/is-active)。推測で書かない。
2026-08-20 a-thily お問い合わせフォーム(動画1本だけで来たケース)
動画からフレームを抜く現実解(このMacに ffmpeg が無い)
「動画で箇所を指定」された時、このPCには ffmpeg も pyobjc も無く、swiftc は2分でもコンパイルが終わらない(AVFoundation import が重い)。JXA の ObjC.import('AVFoundation') も $.AVURLAsset が undefined で不可。
通ったのは headless Chrome(Playwright)で <video> を読ませて canvas で抜く方法。動画をローカルサーバーで配信し、video.currentTime を送って onseeked で toDataURL する。5秒の収録から12枚を数秒で取得できた。
- node は PATH に無い → /Applications/ChatGPT.app/Contents/Resources/cua_node/bin を PATH に足す
- playwright は ~/dev/terasu-lab/node_modules にしか無い=スクリプトはリポ配下に置かないと ERR_MODULE_NOT_FOUND(scratchpad に置くと解決できない)
- ポートは別セッションが既に使っている(8080/8099/8123 は埋まっていた)。lsof -nP -iTCP:$p -sTCP:LISTEN で空きを探してから使う。埋まったポートに繋ぐと別セッションの成果物が返ってきて混線する(FB⑦の実例そのもの)
数値照合は「主要要素8個」より「全要素総当たり」が速くて確実
getBoundingClientRect() をセクション配下の全要素(今回67個)で取り、w|h|x|y|font-size|color|background|font-family を1行に連結して行単位でdiffする。合否が一目で出る。
合格ラインは「差分が display:none の要素だけになること」。display:none は rect が 0,0 で返るため、元ページ(セクションが y=870 にある)と再現物(y=0)で相対座標がズレて必ず差分として出る=これは偽陽性なので無視してよい。今回は 67要素中この5件だけ残して一致。
今回踏んだ3つの罠(次回は先に潰す)
- body の初期 margin 8px で幅が16px縮む(1440→1424)。
<style>にbody{margin:0}を書くとスコープ漏れgrepに落ちるので、<body style="margin:0;background:#fff">の属性で吸収する。属性ならセクションdivをホストHPに貼っても付いてこない=ゲートも思想も両立。 body,html{font-size:62.5%}の site は、セクションの継承font-sizeが 10px ではなく 6.25px(html で62.5%→10px、body で更に62.5%→6.25px の二重適用)。ルートにfont-size:10pxを入れると継承値が全部ズレる。「rem基準=10px」と決め打ちせず、必ず元サイトの実測(対象要素の computed font-size)に合わせる(トップ10の#2の実例)。rem 自体はXrem → X*10pxに機械変換してホスト非依存にした。:placeholder-shownを潰す正規表現バグ。擬似要素を剥がす時に/::?(before|after|placeholder|…)\b/と書くと:placeholder-shownの「placeholder」だけ消えてinput[type="text"]-shownという不正セレクタになり、そのルールが丸ごと落ちる(入力欄の color が #ddd → 黒になった)。placeholder(?!-)にする。
効いた手順(このサイト系=WordPress + 単一 style.css)
実ページを Playwright で開き、document.styleSheets を走査してセクション内の要素に実際にマッチするセレクタだけを集める → そのセレクタ集合で元CSSの原文から該当ルールを切り出す(Chromeの cssText は calc(calc(770/1400)*100vw) を calc(55vw) に潰す・border-top-color: ; のような壊れた直列化もするので、原文から取り直すのが正解)。185行で完全一致。
Google Fonts などクロスオリジンのstyleSheetは cssRules アクセスで例外を投げる。try/catch をループの外に置くとそこで全部止まって :root 変数を1つも拾えない(トップ10の#1に直結)。変数は元CSSから正規表現で別途拾うのが安全。
2026-08-20 動画だけで来た素材の扱い(a-thily 創業ヒストリー)
ffmpeg が無いMacで動画からフレームを抜く(このPCは ffmpeg 未インストール)
qlmanage は1枚しか取れず、JXA の ObjC.import('AVFoundation') はブリッジが効かず $.AVAsset が undefined(BridgeSupport はあるのに使えない)。pyobjc も未導入。
正解=Playwright(Chrome) で動画を再生して requestVideoFrameCallback で canvas に描き出す。
- chromium.launch() の既定ビルドは H.264/AAC を再生できない → {channel:'chrome'} で実機Chromeを使う(実測: 既定ビルドは loadedmetadata すら来ない)
- file:// + setContent は動画を読めない。python3 -m http.server で配信して http://127.0.0.1:<port> から読む
- currentTime を飛ばして drawImage すると全フレーム同一画像になる(seek後の再描画が来ない)。再生しながら requestVideoFrameCallback で拾うと確実
- 素材の判定用なので toDataURL('image/jpeg',0.7) + 1/2.6 縮小で十分(読む前に sips -Z 1100 も必ず通す=画像2000px制限)
「セクション末尾の要素だけ発火しない」=スクロール余地が無いのが真因
元サイトの発火条件は scrollTop > 要素top - 画面高/1.6。セクション単独ページは要素の下にコンテンツが無く最下部までスクロールしても条件を満たせないため、末尾の要素(今回は .fire_txt「燃えるような毎日は…」)だけ opacity:0 のまま残る。
元の条件はそのまま残し、「最下部に到達 かつ 要素が画面内」なら発火を1行足すのが正解(全部を無条件に表示する保険にすると、スクロールで見るはずのアニメが最初から消える)。
同時実行では作業ディレクトリ名まで衝突する
別セッションが同じ /tmp/section_<site>/ を使っていた(同じサイトを別担当が並行処理)。$WORK をサイト別にするだけでは足りず、セッション別に接尾辞を付ける(例 /tmp/section_athily_msg/)。
python3 -m http.server のポートも取り合う(8080/8099/8123 が既に使用中だった)。lsof -nP -iTCP:<port> -sTCP:LISTEN で空きを探してから起動する。
git も同時に走るので .git/index.lock で commit が落ちる → 数秒待ってリトライするループで通す(git add -A は当然禁止・自分のidのファイルだけ add)。
追記(同日)— FV+直後のセクションを1本に繋げる時のsticky背景の作り方
大串「④と⑤は一緒。繋げて」で FV(#idx_main) と ABOUT(#idx_about) を1セクションに統合。
背景(元は position:fixed)をセクション内に閉じ込めて追従させるまでに、3段階ハマったので手順を固定する。
position:fixedの中にstickyを置いても効かない(スコープにtransformを当てて fixed を閉じ込める手は、
子の sticky が死ぬので不採用)。→ 背景レイヤーを FV の外へ出し、SCOPE直下に
<div class="bg-layer">として置く(CSSの#idx_main .img-backを.bg-layer .img-backに機械置換)。overflow: hiddenは sticky を殺す(スクロールコンテナ化して、そのコンテナが動かないので固定されない)。
背景ラッパーはoverflow: clipにする。- 真犯人は
body{overflow-x:hidden}。スコープ化するとSCOPE{overflow-x:hidden}になり、
セクション全体がスクロールコンテナ化して sticky が完全に無効になる。overflow-x: clip; overflow-y: visible;に置換する。
ただし元サイトがfloatレイアウト(main/#idx_main/#idx_containarがfloat:left)だと、
BFCが消えてセクション高さが0に潰れる→display: flow-rootを併記して包含し直す。
最終形(これをテンプレとして使う):
SCOPE { position: relative; display: flow-root; overflow-x: clip; overflow-y: visible; }
SCOPE .bg-layer { position: absolute; inset: 0; height: 100%; overflow: clip; z-index: 0; }
SCOPE .bg-layer .img-back { position: sticky; top: 0; width: 100%; height: 100vh; }
検証は必ずスクロール途中で測る(window.scrollTo(0,1500) して getBoundingClientRect().top が 0 なら効いている)。
全画面キャプチャだと sticky は自然位置で写るので「効いていない」と誤判定する。
追記(同日)— 共有worktreeでは cp の直前が勝負/自分が置いたはずのファイルが別物になる
sed ... > 新id.html をリポの中でやった数分の間に、別セッションが元ファイルを自分の版で上書きしており、
相手の中身が自分のid名で commit・push された(luma-top-fv-slider / luma-top-about-parallax が相手の
luma-top-fv / luma-top-about と中身完全一致になっていた)。<title> で照合して初めて気づいた。
- リネームや加工はリポの外(作業ディレクトリ)で完結させ、完成品だけを最後に1回コピーする
- push後に <title> や自分だけのマーカーで「本当に自分の中身か」を照合する(200確認だけでは足りない)
- 他セッションがインデックスにファイルをstageしていることがある。git commit -m "..." -- <自分のパス…> の
パス限定コミットなら、相手のstageを巻き込まず自分の分だけ入れられる(git add -A はもちろん論外)
- git pull --rebase がインデックス汚れで止まる時は --autostash。本番反映の確認は「バイト数+title」で
(未デプロイだと Lab は一覧ページ(約1.3MB)を返すので、200だけ見ると反映済みと誤判定する)
2026-08-20 ANA新千歳空港(ctsap)お知らせ一覧+ページネーション
- 自社ホストのwoff2はCORS未対応が多い=そのまま持ってくると別ドメイン(Lab)でフォントが読めない。
curl -sI <woff2> | grep -i access-controlが空なら、同一書体をGoogle Fonts(Noto Sans JP / Inter / Material Icons Outlined)に置換する。フォント名が"Inter Variable"のようなセルフホスト用の別名になっている時は、CSS内の名前も"Inter"に置換しないと当たらない。再現度の支配要因なので着手直後にCORSを確認する。 - CSS
mask: url(...)はクロスオリジンだと黙って消える(background-imageと違いCORSが要る)。丸ボタンの中のアイコンだけ出ない=これ。SVGをdata URI化して埋め込む(画像URLは原則そのままだが、maskは例外的に埋め込まないと再現しない)。 html:not([lang=en]) body{...}のようなbody前置ルールは、素朴なセレクタ一致では落ちてフォント指定ごと消える。抽出器は「先頭の html/body トークンを剥がしてから一致判定し、.sec-<id> 残りとして出す」正規化を必ず入れる。:not(a,b)のカンマでセレクタ分割が壊れるので、カンマ分割は括弧の深さを見て行う。- 単独プレビューの
body既定マージン8pxで幅が16px狭くなり、数値照合が全部ズレる。Labの既存プレビューと同じく<body style="margin:0">(インライン)で解消。CSSルールにしないのでホストHPに貼っても漏れない。 - 中身が小さいセクション(ページネーション単体など)は
shoot_section.mjsの採用ゲートに落ちる(輪郭1.7%等)。--forceで撮ると全画面の中に点のように写って一覧で見えない。→ 要素だけを狭めのビューポート(960幅・dSF3)でelement screenshotし、normalize_thumbs.pyで4:3化すると見やすい。ただし幅を詰めすぎると端のボタンが切れる(640幅で「次へ」矢印が消えた)=必ず生成物を目視する。 - 同じサイトを別セッションが並行作業していると
/tmp/section_<site>/を共有して踏み合う(DOMを上書きされ、別ページのDOMを見て作りかけた)。サイト別に分けるだけでなくセッション別にも分ける(/tmp/section_<site>2/等)。 - リポの作業ツリーが他セッションの未コミット変更で汚れていると
git pull --rebaseができず push もできない。git commit -- <自分のパス>で他人のステージを巻き込まずコミットし、一時ワークツリー(git worktree add /tmp/... origin/main→ cherry-pick → push)で自分の分だけ流す。作業後はgit worktree removeで片付ける。
2026-08-20 LUMA /dog/ 6件 — rem はスコープ化では効かない(一発格納率に直結・恒久)
起きたこと: 6件とも「フォントも余白も全部1.6倍に膨らむ」状態で出来上がった。数値照合で font-size:25.6px(元は16px)を検出して真因が確定した。
真因: コマンド本体の「html{font-size:10px} 等の rem基準は sec-<id>{ font-size:10px } へ寄せる」は rem では成立しない。
rem は常に <html> 基準で解決されるので、sec-<id> に font-size を移しても rem の基準は変わらない(プレビュー単体なら 16px のまま)。
結果 body{font-size:1.6rem} が 16px ではなく 25.6px になり、そこから継承で全体が1.6倍に膨らむ。1行の宣言で全滅するタイプの崩れ。
正しい直し方(機械変換): 元サイトの html の font-size を実測し(今回 html{62.5%}=10px)、抽出CSS内の Nrem を全部 N×10 px に置換する。
body = body.replace(/(-?[\d.]+)rem\b/g, (m,n)=>(parseFloat(n)*10)+'px');
変換後は html{62.5%} 由来の sec-<id>{font-size:62.5%} は不要になるので落とす(残すと % と em の基準を壊す)。
副次効果としてホストHPの html font-size に左右されなくなる=貼っても崩れない。
同時に見つかった2つ(どちらも数値照合で出た)
1. sec-<id> *{box-sizing:border-box} を"おまじない"で足さない。 元サイトの reset に全称 box-sizing が無ければ、足した瞬間に元と違うレイアウトになる(今回 reset.css に全称指定なし=content-box が正)。元にある物だけ持ってくる。
2. プレビュー単体の <body> 既定 margin 8px が container を左右16px縮め、padding:8.9% 系の内寸が 1184→1171px にズレる。
→ <body style="margin:0"> と style属性で持たせる。<style> に body{} を書かないのでスコープ漏れ grep も通る。
数値照合は「セクション単位に絞って」やる
元サイトは .section_headline 等が全セクションに存在するため、ページ全体から拾うと要素の並びがズレて差分が読めない(今回1回無駄にした)。
元サイト側も #concept #price 等でスコープしてから比較する。要素数が一致して初めて差分が意味を持つ。
白背景+薄いグレーの表はサムネ採用ゲートの誤検知で落ちる
shoot_section.mjs の「色数/輪郭量」判定は、白地に細罫線+薄グレー文字の料金表を「中身が写らない」と誤判定する(実例 luma-price)。
必ず生成物を目視してから --force を使う。v2.5 の「ヘッダー単体が真っ白でゲートに落ちる」と同系統。
macOS画面収録(.mov)からフレームを抜く最速手段
このPCには ffmpeg が無い。Playwright/Chrome に <video> で読ませる方法はデコードでハングする(2分待っても1枚も出ない・2回試して両方ダメ)。
→ qlmanage -t -s 1400 -o <出力先> <file> で先頭フレームPNGが即出る。 尺は mdls -name kMDItemDurationSeconds。
実務上は先頭フレーム1枚で「どのセクションか」は確定できるので、これで十分だった。
同じサイトを別セッションが並行で触っていることがある
今回 ~/dev/terasu-lab に別セッションの一時スクリプト(_fetch_luma.mjs)があり、LUMAトップページ側6件を並行制作していた。
同じリポの作業ツリーを共有しているので、git add -A は絶対に禁止(相手の削除・変更を巻き込む)。
git pull --rebase も相手の未staged変更で失敗することがあるが、明示addした自分のファイルだけの commit なら push は通る。
id接頭辞をサイト+ページで分けていれば衝突しない(今回 luma-*(/dog/)と luma-top-*(トップ)で棲み分け)。
2026-08-20 J-CAT(Studio.design製)ヘッダー+役員一覧で得た知見
参考元: https://j-cat.co.jp/team / 追加: jcat-header-saki-0820 jcat-members-saki-0820(追加者=さき)
Studio.design 製サイトの抽出作法(再現度100%で通った型)
- スタイルは
.sd[data-s-<uuid>]の属性セレクタで当たる。class名はsd / text / link / appear / _boolean1の汎用名だけなので、
オリジナル化=data属性のリネームが本体(data-s-*→data-e01…/data-r-*→data-t01…/--s-color-*→--c01…/--s-font-*→--f01…)。
HTMLとCSSに同じ写像を同時適用すれば見た目は完全に不変。data属性を「CMS痕跡だから削除」すると全スタイルが死ぬので消さずに改名する。 :root{--s-color-*/--s-font-*}は独立した<style>に入っている(sheet末尾)。取りこぼすと色が全部フォールバックになる(崩れる原因#1そのもの)。- リセットは
:where(.render-canvas) a,div,header,img,nav,span{…}。.render-canvas/.StudioCanvasは実DOMに実在する祖先なので、
抽出時はel.matches(セレクタ全文)の完全一致だけで判定できる。→ スコープ化時に:where(.render-canvas)を.sec-<id>へ読み替えると見た目が保たれる。
「セレクタ末尾だけで採用」する緩いフォールバックを入れると.StudioCanvas > .sd{min-height:100dvh}を誤って拾う。使わないこと。 .appearは「外すと表示」型(.sd[data-s-…].appear{opacity:0})。抽出時にクローンからappear/appear-activeを除去すれば表示済み状態になる。- ヘッダーは二重の非表示ゲート:
#header._boolean1{visibility:hidden}(DOM内の<style>に埋まっている)+.sd._boolean1[data-s-…]{opacity:0}。
→ 抽出クローンから<style>/<script>を必ず除去し、この2ルールは採用対象から落とし、単独用にposition:relative / opacity:1 / visibility:visibleで打ち消す。 - html の font-size は 16px(62.5%トリックなし)。rem基準の付け足しは不要(崩れる原因#2の誤対応をしないこと)。
数値照合ゲートの落とし穴(2つとも今回実際に踏んだ)
- body の既定マージン8pxで全要素が最大16pxズレる。
body,html{margin:0}をsec-<id>に読み替えると実body側のマージンが残るため、
ルート幅が 1440→1424 になり x も総崩れに見える。→<body style="margin:0">とインライン指定で解決(ページ級CSSを足さずに済むのでスコープ漏れgrepも0のまま通る)。 display:noneの要素はgetBoundingClientRect()が全部0になるため、元では「ルート基準 y=-254」、再現では「y=0」と出て偽の不一致になる。
Studioは同じ内容をPC用/SP用で二重に持つのでこれが必ず出る。→ 比較器でw===0 && h===0の要素は対象外にする(display値での判定は不可=非表示の親の下でも子のcomputed displayはflex等のまま)。
縦長セクションの遅延画像
scrollIntoView だけでは画面外の loading="lazy" が data:image/svg のまま抜ける。
→ セクション上端-200px から下端+400px まで 400px刻みでスイープしてから抽出する(役員5枚とも実URL化できた)。
格納時:他セッションと同居しているときの安全策
build.pyはentries/を全部集約するので、他人の未コミット entries がsections-data.jsonに混入する(今回ANA新千歳空港の7件が同居)。- CI(
.github/workflows)がbuild_core.py → build.py → build_lab.pyを毎回回すので、
sections-data.jsonはコミットしなくてよい。ローカルでは検証のためだけにbuild.pyを回し、git checkout -- sections-data.jsonで戻してから
自分の entries とサムネだけを add する=退避/復元の小細工なしで巻き込み事故をゼロにできる(今回この方法で通した)。 - ヘッダー単体セクションのサムネは上下に大きな余白が入るが、既存の
ctsap-header-saki-0820と同じ体裁なのでそれが正(撮り直し不要)。
コマンドが /section で起動しないとき
~/.claude/commands/section.md は Vault への symlink。アプリ側の登録が古いと Unknown command: /section になるが、
symlink 先の実体(22_AI運用ルール/コマンド集/section/section.md)を直接読めばそのまま同じ手順で実行できる(再起動を待つ必要はない)。
2026-08-20 べすとほうむEP(besthome-ep.com)5件 — WordPress自社テーマ系の落とし穴
#top のような「ページ級のid前置」でヘッダーの配色指定が丸ごと落ちる(トップ10 #5 の実例)
- 元サイトは
headerのCSSをcommon.cssに、トップページだけの白抜き指定をtop.cssの#top header …に分けていた
(<body class="home …" id="top">)。^headerで抽出すると白抜きが全部落ちて、黒文字のヘッダーが写真に埋もれる。 - 対処=抽出前にCSSを
#top→bodyに置換してから拾い、スコープルートに読み替える。
python TOP_ID = TOP.replace('#top ', 'body ') # body.〜 も同様 pred = lambda s: re.match(r'^body\s+header\b', s.strip()) - 前置は
body.home.archiveだけでなく#topのような id もある。抽出前に必ず
grep -oE '<body[^>]*>' dom.htmlで body の class/id を確認し、その両方で前置ルールを探す。
rem基準は「sec-<id>{font-size:10px} に寄せる」では直らない → rem→px の機械換算が正
html{font-size:62.5%}の rem は常に html を見るので、sec-<id>に font-size を置いても 1rem=16px のまま=全部1.6倍に崩れる。- 抽出CSSに
(\d*\.?\d+)rem → ×10 pxを一括適用(10px基準なら 1.6rem→16px)。
これでhtml{}を1つも書かずに寸法が完全一致した(実測 24項目すべて一致)。既存の良いプレビュー(athily 等)も rem は 0 件=これが慣習。
数値照合で「幅だけ一律16px小さい」=プレビューの <body> の既定マージン
getBoundingClientRect()比較で高さ・font-size は全一致なのに幅だけ常に -16px なら犯人はこれ(8px×2)。body{margin:0}は書けない(貼ると崩れる)ので<body style="margin:0">のインライン属性で消す。
属性なのでセクションのマークアップには何も付いてこない=ホスト汚染ゼロ。スコープ漏れ grep も通る。
.visually-hidden を拾い忘れるとロゴの代替テキストが画面に出る
- ロゴが
<svg>+<span class="visually-hidden">社名</span>の構成だと、社名が縦組みで露出する。 - ヘッダー系を抽出するときは
^headerに加えて.visually-hidden/.sr-only/.screen-reader-textを必ず同梱する。
低いセクション(ヘッダーの帯だけ等)はページの地色も合わせないとサムネに白帯が写る
- セクションが128pxしかないと、撮影範囲にページ下部の白が入り、4:3正規化で黒地の中に白い横線が残る。
- 黒地セクションは
<body style="margin:0;background:#000">にしてから撮る。
(上下に大きな余白が入るのは既存のヘッダー単体セクションと同じ体裁=それが正)
同一リポで別セッションが並走しているときの事故と復旧
git add <自分のファイル>だけでも、インデックスに残っていた他セッションのリネームが自分のコミットに紛れ込むことがあった
(jcat-contact-ogushi→sakiのリネームが同梱された)。commit 直後にgit show --stat HEADで中身を必ず確認する。- rebase が「untracked working tree files would be overwritten」で止まったら、消す前に origin と md5 照合する。
今回は11件すべて origin と完全同一(別マシンから既にpush済みの同じ成果物)だったので、退避→rebase→復元で無傷で通せた。
bash a=$(md5 -q "$f"); b=$(git show origin/main:"$f" | md5 -q); [ "$a" = "$b" ] && echo same - zsh は
for f in $VARで改行分割しない(1回しか回らない)。ファイル一覧のループは
while IFS= read -r f; do … done < list.txtで回すこと(同一性チェックが空振りして「差分0」に見える事故)。
動画素材の中身確認(ffmpegが無いMac)
mdfind -name ffmpegで既存の node_modules 内ffmpeg-staticが見つかることが多い(今回~/dev/_section_work_0817/node_modules/ffmpeg-static/ffmpeg)。
swiftcでAVFoundationのフレーム抽出ツールを作るのはビルドに2分以上かかって詰まるので先に mdfind する。- 抽出は
-vf "fps=2,scale=1400:-1"(2000px以下にしてからReadする=画像サイズ制限に引っかからない)。
補足(既存知見の確認)
- 「
sections-data.jsonはコミットしなくてよい(CIが再生成)」は今回コミットしてしまったが実害なし(CIが上書き)。次回はgit checkout -- sections-data.jsonしてから add する。
2026-08-20 ANA新千歳空港トップ 7セクション(さき・実コード抽出をCSSOM機械抽出に切替)
抽出を目視のコピペではなく ブラウザのCSSOMから機械抽出する方式に変えたら、
getBoundingClientRect() の全要素照合が 1440px / 390px とも差分0 で7件そろった(初回で一発)。
以下は同じ方式を再利用するときに必ず踏む地雷。全部この日に実害を出して直したもの。
CSSOM抽出で必ずやること(順番に効く)
- 宣言は
rule.style.cssTextで取る。プロパティ列挙(style[i])は禁止。
border:1px solid var(--x)のように var() を含むショートハンドは、列挙すると全ロングハンドが空文字になって消える。
実害=カードの丸い枠線と-webkit-maskの飛行機アイコンが丸ごと消えた。 - シートの
media属性を尊重する。<link media="print">(print.css)のルールはsheet.cssRulesからは
メディア無しで出てくるので、そのまま入れると.c-plane-icon{display:none}のような印刷用指定が画面に効く。
sheet.media.mediaTextを@mediaで包む。 .is-*/.js-*の状態クラスは、素の一致で落ちる。
スナップショットに.is-openは無いので.js-dropdown.is-open .l-header__submenu{...}が捨てられ、
メガメニューが永久に開かなくなる。一致しなかったら状態クラスを外して再判定し、採用するセレクタは元のままにする。- 自前の要素リセットを足さない。 参考サイトの reset(
img{max-width:100%;height:auto;vertical-align:top}等)は
「素タグのルール」として自動で拾えている。そこに:where(img){display:block}を足すと
インラインの行ボックスが消えてロゴのaタグが35px→31pxになる。足すのはbox-sizingの保険くらいでよい。 - html/body の基本指定は「算出値」でなく「指定値」で移す。
line-height:2.2(無単位)を算出値35.2pxで焼くと、子の15px文字が2.2倍で再計算されず高さがズレる。 - メディアクエリ内の html/body 指定も拾う。 SPで
html{font-size:0.8125rem}に落とすサイトだと、
拾わないと 390px幅で全文字が16px基準のまま(実測:フッターの各項目が13px→16px、高さ169→197px)。 <body>の既定マージン8px。プレビューHTMLの器が持っているだけで 全要素が横16px細くなる(1440→1424)。
ただしbody{margin:0}を<style>に書くと貼った先を壊すので、<body style="margin:0;padding:0">と属性で打つ。
クロスオリジンで死ぬもの2つ(Labは別ドメインなのでどちらも踏む)
mask-imageは同一オリジン or CORS必須。参考元のSVGを絶対URLで参照してもマスクだけ効かない(円は出るが中の飛行機が出ない)。
→ SVGだけ data URI に埋める(画像は原URLのままというルールと矛盾しない・アイコンは数KB)。@font-faceも CORS必須。ctsapはAccess-Control-Allow-Origin無しで配信していてフォントが1つも読めない。
→ 同じ書体を Google Fonts から読む(Inter Variable→Interのように名前も置換)。base64で埋めると1書体1MBで現実的でない。
動きは「実測」でなく「元の計算式」を読む
js-infinite-slider の速度を transform の差分から測ったら 測定ごとに 20〜39px/s とブレて当てにならなかった
(画面外だと止まる・周回のwrapを跨ぐ)。app.js を読んだら式が書いてあった:
px/秒 = (先頭アイテム幅 + gap) × 1000 ÷
data-infinite-speed/ 方向はdata-infinite-direction(reverse=右へ)
クローン数 =ceil((wrapper幅 + track幅) / track幅)、ループ距離 = track幅 + gap
これで ESG=32px/s、採用=21.2px/s と一意に決まる。バンドルJSは丸ごと積まずに、式とパラメータだけ読んでスコープ内に再実装するのが正解
(実際 app.js をCDNから読み込めば動くが、253KBのサイト全体JSを貼るとホストHPを乗っ取る)。
data-slider-inited="true"は消さない。 CSSに.js-infinite-slider[data-slider-inited=true]{opacity:1}があり、
消すと スライダーが opacity:0 のまま真っ白になる(ESGの写真帯が丸ごと消えた)。
サムネ撮影ツールの前処理が壊すもの(素撮りに切り替える判断基準)
shot_common.mjs の前処理は opacity<0.15 を 1 に戻し、さらに filter を全部 none にする。つまり:
| 症状 | 原因 | 対処 |
|---|---|---|
| 意図的に薄い装飾が濃く出る | opacity:0.06 の背景地図が1.0にされる(採用情報の北海道が真っ青に) |
素撮り(前処理なし)で3枚撮り直す。filter:opacity() に逃がしてもfilterごと消されるので無駄 |
| ヘッダー単体が白紙同然 | 96pxのバーが4:3に収まるだけ。しかも前処理が閉じたメガメニューを勝手に開いて画角外に出す | 1つだけ hover で開いた状態を素撮り(mouseenter を dispatch)。v2.5の指摘どおり |
素撮りで用意するのは3枚(この3枚セットを必ず作る・カードだけ差し替えると詳細ページが古いまま):
<id>.png(1440幅・2倍・全高 → normalize_thumbs.py で4:3化)/ <id>__detail.png(1440×1000・2倍)/ <id>__sp.png(390×760・2倍・モバイルUA)。
別セッションが同じリポで作業中のときのpush(今回実際に衝突しかけた)
git status が 他人の staged ファイル + 未pushコミット2件 + submodule変更 で埋まっていた。
git pull --rebase は相手の作業ツリーを巻き込むのでやらない。作業ツリーに一切触らず自分のファイルだけ push する:
git fetch origin
export GIT_INDEX_FILE=/tmp/mypush_idx && rm -f "$GIT_INDEX_FILE"
git read-tree origin/main
for f in <自分のファイル>; do h=$(git hash-object -w "$f"); git update-index --add --cacheinfo 100644,$h,"$f"; done
TREE=$(git write-tree); C=$(git commit-tree "$TREE" -p origin/main -m "...")
git push origin "$C:main"
- zsh は
for id in $IDSで単語分割しない(bash と違う)。変数展開に頼ると1語扱いになり、
今回は staged 0件の空コミットを1つ push してしまった(実害なし)。リストはループにベタ書きする。 - CIが
core/build_core.py→build.py→build_lab.pyを回すので、コミットするのは
entries/*.jsonとimg/sections/*とcode-previews/*.htmlだけでよい(sections-data.jsonは触らない=相手の生成物を汚さない)。
このMac(咲輝機)の環境メモ
- node は ChatGPT.app 同梱のみ:
export PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH" - swift/swiftc は壊れている(
redefinition of module 'SwiftBridging')。AVFoundationでの動画フレーム抽出は諦める。
ffmpeg も無いので、動画は Playwright に<video>で読ませてcurrentTimeを送りながら撮るのが確実(HTTPで配信すれば数秒で済む)。 python3 -m http.serverは cwd が Google Drive 配下だとPermissionError(os.getcwd()で落ちる)。
cdしてから起動するか、そもそも検証はfile://で足りる(絶対URLで自己完結しているため)。
2026-08-20 J-CAT(STUDIO製サイト)5件 — 抽出を「機械で完全一致」させる型が確立した
成果: 5件すべて 数値照合MATCH(全要素3px以内)+レスポンシブ✅ で一発格納。
partner 164要素 / footer 96 / philosophy 97 / news 105 / services 418。
汎用抽出スクリプト(これが本体・今後はこれを使う)
セレクタ手作業でCSSを拾うのをやめ、ブラウザに「この要素にマッチするルール」を判定させる。
document.styleSheets を再帰で歩き、els.some(e => e.matches(セレクタ)) で残す/捨てるを決める。
これで STUDIO のような .sd[data-s-<uuid>] だらけのサイトでも取りこぼしゼロになる。
この方式でハマる4点(全部実害が出た・毎回チェックする)
| # | 症状 | 真因 | 対処 |
|---|---|---|---|
| A | borderだけ消える・バッジが1px小さい | style[i] の列挙は var() を含むショートハンドを落とす(border-bottom: 1px solid var(--x) が消える) | 宣言は r.style.cssText をそのまま使う(個別プロパティを組み立てない) |
| B | 全部 display:block で縦積み・レイアウト総崩れ | 基盤CSSが :where(.render-canvas) .sd{…} のように祖先クラス前提で書かれ、単独プレビューに祖先が無い | 祖先チェーンのclassを全部集めてスコープdivの内側にラッパーを1枚作る(.sec-id > div.render-canvas...) |
| C | タブレットだけ横スクロール・SP指定が効かない | @media グループをメディア無しより先に出力していて、PC値が後勝ちしていた | メディア無し → @media を max-width 降順で出力する(順序が全て) |
| D | 画像が灰色/サムネが空白 | loading="lazy" のまま+一気に最下部へ飛ぶ抽出で遅延読込が発火しない | loading 属性を除去+600pxずつ段階スクロールしてから対象を scrollIntoView して待つ |
CMSがJSで差し替える画像は抽出できない(J-CAT News)
STUDIO CMS の記事サムネは 自動化ブラウザ(headless / headful / --dump-dom すべて)でプレースホルダのまま(180バイトのwebp)。
実ブラウザでは出るので、プレビューが灰色ボックスになったらこれを疑う。
→ 各記事ページの og:image から実画像URLを取ってカード順に差し込むのが最短・確実(=参考元の画像そのまま を満たす)。
curl -sL "<記事URL>" | grep -oE '<meta[^>]*og:image[^>]*>' | grep -oE 'https://[^"]+' | head -1
数値照合はオフセット2を固定する
再現HTMLは .sec-<id> の中に ラッパーdiv + ルート要素 の2つが余分に入る。
元は root.querySelectorAll('*')、再現は同じ配列の index+2 が対応する。
ここを自動検出しようとすると誤検出して「全要素ズレている」ように見え、無いバグを追いかける(今回30分溶かした)。
他セッションと同じリポで同時作業していた時のpush
別セッションが同じ ~/dev/terasu-lab で作業していて、git pull --rebase が
「untracked working tree files would be overwritten」で止まる(相手がpush済み・ローカルに未追跡で同名ファイルが残っている状態)。
- 消さない。git hash-object と git rev-parse origin/main:<path> を突き合わせ、内容一致を確認してから退避(mv)する。
- 一致していれば rebase 後に git が復元するので実害ゼロ(今回28件・相手の作業は無傷)。
- submodule core の差分は --autostash で逃がす。
このMacの動画確認
ffmpeg / ffprobe / PyObjC いずれも無い。qlmanage -t で先頭フレーム、複数フレームは
Chrome headless に <video> を読ませ currentTime を送って canvas に描く(--allow-file-access-from-files で file:// のままいける)。
2026-08-20 HERP Trust(STUDIO製)で踏んだ3つの地雷+検品の穴
大串FB「⑤開けない」=FAQのプレビューが真っ白だった。数値照合(
getBoundingClientRect)は
全項目一致していたのに真っ白。原因は下記①。数値だけで合格にしてはいけない。
① スコープ用ラッパーの背景が z-index:-1 のセクションを隠す(真っ白の真犯人)
STUDIO は各セクション(.sd)に position:relative を敷き、一部セクションに z-index:-1 を付ける。
元サイトでは白背景が body 側なので問題なし。ところが抽出時に body{background:#fff} を
.sec-<id>{...} へ寄せると、ラッパーの背景がペイント順で負のz-index子より後に描かれ、中身を丸ごと覆う。
- 症状: 計算値(色・opacity・visibility・座標)は全部正常なのに何も描画されない/elementFromPointも拾わない
- 対処: ラッパーに isolation:isolate(独立スタッキング文脈にすると、自身の背景→負z-index子の順で描かれる)
- 検出: CSSルールを前から順に足しながらスクショのバイト数で二分探索すると一発で犯人ルールが出る
② .sec-<id> を素で前置すると詳細度が+1され、外部CSSに不当に勝つ(アイコン豆腐)
.fa{font-family:FontAwesome}(外部CDN・詳細度0-1-0)に対し、こちらの .sec-x .hpt-sd{font-family:…} は
0-2-0 になって勝ってしまい、Facebookアイコンが□になった。
- 対処: スコープは :where(.sec-<id>) で付ける(:where() は詳細度0)。元サイトの優先順位がそのまま残る。
- 併せて reset も :where() ベースにし、reset → 元サイトCSS の順に並べて元のカスケードを再現する。
③ 入れ子 <li> は outerHTML を再パースすると平坦化される
<li><div><li>… という DOM(JSで組まれたSTUDIOサイトに実在)を outerHTML で書き出して読み直すと、
HTMLパーサの規則で外側の li が閉じられ、構造が壊れる(3カラムが崩れて縦積みになった)。
- 対処: 抽出時(ブラウザ内)に li li を div へ置換してから直列化する。li を対象にしたCSSが無いことを
grep(document.styleSheets を走査)で確認してから実施すれば見た目は完全に不変。
検品ゲートに「描画量」を追加する(数値照合だけでは穴がある)
getBoundingClientRect の一致はレイアウトの一致であって描画の一致ではない。
各件つき最低1枚はフルページのスクショを撮り、png bytes が極端に小さくないかを見る(真っ白は数KB)。
今回 issue/price だけ目視して faq を数値のみで通した結果、真っ白のまま大串に送ってしまった。
STUDIO製サイトの抽出メモ
- CSSは
[data-s-<uuid>]属性キー+:where(.render-canvas) .sd系。.render-canvasは祖先なので
プレビューは<div class="sec-x"><div class="hpt-canvas">…の二重ラッパーにしないとルールが当たらない。 .appearはCSS定義が無いことがある(=アニメ発火ゲートではない。外す必要なし)。- 画像は初期DOMだと
data:image/svg+xmlのプレースホルダ。全ページをスクロールしてからcurrentSrcを拾う。 @font-faceはクロスオリジンCSS(fonts.studio.design等)からは読めないので<link>をそのまま同梱。
ただし Font Awesome 6 の @font-face は同一オリジンのentry.css側にあるので、
こちらは抽出して出力しないとブランドアイコンが豆腐になる(FA4のCSSリンクだけでは足りない)。
このMacの動画確認(2026-08-20 追記・上の記述を訂正)
ffmpeg/ffprobe/PyObjC 無し。Swift も CommandLineTools のSDK不一致でビルド不可、
JXA の AVFoundation ブリッジも不可、Chrome(headless・channel:chrome)は macOS の .mov をデコードできず
loadedmetadata が来ない(file:// でも http:// でも同じ)。
→ 現状で確実なのは qlmanage -t -s 1400 -o <dir> <file.mov> の先頭フレーム1枚だけ。
複数フレームが要るときは大串に「動画のどこか」を1行で聞くほうが速い。
動画から複数フレームを抜く方法が確立(2026-08-20・上の「qlmanageの1枚だけ」を訂正)
上記は「Chromeが.movをデコードできない」で止まっていたが、3点セットで完全に解決した(Re Loop contact で実証)。
1. .mov → .mp4 に変換(macOS標準・追加インストール不要)
avconvert --source in.mov --output out.mp4 --preset PresetHighestQuality
※ --quiet は廃止済みオプション(付けるとエラー)。元が H.264 でも コンテナが .mov だとChromeが読まないのが真因。
2. localhost で配信し、プレイヤーHTMLも同じオリジンに置く(python3 -m http.server)
→ setContent() の about:blank から http://localhost の動画を読むと canvas が tainted になり toDataURL が SecurityError。
必ず page.goto('http://localhost:PORT/player.html') にして同一オリジンにする。
3. playwright は chromium.launch({channel:'chrome'})(バンドルChromiumはH.264非対応)。
あとは v.currentTime を進めて canvas.drawImage → toDataURL で任意枚数抜ける。
→ 動画1本でも「どこの箇所か」を大串に聞かずに特定できる=FB②(着手前の確認禁止)を守れる。
node のパスが変わっている(2026-08-20)
memory codex-bundled-node の旧パス(.../app.asar.unpacked/node_modules/cua_node/bin)はもう存在しない。
現行= /Applications/ChatGPT.app/Contents/Resources/cua_node/bin(node v24)。export PATH=...:$PATH で使う。
rem基準が「vw」のサイトは rem→calc(N*var(--u)) に機械変換する(2026-08-20・重要)
崩れる原因トップ10 #2 の発展形。Re Loop は html{font-size:0.6613756614vw}(=1512pxで10px)/SPは 2.5445292621vw(=393pxで10px)。
sec-<id>{font-size:10px} に寄せる従来の手当てでは直らない——rem は常に html を見るので、スコープ化した瞬間に全サイズが16px基準に化ける。
→ CSS内の全 Nrem を calc(N * var(--u)) に一括置換し、sec-<id>{--u:0.6613756614vw} / media内で --u:2.5445292621vw を切り替える。
calc(100vh - 30rem - 6rem) のような入れ子も calc(100vh - calc(30 * var(--u)) - ...) で有効。vw可変の再現までそのまま効く。
数値照合で必ず出る「あと2つ」(2026-08-20・毎回チェックすると一発で合う)
Re Loop で getBoundingClientRect() 照合が 18項目中18件NG → 下の2つを直しただけで 0ズレ(完全一致)になった。
1. <body> の既定 margin 8px が全体を右に押す(x が 150→158、幅が 848→837)。
→ 単独プレビューの <body style="margin:0"> で消す(CSSに body{} を書くとスコープ漏れになるのでインライン属性で)。
2. リセットとタイポグラフィの記述順。元CSSは input{line-height:1.15}(reset) → h1..input,textarea{line-height:1.8}(base) の順で
後者が勝つ。抽出時に順序を入れ替えると input の高さが 58px→50px に縮む(フォーム全体で130px以上ズレる)。
→ 元ファイルの読み込み順・記述順をそのまま保つ。
3. クラスの無い div の font-size は元サイトだと html から 10px を継承している。
→ ルートに sec-<id>{font-size:1rem}(=calc(1*var(--u)))を置くと継承チェーンまで一致する。
白基調のフォーム系はサムネ検品が誤検知する(2026-08-20・Re Loop contact で実測)
shoot_section.mjs が ⛔ 中身が写らない(色41種/輪郭2.0%) で落ちた。画像を見ると中身はしっかり写っている。
真因は判定式の3本目 isPoorShot = … || (flatRatio >= 0.92 && edgeRatio < 0.03)。
実測 flatRatio 0.95 / edge 0.026 =「白地に淡いグレー(#f2f2f2)の入力欄」が量子化で白に吸収され、画面の95%が一色と数えられていた。
- 推測で直さない。core/tools/shot_common.mjs の measureShot をリポ内から呼べば実測値が出る(tools/ に一時 .mjs を置く//tmp からだと playwright が解決できない)。
- 対処は①上部の余白を詰める→②実画像を目視→③--force。上部を詰めるだけでは 2.0%→2.5% で閾値を越えられない
(フォーム系は本質的に淡いため)。目視で中身が写っていることを確認したうえで --force が正解。
--force でも 3枚(カード/__detail/__sp)と 4:3 正規化はすべて通常どおり走る。
セクション上部の「ヘッダーが居た余白」は単独プレビューでは詰める(2026-08-20)
下層ページは main{padding-top:30rem} のようにヘッダー分を空けている。セクションだけ抜くと上に約290pxの白が残る
(サムネの過半が白 = 上の検品落ちの主因にもなる)。
→ トップ10 #8「単独プレビューで前提が無い指定は打ち消す」の正当な適用。上余白と sticky の top を同じ値で縮める
(例 30rem→8rem)。フォーム本体の寸法は1mmも変わらないので、getBoundingClientRect() 照合(w/h/x)は0ズレのまま。
2026-08-20 harimanics 沿革タイムライン(さき)で得た知見
一括リネームで「画像URL」まで壊れる(404)— 新パターン
class="history_2023_02" のように クラス名と画像ファイル名が同じ文字列だと、
リネームの一括置換が images/about/history_2023_02.jpg のURL側まで書き換えて404になる。
単独プレビューでは「画像だけ出ない」ので気づきにくい(レイアウトは崩れない)。
→ リネーム直後に必ず画像404を機械チェックする(登録前ゲートに追加推奨):
await p.evaluate(()=>[...document.images].filter(i=>!i.naturalWidth).map(i=>i.src))
rem基準は sec-<id>{font-size:10px} では再現できない(本体ルールの補足)
rem は常にroot(html)基準なので、sec-<id> に font-size を置いても効かない。
html への指定は禁止(貼ると崩れる)なので、該当箇所の rem を px に手変換するのが唯一の正解
(html{62.5%}=10px基準なら 2.4rem→24px)。今回は6箇所だけだったので全部px化して完全一致した。
着手時に元サイトの html の font-size を必ず実測(62.5%=10px / 未指定=16px)。
ffmpeg が無いMacで動画のコマ送りを取る方法(実証)
swiftスクリプト(AVFoundation)はコンパイルが重く2分でも終わらない=実用外- Playwright の同梱Chromiumは HEVC(画面収録) をデコードできない(readyStateが上がらない)
video要素を直接screenshot()すると真っ黒(ハードウェアデコード面が取れない)- 正解=
chromium.launch({channel:'chrome'})で実Chromeを使い、canvas.drawImage(video)→toDataURL()
--allow-file-access-from-filesと、file://ディレクトリにgotoしてからsetContentで相対srcを解決する - 大まかな1枚だけでよければ
qlmanage -t -s 1200 -o . <mov>が最速(箇所特定はこれで足りることが多い)
作業ディレクトリ名は「サイト名」だけだと衝突する
/tmp/section_<site>/ は本体ルール通りに分けたが、同じサイトを別セッションが同時に処理していて衝突した
(先に別セッションの動画フレームが入っていた)。→ /tmp/section_<site>_<担当>/ まで含めるのが安全。
同様に、同じリポで別セッションが同サイトの他セクションを作業していることがあるので
git add は自分のidのファイルだけ(-A 禁止)を厳守する。git pull --rebase は他セッションの
未staged変更で失敗するので、失敗しても自分のcommitのpush成否を git status -sb で確認する。
うまくいった型
スクロール連動の横スクロール年表は GSAPではなく素のJS(window.scrollY から translateX を算出)だった。
元サイトのインラインJSから setupTimeline を丸ごと移植し、セレクタを sec-<id> 配下に限定するだけで完全再現できた。
単独プレビューでは固定ヘッダー前提の top: calc(100px + 48px) を top:48px に、headerH も合わせて調整すると自然に収まる。
数値照合の結果=スクロール器 8140x500・全10itemの寸法とx座標が元サイトと完全一致。
2026-08-20 株式会社エースリー(a-thily.co.jp)トップ6件 — 崩れの真因4つ
rem基準は「html の指定」だけ見ると間違える(body,html{font-size:62.5%} の二重適用)
エースリーは body,html{font-size:62.5%} と両方に書かれており、body が html(10px) の 62.5% = 実効 6.25px。
html だけ見て「10px基準」と判断すると全フォントが1.6倍になる(実測 h2 が 126px→201.6px)。
→ 基準は grep で決めず、元サイトで getComputedStyle(該当セクション).fontSize を実測する。
rem は「ホストの html」を見る=スコープ化だけでは閉じない
sec-<id>{font-size:6.25px} を敷いても rem はルート要素(html)基準なので効かない(ホストが16pxなら1.6倍のまま)。
→ 生成CSSの Nrem を (N×10)px に機械変換するのが正解。スコープ内で完全に閉じ、貼り先のhtml指定に一切依存しない。
re.sub(r'(-?\d*\.?\d+)rem', lambda m: f'{float(m.group(1))*10:g}px', css)
素タグ a{color:…} の抽出漏れ=リンクが「既定の青」になる
抽出条件を「クラスを含むセレクタ」にすると a{color:#212121} が落ち、rgb(0,0,238) の青が出る(大串FB「テキストの色が入ってない」)。
→ ベースCSSに sec-<id> a{color:<実測値>} を必ず補完する。トップ10 #9(素タグのルート指定)の亜種。
単独プレビューは「スクロール不能」でアニメが永久に発火しない
元サイトの発火条件 rect.top < 画面高/1.6 をそのまま移植すると、セクション高さ < ビューポートのページでは
一生 on が付かず中身が消える(member が実例)。逆に最初から on を付けると動きが無いと指摘される(大串FB)。
→ 発火判定に ①スクロール不能なら全on ②最下部到達なら残り全on の2条件を足す。これで動きも残り消えもしない。
サムネは「アニメ発火済み」「メニュー開いた状態」で撮る
- スクロール発火型は fullPage 撮影でビューポート外が未発火=本文が消えたサムネになる。
→ 撮影直前だけels.forEach(on)を注入したコピーで撮り、撮影後にプレビューを元に戻す。 - ヘッダー単体は閉じた状態だと採用ゲートに落ちる(
中身が写らない)。
→.child{opacity:1;visibility:visible;transform:none}を撮影用に注入しメガメニューを開いて撮る。
→ 器のmin-heightは「ヘッダー高+メニュー高」に合わせる(560pxだと白余白が目立つ→380pxに)。
環境の落とし穴(このPC)
--dump-domの Chrome が遅延して dom.html を上書きし、セクションが消えたDOMになることがある。
→ WordPress系はcurlで十分(今回は curl で全セクション取得できた)。1行HTMLへのgrep -oEは
catastrophic backtracking で固まるのでPythonで処理する。- ローカルサーバは他セッションが 8090/8091/8097-8099/8123 等を使用中。空きポートを
lsofで確認してから起動する。
固定サイドナビぶんの左余白は、単体セクションでは左右を揃える(2026-08-20 大串FB・恒久)
指摘: ReLoop(re-loop.jp)のフッターで「②左右の余白が違う」。
真因: 元サイトは左端に幅9u(87px)の固定サイドナビがあり、フッターは padding: 3u 3u 3u 12u
(=左だけ 9u ぶん多い)でナビを避けている。数値照合は元サイトと1pxも違わず一致していた=再現は正しい。
単体で切り出すとナビが無いので、左だけ広く見える(左87px / 右29px)。
直し方: 元サイトのPC表示ではなく「抜き出した後の収まり」を直す(Step3.5と同じ思想)。
padding: 3u 3u 3u 12u → padding: 3u(左右対称に)。中身の設計は変えない。
横展開のチェック(着手前に毎回): 固定サイドナビ/固定ヘッダーを持つサイトから抜くときは、
padding-left と padding-right の差がナビ幅とほぼ一致していないか見る。一致していたらナビ避けの余白=打ち消す。
ただしナビ幅を超える左右差は元サイトの意図した非対称レイアウト(例 ReLoopのFV/Projectsは左15u・右3u〜6u)なので、
勝手に揃えず大串に確認する。
教訓: 数値照合PASSでも「単体で見たときの収まり」は別問題。登録前に必ず単体プレビューを一度目で見る。
2026-08-20 NIPPON WONDER FACTORY(CTAボタン単体)で得た4つの学び
① 「アイコン(SVG)だけ消える」= リセットの svg{display:block} 拾い漏れ
wonder-factory.jp のボタンを抽出したところ、矢印アイコンだけが表示されなかった。
真因は元サイトのリセットにある audio,canvas,embed,iframe,img,object,svg,video { display:block; vertical-align:middle; max-width:100% }。
これを落とすと svg が inline になり、ベースライン分(実測15px)下にずれる → 親 .__inner{height:11px;overflow:hidden} に切られて完全に消える。
- 症状: 数値照合(ボタン寸法・色・transform)は全部一致するのにアイコンだけ無い
- 検出: getComputedStyle(svg).display を元サイトと比較(block vs inline)/svg の rect.y が親と一致するか
- 対処: sec-<id> img, sec-<id> svg, sec-<id> video, sec-<id> canvas { display:block; vertical-align:middle; max-width:100%; height:auto } をスコープ版で補完
- 崩れる原因トップ10 #9(素タグのルート指定が抽出条件から漏れる)の亜種。アイコン・画像が消えたらまず素タグの img/svg リセットを疑う
② ボタン等「小さい部品1個」のセクションはサムネ採用ゲートに必ず落ちる
shoot_section.mjs は撮影幅 W=1440 固定。300×70 のボタン単体だと ⛔ 中身が写らない(輪郭1.7%) で保存されない。
- 対処(ヘッダー/メガメニューを「開いた状態で撮る」のと同じ考え方):
① 画角を寄せる(幅780でPCレイアウトを維持。767px以下にすると SP のメディアクエリが発火して別デザインになるので下げすぎない)
② 動きが本体のセクションは"途中の状態"で固定して撮る(今回は ::before を translateY(50%) に固定=黒とピンクの波が両方写る)
- 3枚セットで作る(<id>.png 2880×2160 / <id>__detail.png 2880×2000 / <id>__sp.png 780×1520)。カード用だけ差し替えると詳細ページが古いまま(v2.6の教訓)
- 非整数倍率になる時は大きめに撮って sips -z <h> <w> で正規化する
③ 動画素材のフレーム抽出(このMacは ffmpeg 無し)
ffmpeg が入っていないので、動画の中身確認はこの手順が最短:
1. qlmanage -t -s 1600 -o <dir> <file.mov> … 1枚だけ即取れる(中身の当たりを付ける用)
2. 複数コマ欲しい時は /usr/bin/avconvert --source rec.mov --output rec.mp4 --preset PresetHighestQuality で H.264 に変換
→ chromium は macOS 画面収録の HEVC を再生できない(video.error.code=4)ので変換が必須
3. python3 -m http.server <空きポート> で配信し、playwright で <video> を currentTime シークしてコマ撮り
→ file:// 直読みは再生できない/ポート8080・8099は他セッションが使っていることが多いので空きポートを使う
- swift + AVFoundation でのフレーム抽出は swift実行が2分でタイムアウト・swiftcもこの環境ではビルドに失敗した(使わない)
④ CIが cancelled になっても慌てて再pushしない
同時作業日は他セッションのpushが連続し、concurrency: deploy-terasu-lab の待ち行列で自分のrunが cancelled 表示になる。
だが後続runは main の HEAD(=自分のコミットを含む) をビルドするので、内容はそのまま反映される。
- 確認は run の成否ではなく本番URLのHTTP 200と /sections に id が出ているかで見る(gh run watch の X 表示だけで失敗と判断しない)
- build.py をローカルで走らせると他セッションの code-previews/*.html まで書き換わる(CIが同じ処理をするので放置でよい)。絶対に git add -A しない
⑤ 条件付き html[lang=xx] ルールがスコープルートに化けて全体を壊す(2026-08-20 BRUTUS)
抽出時に「先頭の body クラス文脈(.home .x)を除去する」処理を素朴に書くと、
html[lang=ko],html[lang=ko] *{font-family:system-ui,sans-serif!important} のようなそのページに当てはまらない条件付きルールまで
.sec-<id>,.sec-<id> *{...!important} に化け、セクション全体のフォントを乗っ取る(実際に全要素が system-ui になり、幅が 499→685px にズレた)。
- 対処=先頭が html/body でも、[属性]・#id・body文脈以外のクラスが付いていたら「不採用」にする(除去して残さない)
- 症状の見分け方: computed の font-family が指定と違う → 生成物を grep -o 'font-family:[^;}]*' して !important の混入を探す
⑥ 遅延描画サイトは Playwright 内蔵Chromiumだと中身が出ない → channel:'chrome'
brutus.jp のフッターは内蔵Chromiumだと .c-page-foot が 596px の空箱のまま(子要素0件)で、数値照合が全部❌になる。
実Chrome(chromium.launch({channel:'chrome'}))に変えた瞬間に全項目一致した。
- 元サイトのDOM取得を実Chrome headlessでやったなら、照合も実Chromeで揃える(ブラウザを混ぜない)
- あわせて「最下部まで段階スクロール→待機」を入れる(遅延読み込みの発火)
⑦ 数値照合は「セクション限定セレクタ」で測る
.c-base-headline__heading__en のような共通クラスは元ページ内の他セクションにも大量にあるため、
そのまま測ると元だけ4件・再現1件で必ず❌になる。.c-home-regulars .c-base-headline__heading__en のようにセクションのルートを前置して測る。
- 残差が 1px 以内の時は Range.getBoundingClientRect() でテキストの実描画幅まで見る。
padding/letter-spacing/font-feature(palt)/weight が全同値で 0.6px 差ならサブピクセル丸め=元CSSを書き換えてまで合わせない(それは「寄せる」になる)
⑧ ロゴが出ない=SVGスプライト <use href="#icon-logo">
セクションHTMLだけ切り出すと <symbol> がページの別の場所にあるためロゴが消える。
DOM から該当 <symbol> を抜き、body先頭に <svg style="position:absolute;width:0;height:0;overflow:hidden">…</svg> として同梱する。
- 汎用化: 断片内の href="#xxx" を集める → dom.html から同id の symbol をタグバランスで切り出す
⑨ 動画からのフレーム抽出(このMacに ffmpeg が無い場合の正解)
ffmpeg 無し・swiftスクリプトは初回コンパイルで2分超えて実用外。Playwright + <video> + currentTime シークが最速。
1. 動画を python3 -m http.server <port> --directory <dir> で配信(--directory 必須。サブシェルの cd は効かない)
2. <video id="v" src="v.mov" muted preload="auto"> のページを開き、v.currentTime=t → locator('#v').screenshot() をループ
- node は PATH に無い。export PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH"
- スクリプトは ~/dev/terasu-lab/ 内に置く(playwright は そこの node_modules にしか無い。scratchpad から実行すると ERR_MODULE_NOT_FOUND)
⑩ 同時作業日は git commit もパス指定でやる
git add はしていないのに git status に他セッションのファイルが staged で出る(インデックスは1つ=共有)。
そのまま git commit -m すると他セッションの作業を自分のコミットに巻き込む。
- 対処=git commit -m "..." -- <自分のパスだけ列挙>(pathspec commit)。インデックスの他のファイルは触らない
- なお相手セッションが先にcommitして自分の分を巻き込むことも起きる(自分のcommitは "no changes added" になる)。
その場合内容は正しく入っているので履歴を直そうとrebaseしない(進行中の他セッションを壊す)。git log -- <自分のファイル> で入ったコミットを確認して先へ進む
⑪ ミニマルなセクションはサムネ検品に落ちる(崩れではない)
白背景・黒細線・要素が小さいセクション(ページネーション、シンプルなボタン帯など)は
shoot_section.mjs の採用ゲートに 中身が写らない(色3種/輪郭1.4%) で落ちる。
撮影は W=1440固定・中身が低いと contentH が下限200pxに丸められるため、
1440×200 の中に幅200px程度の要素がポツンと写る=輪郭比率が低いのは仕様どおりの結果。
- 判定手順=まず数値照合(getBoundingClientRect())が元サイトと一致しているか見る。一致していれば崩れではない。
次に同じ kinds の既存セクションのサムネを実際に開いて見比べる(例 ctsap-pagination-saki-0820.png は同じ余白量で採用済み)。
同水準なら --force で保存してよい(shoot_section.mjs v2.6 以降は末尾で normalize_thumbs が自動実行され4:3・2880×2160になる)。
- --force を使ったら報告に理由を明記する(「検品に落ちたが中身は正常・既存同種と同水準」)。黙って force しない。
- 2026-08-20 実例: brutus-pagination-saki-0820(BRUTUS「01 / 45」+円形矢印)。3幅すべて寸法完全一致 → force保存で採用。
⑫ 数値照合で font-size だけ全体的にズレる=rem基準をJSが可変にしている(2026-08-21 marumi)
getBoundingClientRect() 照合で 幅は合うのに font-size と余白だけ一律 1.1倍ズレるときは、
CSS の :root{--fz:10px} を JSが上書きして vw連動にしている(CSSだけ読んでも絶対に分からない)。
- 実例(shizai.marumi.com / common/js/basic.js):
js
if (display_mode == 'n') setProperty('--fz', (vw/375)*10 + 'px'); // SP
else { vw = clamp(vw,1300,1920); setProperty('--fz', (vw/1300)*10+'px'); } // PC
→ スコープ版は --sec-rem: clamp(10px, calc(100vw / 130), 14.7692px) / SPは calc(100vw / 37.5) で完全一致(8/8)。
- 手順=照合でfsが合わない → grep -n "fz\|setProperty\|documentElement.style" <site>/common/js/*.js を先に見る。
「10px固定」と決め打ちして進めない(トップ10の#2の実例。CSSの記述だけを信じるのが罠)。
- grep -oE '.{100}fz.{140}' のような両側ワイルドカードのgrepは巨大JSでハングする(120秒タイムアウトの実害)。
python3 -c "import re; [print(s[m.start()-160:m.start()+180]) for m in re.finditer('fz', s)]" で見る。
⑬ 動画素材からのセクション特定:ffmpegが無いMacでのフレーム抽出(2026-08-21)
このPCには ffmpeg も pyobjc も無く、swift はCLTのmodulemap衝突で動かない(redefinition of module 'SwiftBridging')。
- 実際に動いた方法=Playwright(Chrome) で動画を再生しながら canvas に描いて PNG 化
(chromium.launch({channel:'chrome'})=H.264/.mov が再生できる。playwright同梱chromiumは不可の可能性)。
1. 動画を作業ディレクトリへASCII名でコピー(日本語ファイル名のまま扱わない)
2. python3 -m http.server で配信し <video> に読ませる(file:// はcanvasが汚染される)
3. currentTime でのシークは効かない(python の http.server が Range 非対応 → 全フレーム同一画像になる。md5で気づける)
→ v.play() して while (v.currentTime < t) await sleep(30) で再生しながら間引き取得する
4. sips -Z 900 に落としてから読む(画像1枚=数千文字。コンテキスト肥大の主犯)
- 同じ画像が出てきたら md5 で必ず疑う(見た目が同じ=抽出できていない、を静かに見逃す)
2026-08-21 rem基準は sec-<id>{font-size:10px} では 効かない(崩れ原因#2の真因・恒久)
サイト: OND°(https://www.ond-crc.jp/ )/ニュース一覧グリッド・お問い合わせ帯
何が起きたか: コマンド本体(設計思想3・崩れ原因#2)の指示どおり html{font-size:10px} を
.sec-<id>{font-size:10px} に寄せたが、全要素が1.6倍で描画された(数値照合で発覚)。
| 項目 | 元 | 寄せただけ | calc変換後 |
|---|---|---|---|
.p-home-section__body |
1240x500 | 1424x520 | 1240x500 ✅ |
.entry |
250x250 | 260x260 | 250x250 ✅ |
.m-contact |
1340x242 | 1424x386 | 1340x242 ✅ |
真因: rem は root em = <html> の font-size 固定。祖先要素に font-size を置いても rem は一切変わらない。
.sec-<id>{font-size:10px} は「子の em」を変えるだけで、rem は既定の16pxのまま
→ 12rem(=120px想定) が 192px になり、余白・最大幅・文字サイズが全部ズレる。
単独プレビューでは"それっぽく"見えてしまうので目視では絶対に気づけない(数値照合でのみ検出できる)。
正しい直し方=rem を CSS変数 calc に機械変換する(html を触らないのでホストHPに貼っても崩れない):
# 1) 基準値を変数で持つ(元サイトの html の切り替えをそのまま写す)
.sec-<id>{--r:10px}
@media only screen and (min-width:1600px){.sec-<id>{--r:12.5px}}
@media only screen and (max-width:1023px){.sec-<id>{--r:2.57vw}}
# 2) スコープ化済みCSSの全remを一括変換(生成後の文字列に対して1回かけるだけ)
REM = re.compile(r'(?<![\w.#-])(\d*\.?\d+)rem\b')
css = REM.sub(lambda m: f'calc({m.group(1)} * var(--r))', css)
top:calc(50% - .8rem)→calc(50% - calc(0.8 * var(--r)))とネストしても正しく効く- body側の
font-size:1.6remも同じ変換で.sec-innerに載るので、html/body 2段構造が完全再現できる
教訓: rem を使っているサイトは「寄せる」だけでは必ずズレる。 getBoundingClientRect() の数値照合を
やっていなければ、そのまま格納して「サイズが全部おかしい」案件になっていた。照合ゲートは絶対に省略しない。
同時に効いた小ネタ
html{font-size:…}が3段階(既定10px / min-1600px:12.5px / max-1023px:2.57vw)ある設計。1つ見つけて満足しない(grep -oE 'html\s*\{[^}]*\}'で全部出す)a{color:#000;text-decoration:underline}とh1..h6{font-weight:inherit}は拾わないと即バレる(リンクが青くなる/見出しが太くならない)。スコープ版で必ず入れる- mask-image の
url(../imgs/…svg)はdata URI 化すると、絶対URL化とCMSパス除去を同時に満たせる(215バイトの矢印SVGなら軽い) networkidleが来ないサイトがある(OND°)→domcontentloaded+ スクロール + 待機に切り替える- このPCは node が PATH に無い →
export PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH"。
ただし playwright は~/dev/terasu-lab/node_modulesにしか無いので、比較スクリプトはtools/に置いて terasu-lab から実行する(終わったら消す)
STUDIO製サイトは「属性セレクタ丸ごと」で抜ける(2026-08-21・東急新卒採用サイト)
https://www.tokyu-recruit.jp/(Nuxt + STUDIO)で3件抽出。STUDIO製と分かったら以下の型に乗せるだけで数値完全一致まで行ける(実測:全277要素の getBoundingClientRect() が1pxも違わず一致)。
見分け方
DOMのclassが sd appear text sd appear image sd appear のような汎用名だけで、各要素に data-s-<uuid> が付いていたらSTUDIO。
CSSは <style> が50個以上に分かれ、.sd[data-s-<uuid>]{…} の形で入っている。
抽出の型(4点)
- ベースCSSは全部
:where(.render-canvas) …にスコープ済み →render-canvasをsec-<id>に置換するだけでスコープ化が完了する。
:where()のまま置換すること(.sec-xに変えると詳細度が上がり、.sd[data-s-…]とのカスケードが逆転して崩れる)。
data-s ルール側も:where(.sec-<id>)を前置=詳細度を1ミリも動かさないのが要点。 - CSS変数は最後の
<style>に:root{--s-color-… --s-font-…}として全部ある(東急は685バイト)。これを.sec-<id>{…}にして先頭へ。 - 画像はDOMに無い。
<div class="image" data-r-…><style></style></div>の空styleへJSが実行時に注入する
(.sd[data-r-…]:before{background-image:url(https://storage.googleapis.com/studio-design-asset-files/…)})。
→--dump-domでは画像ゼロになる。Playwrightで実行後DOM+document.styleSheetsの cssText を取るのが正解。 @font-faceは entry.css 側にある。使う font-family 名で絞って同梱(Material Icons を落とすと矢印がkeyboard_arrow_rightの文字になる=トップ10の⑩と同じ症状)。
このサイトで踏んだ崩れ2つ(STUDIO共通なので次も出る)
| 症状 | 真因 | 直し方 |
|---|---|---|
| セクションの高さが必ず画面高になる | .render-canvas{min-height:100dvh} と .StudioCanvas>.sd{min-height:100dvh} がラッパ/セクションに誤爆(元はページ全体の器の指定) |
末尾に .sec-<id>{min-height:0} と .sec-<id> > .sd{min-height:0;overflow:visible} を足して打ち消す |
| PCでは出るのにSPで見出しが消える | .appear は出現アニメの状態クラスで、JSがスクロール時に外す。PC幅でDOMを取ると「PCでdisplay:noneだったSP用要素」は発火せず appear が残り opacity:0 のまま |
PC/SP/TBの3幅でDOMを取り、どれかで appear が外れている要素は外す(文書順で突き合わせてマージ)。東急では9件が該当 |
数値照合スクリプトの注意
元DOMには注入用の <style> が要素として混ざるため、sel, sel * で拾うとインデックスが1つずつズレて全部DIFFに見える。
filter(e => !['STYLE','SCRIPT'].includes(e.tagName)) を必ず入れる。textContentも比較に入れると同じ理由で偽陽性になるので、寸法+opacity+displayだけで比較する。
素材(動画)が消えるケース
受領直後は ls できたのに、作業中にデスクトップから実体が消えて cp も qlmanage も失敗(TCCではなく本当に無かった)。
動画・画像を受け取ったら、真っ先に作業ディレクトリへコピーしてから着手する。ffmpeg がこのPCに無いので、
フレーム抽出は Playwright か qlmanage -t(遅い・2分でタイムアウトした)。swiftc は SDK 不整合でビルド不可。
2026-08-21 松下組 お問い合わせ導線(matsushitagumi-contact-saki-0821)
参考元 https://matsushitagumi.com/contact/ の .t-contact-means(電話/メールの2ブロック)。
4幅(1440/1024/768/390)で寸法・実文字幅とも完全一致・修正ゼロで格納できた回。効いた型を残す。
sec-<id>{font-size:10px} では rem は直らない(コマンド本体の記述より正確な事実)
rem は 常にルート要素(html)基準なので、html{font-size:62.5%} を sec-<id>{font-size:10px} に移し替えても
scoped側では効かない(ホストHPに貼ると 1.4rem が 22.4px になる)。
→ rem値を px に機械変換して書くのが唯一確実な方法(10px基準なら 2.5rem→25px・calc(1rem + 2.65vw)→calc(10px + 2.65vw))。
視覚は完全に等価で、単独でもホストHPでも同じ寸法になる。これを標準にする。
幅が 1024px以下でだけ 14px ズレる = body の既定マージン8px
元サイトは巨大リセットで body{margin:0} が効いている。抽出側は sec-<id> 配下しかリセットしないので
body の 8px マージンが残り、width:90% の器が両側で 7px ずつ内側に寄る(1440では max-width:1100 で頭打ちになるため気づけない)。
→ <style> に body{} を書くのは禁止なので、<body style="margin:0"> のインライン属性で消す。
セクション本体(div)を貼るときは付いてこないのでホストHPを汚さない。単独プレビューだけ正しくなる。
Adobe Fonts(Typekit)の扱い
- kit CSS(例
https://use.typekit.net/tll5pmd.css)は 他ドメインからでも読めるので、<link>1本でそのまま同梱してよい(画像を原URLで残すのと同じ思想)。kit id は Playwrightのpage.on('request')でネットワークを見て取る(DOM/JSをgrepしても出てこない=JSが動的に挿す)。 document.fonts.check('40px azo-sans-web')は当てにならない。元サイトでも false を返した(weight指定違いで落ちる)。
→ フォントが効いているかはRange.getBoundingClientRect().width(実文字幅)を元サイトと突き合わせるのが確実。今回は 275/207/207/211 が4幅とも一致。
数値照合は「元と再現でセレクタ名が違う」ので対応表で回す
リネーム後は .p-contact__title ↔ .contact-row__title のように名前が変わる。
{orig:{sel:{…}}, repro:{sel:{…}}} のキー対応表を作って同じキー同士を比較すると、そのまま4幅ループに乗る。
差が出たら 幅x高さ@x の3値のどれがズレたかで真因が割れる(全要素が一律 -14px/+7px なら器の幅=body/スクロールバー系)。
実務メモ(このPC)
- node は
/Applications/ChatGPT.app/Contents/Resources/cua_node/bin/node(Contents/Resources/cua_node直下。過去メモのパスから移動している)。
Playwright は~/dev/terasu-lab/node_modulesにあるので、スクリプトは~/dev/terasu-lab/tools/に置いて実行する(/tmp から実行するとERR_MODULE_NOT_FOUND)。 - 検証用の一時スクリプトを
tools/に置いたらcommit前に必ず消す。 sections-data.jsonは git add しない。他セッションの差分が既に乗っていることがあり、巻き込んで commit してしまう(CIが全件再生成するので不要)。- ローカルプレビュー用の
python3 -m http.serverはセッションを跨ぐと死ぬ/古いセッションのサーバーが同じポートを別ディレクトリで掴んでいることがある(8099で別リポの中身が出た)。起動直後に必ず自分のidで200を確認してから計測に入る。
2026-08-21 竹中大工道具館「植物×匠」5件(FV/NEWS/みどころ/展示構成/開催概要)
結果: 5件とも全要素で数値完全一致(143/44/78/71/72要素の w・h・x・y・font-size・color)。到達までに踏んだ地雷は以下。どれも他サイトでも必ず起きる汎用パターン。
① セレクタのコンマ分割は「カッコ対応」でやる(CSS全滅の新パターン)
rule.selectorText.split(',') で分けると :where(ul, ol) が :where(ul と ol) に割れる。
出力した <style> はそこで波カッコの対応が壊れ、以降のルールが全部死ぬ(実測: 83ルール→19ルールしか解釈されない)。
- 症状は learning.md 既出の「base64二重書き込み」と同じ=文字だけ・リンクが青い下線、高さが元の4倍。
- 検出は必ず document.styleSheets[N].cssRules.length を数える(目視では「ちょっと崩れてる」に見えて原因に辿り着けない)。
- 対処: 深さカウンタ付きの splitTop(sel, ',') を使う(( [ で depth++)。scopeSel 側の分割も同じ関数で。
② :not(:focus) を含むルールが丸ごと落ちる
疑似クラスを剥がす処理で :not(:focus):not(:active) が :not():not() になり、querySelectorAll が throw → そのルールを「使っていない」と誤判定して捨てる。
今回は .visually-hidden(スクリーンリーダー用テキスト)が効かず、隠し文が画面に出てレイアウトが下へ押された。
→ 剥がす正規表現に :not\(\s*:[^()]*\) を丸ごと削除を足す。
③ JSが入れるCSS変数(--vw / --vh)を必ず補う
margin-right:calc(50% - 50 * var(--vw)) のように JSが setProperty("--vw", …) した変数前提のCSSがある。
無いと calc ごと無効化され、画面幅より広く出す設計(1390px幅の帯)が 100% に潰れる。
- 着手時に grep -oE 'setProperty\("--[a-z-]+"' main.js で洗い出すのを定形にする。
- 対処は sec-<id>{ --vw:1vw; --vh:1vh; }。JSと同じ値になり、貼り先でも自動追従する(px固定にしない)。
④ body の継承値は「px」で持ってこない(縦書きが潰れる)
:where(body){line-height:1} の計算値は 16px。これをそのまま sec-<id>{line-height:16px} にすると、
font-size 29px の子が line-height 16px を継承して縦書きの行が重なる(幅 88px → 62px)。
→ px なら lineHeight / fontSize の無単位比率に直して出す。text-align font-weight も body から拾い忘れると効いてくる(本文の中央寄せが消える)。
⑤ SVGスプライト(<symbol> / clipPath)はセクションの外にある
矢印・有機シェイプのバッジ・FVのマスクは、body 直下の非表示 <svg> に <symbol id="i-arrow"> として置かれ、
セクション内は <use xlink:href="#i-arrow"> だけ。そのまま切り出すとアイコンが 0×0 で消える。
→ 部分木の href="#…" と url(#…) を集めて外にある定義を取り込み、id に -<短縮id> を付けて貼り先との衝突を防ぐ(参照も同時置換)。
⑥ shoot_section.mjs は動画ループのセクションで黙ってサムネが欠ける(本体修正済み)
waitUntil:'networkidle' はループ再生の <video> があると永久に来ない → goto タイムアウト → catch(e){return false} で握りつぶし、
カード用だけ出て __detail.png / __sp.png が無い状態になる(詳細ページが古いまま/空になる)。
→ 2026-08-21 に gotoSafe()(networkidle 20秒 → 失敗したら load + 3秒待ち)を追加して3箇所とも置換。同じ理由で check_responsive.mjs も動画セクションでは落ちるので、その件だけ同じ判定を自前スクリプトで回す。
⑦ このPCに ffmpeg は無い。動画のコマ送りは Playwright で「再生しながら」撮る
swiftc は CLT のモジュール衝突で通らない(redefinition of module 'SwiftBridging')。
currentTime を seek して canvas に drawImage する方式は、9コマ全部が同じ1フレームになる(seek 完了と描画が別)。
→ playbackRate=4 で play() しながら一定間隔で locator('#v').screenshot() が確実。
動画は python3 -m http.server で配ってから <video> に読ませる(file:// は canvas が汚染される)。
一発格納できた要因(次回もこの順で回す)
- 数値照合は 元と再現を同じ幅で開いて全要素の
w|h|x|y|font-size|colorを並べる(差が1件でも残っているうちは見た目を見ない) - 差が出たらまず
cssRules.length(CSSが死んでいないか)→ 次に器の幅(body margin / vw)→ 次に継承値(line-height / text-align) - リネーム後にもう一度同じ数値照合(prefix対応表で名前を正規化して比較)。今回はリネームで壊れゼロを機械で確認できた
2026-08-21 GRUST(grust.co.jp)6件 — 「CSS抽出の一般解」を機械化できた回
参考元= https://www.grust.co.jp/ /受領=スクショ4+動画2=6件 → 6件とも作成・格納(追加者=さき)。
6件とも 元サイトと getBoundingClientRect() が完全一致(差はスクロールバー16px分のみ)。
一番効いた発見=「先頭コンパウンドの剥がし」は body の class/id に限定する
CSSからルールを拾う時、.p-phil-message__body p { … } のように知らないクラスが先頭にあるルールを
「先頭を捨てて p {} として拾う」実装にしていたら、他ページ用の指定が素タグ指定として大量に混入し
1セクションで 914ルールに膨張した(svg90/p62/a30…)。
→ 剥がしてよいのは <body> の class / id(今回 is-loading is-opening is-change-start / #index)だけ。
これに直すだけで 620 → 136ルール(719行)に落ち、見た目は不変。「長い=抽出ミス」の主犯はこれ。
併せて要る判定4つ(どれも実害が出た)
| 判定 | 例 | 効果 |
|---|---|---|
負条件(:not)はページ文脈を見て落とす |
body:not(#index) .u-show-index{display:none} |
index を再現するのに拾うとロゴが消える(実際に消えた) |
| 状態クラスはルートに寄せる | body.init-end .l-header__nav__item .link{opacity:1} |
登場アニメの発火条件。ラッパーに init-end を付けて .sec-<id>.init-end … にスコープ化=ナビが透明のままを回避 |
url() の絶対化は正規表現に注意 |
url\(\s*((["']?)[^)]*?)\s*\) は空マッチして全部CSS自身のURLになる |
@font-face の src が style.css を指しフォントが全滅する(1回踏んだ) |
| ワイルドカード扱いのクラスだけの部分は捨てる | .is-opening{} .ps__rail-y{} 単独 |
is-/js-/u-/swiper/ps を「実行時に付く」として通すと、実在クラスを1つも含まないルールまで拾う |
スクロール駆動FVは「終端の状態クラス」を付けると単体で完成状態になる
p-top-mv の GROW FOR TRUST は .p-top-mv__fixItem が position:fixed+JSで進行。CSSに
.is-absolute(スクロール終端の状態) が用意されていて、これを付けると
.is-absolute.is-bottom .svg-mv-copy{opacity:0} が効き、波動画が文字マスクから覗く完成状態になった。
→ 単体化で困ったら、まず その効果の"終わりの状態クラス"がCSSに無いか探す(自作で寄せない)。
Swiper は「JSだけCDN・CSSは元サイトから」
CDNの swiper.min.css を読むと矢印が青い「❮ ❯」のテーマ矢印になる(元サイトは丸ボタン)。
site.css 側に swiper のCSSが入っていたので、CSSはスコープ化して同梱/JSだけCDNにしたら一致した。
(v2.5 のトップ10 #10 と同じ現象。「テーマCSSはCDNから取らない」は Swiper でも同じ)
ヘッダー単体のサムネは「開いた変種を一時的に置いて撮る→本番ファイルを戻す」
閉じた状態だと採用ゲートに落ちる(色18種/輪郭0.6%)。手順:
1. cp code-previews/<id>.html /tmp/<id>_real.html(必ず先に退避)
2. 開く対象に shot-open を足し、@media(min-width:801px){ .sec-<id> .l-header__nav__item.shot-open .l-header__nav__box{opacity:1;transform:translate3d(0,0,1px);pointer-events:all} } を </style> 前に追記/min-height も足す
3. node tools/shoot_section.mjs <id> → 4. 退避したファイルを戻す
class属性は抽出時に正規化されて末尾スペースが消えるので、... js-nav-toggle " のような
生DOMのままの文字列で置換すると当たらない(無言で失敗して同じ結果になる。1回踏んだ)
環境まわり(このMacで詰まった順)
- ffmpeg / ffprobe が無い・swiftc も壊れている(arm64e swiftinterface エラー)→ 動画のコマ確認は
Range対応の自前HTTPサーバ + ブラウザでvideo.currentTimeを送ってスクショが最短。
python3 -m http.serverは Rangeを返さないので seek できない(readyStateが 1 で止まる)=ここが罠 - Desktop直下がサンドボックスで見えない(
lsが空・mdlsだけ通る)→dangerouslyDisableSandboxで確認したら
実体はDesktop/名称未設定フォルダ/にあった。「ファイルが無い」と判断する前にmdfind -nameで実在確認 - node がPATHに無い →
export PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH" - pjaxサイトは
--dump-domが別ページを返すことがある(GRUSTはphilosophyページのDOMが返ってきた)
→curlで生HTMLを取るのが確実(サーバーレンダリング済みなら全セクション入っている) shoot_section.mjsは動画ループで networkidle に届かず初回タイムアウトすることがある(再実行で通る)- CIは他セッションのpushで自分のrunがcancelledになる。
gh run listで最新runの成否を見て判定する
追記(同日): <body> の既定 margin 8px が「サムネの下端切れ」と「幅16pxズレ」の犯人
症状: ①お問い合わせ帯のサムネで枠の下辺だけ写らない(テキストは全部見えるので気づきにくい)
②数値照合でセクション最上位の幅だけ 1440 → 1424 と16px小さい(スクロールバー差だと誤診しやすい)
真因: プレビューHTMLの <body> に既定マージン8pxが残っていた。
- 左右 8+8=16px → ルート要素の幅が16px縮む(内側は max-width 中央寄せなので一致してしまい、原因が見えにくい)
- shoot_section.mjs は中身の実高さ(例242px)で ビューポートを242pxに縮めてから clip{y:8, height:242} を撮る。
必要なのは 8〜250px なのにビューポートが242pxしかない → 下端が撮れない。低いセクションほど致命的
(実測: 中身242px のはずが 202pxしか写っていなかった)
直し方: <body style="margin:0"> とインライン属性で潰す。
- body{margin:0} を <style> に書くと "ホストHPに貼ると崩れる外来グローバルCSS" になりスコープ漏れgrepにも引っかかる
- インライン属性なら sec-<id> の外に1行も漏れない(貼るのは <div class="sec-…"> 以下だけなので無害)
- 検証: img/sections/<id>.png の非白ピクセルの上下端を測り、実寸高さが元の高さと一致するかを数値で見る
python
from PIL import Image
im=Image.open(p).convert('L'); w,h=im.size; px=im.load()
ys=[y for y in range(h) if any(px[x,y]<200 for x in range(0,w,4))]
print('中身の実寸高さ', (ys[-1]-ys[0]+1)/2) # ← セクションの実高さと一致するか
追記(同日): 数値照合は document.fonts.ready を待たないと偽の差分が出る
ボタン幅が 188 vs 186 と2px合わず、computed(font-family/weight/size/letter-spacing/gap)は完全一致。
原因は再現側だけWebフォント未適用のまま測っていた(フォールバックのヒラギノで描画)。
比較スクリプトに以下を入れたら 9/9 完全一致になった。この2行が無いと「原因不明の数px差」を延々追うことになる。
await p.evaluate(()=>document.fonts.ready);
await p.evaluate(()=>document.fonts.load('700 18px "Zen Kaku Gothic New"'));
await p.waitForTimeout(1500);
追記(同日): ローカル検証サーバは --directory を絶対パスで
python3 -m http.server 8080 --directory code-previews は、シェルのcwdが戻る環境だと404を返し続ける。
気づかずブラウザ計測すると「要素がnull」で混乱する。必ず絶対パス、起動直後に curl -o /dev/null -w "%{http_code}" で200を確認してから測る。
2026-08-21 「カーソル合わせてもアニメが出ない」の真因はホバーCSSの祖先クラス前置(恒久・毎回チェック)
大串FB(udatsu 3件): 「①上につまりすぎ、カーソル合わせてもアニメがでない ②カーソル合わせたら背景画像出るはずなのにでない、アニメがでない ③カーソル合わせてもアニメがでない」
→ 3件とも同じ1つの原因。数値照合が全項目一致していても、ホバーは丸ごと死んでいた。寸法一致=完成ではない。
原因1:ホバーCSSが .no-touch 前置で全滅(最頻・最重要)
多くのサイトは Modernizr 系で html/body に no-touch(他に desktop chrome mac wf-active 等)を付け、
ホバー指定を全部 .no-touch ~:hover で書く。udatsu は hoverルール50件中ほぼ全部がこの形だった。
.no-touch .ui-btn:hover.type-link .btn-circle:before{opacity:1;transform:scale(1)}
セクションを切り出すと .no-touch を持つ祖先が消えるので、.sec-<id> .no-touch .ui-btn:hover … となり1つも発火しない。
対処=抽出時に root の祖先クラスを全部集めておき、スコープ化のときにセレクタ先頭の「祖先を指すトークン」を落とす。
// 抽出時:root の祖先クラスを収集して JSON に入れる
const ancestors=[]; let e=roots[0].parentElement;
while(e){ String(e.className||'').split(/\s+/).filter(Boolean).forEach(c=>ancestors.push(c)); e=e.parentElement; }
# スコープ化時:先頭トークンのクラスが全部 ancestors に含まれるなら、そのトークンごと削除
# .no-touch .ui-btn:hover .btn-circle → .sec-<id> .ui-btn:hover .btn-circle
検品: grep -c 'no-touch' code-previews/<id>.html が 0 であること(>0なら確実にホバーが死んでいる)。
原因2:ホバー演出が :hover ではなく JSが付ける状態クラスのことがある
udatsu のフッターは、ホバーで section.home-footer に is-service / is-company が付き、
そのクラスのCSSが「見出しの色反転・説明文フェードイン・背景写真の表示」を全部やっていた(:hover は1件も無い)。
抽出時は状態クラスが付いていないので .home-footer.is-service … は root.matches() に落ちて全部捨てられる。
対処は2段構え。
1. 抽出の判定セレクタから .is-* / .has-* / .js-* を除去してマッチさせる(状態クラス側のルールを拾う)
2. 再現HTMLに 元と同じトリガーでクラスを付けるJSを入れる(推測の自作アニメではなく、実装の再現)
el.addEventListener('mouseenter',()=>sec.classList.add('is-service'));
el.addEventListener('mouseleave',()=>sec.classList.remove('is-service'));
見つけ方=元サイトで hover 前後の class と inline style の差分を取る(computed だけ見ていると原因が分からない)。
原因3::not(:hover) が空 :not() になって例外→ルールごと捨てる
擬似クラスを除去する実装だと .menu-lists:hover .menu-list:not(:hover) が :not() になり querySelector が throw。
「他のリンクだけ薄くなる」系が丸ごと消える。除去後に :not() を削ること。
原因4(①の「上につまりすぎ」):セクションの上余白は直前セクションの padding-bottom が作っている
.home-journal{padding:0 0 180px}=自分は下だけ。上の余白は直前 .home-renovation{padding-bottom:180px} 由来。
単体で切り出すと上が詰まって見える。 直前要素の padding/margin-bottom を実測して同量を padding-top で補う(MQごとに実測。今回 PC180px / ≤1024px 90px)。
登録前チェックに追加(数値照合と同格)
grep -c 'no-touch'(およびdesktopno-mobile等の環境クラス)が 0- 主要インタラクションを実際に hover して computed 差分を数える(0件なら死んでいる)— 目視より確実
const before=snap(); await p.hover(sel); await p.waitForTimeout(1000); const after=snap();
// opacity/transform/color/borderColor/backgroundColor を ::before ::after 込みで比較し、変化0件なら❌
- 元サイトの同じ状態のスクショと並べて突き合わせる(「元もこう見える」ことまで確認して初めて完成)
2026-08-21 rossa.co.jp(六甲サービスセンター)3件 — Lottieアイコンと同時コミット事故
file:// では blob URL の fetch がブロックされる(Lottieが真っ白になる)
参考元の Lottie JSON(/assets/images/top/icon0N.json)は CORSヘッダーを返さないため、
lottie-player の src に元URLを入れると lab のドメインから読めない(アイコンが出ない)。
→ 実データをインラインで同梱して読ませる。このとき blob URL を経由してはいけない:
// ❌ localhost では動くが file:// では fetch がブロックされ描画されない
p.load(URL.createObjectURL(new Blob([JSON.stringify(data[name])], {type:'application/json'})));
// ✅ オブジェクトを直接渡す(fetchが起きないのでどこでも描画される)
p.load(data[name]);
サムネ撮影(shoot_section.mjs)は file:// で開くので、
「localhost のプレビューは正しいのにサムネだけ中身が無い」時はまずこれを疑う。
freezeLottie の seek('100%') が線描画アイコンを消す
shoot_section.mjs の prepare() は Lottie を 最終フレームへ送って止める。
線を描いていくタイプのアイコンは 最終フレームが「描き終わって消えた」状態のことがあり、
サムネだけアイコンが欠ける(実測:4個中3個が消え、1個は途中の枠だけ)。静止の正は frame 0。
→ --force で誤魔化さない。v2.6 の指針どおり Lottieだけ止めずに素撮りして差し替える
(freezeVideo/freezeAnim/oneSlide/reveal は共通ツールと同じ順で当て、freezeLottie だけ抜く。
カード=1440×1080@2x を clip → normalize_thumbs.py、詳細=1440×1000@2x、SP=390×760@2x isMobile。3枚セットで作り直す)。
検品の「色N種/輪郭N%」で落ちた時は、まず素撮りを目視して
「本当に中身が出ていないのか/白基調で薄いだけなのか」を切り分けてから対処する。
同時作業中の git commit はパス指定で打つ(index共有事故)
明示 git add していても、別セッションが同じリポで add した直後に自分が git commit すると
相手のstageぶんまで丸ごと入る(実際に他人の nakamura 系5セクションを巻き込んだ)。
git add -A を避けるだけでは足りない。
git commit -m "..." -- entries/<id>.json img/sections/<id>*.png code-previews/<id>.html # ← パス指定
巻き込んでしまったら git reset --soft HEAD~1(indexは保持=相手のstageは残る)→ 上のパス指定commitで切り直す。
.git/index.lock が出たら消さずに待つ(相手のgitが動いている)。
効いたやり方
- CSS抽出は「セクション内で実際に使われている class/tag/属性/id」だけを通すフィルタ+全ルール
sec-<id>化。
:not(...)は中身をクラス条件から外す(body:not(.wp-admin)系のベース指定が丸ごと落ちるのを防ぐ)。
CSSコメントを先に除去しないと/*! destyle */*,:before,:after{...}の先頭セレクタが壊れて box-sizing が落ちる。 - 単独プレビューの body 余白は
<body style="margin:0">(属性)で消す。
<style>にbody{margin:0}を書かないのでスコープ漏れgrepも通り、貼り付け時にも漏れない。 - Splide は生成物を素のマークアップへ戻してから再mountする(生成済みの
<button class="splide__arrow">を残すと矢印が二重になる)。 - 数値照合は
getBoundingClientRect()を両方セクションを表示状態にしてから取る。
スクロール位置が違うと「is-active の transform が当たっていない」だけで差が出て、崩れと誤認する(今回1回踏んだ)。
2026-08-21 グローウィン・パートナーズ(growin-fv / growin-value)
「スクロールできない」の真犯人=body由来の overflow-x:hidden をスコープ根に持ち込んだこと
元サイトの body{overflow-x:hidden;overscroll-behavior-y:none} を sec-<id> に寄せると、
ラッパー自身が独立したスクロールコンテナになる(overflow-x:hidden は overflow-y を auto に格上げする)。
結果、①親ページが動かない ②中の position:sticky がまったく効かない(カードが重ならない)。
大串FB「スクロールできない」の中身はこれ。→ 抽出後に必ず打ち消す:
.sec-<id>{overflow-x:visible;overflow-y:visible;overscroll-behavior:auto}
body の overflow / overscroll / scroll-behavior はページ級=セクションに持ち込まないを恒久ルールにする。
サムネだけ絵が違う ②=oneSlide と reveal が「反復要素・transformのズレ」を壊す
shoot_section.mjs の前処理は同型の要素が重なって並ぶもの=スライダーと見なすため、
FVの「20枚の円を transform で少しずつずらした残像カプセル」が 円1個に潰れた(実測)。
- oneSlide … 同じ形が重なる20枚を「スライド」と判定して1枚目だけ残す
- reveal … 薄い要素に transform:none !important を当ててズレそのものを消す
→ --keep-slider --no-reveal を付けて撮る。--no-reveal は今回 tools/shoot_section.mjs に新設(既定はON のまま・オプトイン)。
判定の目安: 「同じ部品を何枚も少しずつズラして1つの絵にしている」セクションは両方付ける。
プレビューは正しいのにサムネだけ違う時は、まずこの2つを疑う(CSSを疑って時間を溶かさない)。
rem基準を「スコープ化しつつ数値を1pxも狂わせない」方法
元サイトが html{font-size:calc(7*1px + 0.625vw)}(可変rem)だと、sec-<id>{font-size:…} に移すだけでは
rem はルート基準のままなので効かない(トップ10 #2 の変種)。今回は全 Nrem を機械変換して解決:
Nrem → calc(N * var(--rfs)) /* --rfs は sec-<id> と各@mediaに定義 */
:root の変数定義に含まれる rem も一緒に変換されるので、var(--font-size-…) 経由も正しく効く。
結果、元サイトと getBoundingClientRect() が完全一致(1440px・見出し96px/カード1400×700 まで一致)。
複数セッション同時実行で実際に起きた事故2つ(どちらも構造的)
- 作業ディレクトリを他セッションに消される。
/tmp/section_<site>は同じサイトを別セッションが触ると衝突する
(今回/tmp/section_growinを別セッションが使用中→作業途中で丸ごと消えた)。
→ サイト+箇所で分ける(/tmp/section_growinfv)。消えても困らないよう、成果物は早めにcode-previews/へ置く。 gitのindexに他セッションのstageが載っている。この状態でgit commitすると他人の作業ごとコミットする。
→git commit -m "…" -- <自分のパス…>でパス指定コミット(indexの他人の分はそのまま残る)。
git add -A禁止と同じ理由で、git commitも無指定は禁止にする。- push が重なると CI が
cancelledになる。runの成否より本番の実体で確認する
(/sectionsに自分のidが出るか。詳細ページの200はフォールバックでも返るので当てにしない)。
2026-08-21 LIG(liginc.co.jp)3件 — Tailwind製サイトの抽出で踏んだ罠と、その機械的な直し方
動画3本(Solutionsカード横スクロール/Explore the Knowledge丸ボタン/Solutions全画面スライダー)。
3件とも「元サイトと寸法が完全一致」まで持っていけたので、そこに至る型を残す。
Tailwind製サイトからCSSを抜くときの2大事故(どちらも「md:が全部効かない」という同じ症状で出る)
-
クラス名トークンの正規表現でバックスラッシュを許すと壊れる
Tailwind の実CSSは.md\:h-\[373px\]のようにエスケープされている。
トークン抽出の文字クラスに\が含まれているとmd\で切れて、md: 系の指定が丸ごと落ちる
(症状=カード幅が 23rem でなく 17rem になる/見出しが text-xl のまま)。
→\.((?:\\.|[^\s.,:>+~()\[\]{}#*"'=\\])+)のようにエスケープ側を先にマッチさせる。 -
CSSを自前で分割するとき、セレクタ内の
\'を文字列開始と誤認すると以降が全滅
.before\:content-\[\'\'\]:before{content:""}でパーサが暴走し、そこから後ろのCSS全部が1個のゴミブロックになる。
→ クオートの直前が\なら文字列扱いしない。検算=分割したブロック数がブラウザのdocument.styleSheets[i].cssRules.lengthとほぼ一致するかを必ず見る(今回 2697 vs 2690 で一致確認)。
rem は「スコープ化」では絶対に再現できない(コマンド本体の記述だけでは足りない)
html{font-size:…} を sec-<id>{font-size:…} に寄せても、rem はルート要素基準なので効かない(em は効く)。
LIG は <768px: 1rem=4vw / >=768px: 1rem=1.66667vw(1440幅で 1rem=24px)。
→ 宣言部の Nrem だけを calc(N * var(--sec-rem)) に機械変換し、--sec-rem をメディアクエリで切り替える。
.sec-x{--sec-rem:4vw} @media (min-width:768px){.sec-x{--sec-rem:1.66667vw}}
変換していいのは {} の中身だけ。セレクタを変換すると .md\:w-\[23rem\] が .md\:w-\[calc(...)\] になってHTMLと一致しなくなる(これも「md:が効かない」症状で出るので上の2つと紛らわしい)。
JSが left: 2 + "rem" のようにremを書き込む箇所も同じ理由でズレる → スコープ根の font-size を実測して px で渡す(=サイトと同じ実寸)。
フォントが読めないときの詰め方(今回 acumin-pro / Adobe Typekit)
- Typekit の kit(
use.typekit.net/xxxx.css)はドメイン制限で他ドメインからは 412。Referer偽装でも通らない → 代替する。 - 実サイトが自前配信している woff2 も、他ドメインからは CORS で読めない(
No 'Access-Control-Allow-Origin')。
日本語が謎の書体になったらこれ。→ 同じ書体を Google Fonts から読む(Noto Sans JP 等)。 - 代替書体は可変幅(wdth軸)を持つものを選ぶと数値で詰められる。acumin-pro ≒ Archivo。
実テキスト幅を Range で測って比較 →font-stretch:105%で誤差 0〜2% に収束(100%だと一律 -4〜5% 細い)。
// 実テキスト幅の測り方(ブロック幅では font の差が出ない)
const rg=document.createRange(); rg.selectNodeContents(el); rg.getBoundingClientRect().width
「JS実行後のDOM」を土台にするときの落とし穴(GSAPが効かなくなる)
--dump-dom で取った DOM には JSが書いた style="opacity:0;transform:translate(0%,110%)" が残っている。
この状態で gsap.set(el,{yPercent:0}) を打ってもまったく効かない(GSAPが既存transformを y=205.9px としてキャッシュし、yPercent:0 と足し算になる)。
→ 1文字divなどに残った transform/opacity のインラインstyleを外してから、元コードのJSに初期値を打たせる。
症状は「見出しだけ出てこない(親が overflow:hidden なので完全に消える)」。opacityだけ見るフォールバックでは検知できないので、
フォールバックは opacity と transform の両方を見る。
WebGLキャンバス背景は抽出不可 → 実測色を単色で置く
LIGの地色は #bgCanvas(three.js)。CSS上の html は rgb(26,26,26) だが、実画面は rgb(50〜53)。
CSSの値をそのまま使うとセクションだけ黒すぎる。→ 実スクショのピクセルを測って #333 を採用。
from PIL import Image; Image.open('ref.png').convert('RGB').getpixel((30,300))
shoot_section.mjs は「transition遅延つきの出現要素」をフェード途中で固定することがある
スライダーの説明文(transition: … .3s … .1s の遅延つき)だけが毎回サムネから消えた(見出し・画像・ページャーは写る)。
プレビュー自体は正常(localhost/file:// で 3.5秒待てば opacity:1)。再現性がある=ツール側の固定タイミングの問題。
→ v2.6 の指示どおりアニメを止めず素撮りして3枚とも差し替える。サイズはツール出力と同じにする:
<id>.png 2880x2160(1440x1080 @2x)/__detail.png 2880x2000(1440x1000 @2x)/__sp.png 780x1520(390x760 @2x)。
このMacの node のパスが変わっている
/Applications/ChatGPT.app/Contents/Resources/app.asar.unpacked/node_modules/cua_node/bin はもう無い。
現在は /Applications/ChatGPT.app/Contents/Resources/cua_node/bin/node(v24.19.0)。
playwright は ~/dev/terasu-lab/node_modules にあるので、検証スクリプトはリポ直下に置いて実行する(外に置くと ERR_MODULE_NOT_FOUND)。
今回効いた検証の順番(そのまま再利用できる)
- 元サイトを playwright で開いて
htmlの font-size / body背景 / 主要要素の fontSize を実測(=rem基準の確定。ここを外すと全部ズレる) - 抽出 →
getBoundingClientRect()を元/再現で同幅(1440)で突き合わせ、w x h @xが一致するまで直す - 一致しない時は症状からトップ10表を引く(今回は #2 rem基準・#3ではなく「セレクタまで変換した」自作バグ・#6 出現アニメ)
check_responsive.mjs→ スコープ漏れ grep → 外部ホスト grep(参考元ドメインが出るか/別サイトが混じっていないか)document.styleSheets[].cssRules.lengthが二桁以上あるか(base64二重書き込み事故の検知)
waitUntil:"networkidle"は計測タグが多いサイト(LIG等)ではタイムアウトする。domcontentloaded+waitForTimeout(9000)で取る。
送られてきた動画(画面収録)の中身を確認する手順(2026-08-21・Wpc. Patterns案件)
動画は「どのセクションか」を特定する唯一の材料なのに、この環境には ffmpeg / ffprobe が無い(pyobjc も無い)。
フレームを見ずに「スクショと同じ箇所だろう」と決めつけるとFB③(動画ぶんが作られない/別セクションを作る)に直結する。
今回も、スクショ=ドロワーメニュー/動画=お知らせの横スクロールでまったく別のセクションだった。
手順(macOS標準 + playwright だけで完結・所要1分)
# ① macOS標準の avconvert で H.264 に変換(画面収録は HEVC で Chromium が読めない)
cp "<録画.mov>" /tmp/section_<site>/v.mov
avconvert --preset PresetAppleM4V720pHD --source /tmp/section_<site>/v.mov --output /tmp/section_<site>/v720.m4v
# ② file:// はブロックされるので http で配信して playwright で seek→スクショ
(python3 -m http.server 8392 --directory /tmp/section_<site> &)
# player.html = <video id="v" src="v720.m4v"> を置き、currentTime を12点動かして locator('#v').screenshot()
# ③ 12枚をHTMLのgridに並べて1枚のコンタクトシートにして読む(画像1枚で済む=コンテキストを食わない)
file://の動画は setContent したページから読めない(タイムアウトする)。必ず http 経由にする- 画面収録は HEVC。変換せず Chromium に渡すと
readyStateが上がらず無限待ちになる - フレームは必ずコンタクトシート1枚にまとめてから読む(12枚を個別に読むとコンテキストが飛ぶ)
同案件で効いた小ネタ
- body の 8px マージンで位置が全部8pxズレる。既存プレビューの作法どおり
<body style="margin:0;background:#fff">を付ける(<style>にbody{}を書くとスコープ漏れgrepに落ちる)。これで@x,yが元サイトと完全一致した - destyle 系の
h1..h6{font-size:inherit;font-weight:inherit}を入れ忘れると見出しだけ計算値がズレる。スコープ版リセットに必ず入れる - CSSに残っている「死んだルール」を拾わない。今回
.p-top-news h3{font-size:15px}があったが、実DOMは<p class="__title">で当たっていなかった。これを.news__titleに付け替えたせいでカード高が 507 vs 535 とズレた。セレクタは実DOMに当たっているかを確認してから移植する - flex の
align-items:stretchで li の高さは最も高いカードに揃う。1枚だけ文字量が違ってもliの実測値は一致するので、寸法照合では器(wrapper/list/li)の一致を優先して見る
2026-08-21 データ・アプリケーション採用サイト(dal.co.jp/recruit)5件
動画2本ぶんを作り忘れ、スクショ3枚ぶんだけ作って報告した(本コマンドFB③の再発)
何が起きたか: 動画2本+スクショ3枚を受領。動画のフレーム抽出に失敗したまま、添付表示された静止画3枚(FV/バナー/フッター)だけを見て「3件できました」と報告。大串の「動画の二つは?」で発覚。実際の動画は Future(横スクロール)と Project(ホバー背景切替) で、作った3件と1件も重なっていなかった。
真因: 「動画が開けない」を作業続行の判断材料にせず放置したこと。素材の棚卸し表(受領N件⇔id対応)を作っていれば、動画2件が未着手のまま報告に進めなかった。
→ 動画が開けない時点で止めて開ける手段を確保する。棚卸し表と件数照合は「報告直前」でなく着手前に作る。
HEVC変換なしで動画フレームを撮る(既存手順より1工程短い)
既存手順(avconvert で H.264 化)は不要だった。playwright を channel:'chrome'(実Google Chrome)で起動すれば HEVC/H.264 とも再生できる。
const b = await chromium.launch({channel:'chrome'}); // ← ここだけ。Chromium同梱版だと読めない
// 動画は http 配信(file:// は不可)。seek は効かないことがあるので play() して間隔撮り
await p.evaluate(()=>document.getElementById('v').play());
for (let i=0;i<7;i++){ await p.waitForTimeout(650); await p.locator('#v').screenshot({path:`f_${i}.jpg`}); }
currentTimeの seek は無音で失敗する(全フレーム同一になる)。同一サイズのファイルが並んだら seek 失敗を疑う。再生しながら撮れば確実- swiftc / osascript(JXA) + AVFoundation はどちらも失敗した(
swiftcは module 再定義エラー、JXA は AVURLAsset がブリッジされない)。この2つは試すだけ時間の無駄
崩れ原因(トップ10に追加すべき2件)
| 症状 | 真因 | 対処 |
|---|---|---|
| 背景写真・背景レイヤーが丸ごと出ない | 元サイトが z-index:-1 の背景レイヤー(.project_bg/.project::before)で背景を敷いている。そこへスコープ根に background:#fff を塗ると、負のz-indexは根の背景より下に描かれるので完全に隠れる。共通リセットが .sec-<id>{background:#fff} を持っていると気付かず毎回踏む |
そのセクションだけ .sec-<id>{background:transparent} にし、白は body 側(<body style="background:#fff">)で出す。元サイトも body で白を出している |
| サムネがホバー状態+変な余白色で撮れる | shoot_section.mjs の reveal 強制が is-active 等を全要素に付けるため、ホバーで背景が変わる系は「ホバー中」で撮影される。余白色も背景写真から拾って茶色などになる |
node tools/shoot_section.mjs <id> --no-reveal で素の状態を撮る。ホバー/クリックで見た目が変わるセクションは既定で --no-reveal を疑う |
rem基準が幅で3段階に変わるサイトの完全再現
dal は html{ font-size } が 100vw/37.5(〜767)→ 100vw/100(768〜1000)→ 62.5%=10px(1001〜)。rem は root 参照なので sec-<id>{font-size} に寄せても効かない。
→ 全 rem を calc(N * var(--u)) に機械変換し、--u をブレークポイントごとに定義すれば完全にスコープ内で再現できる(負値 -1.5rem も拾うよう正規表現に -? を入れる/remove が誤ヒットするので残存チェックは目視確認する)。
.sec-x{ --u: 2.6666667vw; font-size: var(--u); } /* font-size も合わせると em 継承まで一致する */
@media (min-width:768px){ .sec-x{ --u: 1vw; } }
@media (min-width:1001px){ .sec-x{ --u: 10px; } }
- box-sizing を勝手に border-box にしない。dal は全域 content-box だった。元サイトを grep してからスコープ版リセットを書く(
grep -n box-sizingが0件なら content-box を明示する)
検品・CIの誤判定
check_responsive.mjsがnetworkidleでタイムアウトすることがある。参考元の重い画像を大量に読むセクション(背景写真3枚など)で発生。レイアウト不良ではないので再実行すると ✅ になる。落ちたら1回は再実行して切り分けるgh run watchが X(失敗)に見えて実はcancelledのことがある。他セッションのpushに追い越されただけ。gh run listで conclusion を見て、git merge-base --is-ancestor <自分のsha> origin/mainで自分の変更が載っているか確認してから、最新のrunを待つ
2026-08-21 UVO(uv0.jp)5件で判明した落とし穴(全部が実害を出した・毎回チェック)
1. CSSの mask:url() はクロスオリジンだと効かない(アイコンが消える/四角い塊になる)
- 症状: 矢印「>」や外部リンク「□」がまったく描画されない(mask失敗で要素ごと透明)、または白い四角のベタになる(mask読込失敗で非マスク描画)。
- 真因: 元サイトは
background-color:#fff+mask:url(../images/c_icon_arrow.svg)でアイコンを塗る作り。URLを絶対化しただけだと別ドメインからのmask取得になり効かない。 - 対処: SVGをdata URI化して同梱する(500〜600バイト程度なので軽い)。
- data URI 化のときは
()を必ず %28 %29 にエンコードする。SVGのtransform="translate(-0.284 -0.428)"に括弧が入るため、素のurl(data:...)は最初の)でCSSのurl()トークンが切れて全部壊れる。
Pythonならurllib.parse.quote(svg, safe="/:=;,")(()'空白を安全リストに入れない)。
検証は拡大スクショで。15px程度のアイコンは等倍スクショでは潰れて見えないので「無い」と誤判定する。
2. destyle系の img{display:block;max-width:100%} を拾い忘れると画像が原寸で出る
- 症状: 斜めカット3カラムの高さが 675px→800px(
max-height:50remに張り付く)。 - 真因: 元サイトのリセットに
img{display:block;max-width:100%}があり、画像(1064x1500)が親幅に収まっていた。抽出時にリセットのimg指定だけ落とすと原寸で描画される。 - 対処: スコープ版リセットに
sec-<id> img{ display:block; max-width:100%; }を入れる。数値照合をやれば必ず捕まる(目視では「なんか大きい」で流しがち)。
3. <body style="margin:0"> がLabの既存慣例(body へのCSSルールは書かない)
- 素のままだと body の 8px マージンで全幅セクションが 1440→1424 になり、左右に白フチが出る。
body{margin:0}をCSSに書くとスコープ漏れ検証に引っかかる → HTMLの属性で当てる(既存25件が同じやり方)。
4. position:fixed の全画面ナビは、ヘッダーの中に置いたままだとヘッダーの箱に閉じ込められる
- 元サイトの
.l-header-navは header の子だがposition:fixedでビューポート全面を覆う。単独プレビューでabsoluteにすると親(header)の 1320x167 の箱にクリップされ、帯状にしか出ない。 - 対処: ヘッダーの top/left を負値で打ち消す(
top:-3.4375rem; left:-3.75rem; width:100vw; height:100vh)。
ナビを header の外に出す解決はやらない:header(z-index:20) の中に nav(z-index:10) がある構造が、
「ロゴだけ幕の上に残り、MENU/SHOPは幕の下に沈む」という元の見え方そのものを作っているため。
5. 確認用ローカルサーバーのポートが他セッションと衝突する(違うHPを見る新しい経路)
- 実害:
python3 -m http.server 8099が落ちた後、別セッションのサーバーが同じ8099を占有していて、
こちらは気づかず 404ページをスクショして「できた」と判断しかけた。 - 対処: セッション固有のポートを使う(例 8177)。撮影前に必ず
curl -s http://localhost:<port>/ | grep <自分のid>で中身を確認してから撮る。
→ コマンド本体の「違うHPのプレビューを混ぜない」にポート衝突という経路を足すこと。
6. 白地に文字だけのセクションはサムネ採用ゲートに落ちる(flatRatio>=0.92 && edgeRatio<0.03)
- NEWS一覧が「中身が写らない(色9種/輪郭2.9%)」で不合格。画面の95%が白だったため。
- 真犯人は前セクションとの間隔(
padding-top:13.75rem=220px)。これは"隣との余白"であってセクションの中身ではないので詰めてよい。
4.375rem に詰めたら一発で ✅(中身605px)。--forceを使う前に、まず余白を疑う。
7. モリサワ(TypeSquare)のフォントは外部から読めない=再現の上限
morisawafonts.net/c/<key>/mf.cssはドメイン限定配信。A1ゴシック Mは当たらないのでHiragino Sansにフォールバック。- 影響は文字幅で最大4px(数値照合で確認済み)。英字は元サイトと同じ Google Fonts の Roboto が使えるので差は出ない。
- 報告時に「差は◯px・原因はフォント」と数字で言えば、崩れとの区別がつく。
2026-08-21 non-no web(FV=Swiper cube/カバーフロー型カルーセル)— 抽出の型が1段階上がった回
「使用クラスで絞る」より 実DOMで matches 判定して抽出する方が精度が高い
クラス名の集合で base.css を絞ると、.inner .wrap .slide のような汎用名が他セクションのルールまで大量に拾う(実測 1491行)。
代わりに Playwright で元サイトを開き、対象セクション配下の全要素に対して el.matches(sel) が真になるルールだけを
selectorText + style.cssText で抜くと 93件 / 62件まで落ち、しかもカスケード順が保たれる(出力 504行 → 目安内)。
for (const sheet of document.styleSheets) for (const r of sheet.cssRules)
if (r.type===1) for (const one of r.selectorText.split(','))
if (els.some(e => e.matches(clean(one)))) keep.push({media, sel:one, decl:r.style.cssText});
clean()で:hover/:focus/:active/::before等を外してから判定する(付いたままだと hover ルールが全部落ちる)- 抽出前に対象セクションを
scrollIntoViewIfNeeded()して初期化させる。
non-no はIntersectionObserverで画面内に入ってから Swiper を起動する作りで、未初期化のまま抽出すると
.wrap-txt.active(表示中の説明文)のような状態クラスのルールが丸ごと落ちる=プレビューで本文が出ない。
「元サイトで見えているのに再現で出ない」ときは、まずこれを疑う。
今回踏んだ崩れ(全部 再発防止済み)
| 症状 | 真因 | 対処 |
|---|---|---|
| カウンターの花形バッジが完全に消える | mask-image がクロスオリジンだとChromeに拒否される(リクエストすら飛ばない)。マスクが全消しになり、中の数字ごと消える |
UIアイコンのSVGは data URI 化(写真は参考元URLのままでよい) |
| 矢印アイコンが出ない | CSS内の相対 url(../image/…) が404 |
CSSのURLを基準に urljoin で絶対化(トップ10の#4) |
| 矢印とカウンターが17pxずれる | 未使用CSSを削るフィルタが、クラスを持たない素タグのリセット(ul{margin:0})まで落とした |
フィルタは「クラスなしセレクタは残す」(実DOMマッチ済みなので安全) |
| PC/タブレットで8pxはみ出し(検品⛔) | <body> 既定の margin 8px |
<body style="margin:0"> とインライン属性で消す。CSSに body{} を書くとスコープ漏れ検査に引っかかる |
| タブレットで横スクロール | 元サイトの .wrap-content{width:1280px} 固定(元も768〜1280では横スクロールする設計) |
max-width:1280px; width:100% に。PC1440の見た目は不変 |
rem基準は sec-<id>{font-size:10px} では直らない
rem はhtml基準なので、html{font-size:62.5%} をスコープルートに移しても効かない(10px想定が16pxで描画され全部1.6倍)。
→ 抽出CSS内の Nrem を N*10px に一括変換するのが正解。事前に @media のクエリに rem が無いことだけ確認する。
ライブラリは同梱してよい(外部依存ゼロにできる)
Swiper 11.0.5(148KB)を <script> にインライン同梱 → 参考元ドメインへのJS依存が消え、バージョンも完全一致。
最終的な外部ドメインは fonts.googleapis.com / fonts.gstatic.com / 画像CDN だけになり、ドメイン照合が一目で通る。
JS駆動スライダー(cube等)はサムネ撮影が落ちる
shoot_section.mjs は「中身が写らない(色17種/輪郭0.2%)」で不採用になる=回転の途中で撮れている。
freezeAnim はWeb Animations APIしか止めないので、Swiperの3D変換は止まらない。
→ Swiper APIで正面に固定してから自前で撮る(autoplay.stop() → setTranslate(snapGrid[activeIndex]*-1) → updateProgress())。
撮ったら <id>.png / <id>__detail.png / <id>__sp.png の3枚を置いて normalize_thumbs.py を通す(1枚だけ差し替えると詳細ページが古いまま=v2.6の指摘)。
この環境(さきPC)の道具
nodeはPATHに無い →export PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH"。
playwright は~/dev/terasu-labの node_modules にしかないので、スクリプトはリポ直下に置いて実行する(/tmp から実行するとERR_MODULE_NOT_FOUND)- ffmpeg が無い。
swiftcはSDK不整合で使えない → ObjC + AVFoundation をclangでビルドして動画からフレーム抽出できる
(AVAssetImageGenerator/-framework Foundation -framework AVFoundation -framework CoreMedia -framework AppKit) - リポ直下の作業用スクリプトを
rm -f _tmp_*.mjsで消さない。他セッションがコミット済みの_tmp_*.mjsが17個あり、巻き込んで削除した(git checkout --で復元)。自分が作ったファイル名だけを消す - push が
cannot pull with rebase: You have unstaged changesで止まるときはgit -c rebase.autoStash=true pull --rebase(他セッションの未コミット変更を退避→自動復元)
2026-08-21 リビタ(www.rebita.co.jp)6件 — 「矢印/中身が消える」の新パターン=ルート状態依存CSSの誤適用
html:not(.wf-active, .wf-timeout) *{overflow:hidden}(Webフォント読込中に中身を隠すFOUT対策)を全要素に当ててしまう事故。
抽出時に先頭のhtmlを剥がすと:not(.wf-active…) *が残り、sec-<id> *に化けて全要素が overflow:hiddenになる。
症状は「カルーセルの矢印が消える」「セクション内の余白が数px〜15pxずれる」。要素は存在しgetComputedStyleも正常なので気づきにくい
(elementsFromPointにその要素が出てこないのが決定的な手がかり=祖先にクリップされている)。
→ 対策=normalize でhtml/body/:rootを剥がした残りが:や[で始まるルールは捨てる(単独プレビューではルートの状態が再現できないため)。:not(a, b)をカンマで split するとセレクタが壊れる。セレクタリストの分割は括弧の外のカンマだけで行う。- rem基準が vw のサイト(
html{font-size:.6944vw}=1440で10px / SPは2.667vw)は、rem をスコープに寄せても効かない(remは:root基準)。
→sec-<id>{--rb: .6944vw}を定義し、CSS中のN remをcalc(var(--rb) * N)に機械置換すれば、vw連動もブレークポイントもそのまま再現できる。 - 状態クラスが付いたルールは matches 判定で落ちる(
.appearなど)。判定時だけ状態クラス群を剥がして拾い、
HTML側に最終状態(appear)を焼き込む。ScrollSmoother/lenis のサイトはwindow.scrollToが効かないのでmouse.wheelで降ろす。 - サムネ検品は「薄いグレー地+細字」で誤検知する(メガメニューが
色54種/輪郭2.7%で不合格)。
画像を自分で目視してから--forceで採用してよい。真っ白との区別は必ず目で行う。 - ローカルの
python3 -m http.serverは同じポートを別プロセスが掴んでいると古い内容を配信し続ける。
「直したのに変わらない」時はfile://で直接開いて検証するのが確実(ポートの取り違えを構造的に排除できる)。 - 作業用
/tmpはセッションをまたぐと消える。抽出スクリプトと中間JSONは~/section_work/<site>/のような永続パスに置く。 - 数値照合はクラス名を比較キーに入れない(オリジナル化でリネームすると全件差分になる)。寸法・font-size・color だけを突き合わせる。
- Typekit(
use.typekit.net)は参照ドメイン制限で 412 → Lab からは読めない。
neue-haas-grotesk-text→"Helvetica Neue"/source-han-sans-japanese→"Noto Sans JP"(Google Fonts) が実測で最も近い。文字幅が数px変わるのは許容。
2026-08-23 GBSメディカル(Next.js + Tailwind v4)4件 — 「CSSOM抽出 + :where() スコープ + --sec-rem」で一発格納できた回
受領=スクショ1+動画3の計4件(FV/3つの価値/改善ストーリー/メガメニュー)。4件とも数値照合ほぼ0差で通った。
汎用ツールを22_AI運用ルール/コマンド集/section/tools/(extract.mjsbuild.pycmp.mjs)に置いた。Tailwind製サイトは次回これを使う。
使い方(3コマンド)
WORK=/tmp/section_<site>; mkdir -p $WORK # サイトごとに分ける(混線防止)
node extract.mjs "<URL>" "<rootSelector>" "<id>" $WORK/x.json [openmenu] ["後から付くクラス名 空白区切り"]
PREVIEW_BG="#fff" BODY_PAD="70vh" SKIP_FONTFACE="zen old mincho|noto sans jp" \
python3 build.py $WORK/x.json <id> "<URL>" "<title>" out.html "$(cat head.html)" [extra.css] [extra.js]
node cmp.mjs "<URL>" "<rootSel>" "http://127.0.0.1:PORT/out.html" ".sec-<id> > <rootTag>" none 40
extract.mjs= CSSOMで「実際にマッチするルールだけ」抜く(Tailwind全量インラインは重すぎ・grep抽出は漏れる)。ルート複数指定は"a||b||c"。build.py= スコープ化+URL絶対化+リンク無効化+自己完結HTML化。cmp.mjs= 元サイトと同幅でgetBoundingClientRect()突き合わせ(登録前ゲート)。
実害が出た10点(全部この回で踏んで直した)
| # | 症状 | 真因 | 対処 |
|---|---|---|---|
| 1 | メガメニューの×が出ない・ハンバーガーのまま | スコープ前置 .sec-id .foo で詳細度が上がり、Tailwindの「同詳細度=後勝ち」が壊れて .scale-0 が in-[html.is-menu-opened]:scale-100 に勝った |
前置は :where(.sec-id) で包む(詳細度0)。html→.sec-id 置換も同じ |
| 2 | タブレットで横スクロール・全部デカい | 元サイトが html{font-size:calc(100/var(--dynamic-layout-base)*16vw)} でrem自体をvwスケール。font-size を .sec-id に移しても rem は html 基準のままなので効かない |
.sec-id{--sec-rem:<元の式>} を定義し、CSS中の Nrem を calc(N*var(--sec-rem)) に機械置換(htmlを触らずに再現=貼っても漏れない) |
| 3 | タブレットで余白がPC値のまま | --spacing-container-x-gap 等がメディアクエリで変わる変数なのに1440pxの実測値を焼き込んだ |
390/768/1024/1440 で実測し、差分だけ @media (min-width:Npx) で段組み |
| 4 | in-[html.is-menu-opened]:scale-0 が丸ごと死ぬ |
html置換の正規表現がエスケープされたクラス名の中の html まで壊した |
クラストークン \.(?:[\w-]|\\.)+ を退避してから置換 |
| 5 | 非アクティブ楕円の高さが合わない | h-0 のように状態切替で後から付くクラスは初期DOMに無く抽出漏れ |
抽出時にプローブ要素(そのクラスを付けたdiv)を挿して拾う |
| 6 | 見出しの寸法が測るたび変わる | Typekit/Google Fonts の読み込み前後で文字寸法が変わる | 照合前に await document.fonts.ready+0.8s |
| 7 | 元サイト側が「アニメ前」の値で測れる | 対象が画面外だと登場アニメ未発火 | 照合前に元サイトも scrollIntoView してから測る |
| 8 | FV背景が scale(1.058) opacity .7 で固定 |
オープニングアニメ途中の inline style をHTMLごと拾った | WAIT_MS=16000 まで待って抽出(data-opening 付き要素があるサイトは要注意) |
| 9 | :where(...) 始まりのセレクタが壊れる |
^::?[a-zA-Z-]+ で疑似要素と誤判定して .sec-id:where(...) にしていた |
疑似要素は :: 始まりだけ。:is()/:where() は html置換に回す |
| 10 | 色/サイズが全部おかしい | --gfont-* 等 next/font の変数は :root に無い |
ルート要素の computed から全カスタムプロパティを実測して焼く |
動きは「実測 → 数式化 → 再現」で合う(推測で書かない)
- 改善ストーリー(重なるカード): スクロール量ごとに scale を測ると
1 → 0.84 / 0.92 / 1.0でクランプ。後続カードが「画面下端→sticky位置」に来る間に 0.08×残枚数ぶん縮むと判明 → GSAP ScrollTrigger(cdnjs 3.13.0)でstart:'top bottom+=28' / end:'top <stickyTop>px' / scrubにしたら全スクロール位置で一致(最大0.6%差)。 - 3つの価値(楕円の巡回): JSはクラス付替でなくReactの再レンダリング。アクティブ/非アクティブのクラス文字列を両状態で採取し、位置は
(j-active) mod 3で上中央(5rem,-4.5rem)/左下(-6.25rem,6.25rem)/右下(6.25rem,6.25rem)を巡回。閾値は li の上端が520pxを下回った最大index、移動 0.4s(実測)。 - メガメニュー:
htmlにis-menu-openedを付け、dialogの--clip-bottomを 100%→0%、オーバーレイ opacity 0→1(0.6s)。Labでは開いた状態を初期表示にして×とオーバーレイで閉じられるようにする。
付随Tips
- Next.js/Vercel と Typekit の資産は外部から使える(woff2・画像・kit CSS とも
access-control-allow-origin: *)。<link href="https://use.typekit.net/xxx.css">でTypekitフォントもそのまま出る。 - @font-face が244個(next/font のunicode-range分割)ある場合は Google Fonts の同一フォントに置換(252KB→44KBに縮む・視覚同一)。
SKIP_FONTFACEで外す。 - ffmpeg が無いMacでの動画フレーム抽出:
swiftはSDK不整合で不可、PyObjCも無い。playwright(channel:'chrome')でローカルHTTP配信した .mov を<video>に読ませ、play()しながらrequestAnimationFrameで canvas に描くのが唯一通った(currentTimeのseekだけだと全部同じ先頭フレームになる)。file://は setContent 経由では読めないのでHTTPで配信する。 - サムネは
shoot_section.mjs一発でOK(v2.6で normalize 自動)。メガメニューは初期表示が開いた状態なら真っ白にならない。
2026-08-24 STUDIO(studio.design)製サイトの抽出 — shibuyasl-event-list(イベント一覧カード)
対象: https://shibuyastreetlive.jp/event の「イベント情報一覧」(サムネ+ステータスバッジ+日付+タイトル+黒丸矢印の横長カード/スクロールで右から64pxスライドイン)。
結果: 数値照合10項目すべて一致・レスポンシブ✅・動きも再現して格納(shibuyasl-event-list-saki-0824)。
STUDIO製の見分け方(これが出たら以下の手順に切り替える)
.StudioCanvas / .render-canvas / class="sd" / [data-s-<uuid>] の属性セレクタ / /_nuxt/entry.<hash>.css。
デザイン指定は要素ごとの data-s-<uuid> 属性セレクタで、基幹レイアウトだけ entry.css に入っている(この分離が事故の元)。
実害が出た6点(全部この回で踏んで直した)
| # | 症状 | 真因 | 対処 |
|---|---|---|---|
| 1 | カードが全部縦積み・画像0×0 | entry.css の <link> に crossorigin が付いていて cssRules がCORSでブロックされ、.sd{display:flex;...} の基幹ルールを1本も収集できていなかった(ブラウザ内マッチだけだと気づけない) |
先に curl で落とし、page.addStyleTag({content: css}) で注入してから収集する。注入分は出力の先頭に置く(元の cascade 順) |
| 2 | 同上(注入しても直らない) | 基幹ルールが :where(.render-canvas) .sd{...} と祖先前提で書かれており、^(html\|body\|:root...) の除去正規表現に掛からず捨てられた |
先頭の :where(<ページ級祖先>) も剥がしてからスコープ化する(トップ10 #5の派生形。:where() 包みは要注意) |
| 3 | サムネ画像が全部404 | uuid を一括リネームしたら、STUDIOの画像URLに含まれる uuid まで書き換えて壊れた | 置換は文脈限定(data-s-<uuid> と --<var>-<uuid> だけ)。画像URLは触らない=原URL維持のルールとも整合 |
| 4 | 動きが再現できない(静止画になる) | STUDIOは再生後に appear クラスを外すので、ライブDOMからの通常マッチでは .sd[data-s-X].appear{opacity:0;translate:64px 0} を1本も拾えない |
セレクタから .appear/.appear-active を外した形でマッチ判定して収集 → HTML側にも appear を戻す → IOで appear→appear-active に付け替え(stagger は CSS の transition-delay が担当するので数値は触らない) |
| 5 | タブレットで横スクロール(786>768) | 装飾シェイプ(position:absolute の巨大な丸)は元サイトでは親の overflow:clip で切られていたが、その親はセクション外なので落ちる |
スコープルートに overflow-x: clip を持たせる(元のPC表示は不変) |
| 6 | サムネだけ大見出しが出ない(__detail.png も) |
① shoot_section.mjs は 1.2秒で凍結するのに、見出しは delay 300–950ms + duration 500–1400ms でまだ動作中/② そもそもoverflow:hidden の内側に102pxずらして隠れている子は IntersectionObserver が永久に発火しない(見えないから交差しない=鶏卵) |
②を直す=親が出るとき、overflow で切られている子孫も一緒に出す(表示は変わらない)。①は撮影のときだけ transition-duration:0s !important を差し込んだ一時版で撮り、公開ファイルはアニメ入りに戻す(3枚とも直る) |
手順(STUDIO製はこの順で通る)
curlで/_nuxt/entry.<hash>.cssを保存 → Playwright で本番を開きaddStyleTagで注入- セクションルートは余白を持つ親まで広げる(今回は
1440×3019の器。内側だけ取ると 64px の左右余白が落ちる) - マッチしたルールを
entry→inlineの順に出力、:where(祖先)とhtml/body/:rootは剥がして.sec-<id>に寄せる :rootの--s-color-* / --s-font-*は必ず先頭に(--f-3d90083b: 'Noto Sans JP'等。落とすと色もフォントも全滅)- リネーム:
data-s-<uuid>→data-el-eN/--s-color-→--c-/--s-font-→--f-/.sd→.el(CSS側の.sdを忘れない)/CMS条件クラス(_Z4zCxeYb等)はCSSが1本も無いので削除
caption の18字ゲート
add_section.py は「分類|会社名」が18字を超えると停止する。英字社名が長い時はサイト自身が使っている日本語表記にする
(ブログ一覧|SHIBUYA STREET LIVE=25字で停止 → ブログ一覧|渋谷ストリートライブ=16字でOK)。業種で代用はしない。
動画フレーム抽出(ffmpeg無しMac・今回通った手順)
swift は CommandLineTools の modulemap 衝突(redefinition of module 'SwiftBridging')でコンパイル不可、PyObjCも無し。
Playwright + channel:'chrome'(H.264/HEVCが再生できる実Chrome)で、
動画と同じディレクトリに置いた HTML から <video src="rec.mov"> を読ませ、currentTime をセット→onseeked を待って screenshot で1枚ずつ取れた。
- file://….mov に直接 goto するとダウンロード扱いで page.goto: Download is starting になる
- setContent + file:// の src は about:blank 由来で読めない → 兄弟ファイルとして置くのが要点
- ポート8137は ~/local-previews の launchd 常駐サーバーが使用中。空きポートを確認してからローカル配信する
2026-08-24 TERASU公式HP FV(terasu-fv-saki-0824)— 自社サイトを抜く時の落とし穴4つ
① <head> のインライン <style> を拾わないと寸法が合わない(今回の最大の罠)
外部CSS(style.css)だけ抽出して満足するとposition もサイズも別物になる。TERASU公式HPは index.html の
<head> にインライン <style> が16ブロックあり、そのうちFVに実際に効く上書きが10件あった。
- .p-top__mvBgImg{position:absolute!important}(外部CSSは fixed)
- .p-top__mvBgImg{width:130vw!important;height:130vh!important;margin-left:-65vw!important;margin-top:-15vh!important}
- .p-top__mvBgBody{background-color:#252525} / .js-bgslider img{object-position:50% 0} ほか
→ これを落とすと 1967x1329 が 1649x1034 になる(数値照合で即バレる/目視では気づけない)。
手順に追加: 外部CSS抽出のあと、必ず <style> ブロック全部をパースして
「セレクタのクラス集合 ⊆ 使っているクラス集合」のものを全部拾う(!important 付きが多いので上書き順は最後に置く)。
data-shape= data-edge= のようなURLパラメータ前提の実験CSSは除外する(live DOM に属性が無いことを確認して落とす)。
② インライン <script> にも本番挙動がある
.p-top__mv.is-fvclip-unlocked を付ける ver12 のインラインJS(円の描画半径が画面四隅を超えたら clip-path:none にする合成レイヤ対策)が
index.html 側にあった。これが無いとスクロール後半で clip-path が巨大化したままになり、元サイトと挙動が変わる。
→ <script>(src無し)も必ず全部 grep する(circle-scale|mvcircle|slider|mvtxt 等の当該クラス名で)。
③ JSの分岐条件を「引数名から推測」してはいけない
pageonload(e) の e を isPC だと決め打ちして if(isPC) でスクロール連動を囲んだが、
元サイトを390px/768pxで実測したら円は普通に動いていた(--circle-scale 50→351/クローンも5枚生成済み)。
e は isPC ではない。分岐は推測せず、元サイトを各幅で開いて計算値を読む(getComputedStyle(el).getPropertyValue('--x'))。
見抜き方=let o=5; matchMedia('(max-width:1024px)') → o=20 のようにモバイル用の値が用意されているなら、
そのブロックがモバイルで死ぬ実装はありえない。
④ スクロール駆動セクションは「寸法」だけでなく「アニメの計算値」を突き合わせる
getBoundingClientRect() の一致だけでは、スクロール連動の傾き(interpolate の範囲・trigger/end)が合っているか分からない。
同じ scrollY に両方を送って --circle-scale / overlay opacity / blur / 付与クラスを読むと小数まで検証できる。
今回 y=450 / 900 / 1533 の3点で circle-scale=86.991369 / 123.982737 / 176.017263、bgOpacity=0.586701、blur=29.3478px、
is-fvclip-unlocked の切替タイミングまで元サイトと完全一致を数値で確認できた。
%で比較してはダメ(元サイトは後続セクションがあるのでページ全長が違う=同じ%は別の場所)。絶対 scrollY を揃える。
自社サイトは正本リポから抜く(DOMより確実)
TERASU公式HPの正本は ~/dev/terasu-hp/(index.html + _assets/theme/style.css + assets/js/app.js)。
FVコピー「出会いを生む/ホームページを。」はSVGパスなのでDOM検索で本文がヒットしない(grep 出会いを生む → 0件)。
リポから抜けばインライン <style>/<script> の上書きも同時に見つかる(①②はDOMだけ見ていたら気づきにくい)。
単独プレビューの body 既定マージン8pxで全要素が8pxずれる
.sec-<id> *{margin:0} はbody自身には効かないので、単独プレビューだと全部が x8 y8 になる。
素の body{margin:0} を書くとスコープ規約違反(貼ると崩れる)→ <body class="tfv-standalone"> + .tfv-standalone{margin:0;padding:0}
にすれば、スコープ漏れ grep も通り、ホストHPに貼っても無害(ホストのbodyにそのクラスは無い)。
動画フレーム抽出のもう1つの手(clang + AVFoundation)
swift は modulemap 衝突で使えないが、Objective-C を clang でコンパイルすれば AVAssetImageGenerator が使える(依存ゼロ・数秒)。
clang -fobjc-arc -framework AVFoundation -framework AppKit -framework CoreMedia -o grab grab.m
./grab "<動画>" <出力dir> 12 # 等間隔12枚
sips -Z 1400 <出力dir>/*.png # 2000px以下に必須リサイズ
ffprobe 不在でも mdls -name kMDItemDurationSeconds -name kMDItemPixelWidth <動画> で尺と解像度は取れる。