Arganoでデザイナーをしている麻生です。

弊社デザインチームで行っている、Webサイト制作業務における事前調査としてどのような取り組みを行なっているかと事前調査の中で行っている自動化について紹介します。 従来は複数の競合サイトのページを手作業でキャプチャしFigmaに整列させる作業をしていましたが、AIを利用することでJavaScriptの深い知識がなくてもPlaywrightを利用したスクリプトを実装して自動化できました。 本記事では、自動化に使用した2つのコードの紹介と、事前調査の方法、Geminiを活用したスクリプト作成、完成までに苦戦したポイントについて詳しく解説します。

開発の背景


事前調査について

Webデザインにおいて、十分な事前調査をせずに制作へ入ってしまうと、どうしても「自分の中にある引き出し」の中だけで同じようなレイアウトを繰り返してしまい、質の高いアウトプットを出すことができません。デザインのマンネリ化を防ぎ、要件に最適な情報設計をするために、事前調査は必要不可欠なプロセスと考えています。

Figmaを使う理由

弊社デザインチームでは、事前調査のツールとしてFigmaを活用しています。

普段のデザイン制作やレビューをFigmaで行っていることに加え、調査からFigmaを使うことで以下のメリットがあるためです。

  1. 思考プロセスや制作意図がチームに一目で伝わるようになる: 思考のプロセスがFigma上にまとまっているため、チーム内での意図共有やレビューがスムーズになる。
  2. 調査からデザイン制作へすぐに移行できる: 参考サイトのスクリーンショットと制作中のキャンバスが同じ場所にあるため、ツールを切り替える必要がなく、すぐに制作へ入れる。

内容とデザインの2軸で行う調査手法

Webサイト制作における事前調査を内容(情報設計の分析)とデザイン(UI/ビジュアルの参考)の2つの軸に分けて実施しています。

その理由は、内容が良いサイト=デザイン面で優れているとは限らないためです。これらを混同せず、それぞれ別のアプローチで調査をしています。

1. 内容の調査・分析(情報設計)

まず、制作するサイトの狙っているキーワードで検索をした後、1ページ目の上位表示サイトを中心に5〜10サイトほどピックアップします。この際、ページの役割が一致しているかを厳しく精査します。例えば、企業のTOPページを作る場合、検索上位に競合サイトのブログ記事が入っていても、ページの役割が異なるため分析対象からは除外します。

集めたサイトはセクションごとに分割し、セクションの配置順からコンテンツの優先順位を確かめ、共通するセクションからキーワードに対して必要な要素を分析・抽出します。この結果をもとに、実際に制作するWebサイトのワイヤーフレーム(情報設計)を作成します。

内容の調査・分析

2. デザインの参考調査

情報設計が終わったら、分割したセクションごとに、内容が似ていて、尚且つ参考にしたいデザインをいくつかピックアップします。

チーム内でレビューをし、「メインビジュアルはこの方向性で」「サービスの特徴セクションはこの見せ方が良い」と、セクション単位でデザインの方向性をすり合わせていきます。

デザインの参考調査

事前調査を効率化するために使っていた手動スクリプト

内容分析をするために、各参考サイト全体のスクリーンショットをFigmaへ配置し、チームで比較できる状態を作る必要があります。

全自動化に至る前は、スクロール位置の計算や微調整の手間を省くため、ブラウザのデベロッパーツール上で1画面(1080px)ずつスクロールさせながら撮影をしていました。

let currentY = window.scrollY;

function nextShot() {
  currentY += 1080;
  window.scrollTo({ top: currentY, behavior: "instant" });
  return currentY; // 実行すると1080pxスクロールして現在のY座標を返す
}

nextShot();

手動スクリプトで発生していた課題

手動スクリプトの導入により、縦に長いサイトでも高画質なままFigmaへ配置できるようになりました。しかし、実際の調査業務においては依然として以下の課題が残っていました。

  • 撮影の手間とやり直しのストレス: Consoleで関数を実行しながら手動で撮影を繰り返す必要があり、途中で少しでも手動スクロールしてしまうと座標がズレて一から撮り直す必要があった。
  • Figma上での配置作業の負荷: 撮影した複数枚の画像をFigmaへ持ち込んだ後、縦に並べて隙間なく整列させる作業に多くの時間を割いていた。

Geminiを活用したスクリプト作成

今回の自動化にあたり、最初からPlaywrightを使おうと決めていたわけではありません。先ほど紹介したConsole用の手動スクリプトをそのままGeminiに提示し、「これを勝手に実行してローカルフォルダに自動保存できないか?」 と相談したところからスタートしました。

Geminiからは「ブラウザのConsole単体では、セキュリティ制限によりローカルへの自動ファイル保存が難しい」という回答とともに、以下の3つの選択肢が提示されました。

  1. ブラウザ標準機能: ChromeのCapture full size screenshot(手動・静止画)
  2. Playwright / Puppeteer: Node.jsを使ったブラウザ自動化(★推奨)
  3. Console+html2canvas: ブラウザのダウンロード機能をハックする方法

ブラウザの標準機能やConsoleスクリプトでは、2倍の高画質に設定することや、撮影画像をローカルの指定フォルダへ連番で自動保存するといったOS側の制御がセキュリティ上不可能でした。

Node.js上でPlaywrightを動かすことで、高画質撮影と、ローカルへの自動フォルダ整理・一括保存を連携させられるため、Playwrightを採用することに決定しました。

実装内容

最終的に完成したスクリプトとその利用方法について説明します。

1: Playwrightによる分割スクリーンショット

Playwright実行時のターミナル画面、または自動出力された連番画像の保存フォルダ

適当な作業ディレクトリを作成し、Playwrightをインストールします。

npm init -y
npm install playwright
npx playwright install chromium

その後、同じディレクトリに以下のscreenshot.jsを作成し、node screenshot.jsで実行してください。

撮影用スクリプト (screenshot.js)

const { chromium } = require('playwright');
const fs = require('fs');

(async () => {
  // ==========================================
  // 【設定項目】
  // ==========================================
  const targetUrl = 'https://example.com'; // 撮影したいURL
  const viewWidth = 1920;
  const viewHeight = 1080;
  const scale = 2; // Retina解像度相当
  const waitBeforeStart = 5000;
  const waitAfterScroll = 1200;

  // 固定・追従要素の削除(2枚目以降)
  const removeFixedElements = true;
  // ==========================================

  const browser = await chromium.launch({ headless: false, args: ['--start-maximized'] });
  const context = await browser.newContext({
    viewport: { width: viewWidth, height: viewHeight },
    deviceScaleFactor: scale,
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
  });
  const page = await context.newPage();

  const now = new Date();
  const jstDate = new Date(now.getTime() + (9 * 60 * 60 * 1000));
  const timestamp = jstDate.toISOString().replace(/[:T-]/g, '').slice(0, 15);
  const dir = `./screenshots_${timestamp}`;
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });

  await page.goto(targetUrl);
  await page.waitForLoadState('networkidle');
  await page.waitForTimeout(waitBeforeStart);

  let i = 0;
  let hasNext = true;

  while (hasNext) {
    // 2枚目以降は画面上の追従パーツ(fixed/sticky)を非表示にして被りを防ぐ
    if (removeFixedElements === true && i > 0) {
      console.log(`[INFO] ${i+1}枚目: 追従要素を非表示にしました`);
      await page.evaluate(() => {
        const els = document.querySelectorAll('*');
        els.forEach(el => {
          const style = window.getComputedStyle(el);
          if (style.position === 'fixed' || style.position === 'sticky') {
            el.style.setProperty('display', 'none', 'important');
          }
        });
      });
      await page.waitForTimeout(200);
    } else {
      console.log(`[INFO] ${i+1}枚目を撮影中...`);
    }

    const fileName = `shot_${String(i).padStart(3, '0')}.png`;
    await page.screenshot({ path: `${dir}/${fileName}`, fullPage: false });

    // スクロール判定
    const isBottom = await page.evaluate((vh) => {
      const before = window.scrollY;
      window.scrollBy(0, vh);
      return (window.innerHeight + window.scrollY) >= document.documentElement.scrollHeight - 5;
    }, viewHeight);

    if (isBottom) {
      i++;
      await page.waitForTimeout(waitAfterScroll);
      await page.screenshot({ path: `${dir}/shot_${String(i).padStart(3, '0')}_end.png` });
      hasNext = false;
    } else {
      await page.waitForTimeout(waitAfterScroll);
      i++;
    }
    if (i > 200) break;
  }

  await browser.close();
  console.log('[DONE] 全ての撮影が完了しました。');
})();

苦戦したポイント

分割してスクリーンショットを撮影する際、追従する要素(fixedsticky)がそのまま画面に残っていると、2枚目以降のすべての画像にヘッダーが写り込んでしまうという問題が発生します。

そこで、1枚目はヘッダーがある状態で撮影しつつ、2枚目以降の撮影時のみ、固定要素をdisplay: noneにして非表示にするという処理をコードに組み込みました。

// 2枚目(i > 0)以降の撮影時のみ実行
if (i > 0) {
  await page.evaluate(() => {
    // ページ内の要素をチェックし、固定配置(fixed / sticky)されているものを非表示にする
    document.querySelectorAll('*').forEach((el) => {
      const style = window.getComputedStyle(el);
      if (style.position === 'fixed' || style.position === 'sticky') {
        el.style.setProperty('display', 'none', 'important');
      }
    });
  });
}

2: Scripterを用いたFigma上での自動配置・リサイズ

Figma上でScripterを実行し、一括でリサイズ&隙間なく整列された結果画面

撮影した画像をFigmaにドラッグ&ドロップ後、画像を選択した状態でFigma用プラグイン「Scripter」でスクリプトを実行します。

Scripterとは
環境構築や事前設定なしで、Figma上で直接JavaScriptを実行できるプラグインです。 Figma Plugin APIを直接実行できるため、コードを使ってキャンバス上の要素をリサイズしたり、Auto Layoutフレームを自動作成したりといった操作が簡単に行えます。

出典: Scripter (Figma Community)

選択された複数画像をそれぞれ50%サイズ(Retina解像度の調整用)にリサイズし、隙間(Gap: 0)のないAuto Layoutフレームに自動格納します。

Figma用スクリプト (Scripter)

const nodes = figma.currentPage.selection;

if (nodes.length > 0) {
    const frame = figma.createFrame();
    frame.name = "Combined Images (Hug)";
    
    // オートレイアウト設定
    frame.layoutMode = "VERTICAL";
    frame.itemSpacing = 0;
    frame.paddingLeft = 0;
    frame.paddingRight = 0;
    frame.paddingTop = 0;
    frame.paddingBottom = 0;
  
    // 「内包」設定
    frame.layoutSizingHorizontal = "HUG";
    frame.layoutSizingVertical = "HUG";

    // 座標の初期値
    frame.x = nodes[0].x;
    frame.y = nodes[0].y;

    // 各要素を処理
    for (const node of nodes) {
        if (node.resize) {
            node.resize(node.width * 0.5, node.height * 0.5);
            frame.appendChild(node);
        }
    }

    figma.currentPage.selection = [frame];
} else {
    figma.notify("画像を選択してください");
  }

自動化による効果

撮影スクリプトとFigma用配置スクリプトを作成したことで、撮影からFigmaでの一括配置・整列までのフローが効率化されました。

  • 作業時間の短縮: 1サイトあたり約5〜10分かかっていた一連の作業が、約1〜2分程度に短縮。
  • 解像度と密着精度の向上: 高解像度を維持したまま、固定ヘッダーの重なりや画像の隙間がない状態でFigmaへ配置可能となった。

番外編

PlaywrightでfullPage: trueを使用すればページ全体を1枚で出力できますが、生成された画像をそのままFigmaに配置すると以下の仕様制限を受けます。

アップロードするアセットが4096x4096ピクセルを超える場合、Figmaはアセットの最長のサイズが4096ピクセル以下になるように自動的に比例縮小します。

出典: デザインへの画像と動画の追加 – Figma Learn - ヘルプセンター

この制限に対しては、Figmaプラグイン「Insert Big Image」を使用することで、元画質を維持したまま配置できるようになるので画像が分割されていない場合でもきれいに配置できます。

まとめ

手動での高さ測定や撮影、Figma上でのリサイズ・整列作業を自動化したことで、事前調査における作業時間が削減されました。デザイナーでも今回のケースのようにAIを積極的に活用し、単純作業を自動化することでより多くの時間を制作に充てられるように日頃から工夫をしています。