Site search / サイト内検索

必要な情報を探す

ページ内の言葉を探す検索と、意味が近い内容を探す検索を使い分けられます。

「検索する」を実行すると、入力した検索語をCloudflare Workers AIで数値表現に変換し、その数値表現をCloudflare Vectorizeで当サイトの公開情報と照合します。あわせて、Acecore共通検索API(acecore.net)へ送信し、 Acecore関連サイトの公開情報を下部に表示することがあります。個人情報や機密情報は入力しないでください。個人情報の取り扱い

Insights / 技術解説

サービスCTAから問い合わせフォームへ文脈を引き継ぐ技術設計

サービスページで読んでいた文脈を問い合わせフォームへ引き継ぐための実装設計です。AstroサイトでのミニCTA、URLパラメータ契約、フォーム種別の初期選択、件名prefill、多言語URL、GA計測、生成HTML確認まで、他サイトでも使える形で整理します。

  • 技術
  • Webサイト
  • サービス
  • Astro
  • CMS
サービスCTAから問い合わせフォームへ文脈を引き継ぐ技術設計
この記事の目次
  1. 一つのCTAからフォーム初期化を検証する
  2. 目的はフォーム入力を減らすことではない
  3. CTAコンポーネントに責務を切り出す
  4. URLパラメータをフォームとの契約にする
  5. フォーム側で分類表を持つ
  6. optionにdata属性を持たせる
  7. クライアント側でprefillする
  8. hashでフォーム位置へ移動する
  9. hidden項目を増やさない判断
  10. 多言語サイトでの考え方
  11. 生成HTMLで確認する
  12. AIチャットとの役割分担
  13. まとめ
  14. 2026年10月6日追記:問い合わせ受付から管理記録へ

2026年9月追記: 以下の ServiceSectionActions と service=web の例は、2026年6月のAcecore公式サイトで使った構成の記録です。現在のAcecore Systemsでは、サービス詳細のCTAが category、service、from、entry を付け、問い合わせフォームが短いservice keyを検証して種別と件名を初期化します。現在は計測・連携用のhidden項目も使うため、以下の「hidden項目を使わない」という設計を現行仕様としてコピーしないでください。URLの値を信頼せず、フォーム側の選択肢へ対応づける原則は引き続き有効です。

サービスページを読んだユーザーが「この内容で相談したい」と思ったとき、単に問い合わせフォームへ送るだけでは、少し文脈が落ちます。

ユーザーはフォームでサービス種別を選び直し、件名も自分で書き直す必要があります。受信側も、本文を読むまで「Web制作の相談なのか」「サーバー運用なのか」「Aceserverなのか」を判断しにくくなります。

Acecoreのサイトでは、サービスCTAから問い合わせフォームへ相談対象を引き継ぐPR でこの導線を改善しました。この記事では、Astroでの実装記録としてだけでなく、他のWebサイトでも使える導線設計として整理します。

一つのCTAからフォーム初期化を検証する

有効なservice key、未知のkey、件名を入力済みの状態で戻る操作を試します。種別が許可済み選択肢へ対応し、件名を上書きしないかを確認し、locale別のURLでも同じ値の契約を使います。URLには個人情報や自由記述を入れず、短い識別子だけで相談の文脈を渡します。

MDN URLSearchParams:URLパラメータの読取り仕様

目的はフォーム入力を減らすことではない

この実装の目的は、フォーム項目を自動入力して楽に見せることだけではありません。

本質は、サービスページで生まれた文脈を、問い合わせフォームと受信オペレーションへ正しく渡すことです。

観点 改善したいこと
ユーザー 読んでいたサービスをもう一度選ぶ手間を減らす
フォーム 問い合わせ種別と件名を相談内容に合わせて初期化する
受信側 問い合わせ種別だけで相談対象を分類しやすくする
計測 どのサービスCTAから相談が始まったか追いやすくする
多言語導線 localeに合う問い合わせURLへ送る

見た目としては小さなミニCTAですが、設計対象はCTA、URL、フォーム、翻訳、計測、受信運用までまたがります。

CTAコンポーネントに責務を切り出す

各サービスセクションの末尾に「このサービスについて相談する」CTAを置きます。

最初に避けたいのは、サービスページの各セクションに同じリンク生成とGA属性をベタ書きすることです。サービスが7件あるなら、同じ書き方が7回出ます。文言やURL仕様を変えたくなったときに漏れやすくなります。

そこで、問い合わせCTAをまとめる ServiceSectionActions を作りました。

---
import Icon from "./Icon.astro";
import { t, getLocalizedUrl, type Locale } from "../i18n";

interface Props {
  locale: Locale;
  gaLabel: string;
  gaLocation: string;
  serviceKey: string;
}

const { locale, gaLabel, gaLocation, serviceKey } = Astro.props;
const u = (path: string) => getLocalizedUrl(path, locale);
const contactUrl = `${u("/contact/")}?category=service&service=${encodeURIComponent(serviceKey)}#contact-form`;
---

<a
  href={contactUrl}
  class="ac-btn-outline gap-2 text-sm sm:w-auto"
  data-ga-event="cta_click"
  data-ga-label={gaLabel}
  data-ga-location={gaLocation}
  data-ga-destination={contactUrl}
>
  <Icon name="message-circle" class="text-sm" />
  {t(locale, "pages.services.miniCta")}
</a>

このコンポーネントの責務は3つです。

  • localeに合う問い合わせURLを生成する
  • service keyをURLパラメータへ入れる
  • GA計測用のlabelとlocationを持たせる

CTAはユーザーの行動点なので、UIだけでなく計測点でもあります。data-ga-label と data-ga-location は、あとから「どのサービスから相談が始まったか」を見るために残しています。

URLパラメータをフォームとの契約にする

CTAから問い合わせフォームへ渡す値は、URLパラメータにしました。

/contact/?category=service&service=web#contact-form

ここで重要なのは、URLに入れる値を「表示文言」にしないことです。

Webサイト制作・運用について のような表示文言は、翻訳、表記揺れ、将来の名称変更の影響を受けます。URLには web や server のような短いservice keyだけを入れます。

パラメータ 役割
category サービス相談として処理する入口を示す
service 対象サービスを表す安定したkey
hash フォーム位置へスクロールするために使う

URLパラメータはユーザーが編集できるものです。だからこそ、フォーム側ではURL値をそのまま送信値にせず、既存のoptionへマッピングします。

問い合わせ受付を管理記録へ渡す流れ 経路の実装段階を示します。実顧客への連絡・対応完了は別途確認が必要です。
  1. CTAの文脈を読む 安定したservice keyを、多言語の表示URLと許可済みフォーム選択肢のallowlistへ対応付ける。
  2. 入力と権限を検証 件名は空欄の場合だけprefillし、APIでも利用者の本人確認と操作権限を検証する。
  3. 受付記録へ渡す 受付内容をCRM履歴と後続タスクへ反映し、担当者の実際の対応完了は別の状態として記録する。

フォーム側で分類表を持つ

問い合わせフォーム側では、サービス別の分類を配列で持ちます。

const serviceCategoryOptions = [
  {
    key: "server",
    value: "サーバー構築・運用について",
    label: t(locale, "pages.contact.formCategoryServiceServer"),
    subject: t(locale, "pages.services.server.title"),
  },
  {
    key: "web",
    value: "Webサイト制作・運用について",
    label: t(locale, "pages.contact.formCategoryServiceWeb"),
    subject: t(locale, "pages.services.web.title"),
  },
];

key、value、label、subject はそれぞれ役割が違います。

フィールド 役割
key URLパラメータから探すための安定した識別子
value フォーム送信時に受信側へ届く問い合わせ種別
label 画面に表示する翻訳済みの選択肢
subject 件名の初期入力に使うサービス名

多言語サイトの場合、label はlocaleに合わせて翻訳します。一方で、value は受信側の分類に使うため、日本語の安定した値に寄せました。

これはプロダクトによって判断が分かれるところです。CRMや外部フォームが多言語分類を持てるなら、valueもlocale別にできます。今回のように受信側の運用をシンプルにしたい場合は、表示ラベルと受信値を分けるほうが扱いやすくなります。

optionにdata属性を持たせる

フォームのselectには、サービス別optionを出力します。

<select id="category" name="category" required>
  <option value="" disabled selected>
    {t(locale, "pages.contact.formCategoryPlaceholder")}
  </option>
  <option value="サービス全般について">
    {t(locale, "pages.contact.formCategoryService")}
  </option>
  {
    serviceCategoryOptions.map((option) => (
      <option
        value={option.value}
        data-service-key={option.key}
        data-service-subject={option.subject}
      >
        {option.label}
      </option>
    ))
  }
</select>

data-service-key はURLの service と照合するために使います。data-service-subject は件名を作るために使います。

ここでもURL値をそのまま category.value に入れないのがポイントです。必ずselect内のoptionから選ぶことで、未知のservice keyや不正な値を受信値に混ぜないようにしています。

クライアント側でprefillする

フォームの初期化は、ページ読み込み後の小さなスクリプトで行います。

function initContactServicePrefill() {
  const form = document.getElementById("contact-form");
  if (!form || form.dataset.servicePrefillInitialized === "true") return;

  form.dataset.servicePrefillInitialized = "true";

  const url = new URL(window.location.href);
  const requestedCategory = url.searchParams.get("category");
  const requestedService = url.searchParams.get("service") || "";
  const category = document.getElementById("category");
  const subject = document.getElementById("subject");

  if (
    requestedCategory === "service" &&
    category instanceof HTMLSelectElement
  ) {
    const serviceOption = Array.from(category.options).find((option) => {
      return option.dataset.serviceKey === requestedService;
    });

    category.value = serviceOption?.value || "サービス全般について";
    category.dispatchEvent(new Event("input", { bubbles: true }));
    category.dispatchEvent(new Event("change", { bubbles: true }));

    if (
      serviceOption &&
      subject instanceof HTMLInputElement &&
      !subject.value.trim()
    ) {
      const template = form.dataset.serviceSubjectTemplate || "{service}";
      const serviceName =
        serviceOption.dataset.serviceSubject ||
        serviceOption.textContent?.trim() ||
        "";
      subject.value = template.replace("{service}", serviceName);
    }
  }
}

実装上のポイントは次の4つです。

  • 二重初期化を避けるため data-service-prefill-initialized を見る
  • category=service のときだけ処理する
  • 未知のservice keyは「サービス全般について」へフォールバックする
  • 件名は空欄のときだけ初期入力する

最後の「件名は空欄のときだけ」が重要です。ユーザーが戻る操作やブラウザ補完で件名を持っている場合、勝手に上書きすると体験が悪くなります。

AstroのView Transitionsやクライアントナビゲーションがある場合は、通常の初期ロードだけでなく astro:page-load でも初期化します。

document.addEventListener("astro:page-load", initContactServicePrefill);
initContactServicePrefill();

hashでフォーム位置へ移動する

CTAのURLには #contact-form を付けています。

/contact/?category=service&service=web#contact-form

問い合わせページにはFAQ、LINE、説明文、フォーム以外の連絡手段などがあるため、サービスCTAから来たユーザーはフォーム位置へ直接移動したほうが自然です。

ただし、フォーム側で初期化を行う場合、スクロールタイミングには少し注意します。要素が描画された後にスクロールしたいので、requestAnimationFrame を使っています。

if (window.location.hash === "#contact-form") {
  window.requestAnimationFrame(() => {
    form.scrollIntoView({ block: "start" });
  });
}

フォーム位置への移動は小さな挙動ですが、CTAの意図とフォームの表示位置がずれるとユーザーが迷います。導線設計では、URL、初期選択、スクロール位置まで一体で見ます。

hidden項目を増やさない判断

今回、相談対象サービス のhidden項目は追加しませんでした。

理由は、問い合わせ種別だけで対象サービスを判別できるようにしたかったからです。

フォーム項目を増やすと、次の確認点も増えます。

  • 通知メールに出すか
  • 管理画面やスプレッドシートに列を増やすか
  • 既存の自動返信テンプレートへ影響するか
  • CRM連携やWebhookで扱うか
  • 多言語表示名と受信値をどう分けるか

必要な情報が既存項目で表せるなら、項目を増やさないほうが運用は安定します。今回は お問い合わせ種別 を「サービス全般」とサービス別に分けるだけで、受信側が判別できる形にしました。

もちろん、複数サービスを同時選択したい、広告キャンペーンIDも保存したい、CRMで別フィールドにしたい、といった要件がある場合はhidden項目を追加する判断もあります。

多言語サイトでの考え方

多言語サイトでこの導線を作るときは、3つの値を分けて考えると混乱しません。

種類 例 locale依存
URL key web, server, aceserver しない
表示ラベル About Website Design など する
受信値 Webサイト制作・運用について 運用次第

URL keyは翻訳しないほうが安定します。リンクを共有したり、分析したり、フォーム側で照合したりするためです。

表示ラベルは必ず翻訳します。ユーザーがフォームで見る文言だからです。

受信値は、運用に合わせます。今回は日本語の安定値に寄せました。多言語対応の表示と、受信後の社内運用は別物として設計すると、フォームが扱いやすくなります。

翻訳フロー自体は、Sveltia CMSで多言語ブログを運用する方法 でも紹介しています。

生成HTMLで確認する

この種の実装は、コンポーネントだけ見ても不十分です。build後のHTMLで、実際にリンクとoptionが出力されているかを確認します。

今回確認した観点は次の通りです。

  • /services/ にサービス別CTAが7件出ている
  • 各CTAが ?category=service&service=...#contact-form を持っている
  • /contact/ に data-service-key 付きoptionが7件出ている
  • 「サービス全般について」とサービス別種別が出ている
  • 相談対象サービス のhidden項目が出ていない

たとえば、生成HTMLに対して rg で確認できます。

rg -n "category=service&service=.*#contact-form" dist\services\index.html
rg -n "data-service-key" dist\contact\index.html
rg -n "相談対象サービス" dist\contact\index.html

最後の確認は、出てはいけないものが出ていないことを見るためです。フォーム改修では、追加したものだけでなく、追加しないと決めたものも確認対象にします。

AIチャットとの役割分担

この導線は、問い合わせAIチャットの技術設計 と相性があります。ただし、役割は違います。

導線 得意なこと
AIチャット どのサービスに相談すべきか会話で整理する
サービスCTA 読んでいるサービスの文脈をフォームへ渡す
フォーム 正式な相談内容を受け取り、記録を残す

AIチャットは、ユーザーがまだ迷っている段階に強い導線です。一方で、サービスページを読み終えて「このサービスについて相談する」と決めたユーザーには、会話を挟まずにフォームへ送るほうが自然です。

導線を増やすときは、すべてを同じ役割にしないことが大切です。ユーザーの状態に合わせて、会話、CTA、フォームを使い分けます。

まとめ

サービスページから問い合わせフォームへ文脈を引き継ぐ実装は、見た目以上に効果があります。

今回の設計で重要だったのは、次の点です。

  • CTAをコンポーネント化してURL生成と計測属性をまとめる
  • URLには表示文言ではなく安定したservice keyを入れる
  • フォーム側でservice keyをoptionへマッピングする
  • 受信値、表示ラベル、件名用サービス名を分ける
  • 未知のservice keyはサービス全般へフォールバックする
  • 件名は空欄のときだけ初期入力する
  • hidden項目を増やさず、問い合わせ種別で分類できるようにする
  • build後の生成HTMLでリンク数、option数、不要項目の不在を確認する

問い合わせフォーム改善は、入力欄を減らすだけではありません。ユーザーが読んでいた文脈を受信側まで失わずに渡すことが、実際の相談対応を楽にします。

2026年10月6日追記:問い合わせ受付から管理記録へ

匿名化した連携改修では、問い合わせの受付、CRMの履歴・タスクへの反映、担当者の対応完了を別の状態として扱いました。URLの入力や問い合わせ経路は検証し、経路の値は安定したenumへ対応させます。管理画面のURLを知っていることだけを、履歴やタスクを変更する権限にはしません。

APIでも利用者と操作権限を確認し、顧客情報を公開検索や公開AI回答の材料へ混ぜません。受付や記録の実装が完了しても、実顧客への連絡から業務完了までの通し検証を主張するものではありません。

フォームのprefillと本人の入力を区別し、折返しが必要な受付では希望連絡経路を必須の固定選択として後続タスクへ引き継ぎます。UIとAPIの必須条件、担当者の閲覧・追記権限を合わせ、DB変更後のschemaとconstraintを確認します。必要な本番検証と、実顧客の全DBを開発用に複製することも分けます。

サービスCTAからフォームへ文脈を渡す流れ

  1. Service

    各サービスセクションのCTAに service key を持たせる。

  2. URL

    /contact/?category=service&service=web#contact-form のようにURL契約で渡す。

  3. Form

    フォーム側で該当する問い合わせ種別と件名を初期入力する。

  4. Ops

    受信側は問い合わせ種別だけでサービス文脈を判別できる。

CTAとフォームをつなぐときの違い

ただフォームへ送る場合

  • ユーザーがフォームで同じサービス名を選び直す
  • 件名が空欄になり、何の相談か伝わりにくい
  • 受信側が本文を読まないと対象サービスを判別しにくい
  • サービス別CTAの効果測定が曖昧になる

文脈を引き継ぐ場合

  • CTAのservice keyから問い合わせ種別を初期選択できる
  • 件名にサービス名を入れて相談内容を整理できる
  • 受信側は問い合わせ種別を見れば分類できる
  • GA labelとURLパラメータでCTA別に確認しやすい

導入時の設計チェック

  • URLパラメータは短く安定したservice keyだけにする
  • フォームの受信値はユーザー表示文言ではなく運用上安定した値にする
  • 未知のservice keyはサービス全般へフォールバックする
  • 件名は空欄のときだけ初期入力する
  • hidden項目を増やす前に既存の問い合わせ種別で分類できるか確認する
  • locale別の問い合わせURLをサーバー側で生成する
  • CTAにはGA labelとlocationを付けて効果測定できるようにする
  • build後の生成HTMLでCTA数、option数、hidden項目の有無を確認する

よくある質問

hidden項目で相談対象サービスを送らないのはなぜですか?

2026年6月当時は、既存の問い合わせ種別だけで分類し、hidden項目を増やさない設計でした。現在のSystemsフォームは、分類値とは別にservice・from・entryを連携用のhidden項目へ保存します。

URLパラメータは改ざんされても大丈夫ですか?

未知のservice keyはサービス全般へフォールバックします。送信値はフォーム側のoptionから選ぶため、URL値をそのまま受信値にしない設計にしています。

多言語サイトではどう扱いますか?

CTAのリンク先はlocale別に生成し、フォーム表示ラベルも翻訳します。一方で受信値は日本語の安定した分類名に寄せると、受信側の運用がぶれにくくなります。