本文へスキップ
Acecore
目次
サービスCTAから問い合わせフォームへ文脈を引き継ぐ技術設計

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

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

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

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

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

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

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

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

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

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

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

そこで、CTAと関連事例リンクをまとめる ServiceSectionActions を作りました。

---
import Icon from './Icon.astro'
import type { ServiceWorkLink } from '../data/works'
import { t, getLocalizedUrl, type Locale } from '../i18n'

interface Props {
  locale: Locale
  gaLabel: string
  gaLocation: string
  serviceKey: string
  workLink?: ServiceWorkLink
}

const { locale, gaLabel, gaLocation, serviceKey, workLink } = 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>

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

  • localeに合う問い合わせURLを生成する
  • service keyをURLパラメータへ入れる
  • GA計測用のlabelとlocationを持たせる
  • 既存の関連事例リンクを同じアクション領域に残す

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

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

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

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

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

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

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

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

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

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

const serviceCategoryOptions = [
  {
    key: 'system-development',
    value: '業務システム・アプリ開発について',
    label: t(locale, 'pages.contact.formCategoryServiceSystemDev'),
    subject: t(locale, 'pages.services.systemDev.title'),
  },
  {
    key: 'web',
    value: 'Webサイト制作・運用について',
    label: t(locale, 'pages.contact.formCategoryServiceWeb'),
    subject: t(locale, 'pages.services.web.title'),
  },
]

keyvaluelabelsubject はそれぞれ役割が違います。

フィールド役割
keyURLパラメータから探すための安定した識別子
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 keyweb, 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数、不要項目の不在を確認する

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

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

Service

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

URL

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

Form

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

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項目で相談対象サービスを送らないのはなぜですか?
受信側で見る項目を増やさず、既存の問い合わせ種別だけで分類できるようにするためです。フォーム項目が増えるほど運用と通知テンプレートの確認点も増えます。
URLパラメータは改ざんされても大丈夫ですか?
未知のservice keyはサービス全般へフォールバックします。送信値はフォーム側のoptionから選ぶため、URL値をそのまま受信値にしない設計にしています。
多言語サイトではどう扱いますか?
CTAのリンク先はlocale別に生成し、フォーム表示ラベルも翻訳します。一方で受信値は日本語の安定した分類名に寄せると、受信側の運用がぶれにくくなります。

コメント

コメントを読み込んでいます...

URL、メールアドレス、宣伝文は投稿できません。

G

Gui

Acecore 代表。業務システム、Web、DB/インフラ、品質保証、AI活用を、事業課題の整理から設計・導入後の改善までつなげて推進している。 C#/.NET を軸にした実装力を土台に、PHP/JavaScript、SQL Server/PostgreSQL/MySQL、Linux/Windows Server なども踏まえ、要件整理、技術選定、品質基準、GitHubベースの開発運用まで一体で設計する。 生成AIは、開発・検証・情報整理などの業務プロセスに取り入れ、小規模チームでも速く確かな成果を出すための実務基盤として活用している。

事業課題整理技術選定システム設計C#/.NETDB・インフラ設計GitHub開発運用生成AI活用AIワークフロー設計品質設計現場連携

開発・Web制作のご相談はお気軽に

要件整理からデザイン・実装・運用まで、ワンストップでサポートします。

関連記事

記事を検索