Skip to content
Acecore

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

by Gui
Table of Contents
サービス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別に確認しやすい
導入時の設計チェック
  • Done: URLパラメータは短く安定したservice keyだけにする
  • Done: フォームの受信値はユーザー表示文言ではなく運用上安定した値にする
  • Done: 未知のservice keyはサービス全般へフォールバックする
  • Done: 件名は空欄のときだけ初期入力する
  • Done: hidden項目を増やす前に既存の問い合わせ種別で分類できるか確認する
  • Done: locale別の問い合わせURLをサーバー側で生成する
  • Done: CTAにはGA labelとlocationを付けて効果測定できるようにする
  • Done: build後の生成HTMLでCTA数、option数、hidden項目の有無を確認する
よくある質問
hidden項目で相談対象サービスを送らないのはなぜですか?
受信側で見る項目を増やさず、既存の問い合わせ種別だけで分類できるようにするためです。フォーム項目が増えるほど運用と通知テンプレートの確認点も増えます。
URLパラメータは改ざんされても大丈夫ですか?
未知のservice keyはサービス全般へフォールバックします。送信値はフォーム側のoptionから選ぶため、URL値をそのまま受信値にしない設計にしています。
多言語サイトではどう扱いますか?
CTAのリンク先はlocale別に生成し、フォーム表示ラベルも翻訳します。一方で受信値は日本語の安定した分類名に寄せると、受信側の運用がぶれにくくなります。

Comments

Loading comments...

Links, email addresses, and promotional text cannot be posted.

G

Gui

CEO of Acecore. Leads business systems, web, databases and infrastructure, quality assurance, and AI adoption from business problem framing through design, rollout, and post-launch improvement. Builds on hands-on C#/.NET capability while also covering PHP/JavaScript, SQL Server/PostgreSQL/MySQL, and Linux/Windows Server, designing requirements, technology choices, quality standards, and GitHub-based development operations as one coherent workflow. Uses generative AI across development, verification, and information organization, treating it as practical infrastructure that helps small teams deliver faster and more reliably.

Business problem framingTechnology selectionSystem designC#/.NETDatabase/infrastructure designGitHub development operationsGenerative AIAI workflow designQuality designOn-site integration

Want to learn more about our services?

We provide comprehensive support including system development, web design, graphic design, and IT education.

Related Posts

Search articles