📚 EFO Bot 実装ガイド

チャットボット導入のための完全ガイド - あらゆるフレームワークに対応

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 でお知らせください。