問い合わせフォーム連携

入門 約12分 最終更新 2026-09-09 Astro / PHP

問い合わせフォームは「フォーム定義を取得してUIを組み立てる」「入力値を送信APIへ渡す」の2段階です。ShareDan CMSはTurnstile・ハニーポット・送信速度チェック・重複送信ブロックをサーバー側で行うため、フロントエンドはトークンや隠しフィールドを取りこぼさず中継するだけで済みます。

前提:APIキーとサイトコードを取得済みで、クイックスタートのステップ2まで完了していること。管理画面でフォーム(key)を作成済みであること。

1. 全体の流れ

X-API-KEY は秘匿情報なので、ブラウザから直接CMSを呼び出すことはできません。フォーム連携は次の三段構成になります。

  • ブラウザ:フォーム定義から入力UIを組み立て、ユーザーの入力とTurnstileの検証結果を集める。APIキーは持たない。
  • 自前サーバー:SDKとX-API-KEYを保持する唯一の場所。ブラウザとCMSの間を中継する。
  • CMS(公開API):フォーム定義の配信と送信の受付、スパム対策の判定を行う。

Turnstileのsite_keyは公開情報なのでブラウザで使って構いません。一方、speed_check_tokenはフォーム定義の取得時に発行され、送信時にそのまま送り返す必要があります。

Node実行環境が無い静的サイトの場合:「自前サーバー」はPHPが動く共有ホスティング(さくらのレンタルサーバ等)に置く小さな中継プロキシ1枚でも実現できます。6. 静的サイト(PHP共有ホスティング)の場合を参照してください(この開発者ポータル自身もその方式です)。

2. フォーム定義を取得する

自前サーバーに GET /api/forms/:key を用意し、cms.forms.get(key) の結果からブラウザに必要な情報だけを返します。fields・Turnstileのsite_key・speed_check_token の3つです。

src/pages/api/forms/[key].ts
import type { APIRoute } from 'astro';
import { cms } from '../../../lib/cms';

export const prerender = false;

// GET /api/forms/:key — フォーム定義をブラウザへ中継する
export const GET: APIRoute = async ({ params }) => {
  const form = await cms.forms.get(params.key!); // キャッシュ禁止。呼ぶたびに新しいトークンが発行される

  return new Response(
    JSON.stringify({
      fields: form.fields,
      turnstileSiteKey: form.turnstile.enabled ? form.turnstile.site_key : null,
      speedCheckToken: form.speed_check_token,
    }),
    { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' } },
  );
};
キャッシュしないこと:cms.forms.get() は呼び出すたびに新しい speed_check_token を発行します。レスポンスをキャッシュ(CDN・ISR・メモリ)すると送信速度チェックが正しく機能しません。
多言語(オプション):多言語オプションを有効にしたサイトでは、cms.forms.get(key, { locale }) でフォーム名・項目ラベル・選択肢ラベルを言語別に取得できます(フォーム定義は常に自動翻訳。name(項目キー)・typeは言語共通)。送信(submit())は言語に依らず同じキーで送ります。詳しくは多言語サイトを作るを参照してください。

3. 定義からUIを組み立てる

fields を type ごとに入力要素へマッピングします。radio・checkbox・select は options(ラベルと値の一覧)から選択肢を描画し、それ以外はtypeをそのままinputのtype属性に使えます。

src/components/ContactForm.tsx
type FormField = {
  type: 'text' | 'email' | 'tel' | 'url' | 'textarea' | 'radio' | 'checkbox' | 'select' | 'file';
  name: string;
  label: string;
  required: boolean;
  options?: { label: string; value: string }[] | null;
  placeholder?: string | null;
};

function FieldControl({ field }: { field: FormField }) {
  switch (field.type) {
    case 'textarea':
      return <textarea name={field.name} required={field.required} placeholder={field.placeholder ?? undefined} />;
    case 'select':
      return (
        <select name={field.name} required={field.required}>
          <option value="">選択してください</option>
          {field.options?.map((o) => (
            <option key={o.value} value={o.value}>
              {o.label}
            </option>
          ))}
        </select>
      );
    case 'radio':
    case 'checkbox':
      return (
        <>
          {field.options?.map((o) => (
            <label key={o.value}>
              <input type={field.type} name={field.name} value={o.value} />
              {o.label}
            </label>
          ))}
        </>
      );
    default:
      return <input type={field.type} name={field.name} required={field.required} placeholder={field.placeholder ?? undefined} />;
  }
}

export function ContactForm({ fields }: { fields: FormField[] }) {
  return (
    <form>
      {fields.map((f) => (
        <div key={f.name}>
          <label>
            {f.label}
            {f.required && ' *'}
          </label>
          <FieldControl field={f} />
        </div>
      ))}

      {/* ハニーポット:人間には見えない位置に置き、常に空欄のまま送信させる */}
      <input
        type="text"
        name="website"
        tabIndex={-1}
        autoComplete="off"
        aria-hidden="true"
        style={{ position: 'absolute', left: '-9999px' }}
      />
    </form>
  );
}

ハニーポット(websiteなど人間には見えない隠しフィールド)は、CSSで画面外に配置し、人間のユーザーには常に空欄のまま送信させます。botはフォームを丸ごと埋めてしまうため、この欄が埋まっていれば機械的な送信とみなせます。

4. 送信する

自前サーバーに POST /api/forms/:key を用意し、ブラウザから受け取った入力値・Turnstileトークン・送信速度チェックトークン・ハニーポットの値をそのまま cms.forms.submit() へ渡します。成功時は201とともに id と submitted_at が返ります。

src/pages/api/forms/[key].ts
import { ShareDanCmsApiError } from '@sharedan/cms-sdk';

// POST /api/forms/:key — 送信をSDK経由でCMSへ中継する(同じファイルに追記する)
export const POST: APIRoute = async ({ params, request }) => {
  const body = await request.json();

  try {
    const result = await cms.forms.submit(params.key!, {
      values: body.values,
      speedCheckToken: body.speedCheckToken,
      turnstileToken: body.turnstileToken,
      honeypot: { website: body.honeypot?.website },
    });
    return new Response(JSON.stringify(result), { status: 201 });
  } catch (e) {
    if (e instanceof ShareDanCmsApiError) {
      if (e.isRateLimited) {
        return new Response(
          JSON.stringify({ message: `しばらく時間をおいて再度お試しください(${e.retryAfter}秒後)` }),
          { status: 429 },
        );
      }
      if (e.isValidationError) {
        return new Response(
          JSON.stringify({ message: '入力内容をご確認ください', details: e.details }),
          { status: 422 },
        );
      }
      if (e.isBadRequest) {
        return new Response(
          JSON.stringify({ message: '認証に失敗しました。ページを再読み込みしてください' }),
          { status: 400 },
        );
      }
    }
    return new Response(JSON.stringify({ message: '送信に失敗しました' }), { status: 500 });
  }
};

5. エラーとバリデーション

cms.forms.submit() は失敗時に ShareDanCmsApiError を投げます。ステータスごとにユーザーへ返すメッセージを分けます。

  • 400(Turnstile検証失敗):isBadRequest — 「認証に失敗しました。ページを再読み込みしてください」と表示する。
  • 422(入力・ハニーポット・速度・重複):isValidationError — 「入力内容をご確認ください」と表示し、detailsを項目別のエラー表示に使う。
  • 429(レート制限):isRateLimited — retryAfter秒後に再送信できる旨を表示する。
ヒント:422のうちハニーポット・送信速度・重複送信によるものは、bot対策としてわざと詳細を出し分けず「入力内容をご確認ください」で丸めても構いません。人間の入力ミスかどうかはdetailsのフィールド名で判定できます。

6. 静的サイト(PHP共有ホスティング)の場合

Node実行環境を持たない静的サイトでも、PHPが動く共有ホスティング(さくらのレンタルサーバ等)なら、下記の小さな中継プロキシ1枚で同じ構成を実現できます。ビルド済みの静的ファイルと一緒に cms-proxy.php を置くだけです。ブラウザは同一オリジンの cms-proxy.php を叩き、X-API-KEYの付与はPHP側でのみ行うため、鍵はブラウザに出ず、別オリジンではないのでCORS設定も不要です。

cms-proxy.php
<?php
// cms-proxy.php — 公開APIをサーバー側で中継する薄いプロキシ。
// ブラウザは同一オリジンの本ファイルだけを叩き、X-API-KEY はサーバー側でのみ付与する。
// Node実行環境が無くても、PHPが動く共有ホスティング(さくらのレンタルサーバ等)に
// 静的サイトと一緒に置くだけで動く。

$API_ORIGIN = 'https://api.cms.sharedan.co.jp';   // 公開APIのオリジン
$API_KEY    = getenv('CMS_API_KEY');              // ← 秘匿。環境変数か公開範囲外の設定で渡す
$SITE_CODE  = getenv('CMS_SITE_CODE');

header('Content-Type: application/json; charset=utf-8');

// SDK は baseUrl の配下に /api/v1/... を付けて叩く(PATH_INFO で受ける)
$path   = $_SERVER['PATH_INFO'] ?? '';
$method = $_SERVER['REQUEST_METHOD'];

// 許可リスト(開放プロキシにしない)。フォーム配信・送信・メンテナンスだけ通す。
$ok = ($method === 'GET'  && preg_match('#^/api/v1/forms/[^/]+$#', $path))
   || ($method === 'POST' && preg_match('#^/api/v1/forms/[^/]+/submissions$#', $path))
   || ($method === 'GET'  && $path === '/api/v1/maintenance');
if (!$ok) { http_response_code(404); echo '{"message":"not found"}'; exit; }

$headers = ["X-API-KEY: $API_KEY", "X-SITE-CODE: $SITE_CODE", 'Accept: application/json'];
$ch = curl_init($API_ORIGIN . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
if ($method === 'POST') {
  // JSON はそのまま透過(ファイル添付があるときは $_POST + $_FILES を CURLFile で再構成する)
  curl_setopt($ch, CURLOPT_POSTFIELDS, file_get_contents('php://input'));
  $headers[] = 'Content-Type: application/json';
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$res = curl_exec($ch);
http_response_code(curl_getinfo($ch, CURLINFO_RESPONSE_CODE) ?: 502);
echo $res === false ? '{"message":"upstream failed"}' : $res;

ブラウザ側はSDKの baseUrl を自ドメイン配下のプロキシに向けるだけです。apiKey・siteCodeはプロキシがサーバー側で付与・上書きするためダミーで構いません。

src/contact.ts
import { ShareDanCmsClient } from '@sharedan/cms-sdk';

// baseUrl を「自分のドメイン配下のプロキシ」に向けるだけ。APIキーはブラウザに置かない。
const cms = new ShareDanCmsClient({
  baseUrl: `${location.origin}/cms-proxy.php`,
  apiKey: 'via-proxy',    // プロキシがサーバー側で本物の鍵を付与する(ダミーで可)
  siteCode: 'via-proxy',
});

// SDK は /cms-proxy.php/api/v1/forms/contact のように叩く(= プロキシ経由でCMSへ)
const form = await cms.forms.get('contact');
await cms.forms.submit('contact', {
  values,
  speedCheckToken: form.speed_check_token,
});
この構成の実例:ShareDan のサンプルサイト(apps/samples/html)とこの開発者ポータル(apps/developers)自身が、この cms-proxy.php 方式でフォームを送信しています。ファイル添付を扱う場合は、$_POST と $_FILES を CURLFile で再構成して multipart 転送してください。
FEEDBACK

このページはお役に立ちましたか?

改善要望・不具合・ドキュメントの分かりにくい点など、お気軽にお寄せください。いただいた内容は今後の改善に役立てます。

返信をご希望の場合のみご記入ください。

スパム対策(レートリミット・ハニーポット・送信速度チェック)はサーバ側で自動適用されます。