EFO ボット 実装ガイド
概要
フォーム最適化(EFO)ボットの実装コードです。エラーハンドリング、TypeScript 対応、アクセシビリティを強化しています。
---
1. シンプル版(ボタン連動なし)
ページ読み込み時に自動的にチャットボットを初期化します。
HTML / JavaScript
html
<script>
(function () {
const SCRIPT_SRC = 'https://rocket-form.nekohack.me/loader.js?id=YOUR_ID';
const LOAD_TIMEOUT = 10000; // 10秒
// 重複読み込み防止
if (document.querySelector(`script[src^="${SCRIPT_SRC}"]`)) {
console.warn('[EFO Bot] Script already loaded');
return;
}
const script = document.createElement('script');
script.src = SCRIPT_SRC;
script.async = true;
script.setAttribute('data-efo-bot', 'true');
// 読み込み成功
script.onload = () => {
console.log('[EFO Bot] Script loaded successfully');
};
// 読み込み失敗
script.onerror = () => {
console.error('[EFO Bot] Failed to load script');
};
// タイムアウト処理
const timeoutId = setTimeout(() => {
if (!window.ChatBot) {
console.error('[EFO Bot] Load timeout');
}
}, LOAD_TIMEOUT);
script.onload = () => {
clearTimeout(timeoutId);
};
document.body.appendChild(script);
})();
</script>React
tsx
import { useEffect, useRef } from 'react';
interface ChatBotWindow extends Window {
ChatBot?: {
toggle: () => void;
open: () => void;
close: () => void;
};
}
declare const window: ChatBotWindow;
const SCRIPT_SRC = 'https://rocket-form.nekohack.me/loader.js?id=YOUR_ID';
const LOAD_TIMEOUT = 10000;
export function useChatBot() {
const scriptLoadedRef = useRef(false);
useEffect(() => {
// 既に読み込み済みの場合はスキップ
if (scriptLoadedRef.current) return;
if (document.querySelector(`script[src^="${SCRIPT_SRC}"]`)) {
scriptLoadedRef.current = true;
return;
}
const script = document.createElement('script');
script.src = SCRIPT_SRC;
script.async = true;
script.setAttribute('data-efo-bot', 'true');
let timeoutId: NodeJS.Timeout;
function handleLoad() {
clearTimeout(timeoutId);
scriptLoadedRef.current = true;
console.log('[EFO Bot] Script loaded successfully');
}
function handleError() {
clearTimeout(timeoutId);
console.error('[EFO Bot] Failed to load script');
}
script.addEventListener('load', handleLoad);
script.addEventListener('error', handleError);
timeoutId = setTimeout(function () {
if (!window.ChatBot) {
console.error('[EFO Bot] Load timeout');
}
}, LOAD_TIMEOUT);
document.body.appendChild(script);
// クリーンアップ
return function () {
script.removeEventListener('load', handleLoad);
script.removeEventListener('error', handleError);
clearTimeout(timeoutId);
};
}, []);
}
// 使用例
export function App() {
useChatBot();
return <div>Your App</div>;
}Vue 3 (Composition API)
vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
interface ChatBotWindow extends Window {
ChatBot?: {
toggle: () => void;
open: () => void;
close: () => void;
};
}
declare const window: ChatBotWindow;
const SCRIPT_SRC = 'https://rocket-form.nekohack.me/loader.js?id=YOUR_ID';
const LOAD_TIMEOUT = 10000;
const scriptLoaded = ref(false);
let timeoutId: number;
function initChatBot() {
if (typeof window === 'undefined') return;
if (document.querySelector(`script[src^="${SCRIPT_SRC}"]`)) {
scriptLoaded.value = true;
return;
}
const script = document.createElement('script');
script.src = SCRIPT_SRC;
script.async = true;
script.setAttribute('data-efo-bot', 'true');
script.onload = function () {
clearTimeout(timeoutId);
scriptLoaded.value = true;
console.log('[EFO Bot] Script loaded successfully');
};
script.onerror = function () {
clearTimeout(timeoutId);
console.error('[EFO Bot] Failed to load script');
};
timeoutId = window.setTimeout(function () {
if (!window.ChatBot) {
console.error('[EFO Bot] Load timeout');
}
}, LOAD_TIMEOUT);
document.body.appendChild(script);
}
onMounted(function () {
initChatBot();
});
onUnmounted(function () {
clearTimeout(timeoutId);
});
</script>Svelte
svelte
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
interface ChatBotWindow extends Window {
ChatBot?: {
toggle: () => void;
open: () => void;
close: () => void;
};
}
declare const window: ChatBotWindow;
const SCRIPT_SRC = 'https:///rocket-form.nekohack.me/loader.js?id=YOUR_ID';
const LOAD_TIMEOUT = 10000;
let scriptLoaded = false;
let timeoutId: number;
onMount(function () {
if (document.querySelector(`script[src^="${SCRIPT_SRC}"]`)) {
scriptLoaded = true;
return;
}
const script = document.createElement('script');
script.src = SCRIPT_SRC;
script.async = true;
script.setAttribute('data-efo-bot', 'true');
script.onload = function () {
clearTimeout(timeoutId);
scriptLoaded = true;
console.log('[EFO Bot] Script loaded successfully');
};
script.onerror = function () {
clearTimeout(timeoutId);
console.error('[EFO Bot] Failed to load script');
};
timeoutId = window.setTimeout(function () {
if (!window.ChatBot) {
console.error('[EFO Bot] Load timeout');
}
}, LOAD_TIMEOUT);
document.body.appendChild(script);
});
onDestroy(function () {
clearTimeout(timeoutId);
});
</script>---
2. ボタン連動版
ユーザーのクリックでチャットボットを開く実装です。
HTML
html
<button id="chatbot-open-btn" aria-label="チャットで相談する" type="button">チャットで相談する</button>
<script>
(function () {
const SCRIPT_SRC = 'https://rocket-form.nekohack.me/loader.js?id=YOUR_ID';
const LOAD_TIMEOUT = 10000;
const RETRY_INTERVAL = 100;
const MAX_RETRY_COUNT = 50; // 5秒間リトライ
let scriptLoaded = false;
// スクリプト初期化
function initScript() {
if (document.querySelector(`script[src^="${SCRIPT_SRC}"]`)) {
scriptLoaded = true;
return;
}
const script = document.createElement('script');
script.src = SCRIPT_SRC;
script.async = true;
script.setAttribute('data-efo-bot', 'true');
script.onload = function () {
scriptLoaded = true;
console.log('[EFO Bot] Script loaded');
};
script.onerror = function () {
console.error('[EFO Bot] Failed to load script');
};
document.body.appendChild(script);
}
// チャットボットを開く
function openChatBot(button) {
let retryCount = 0;
function checkAndOpen() {
if (window.ChatBot && typeof window.ChatBot.toggle === 'function') {
window.ChatBot.toggle();
button.setAttribute('aria-expanded', 'true');
} else if (retryCount < MAX_RETRY_COUNT) {
retryCount++;
setTimeout(checkAndOpen, RETRY_INTERVAL);
} else {
console.error('[EFO Bot] ChatBot not available after retries');
alert('チャットボットの読み込みに失敗しました。ページを再読み込みしてください。');
}
}
checkAndOpen();
}
// 初期化
initScript();
// ボタンイベント
const button = document.getElementById('chatbot-open-btn');
if (button) {
button.addEventListener('click', function () {
openChatBot(this);
});
}
})();
</script>React
tsx
import { useEffect, useRef, useState, useCallback } from 'react';
interface ChatBotWindow extends Window {
ChatBot?: {
toggle: () => void;
open: () => void;
close: () => void;
};
}
declare const window: ChatBotWindow;
const SCRIPT_SRC = 'https://rocket-form.nekohack.me/loader.js?id=YOUR_ID';
const RETRY_INTERVAL = 100;
const MAX_RETRY_COUNT = 50;
export function ChatBotButton() {
const [isLoading, setIsLoading] = useState(false);
const [scriptLoaded, setScriptLoaded] = useState(false);
const retryCountRef = useRef(0);
useEffect(() => {
if (document.querySelector(`script[src^="${SCRIPT_SRC}"]`)) {
setScriptLoaded(true);
return;
}
const script = document.createElement('script');
script.src = SCRIPT_SRC;
script.async = true;
script.setAttribute('data-efo-bot', 'true');
script.onload = function () {
setScriptLoaded(true);
console.log('[EFO Bot] Script loaded');
};
script.onerror = function () {
console.error('[EFO Bot] Failed to load script');
};
document.body.appendChild(script);
return function () {
script.onload = null;
script.onerror = null;
};
}, []);
const openChat = useCallback(() => {
setIsLoading(true);
retryCountRef.current = 0;
function checkAndOpen() {
if (window.ChatBot?.toggle) {
window.ChatBot.toggle();
setIsLoading(false);
} else if (retryCountRef.current < MAX_RETRY_COUNT) {
retryCountRef.current++;
setTimeout(checkAndOpen, RETRY_INTERVAL);
} else {
setIsLoading(false);
console.error('[EFO Bot] ChatBot not available');
alert('チャットボットの読み込みに失敗しました。');
}
}
checkAndOpen();
}, []);
return (
<button onClick={openChat} disabled={isLoading} aria-label="チャットで相談する" className="chatbot-button">
{isLoading ? '読み込み中...' : 'チャットで相談する'}
</button>
);
}Vue 3 (Compotion API)
vue
<script setup lang="ts">
import { onMounted, ref } from 'vue';
interface ChatBotWindow extends Window {
ChatBot?: {
toggle: () => void;
open: () => void;
close: () => void;
};
}
declare const window: ChatBotWindow;
const SCRIPT_SRC = 'https://rocket-form.nekohack.me/loader.js?id=YOUR_ID';
const RETRY_INTERVAL = 100;
const MAX_RETRY_COUNT = 50;
const isLoading = ref(false);
const scriptLoaded = ref(false);
const initScript = function () {
if (typeof window === 'undefined') return;
if (document.querySelector(`script[src^="${SCRIPT_SRC}"]`)) {
scriptLoaded.value = true;
return;
}
const script = document.createElement('script');
script.src = SCRIPT_SRC;
script.async = true;
script.setAttribute('data-efo-bot', 'true');
script.onload = function () {
scriptLoaded.value = true;
console.log('[EFO Bot] Script loaded');
};
script.onerror = function () {
console.error('[EFO Bot] Failed to load script');
};
document.body.appendChild(script);
};
function openChat() {
isLoading.value = true;
let retryCount = 0;
function checkAndOpen() {
if (window.ChatBot?.toggle) {
window.ChatBot.toggle();
isLoading.value = false;
} else if (retryCount < MAX_RETRY_COUNT) {
retryCount++;
setTimeout(checkAndOpen, RETRY_INTERVAL);
} else {
isLoading.value = false;
console.error('[EFO Bot] ChatBot not available');
alert('チャットボットの読み込みに失敗しました。');
}
}
checkAndOpen();
}
onMounted(function () {
initScript();
});
</script>
<template>
<button @click="openChat" :disabled="isLoading" aria-label="チャットで相談する" class="chatbot-button">
{{ isLoading ? '読み込み中...' : 'チャットで相談する' }}
</button>
</template>Svelte
svelte
<script lang="ts">
import { onMount } from 'svelte';
interface ChatBotWindow extends Window {
ChatBot?: {
toggle: () => void;
open: () => void;
close: () => void;
};
}
declare const window: ChatBotWindow;
const SCRIPT_SRC = 'https://rocket-form.nekohack.me/loader.js?id=YOUR_ID';
const RETRY_INTERVAL = 100;
const MAX_RETRY_COUNT = 50;
let isLoading = false;
let scriptLoaded = false;
onMount(function () {
if (document.querySelector(`script[src^="${SCRIPT_SRC}"]`)) {
scriptLoaded = true;
return;
}
const script = document.createElement('script');
script.src = SCRIPT_SRC;
script.async = true;
script.setAttribute('data-efo-bot', 'true');
script.onload = () => {
scriptLoaded = true;
console.log('[EFO Bot] Script loaded');
};
script.onerror = function () {
console.error('[EFO Bot] Failed to load script');
};
document.body.appendChild(script);
});
function openChat() {
isLoading = true;
let retryCount = 0;
function checkAndOpen() {
if (window.ChatBot?.toggle) {
window.ChatBot.toggle();
isLoading = false;
} else if (retryCount < MAX_RETRY_COUNT) {
retryCount++;
setTimeout(checkAndOpen, RETRY_INTERVAL);
} else {
isLoading = false;
console.error('[EFO Bot] ChatBot not available');
alert('チャットボットの読み込みに失敗しました。');
}
};
checkAndOpen();
}
</script>
<button
on:click={openChat}
disabled={isLoading}
aria-label="チャットで相談する"
class="chatbot-button"
>
{isLoading ? '読み込み中...' : 'チャットで相談する'}
</button>
<style>
.chatbot-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>---
3. 高度な実装例(React Hooks)
再利用可能なカスタムフックとして実装した例です。
tsx
import { useEffect, useRef, useState, useCallback } from 'react';
interface ChatBotAPI {
toggle: () => void;
open: () => void;
close: () => void;
}
interface ChatBotWindow extends Window {
ChatBot?: ChatBotAPI;
}
declare const window: ChatBotWindow;
interface UseChatBotOptions {
scriptSrc: string;
loadTimeout?: number;
retryInterval?: number;
maxRetryCount?: number;
onLoad?: () => void;
onError?: (error: Error) => void;
}
interface UseChatBotReturn {
isReady: boolean;
isLoading: boolean;
error: Error | null;
openChat: () => void;
closeChat: () => void;
toggleChat: () => void;
}
export function useChatBot(options: UseChatBotOptions): UseChatBotReturn {
const { scriptSrc, loadTimeout = 10000, retryInterval = 100, maxRetryCount = 50, onLoad, onError } = options;
const [isReady, setIsReady] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const retryCountRef = useRef(0);
const scriptRef = useRef<HTMLScriptElement | null>(null);
// スクリプト読み込み
useEffect(() => {
if (document.querySelector(`script[src^="${scriptSrc}"]`)) {
setIsReady(true);
onLoad?.();
return;
}
const script = document.createElement('script');
script.src = scriptSrc;
script.async = true;
script.setAttribute('data-efo-bot', 'true');
scriptRef.current = script;
const timeoutId = setTimeout(function () {
if (!isReady) {
const timeoutError = new Error('Script load timeout');
setError(timeoutError);
onError?.(timeoutError);
}
}, loadTimeout);
script.onload = function () {
clearTimeout(timeoutId);
setIsReady(true);
setError(null);
onLoad?.();
console.log('[EFO Bot] Script loaded successfully');
};
script.onerror = function () {
clearTimeout(timeoutId);
const loadError = new Error('Failed to load script');
setError(loadError);
onError?.(loadError);
console.error('[EFO Bot] Failed to load script');
};
document.body.appendChild(script);
return function () {
clearTimeout(timeoutId);
script.onload = null;
script.onerror = null;
};
}, [scriptSrc, loadTimeout, onLoad, onError, isReady]);
// ChatBot API呼び出しヘルパー
const callChatBotAPI = useCallback(
(method: keyof ChatBotAPI) => {
setIsLoading(true);
retryCountRef.current = 0;
const checkAndCall = () => {
if (window.ChatBot?.[method]) {
window.ChatBotmethod;
setIsLoading(false);
setError(null);
} else if (retryCountRef.current < maxRetryCount) {
retryCountRef.current++;
setTimeout(checkAndCall, retryInterval);
} else {
setIsLoading(false);
const apiError = new Error(`ChatBot.${method} not available`);
setError(apiError);
onError?.(apiError);
}
};
checkAndCall();
},
[maxRetryCount, retryInterval, onError],
);
const openChat = useCallback(() => callChatBotAPI('open'), [callChatBotAPI]);
const closeChat = useCallback(() => callChatBotAPI('close'), [callChatBotAPI]);
const toggleChat = useCallback(() => callChatBotAPI('toggle'), [callChatBotAPI]);
return {
isReady,
isLoading,
error,
openChat,
closeChat,
toggleChat,
};
}
// 使用例
export function ChatBotExample() {
const { isReady, isLoading, error, toggleChat } = useChatBot({
scriptSrc: 'https://rocket-form.nekohack.me/loader.js?id=YOUR_ID',
onLoad: () => console.log('ChatBot ready!'),
onError: (err) => console.error('ChatBot error:', err),
});
return (
<div>
<button onClick={toggleChat} disabled={!isReady || isLoading} aria-label="チャットを開く">
{isLoading ? '読み込み中...' : 'チャットで相談する'}
</button>
{error && (
<p role="alert" style={{ color: 'red' }}>
エラー: {error.message}
</p>
)}
</div>
);
}---
4. 実装の使い分け
| バージョン | 推奨シーン | 主な特徴 |
|---|---|---|
| シンプル版 | 常時表示(右下アイコン等) | - ページ読み込み時に自動初期化 - ユーザーアクション不要 - 軽量実装 |
| ボタン連動版 | CTA ボタンからの起動 | - クリックイベントで起動 - リトライ機能あり - ユーザー体験を重視 |
| 高度な実装 | 大規模アプリ・複数箇所での利用 | - TypeScript 完全対応 - 状態管理 - エラーハンドリング充実 |
5. トラブルシューティング
Q: スクリプトが読み込まれない
A: ブラウザのコンソールでエラーを確認してください。CORS 設定や CSP ポリシーが原因の可能性があります。
Q: ボタンを押しても反応しない
A: `window.ChatBot` が存在するか確認してください。リトライが上限に達している場合はタイムアウトの可能性があります。
Q: 複数のボタンを設置したい
A: 高度な実装例の `useChatBot` フックを使用し、複数コンポーネントで共有してください。
---
ライセンス・サポート
このドキュメントは MIT ライセンスで提供されています。
ご質問や改善提案は Issue でお知らせください。