AI と作る HTML ベースの LP エディタ EGP Code を内製した理由

こんにちは。メルペイのフロントエンドエンジニアの @mattsuu です。この記事は「Merpay & Mercoin Tech Openness Month 2026」の 2 日目です。

私たちのチームは、マーケターや PM 向けの社内ツール群 Engagement Platform (EGP) を開発しています。ランディングページ (LP) の作成・公開もその一機能で、過去に WYSIWYG コンポーネントエディタ EGP Pages について同じチームから紹介記事を出しています。

今回はその後継としてゼロから作り直した EGP Code を紹介します。AI エージェントと対話しながら LP を作るための、HTML ベースの社内向け LP エディタです。見た目を生成するだけでなく、本番運用に必要なところまで踏み込んで同じ編集体験の中に組み込んでいるのが特徴で、すでに 10 件以上の本番 LP がこの仕組みで作られています。

v0、Gemini Canvas、Claude Design、Figma Make など、AI で UI を作れるツールはすでに数多くありますが、見た目は作れても本番 LP として運用するには、API 連携・品質保証・Native 連携といった社内固有の課題が残ります。EGP Code は、このギャップを埋めるために内製しました。

EGP Pages と AI 編集における課題

EGP Pages は、ブロックを選んで組み合わせるノーコードの WYSIWYG コンポーネントエディタです。ドラッグ&ドロップで LayoutText といった 40 種類以上のコンポーネントから、ページを組み立てます。マーケターがエンジニアの手を借りずに LP を作れるという目的に対して非常によく機能しており、いまも多くの LP が EGP Pages で作られています。

前身ツール EGP Pages の編集画面。コンポーネントを組み合わせてページを作るノーコードの WYSIWYG エディタ

転機になったのは、AI でページを編集したいというニーズが出てきたことです。EGP Pages は人がドラッグ&ドロップで組み立てる前提で設計されており、AI が扱う際にデータ構造が問題になります。例えば、ボタンを押すと数字が増えるページの JSON ツリーは次のようになります。

{
  "components": [{
    "id": "root",
    "elements": [
      { "id": "1", "tagName": "Context",
        "props": { "value": [
          { "name": "count", "type": "code", "value": "0" },
          { "name": "increment", "type": "code", "value": "(count) => count + 1" }
        ]}},
      { "id": "2", "tagName": "Layout",
        "props": { "children": [":=element.3", ":=element.4"] }},
      { "id": "3", "tagName": "Text",
        "props": { "value": "Count: ${context.count}" }},
      { "id": "4", "tagName": "Action",
        "props": { "label": "+1",
          "onTriggerAction": [{ "type": "SET_CONTEXT",
            "payload": { "count": "${context.increment(context.count)}" }}]}}
    ]
  }]
}

人がエディタ越しに触る分には、この構造でも問題ありませんが、LLM に直接編集を任せようとすると課題が見えてきます。

  • ツリー構造が独自: ":=element.3" のような独自記法を AI に都度教える必要があり、プロンプトが長くなります。
  • ロジックが分散: 状態・条件・動作・表示が Context / When / Action / Text に散らばり、挙動の把握にツリー全体を辿る必要があります。
  • JSON 文字列の中に JavaScript が埋まっている: テンプレートリテラルか eval される式かが描画コンポーネント次第で、正しい解釈が難しくなります。

さらにこの JSON ツリーは等価な HTML のおよそ 2 倍のトークンを消費し、編集のたびに API コストとコンテキスト消費が膨らみます。加えてテスト基盤がなく、AI の編集結果を公開前に機械的に検証する手段がありませんでした。これは EGP Pages の設計が悪かったわけではなく、ノーコード時代に最適化された正しい設計でした。ただ AI に編集させるという前提が加わったことで設計を問い直す必要が出てきたのです。

HTML ベースで作り直す

選択肢は2つありました。既存の JSON 表現を AI 向けに改善するか、AI 前提でゼロから作り直すかです。私たちは後者を選び、ページの表現を HTML ベースにしました。HTML は人にも LLM にも馴染みがあり、独自の JSON ツリーや参照記法を毎回プロンプトで教える必要がないからです。

先ほどのカウンターページは、EGP Code では次にようになります。

<body>
  <egp-script timing="page-loaded">
    rx.count = 0;
  </egp-script>
  <egp-script>
    rx.increment = () => { rx.count = (rx.count ?? 0) + 1; };
  </egp-script>

  <p><egp-text>Count: {{rx.count}}</egp-text></p>
  <egp-button :onclick="rx.increment">+1</egp-button>
</body>

機能は同じですが、コード量もトークン消費もおおよそ半分です。ただし、素の HTML だけでは、状態管理や条件分岐といった動的な振る舞いは表現できません。かといって <script> で自由に JavaScript を書かせると、ページの挙動を追いづらくなります。

そこで、状態管理・条件分岐・繰り返しといった動的な部分だけを、少数の Web Components (<egp-*>) に閉じ込めました。「静的な部分は普通の HTML、動的な部分だけ Web Components」という切り分けによって、EGP Pages のように状態・条件・動作が散らばらない構造になっています。

見た目のスタイリングには Tailwind CSS を採用しています。Web 開発者に馴染みのある書き方に寄せることで、人も AI も独自の作法を覚えずに済みます。また、副次的な効果として、外部ライブラリへの依存が最小限になり、LP ごとに独自パッケージが混ざりません。ランタイムは中央で管理する少数のものだけで動くため、npm パッケージ起因のサプライチェーンリスクが問題になる中でも安全面で利点があります。

使い方

EGP Code では、ほとんどの操作を AI エージェントとのチャットで進めます。「LP を作りたい」のように要件が固まっていない依頼では、エージェントはいきなり作り始めるのではなく、文脈に応じた質問を返してくれます。対象デバイス、カラーテーマ、入れたいセクションといった項目を選択肢から答えていきます。

LP を作るときにエージェントが返す質問画面。対象デバイス・カラーテーマ・入れるセクションなどを選択肢で答える

回答を送ると、エージェントが HTML やテストをまとめて生成し、たたき台となる LP ができあがります。

質問への回答をもとにエージェントが生成した LP。チャットに生成内容とテスト結果が並び、右にプレビューが表示されている

大まかな見た目ができたら、ボタンやテキストなどの要素を直接選んで仕上げます。対象をクリックして「文字サイズを大きくして」「文言を〜に変えて」のように頼めば、位置を説明しなくてもエージェントが直す箇所を正確に把握します。複数箇所にまとめてコメントを付けたり、参考にしたい画像を貼って渡すことも可能です。

プレビュー上の要素を選び、「ボタンの背景色を赤にして」のように指示(コメント)を付けている様子

実運用に必要な 3 つの仕組み

LP と聞くと、文章と画像が並んだ静的なページを思い浮かべるかもしれませんが、実際には、エントリー状況で CTA ボタンを切り替えたり、お客さまの属性で見せ方が変わったりと動きを伴う LP も少なくありません。

そのため、見た目だけが整っていれば十分というわけではなく、社内 API との連携、タップ時の分析ログ送出、公開前に表示や挙動を検証する仕組み、アプリと Web の遷移差を吸収する仕組みなど、周辺の仕組みもあわせて必要になります。

EGP Code では、こうした仕組みを編集体験の中に組み込んでいます。以降では、この 3 つの仕組みを順に紹介します。

社内 API 連携と Logging

LP からは、商品一覧の取得やエントリー状況の確認といった社内 API への呼び出しが頻繁に発生します。しかし、社内 API は AI ツールの学習データに含まれていません。EGP Code では、このギャップを「使い方をその場で AI に教える」仕組みで埋めています。

例えば「商品一覧を出す API は?」と聞くと、エージェントは候補の社内 API を用途つきで挙げてくれます。

商品一覧を出す API は?」という質問に対し、エージェントが候補の社内 API を用途つきで挙げている様子(API 名はモザイク処理)

このやり取りの裏側では、エージェントが「関連する API を探す → 使い方を理解する → 型付きで実装する」という流れで動いています。

エージェントが社内 API を扱う流れの図。依頼を受け取り、関連 API を探し、使い方を理解し、型付きで実装するまでの 3 ステップ

この流れを実現するために、いくつかの工夫をしています。まず、社内 API の使い方を、API ごとに Markdown ファイルに記載しています。実際には、次のようなファイルが並んでいます。

api-searchExampleItems.md               # 商品検索 API
api-postExampleEntry.md                   # キャンペーンエントリー API
api-getExampleSegment.md               # ユーザーセグメント取得 API
api-getExampleRecommendations.md # おすすめ商品取得 API
runtime-event-log.md                        # 分析ログ送出
...

すべての API の説明をプロンプトに乗せてしまうと不要にトークンを消費してしまいます。そこで各ドキュメントのタイトルと用途だけを渡しておき、AI が必要と判断したときにだけその本文を読み込ませる形にしています。

次に、社内 API は型付きの薄いラッパー関数越しに呼び出します。LP から見えるのは関数呼び出しだけで、認証ヘッダ・サービス分岐・パスの違いはラッパーが吸収します。誤った使い方があれば Lint が検知します。

分析ログの送出も同じドキュメント参照のしくみで扱っています。 ログが必要になったタイミングで、エージェントが対応するドキュメントを読み込み、API 呼び出しと同じ流れでログ用のコードを生成します。このしくみによって、AI に「商品一覧を出して」と頼むだけで、社内 API を正しく呼んだ動的な LP が組み上がります。

エディタ内で完結するテストと品質保証

AI が生成した API 呼び出しが正しく動いているか、ボタンが期待どおりに反応するかを、変更のたびに人が目視で確認するのは現実的ではありません。そこで EGP Code は、エディタ内にテストの仕組みを内蔵しています。

エディタ内でテストを実行した結果。左に全件パスしたテスト結果、右に対象 LP のプレビュー

エンジニア以外にとってテストは馴染みがなく、自分で作成するのはハードルの高い作業です。そこで、テストを直接書く代わりに、実現したい振る舞いを自然な言葉で書ける Spec タブを用意しています。ここに、LP の仕様を書き残していきます。

Spec タブに LP の仕様を自然な言葉で書いた様子。SPEC.md に表示・操作・データ取得の受け入れ条件が並ぶ

あとは AI とのチャットで「@SPEC.md を元にテストを書いて」と頼むと、文脈に応じてテストが自動で生成されます。テストはエディタ向けに自作した Jest 風の API で書いており、内蔵のモックサーバで fetch を差し替えられるので、本番 API がなくても動的ページの挙動をエディタ内で再現できます。

// ブラウザ上でそのまま実行される
test('エントリーボタンで API が呼ばれる', async () => {
  render(html);
  await userEvent.click(screen.getByText('エントリー'));
  expect(mockEntry).toHaveBeenCalledWith({ campaign: 'X' });
});

テストが失敗すると、その結果は AI にフィードバックされ、AI が自己修正します。仕様を書く → AI が実装とテストを生成する → ブラウザで挙動を確かめるという流れがエディタ内で完結するため、静的な LP から API を使った動的な LP まで、同じ仕組みで品質を担保しながら作ることが可能です。

アプリと Web の差を吸収する Native 連携

キャンペーンページなどの LP は、Web ブラウザだけでなく、メルカリアプリ内の WebView でも開かれます。このとき通常のリンクのままだと、アプリ内では外部ブラウザが開いてしまい、アプリのネイティブ画面へ遷移できません。これを LP ごとに userAgent 判定や Native bridge の呼び出しで書くのは現実的ではありません。そこで、その差をプラットフォーム側で吸収し、LP を作る側は専用の Web Components を使うだけで済むようにしました。

<egp-link href="https://jp.mercari.com/search?keyword=camera">
  カメラを探す
</egp-link>

このリンクをクリックすると環境を自動で判定して、アプリ内なら Native bridge 経由でネイティブ画面へ、Web なら通常のリンクとして開きます。

まとめ

さまざまな AI ツールによって UI を作りやすくなっていますが、実運用の LP に乗せるには、API 連携・品質保証・Native 連携といった作り込みが必要になります。EGP Code は、そこをプラットフォーム側に組み込むことで、UI づくりから運用までを同じ編集体験の中でつなげようとしています。

実際に次のような新しい進め方が出てきています。

  • PM とフロントエンドエンジニアだけで、仕様策定から実装・リリースまでを完遂
  • バックエンドエンジニアが API から LP まで 1 人で構築
  • マーケターが静的な LP を 1 人で制作

テストや API 連携を含む動的な LP は、まだ非エンジニアだけで完結させるには難しい部分が残っています。それでも、誰がどこまで担えるかの境界は少しずつ動いていて、いずれはこうした動的な LP も非エンジニアだけで作れるようになると考えています。

次の記事は @sinmetalさんの「MySQLからSpannerに移行した時のQueryチューニング」です。引き続きお楽しみください。

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加