メインコンテンツへ
Onbit

2026.05.04

サンプル HP/LP を 2 日で 3 → 30 件まで増やした話

技術 日記

はじめに

Onbit サイトの /samples/ には、お客さんに「だいたいこういうトーンの HP / LP が作れます」を見せるためのサンプル集を置いています。先週末まで 3 件しか並んでいなかったのですが、土日の 2 日間で 30 件まで一気に増やしました(LP 15 / HP 15)。

サンプルが足りないと、見積もりや初回ヒアリングの時に「具体的にどんなトーンで?」が口頭でしか伝えられず、結局相手の想像に任せることになりがちです。30 件あれば「この中だとどれが近いですか?」が成立する。最初の壁を一段下げるための作業でした。

ただ、30 件も作るとなるとデザインの設計だけでなく、画像をどう用意するか・モバイルでちゃんと見えるか・カードの一覧が長くなりすぎてレイアウトが崩れないか、と派生する問題が次々出てきます。この記事では、その 2 日間で順番にぶつかった問題と、どう片付けていったかを残しておきます。

基礎となる知識

  • HP / LP: HP は複数ページのコーポレートサイト型、LP は 1 ページ縦長の販促ページ型。Onbit ではこの 2 プランを商品の柱にしています
  • iframe: 別の HTML を 1 つの枠の中に埋め込んで表示する仕組み。サンプル詳細ページで「実物を埋め込みプレビュー」するのに使っています
  • CSP(Content Security Policy): ブラウザに「このサイトはこのドメインしか読み込まない」「iframe に入れていいのはこのオリジンだけ」を宣言するセキュリティ設定
  • Imagen / Pollinations AI: 画像生成 AI。Imagen は Google 系、Pollinations は無料で使える Flux 系のラッパー。今回は両方使い分けました
  • aspect-ratio: CSS の指定で「この箱は縦横 16:9 で表示する」と決めるプロパティ。中の画像との比が合っていないと object-fit: cover で大きく切れます
  • IntersectionObserver: 「要素が画面に入ったか」をブラウザに監視させる API。スクロール連動のフェードイン演出に使うのが定番

解説と使い方

どうして 30 件作ろうと思ったか

土曜の朝の時点で、サンプルは 3 件(cafe-classic / school-warm / salon-modern)しかありませんでした。しかも 3 つともパレットが Onbit 本体と似ていて、「サイトの見本というよりサイトの分身」になっていた。これではトーンの幅が伝わらないので、全部捨てて作り直しから始めました。

目標は、

  • LP プラン(1 ページ縦長 / 4〜6 週間 / ¥5,000/月運用)に整合する 1 ページ完結のもの
  • HP プラン(5〜7 ページ / 2〜3 ヶ月 / ¥7,000/月運用)に整合する複数セクション構成のもの

を、業種・配色・書体それぞれをバラけさせて並べる、ということ。最終的には次のような顔ぶれになりました。

LP 15 件(色とトーンを思いきり振った):

  • ダーク + ゴールド + 明朝(高単価コーチング)
  • 黒 × ライム + Bold sans(ジム)
  • シャンパンピンク + セリフ + script(ブライダル)
  • 木目 + 黒板 + 手書き調(クラフトビール)
  • 青グラデ + glassmorphism(アプリ DL)
  • 赤 × 黄 × 黒 のレトロポスター(イベント告知)
  • Y2K ポップ pink/purple/orange(オンラインサロン)
  • オフホワイト + 細明朝 + 余白多(ヨガ)
  • 白 × ネイビー × ベージュ(不動産)
  • 濃紺 × 山吹色 + 丸ゴ(個別指導塾)
  • BtoB SaaS 無料トライアル
  • 新刊書籍の発売 LP
  • 経営者向け会員制の dark
  • 写真スタジオ
  • フードトラック

HP 15 件:

  • 白 + ミントグリーン(歯科クリニック)
  • ネイビー + アイボリー + 金 + 明朝(士業フォーマル)
  • モノクロ + 大型写真(建築設計)
  • ダーク茶 + ボルドー + 金(レストラン)
  • 黒 + シアン + モノスペース(SaaS / IT)
  • 淡ピンク + クリーム(ペットサロン)
  • 木目 + ディープグリーン + 明朝(工務店)
  • グレージュ + 銅(美容室)
  • 深緑 + 朱色 + 明朝(旅館・和モダン)
  • ブルー + 草色 + 橙(NPO)
  • ナチュラル系のカフェ
  • 国産スキンケア(フェミニン)
  • 神社の公式 HP(伝統・赤×墨)
  • ポートレート写真家(モノクロ)
  • 街のベーカリー(テラコッタ)

「Onbit のサンプル一覧」と聞いて想像しやすい範囲は超えたかな、というラインを目指しました。

画像をどうやって 30 件分用意するか

30 件のサンプルそれぞれに、ヒーロー画像 / セクション画像 / 人物写真 / 一覧用サムネ、と何枚も画像が必要です。手で集めるのは現実的ではないので、AI 生成のパイプラインを 2 種類組みました。

1 つ目: スクリーンショット / 一覧サムネ用

scripts/generate-all-sample-images.mjs という Node スクリプトを書いて、

  1. apps/website/src/content/samples/*.md を全部スキャン
  2. screenshot 未設定のものに対して、tone / category / accentColor / pageType から プロンプトを自動生成
  3. Imagen を呼んで PNG を取得 → sharp(画像変換ライブラリ)があれば webp に
  4. public/samples-preview/<slug>/screenshot.webp に保存
  5. MD の frontmatter に screenshot: パスを自動 inject
  6. レート制限予防に 4 秒間隔

という流れにしました。--slug=lp-yoga-zen で 1 件ずつも、--force で全件再生成も可能です。

肖像権リスク回避で personGeneration: dont_allow を強制しています。

2 つ目: 中身の hero / portrait / 商品画像用

スクリーンショットだけだと、各サンプルの中身が CSS の灰色プレースホルダー(.ph クラス)のまま。これだと「実物っぽさ」が出ないので、Pollinations AI(Flux 系の無料 API)で 75 枚ほど追加生成して、各 index.html.ph<img> に置換していきました。

ここはサブエージェントに「この業種・このトーン・この画角」のプロンプトを生成させて、出てきた画像を 1 枚ずつ目視確認しながら採用 / 再生成、というやり方。完全自動化ではなく半自動の体感ですが、人物の顔崩れやテキスト混入が混ざるとサンプルとして致命的になるので、目視は外せません。

最後に「人物の顔」は全部見ました。歯科医、テック 5 名、サロン 4 名、イベント出演者 4 名、ベーカリー、カフェ、建築、弁護士、NPO、コーチ、トレーナー、子供撮影サンプル、など。崩れは無く、再生成はゼロ件でした。

1 個目の罠: モバイルで iframe プレビューが映らない

20 件まで仕上がったあたりで、サンプル詳細ページをスマホで開いたら 「接続が拒否されました」。iframe が真っ白でした。

調べると、CSP の仕様で 複数の CSP が設定されると、ブラウザは「最も厳しいもの」を採用する ことが原因でした。Onbit のサイトは:

  • 全体に frame-ancestors 'none'(どのサイトからも iframe で読まれない)
  • /samples-preview/* だけ override で frame-ancestors 'self'(同一オリジンの iframe では OK)

を設定していたのですが、ブラウザはこの 2 つの intersection を取るので、'none' が勝ってしまっていた。期待していた override が効いていなかったわけです。

直し方は 2 つあって、

  • 全体の CSP から frame-ancestors 'none' を撤去(X-Frame-Options: DENY で代替)
  • /samples-preview/* の override も frame-ancestors を消して X-Frame-Options: SAMEORIGIN のみに

CSP と X-Frame-Options は本来「どちらでも OK」の重複防御ですが、今回は 片方に絞らないと override が効かない。理屈の上では納得していなかったやり方ですが、実害を取って整理しました。

合わせて、モバイル(< md ブレイクポイント)では iframe を出さず「別タブで開く」リンクに切り替える対応も入れました。スマホで iframe を縮小表示しても、結局読めないので。

2 個目の罠: 画像のアスペクトと CSS のスロットが合わない

中身画像を入れ込んでいくと、随所で「画像が大きく切れている」現象が出てきました。CSS 側のスロットが aspect-ratio: 4/3 を指していて、画像本体は 1:1 で生成されている、とか。object-fit: cover で埋めているので、見た目には押し込まれて切れます。

一晩で 2 ラウンドこの問題を片付けました。

第 1 ラウンド(土曜夜) — 画像側を作り直す方針

  • 工務店 before/after 6 枚: 4:3 → 1:1
  • 旅館 facility 4 枚: 1:1 → 4:3
  • ブライダル ceremony 3 枚: 1:1 → 3:4 縦長
  • ジム before/after 2 枚: 1:1 → 4:5 縦長
  • など

第 2 ラウンド(日曜朝) — CSS 側を直す方針

画像を作り直すのはコストが高い(課金とレート制限)ので、こんどは CSS のほうを画像本来の比率に揃えました。

  • 工務店 / 旅館 / 不動産の hero: 16/8 → 16/9
  • ブライダル hero: 16/10 → 16/9
  • 建築の about 写真: 3/4 → 4/5
  • 美容室 stylist: 3/4 → 4/5
  • ジム trainer: 3/4 → 4/5
  • 旅館 cuisine: 4/3 → 10/7

そして再発を検知するために、scripts/check-image-aspect.py という検査スクリプトも書きました。各サンプルの index.html をパースして、<img> の実画像比と CSS の aspect-ratio 指定を突き合わせ、ズレを警告します。次に同じ罠を踏んだら早く気付ける、はず。

3 個目の罠: カードが多すぎて IntersectionObserver が発火しない

サンプルが 30 件並んだ /samples/ 一覧で、スクロールしてもカードが現れない 現象が出ました。

原因は IntersectionObserver の threshold: 0.12(要素の 12% が画面に入ったら発火)。サンプル 30 件で section が縦長になりすぎて、画面に section の 12% も入らない位置でずっと止まる、という状況になっていました。すると in-view クラスが永久に付かず、カードは opacity: 0 のまま。

一時しのぎとして、reveal-childtransition-delay を上限 360ms にする(でないと最終カードが 2.3 秒遅れて出てくる)対応を入れたのですが、根本原因はそこじゃない。最終的には 一覧では reveal アニメ自体を外して常時表示 にしました。30 件もあるとアニメは邪魔のほうが大きい、という判断です。

サムネを 16:9 に統一 + LINE 公式アカウントの導線

カード一覧は CSS で aspect-video(16:9)を指していたのに、サムネが 1:1 や 4:5 縦長になっている件が 4 件残っていました。

  • lp-coaching-premium: 既存の hero(16:9)に切り替え
  • hp-cosme-pink / lp-foodtruck-vibrant / lp-photo-studio: 16:9 専用 thumb.jpg を新規生成

ここが揃って、ようやく一覧の見た目が安定しました。

合わせて、お客さんに連絡する手段として LINE 公式アカウント の導線を仕込んでおきました。src/config/contact.tsMAIL_TO / LINE_URL / LINE_ID / hasLine を集約して、PUBLIC_LINE_URL 環境変数が設定されている時だけ Footer / FinalCTA / Contact ページに「LINE で送る」ボタンが自動表示される作りです。

LINE 公式アカウント自体はまだ運用方針を詰めていないので、URL を入れる / 入れないをスイッチで切り替えられる構造にしておきました。

翌朝のおまけ: 説明文を話し言葉に + ブログテンプレ整理

ここまでが土日の主作業です。月曜の朝、ついでにいくつか整理を入れました。

サンプル説明文の話し言葉化: 30 件の MD 内の説明文を「想定する向き先 → こんな方に向いています」のように、書き言葉の硬さを抜いて口語に寄せました。語尾も「です・ます」をベースに「人がしゃべっている」雰囲気へ。

ただ、勢いに任せて主要ページ(why-onbit / process-steps / services / Hero / AboutPreview / FinalCTA / about / contact)も話し言葉化したら、主要ページは硬めの方が信頼感が出る ことに気付いて revert。サンプル一覧だけ話し言葉、Onbit 本体ページは元のトーン、という線引きに落ち着きました。

ブログテンプレ整理: ここまで書いてきた既存ブログ 12 本を、技術ブログ用の 6 セクション構造(はじめに / 基礎となる知識 / 解説と使い方 / やってみた結果 / まとめ / 参考文献)に再構成しました。元の本文は保ちつつ、セクションを切り直して導入と前提知識を補ったかたちです。

合わせて、/onbit:blog:new-technical <テーマ> というスラッシュコマンドと、docs/blog-writing/technical-article.md のテンプレを用意しました。次に技術記事を書くときは、テーマを 1 行で渡すと各セクションのドラフトが返ってくる、という運用にしています。

published 状態だった記事も、再構成のレビューが終わるまでは status: draft に戻してあります(中身を確認しながら 1 本ずつ published に戻す予定)。

やってみた結果

数字でまとめると:

項目土曜朝月曜朝
サンプル数330
業種カバー3 業種22 業種
サンプル内画像枚数(合計)0約 150 枚
画像生成スクリプトなしバッチ + 個別の 2 本
アスペクト比検査なしcheck-image-aspect.py
iframe モバイル表示拒否別タブで開くリンクに切替
LINE 導線なしenv 切替で自動表示

うまくいった点:

  • 業種・配色・書体のバラけかたが、当初イメージしていた幅に達した
  • 画像生成のパイプラインができたので、今後 +5 件追加するときの心理的コストが低い
  • アスペクト比の検査スクリプトで、再発が早期に拾えるようになった
  • ブログのテンプレが固まったので、これから書く記事の構造で迷わない

うまくいかなかった点 / ハマったところ:

  • CSP の intersection 仕様を知らずに 30 分溶かした(知ってさえいれば 1 分で直る話)
  • 画像生成 → 表示 → 「切れてる」と気付く → 再生成 / CSS 修正、のループが想像より長かった
  • IntersectionObserver の threshold をデフォルトのまま使っていたツケが、要素数が増えた時に出た
  • 主要ページの話し言葉化はやりすぎだった(信頼感を取りに行く文脈ではトーンを使い分ける必要がある)

今後やりたいこと:

  • 30 件並ぶと「お客さんがどれを見たか」のアクセスログが取りたくなる(Cloudflare Web Analytics で軽くだけ拾う想定)
  • サンプルから「このトーンで実物を見積もる」ボタンへの導線をきっちり作る
  • 月単位で 2〜3 件ずつ静かに足していく(一気に 30 件はもうやらない)

まとめ

土日の 2 日間で、サンプル 3 件 → 30 件、合計 150 枚ほどの画像生成、画像生成のパイプライン化、CSP / アスペクト比 / IntersectionObserver の罠の片付け、LINE 導線、最後にブログテンプレ整理まで、一気に走りました。

サンプル制作所をひとつ建てた、という感覚に近いです。出来上がったページが多いことより、次に同じ作業を半分の手数でできる仕組み が手元に残ったことのほうが、運用していくうえでは大きい気がしています。

参考文献

let's talk

気軽に、お話を聞かせてください

30 分の無料相談です。教室・宿・治療院・小さな店、どんな業種でもまずは聞かせてください。「まだぼんやりしていて」も大丈夫。合わなければ、そっと離れていただいて構いません。