🧜
SvelteフロントエンドでCloudflare Turnstileを導入するメモ
サーバーサイド(TypeScript)でTurnstileを導入するのは公式がわかりやすく迷うことがなかったのですが、クライアントサイドからTurnstileのチャレンジを実行する方法がスッと理解できなかったため、簡易的なSvelteコンポーネントを作成しつつ理解したメモです。
Svelteコンポーネント
コード
TurnstileParameters
とTurnstileAPI
型は公式文書から独自に作成したため、誤りがある場合があります。
Turnstile.svelte
<script module lang="ts">
export interface TurnstileProps {
sitekey: string;
getToken?: (reset?: boolean) => string;
options?: TurnstileParameters;
};
export type TurnstileParameters = {
sitekey?: string;
action?: string;
cData?: string;
execution?: "render" | "execute"; // "render"
theme?: "auto" | "light" | "dark";
language?: string; // "auto"
tabindex?: number; // 0
"response-field"?: boolean; // true
"response-field-name"?: string; // "cf-turnstile-response"
size?: "normal" | "flexible" | "compact";
retry?: "auto" | "never"; // "auto"
"retry-interval"?: number; // 8000ms
"refresh-expired"?: "auto" | "manual" | "never"; // "auto"
"refresh-timeout"?: "auto" | "manual" | "never"; // "auto"
appearance?: "always" | "execute" | "interaction-only"; // "always"
"feedback-enabled"?: boolean; // true
callback?: (token: string) => void; // after success of the challenge
"error-callback"?: () => void; // after fail or error of the challenge
"expired-callback"?: () => void; // after token expires and does not reset
"before-interactive-callback"?: () => void; // before the challenge enters interactive mode
"after-interactive-callback"?: () => void; // challenge has left interactive mode
"unsupported-callback"?: () => void; // when client/browser is not supported
"timeout-callback"?: () => void; // when interactive challenge but was not solved within a given time
};
interface TurnstileAPI {
ready: () => void;
render: (container: string | HTMLElement, params: TurnstileParameters) => string | undefined; // widgetId
execute: (container: string | HTMLElement, params: TurnstileParameters) => string | undefined;
reset: (widgetId: string) => void;
remove: (widgetId: string) => void;
getResponse: (widgetId?: string) => string;
isExpired: () => boolean;
};
const CLIENT_API = "https://challenges.cloudflare.com/turnstile/v0/api.js";
let api: TurnstileAPI | undefined = $state(undefined);
import { onDestroy, untrack } from "svelte";
</script>
<!---------------------------------------->
<script lang="ts">
let { sitekey, getToken = $bindable(), options }: TurnstileProps = $props();
let loaded = $state(false);
let target = $state<HTMLDivElement>();
let widget = "";
getToken = (reset?: boolean) => {
if (reset) api?.reset(widget);
return api?.getResponse(widget) ?? "";
}
function onload() {
loaded = true;
if (!target) return;
if (typeof window !== "undefined" && "turnstile" in window) api = window.turnstile as TurnstileAPI;
widget = api?.render(target, { sitekey, ...options }) ?? "";
}
onDestroy(() => api?.remove(widget));
$effect(() => untrack(() => {
if (!target || !api) return;
widget = api.render(target, { sitekey, ...options }) ?? "";
}));
</script>
<!---------------------------------------->
<svelte:head>
{#if !loaded && !api}
<script src={CLIENT_API} {onload} defer></script>
{/if}
</svelte:head>
<div bind:this={target}></div>
使い方
-
sitekey
とbind:getToken
としてコンポーネントを設置する - 画面レンダリング後に
getToken()
を実行し、サーバー送信用トークンを取得する
補足
- 何も指定しない場合、トークンを保持するフォームフィールドが自動で文書に追加される
- 不要であれば
TurnstileParameters
のresponse-field
にfalse
を指定する
- 不要であれば
-
api.reset
を呼び出さない限り、getResponse
で取得するトークンは変更されない- 手動で新規トークンを取得する場合は
api.reset
後に再取得する - 上記コンポーネントの場合は
getToken(true)
を実行する
- 手動で新規トークンを取得する場合は
- レンダリングされた後、以下のように要素が追加される
<div> <!-- bind:this={target} --> <div> <!-- appended by cloudflare script --> <iframe>...</iframe> </div> </div>
-
TurnstileParameters
の詳細は公式文書に記載がある -
TurnstileAPI
の詳細は公式文書に記載があるが、まとまっていない- 知りたいメソッド名で文書内検索をする必要がある
雑記
似たようなライブラリがあるのですが、Svelte5記法ではないので自分で作成しました。公式文書の中でRenderParameters
という型を明示しているにも関わらず、型内容の具体的な記載が無いのは残念な気持ちになりました。
Discussion