static-portfolio-build — 静的マルチページポートフォリオ
技術的制約:HTML5 + CSS3 + ネイティブ JS のみ。Bootstrap/Tailwind などの UI フレームワーク、外部 CDN フォントは禁止。 メインカラー ≤3 色、フォント ≤2 種類、画像には必ず
alt属性を付与(SVG はrole="img"+aria-label)。 ファーストビューの 3G 想定読み込み時間 ≤3 秒(1.6Mbps / RTT 400ms で試算)。
一、アーキテクチャ:データ駆動、一元管理
各 HTML に作品内容を手書きしない。 JSON データとレンダリング層に分離:
works-data.json データソース(スクリプト自動生成、手動編集禁止)
works-data.js 同データのインライン版(window.__WORKS_DATA__)—— file:// ダブルクリック時の CORS 回避用
works.js レンダリング層:ROLES / cardHTML() / tileHTML() / bindFilter() / bindExpand()
extract-works.mjs 旧単一ページから作品データを抽出
patch-works.mjs 番号再割り当て/モジュール補完/モチーフ割り当て(べき等必須!)
なぜ JS インラインデータが必須か:fetch('./works-data.json') は file:// プロトコルで CORS にブロックされる。ユーザーがダブルクリックで開くと真っ白に。
べき等パッチスクリプト:patch-works.mjs は何度でも安全に実行できるようにする。EXTRA_TITLES を使って既に補完済みのエントリをフィルタリングしないと、2 回目の実行で「マッピングテーブルにありません」エラーが発生。
モジュール番号と実ディレクトリの一致:作品番号はカテゴリ順ではなく、ローカルディレクトリ/GitHub リポジトリの実際の配置に合わせる。「ディレクトリがあるものはディレクトリ番号に合わせ、ないものは空き番を埋める」方式で連続性を確保。番号変更時は全ページの参照(トップカード、職種ページバナー、アバウトページのカテゴリ)を同期させる——トップカードはデータから自動生成させるのがベスト、手書きによる不整合を防ぐ:
document.querySelectorAll('[data-owns]').forEach(function (el) {
var r = WORKS.ROLES[el.getAttribute('data-owns')];
if (r) el.textContent = '担当 ' + r.owns.length + ' モジュール(' + r.owns.join(' / ') + ')';
});
二、スクリーンショットがない場合:インライン SVG 模式図
作品にスクリーンショットがないケースが多い。カテゴリごとに幾何学モチーフを割り当て、インライン SVG を生成(外部画像ゼロ、ファーストビューでの画像リクエストゼロ):
| カテゴリ | モチーフ | 要素 |
|---|---|---|
| 企画 | roadmap | ノード + 破線接続 + マイルストーン |
| ワークフロー | pipeline | 入力→処理→審査→出力 + 矢印マーカー |
| エージェント | neural | 階層ノード + 全結合エッジ |
| データ | metrics | 棒グラフ + 折れ線トレンド |
| オープンソース | terminal | ダークウィンドウ + カラーコード行 + カーソル |
| 業界 | grid | ブループリントグリッド |
すべての SVG に role="img" + 意味のある aria-label を必須。
ハマりどころ:
- SVG 内の
<text>がカードのグラデーションに隠れる → テキストは SVG 外に出し、HTML オーバーレイ.w-cover-tagで対応(position:absolute) preserveAspectRatioはmeetを使用、sliceは不可(切り抜き防止)- 配列インデックスのずれ:モチーフデータを
[x, y, テキスト, 色]で定義したが、レンダリング側が[x, y, width, color]としてl[2]を参照 → 矩形がすべて無効になりカーソルだけ表示。モチーフ定義時はインデックスの意味を事前確認。
三、3D と動き(見た目は良いがカクつかないこと)
背景:SpaceX 風透視グリッド
.bg-grid-floor {
position: absolute; left: -30%; right: -30%; top: 16%; height: 90%;
background-image:
repeating-linear-gradient(90deg, rgba(10,10,10,.16) 0 1px, transparent 1px 74px),
repeating-linear-gradient(180deg, rgba(10,10,10,.15) 0 1px, transparent 1px 40px);
transform: perspective(520px) rotateX(72deg) translateZ(0);
transform-origin: 50% 0%;
mask-image: linear-gradient(to bottom, transparent 0%, #000 11%, #000 52%, transparent 90%);
animation: floorDrift 30s linear infinite; /* background-position を変更、transform は使わない */
}
さらに地平線グロウ線 + 3 つの浮遊ライトスポット(各 24/29/34 秒のブレス)。
パフォーマンスの鉄則(カクつきの分岐点)
filter: blur() と backdrop-filter は絶対に使わない。 これらは毎フレーム背後コンテンツを再ラスタライズ/再サンプリングし、スクロール時のカクつき最大原因:
- ソフトなライトスポットには
radial-gradient(circle, rgba(...).10, transparent 68%)—— グラデーション自体がソフトなので blur 不要 - すりガラス風ナビゲーションはほぼ不透明な背景
rgba(255,255,255,.96)で代用 - セクション背景は半透明の白ベタ、
backdrop-filterは使わない
アニメーションは transform / opacity のみ(コンポジターで処理、リフロー・再描画をトリガーしない)。スクロールパララックスは requestAnimationFrame でスロットリング + translate3d。
カードの 3D 傾き
pointermove で CSS 変数を書き込み、DOM には触れない:
.tilt { transform: perspective(1000px) rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg)); }
.tilt.is-tilting { transition: transform .06s linear; } /* 追従時は transition を短縮 */
.tilt .w-body { transform: translateZ(18px); } でパララックス浮遊効果;::before の放射状ハイライトが --mx/--my に追従。
ホバー移動とエントリーアニメーションが transform を奪い合う → ホバー時は box-shadow/border-color のみ変更、移動はすべて .tilt に任せる。
四、エントリーアニメーション:純 CSS の opacity:0 は絶対に避ける
これが最大のハマりどころ。 純 CSS で初期非表示+JS で .is-revealed 追加方式は、IntersectionObserver がコールバックしない場合(ヘッドレスブラウザ、JS エラー、異常パス)ページ全体が永久に非表示になる。
正しい方法:JS で必要な要素だけに非表示クラスを付与、未付与の要素はデフォルトで表示:
.w-card.reveal-armed { opacity: 0; transform: translateY(18px); }
.is-revealed { opacity: 1 !important; transform: none !important; }
list.forEach(function (el) {
var r = el.getBoundingClientRect();
var inView = r.top < innerHeight + 150 && r.bottom > -150;
if (!inView) el.classList.add('reveal-armed'); // ビューポート外のみ非表示化
io.observe(el);
});
// フォールバック:3 秒後も非表示の要素を強制表示
setTimeout(function () { /* force reveal */ }, 3000);
ビューポート内は即座に表示、ビューポート外のみアニメーション対象 — ファーストビューがアニメーションに邪魔されず、異常パスでも白画面にならない。
五、スクリプト注入:<script> の開始・終了タグペアを検証
複数ページに一括でコードを注入する際、<script> と </script> の数が一致していることを必ずチェックする。
実際の事故:注入時に <script> 開始タグが一つ多い → それ以降のインラインスクリプトブロックが JS パーサーに飲み込まれる → 4 つの職種ページレンダリングコードがすべて無効に。ページは一見正常(タイトル、ナビ、責務リストは表示)で、作品エリアだけ空白、目視ではほぼ発見不可。
正規表現注入時の注意:LastIndexOf('<script>') は <script src=...> にもマッチするため、定義がタグの外側に差し込まれ、表示テキストとして可視化される。明示的なアンカー文字列を使用する。
六、検証方法(再利用可能)
スクリーンショット検証(重要:同名ファイルはキャッシュされる)
vision_html_screenshot は同名ファイルに対してキャッシュを使用する。CSS 変更後は Copy-Item で一時的に .verify/ 内の別名ファイルにコピーしてからスクリーンショットを撮り、検証後サンドボックスを削除する。
同期プローブ(load + setTimeout は使わない)
ヘッドレス環境では window.load + setTimeout は信頼できない。プローブをレンダリングコールバック内部に直接記述(同期的に実行)、position:fixed;bottom:0 の <pre> に出力する:
WORKS.loadData(function (data) {
root.innerHTML = WORKS.listByRole(data.items, 'fde');
var d = document.createElement('pre');
d.style.cssText='position:fixed;bottom:0;left:0;right:0;z-index:99999;background:#000;color:#0f0;font:11px monospace;padding:4px;margin:0';
d.textContent = 'CARDS=' + root.querySelectorAll('.w-card').length;
document.body.appendChild(d);
});
⚠️ プローブの位置指定に top を使ってはならない — カードの 3D 傾き transform が fixed 要素を押しずらす。
一括検証(ページごとにファイルを読むより効率的)
- デッドリンク:正規表現で
./xx/yy.mdを抽出 →Test-Pathで個別検証 - 番号の連続性:
Compare-Objectで期待シーケンス vs 実際 - ディレクトリ対応:PowerShell は
'01'を int の 1 に変換するため、[string]で明示的にキャスト必須 - 高負荷機能の残存チェック:
[regex]::Matches($css,'filter:\s*blur|backdrop-filter')、コメント内の記述に注意
七、GitHub Pages へのデプロイ
gh repo create <user>/<repo> --public --description "..."
git remote add origin https://github.com/<user>/<repo>.git
git push -u origin main
デプロイ前の必須チェック:
git ls-filesで依存ディレクトリが実際に追跡されているか確認(ローカルにファイルがあっても git 追跡とは別)- リモートに既存コンテンツがある場合は先に
git ls-tree --name-only origin/mainで比較、上書きで失わないように - ローカルの相対パスで依存している兄弟プロジェクトディレクトリ:一緒にプッシュするか、GitHub リポジトリ URL に変更する
- GitHub Pages は Settings → Pages → Source でブランチを選択
- サブアプリ(例:
hitech-self-assessment/)がある場合、メインサイトの置き換えで上書きされないよう注意
パフォーマンス推定:総バイト数 / 8 / 1.6e6 + 4 * 0.4(秒)
八、納品前自己チェック(web-delivery-checklist 準拠)
- 全 4〜5 ページのスクリーンショット検証済み(トップページのみではない)
- デッドリンク 0、alt 漏れ 0
-
<script>開始・終了タグペア一致、CSS 中括弧のバランス -
filter: blur/backdrop-filterの実際の使用数 = 0 - 3G 推定読み込み時間 < 3 秒
- メインカラー ≤3、フォント ≤2、外部 CDN リソース 0
- モバイル(390px)でハンバーガーメニューの開閉が可能、
aria-expandedが同期 -
prefers-reduced-motion設定時は全アニメーションをオフ