1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Claude開発】小泉構文メーカーを作って2ヶ月で1.3万回使われた話【Gemini API × JavaScript】

Last updated at Posted at 2025-07-31

はじめに

こんにちは。ホネグミ代表、エンジニア×マーケターのなおきちです。
今回は、AIを使って「小泉構文」を生成するWebアプリを作ったら、予想以上にバズって2ヶ月で13,000回も使われた話をします。

本記事について

今回の診断テスト開発およびこの記事執筆は、Claude(Anthropic社のAI)との協業で進めました。AI時代の新しい開発スタイルの実践例として参考になれば幸いです。

この記事で分かること

  • Gemini APIを使ったテキスト生成の実装方法
  • 無料枠を最大限活用するフォールバック機能の作り方
  • WordPressでのAjax実装とカウンター機能
  • 個人開発サービスを1万回使ってもらうための工夫

この記事の対象者

  • API連携に興味があるエンジニア
  • 個人でWebサービスを作りたい人
  • Gemini APIの実装例を見たい人

動作環境・使用技術

  • フロントエンド: HTML5, CSS3, JavaScript (ES6)
  • API: Google Gemini API (gemini-2.0-flash-lite, gemini-2.0-flash, gemini-1.5-flash)
  • バックエンド: WordPress (PHP 8.2)
  • Ajax通信: WordPressの標準Ajax機能

自己紹介

ホネグミ代表、応用情報技術者の資格を持つエンジニア×マーケターです。これまでIT系の会社役員を4年、独立して4年目になります。クライアントワークでは「こうしたい」を技術で形にすることを専門としていますが、最近は個人開発でAIを「無駄に」使った社会風刺的なサービスも作っています。

完成品・デモ

スクリーンショット 2025-07-31 152952.png

「お腹が空いた」と入力すると...
→「お腹が空いているということは、つまり空腹だということなんです。これは意外に知られていない事実ですが、空腹の時というのは、お腹が空いている状態なんですね」

といった具合に、小泉進次郎氏特有のトートロジー(同語反復)を生成します。

技術構成

使用したAPI・ライブラリ

  • Google Gemini API: メインのテキスト生成
  • WordPress Ajax: サーバーサイド処理
  • Font Awesome: アイコン表示

アーキテクチャ概要

フロントエンド (JavaScript)
         ↓ Ajax
WordPressバックエンド (PHP)
         ↓ HTTP Request
Google Gemini API
         ↓ Response
WordPressバックエンド
         ↓ JSON Response
フロントエンド (結果表示)

実装手順

1. Gemini API の準備

まず、Google AI Studioでプロジェクトを作成し、APIキーを取得します。

wp-config.php
// wp-config.php での API設定
define('GEMINI_API_KEY', 'your_api_key_here');

2. フォールバック機能付きAPI呼び出し

無料枠の制限に対応するため、複数のモデルでフォールバック機能を実装しました。

api-handler.php
function call_gemini_api_with_fallback($prompt) {
    $models = [
        'gemini-2.0-flash-lite',
        'gemini-2.0-flash',
        'gemini-1.5-flash'
    ];
    
    foreach ($models as $model) {
        $result = call_gemini_api($prompt, $model);
        if ($result['success']) {
            return $result;
        }
        // ログ出力(デバッグ用)
        error_log("Model {$model} failed, trying next...");
    }
    
    return ['success' => false, 'error' => 'All models failed'];
}

function call_gemini_api($prompt, $model) {
    $api_key = GEMINI_API_KEY;
    $url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$api_key}";
    
    $data = [
        'contents' => [
            [
                'parts' => [
                    ['text' => $prompt]
                ]
            ]
        ]
    ];
    
    $options = [
        'http' => [
            'header' => "Content-type: application/json\r\n",
            'method' => 'POST',
            'content' => json_encode($data)
        ]
    ];
    
    $context = stream_context_create($options);
    $response = file_get_contents($url, false, $context);
    
    if ($response === FALSE) {
        return ['success' => false, 'error' => 'HTTP request failed'];
    }
    
    $json_response = json_decode($response, true);
    
    if (isset($json_response['candidates'][0]['content']['parts'][0]['text'])) {
        return [
            'success' => true, 
            'text' => $json_response['candidates'][0]['content']['parts'][0]['text']
        ];
    }
    
    return ['success' => false, 'error' => 'Invalid response format'];
}

3. WordPressでのAjax実装

ajax-handlers.php
// Ajax ハンドラーの登録
add_action('wp_ajax_koizumi_generate', 'handle_koizumi_generate');
add_action('wp_ajax_nopriv_koizumi_generate', 'handle_koizumi_generate');

function handle_koizumi_generate() {
    // セキュリティチェック(必要に応じてnonceを追加)
    if (!isset($_POST['text']) || empty(trim($_POST['text']))) {
        wp_send_json_error(['message' => '入力テキストが空です']);
        return;
    }
    
    $input_text = sanitize_text_field($_POST['text']);
    
    // プロンプト作成(詳細は企業秘密!)
    $prompt = create_koizumi_prompt($input_text);
    
    // API呼び出し
    $result = call_gemini_api_with_fallback($prompt);
    
    if ($result['success']) {
        wp_send_json_success(['koizumi_text' => $result['text']]);
    } else {
        wp_send_json_error(['message' => 'API呼び出しに失敗しました']);
    }
}

function create_koizumi_prompt($input_text) {
    // プロンプトの詳細は企業秘密ですが、
    // 基本的には小泉構文の特徴を分析して
    // 適切な変換ルールを指示しています
    return generate_prompt_for_koizumi_style($input_text);
}

4. フロントエンド実装

main.js
document.addEventListener('DOMContentLoaded', function() {
    const generateBtn = document.getElementById('generate-koizumi-btn');
    const inputText = document.getElementById('input-text');
    const resultSection = document.querySelector('.result-section');
    const loadingSection = document.querySelector('.loading');
    const koizumiText = document.getElementById('koizumi-text');
    
    generateBtn.addEventListener('click', generateKoizumi);
    
    async function generateKoizumi() {
        const text = inputText.value.trim();
        if (!text) {
            alert('変換したい文章を入力してください。');
            return;
        }
        
        // ローディング状態に切り替え
        toggleLoadingState(true);
        
        try {
            const response = await fetch('/wp-admin/admin-ajax.php', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                },
                body: `action=koizumi_generate&text=${encodeURIComponent(text)}`
            });
            
            const result = await response.json();
            
            if (!result.success) {
                throw new Error(result.data?.message || 'APIエラーが発生しました');
            }
            
            // 結果表示
            displayResult(result.data.koizumi_text);
            updateUsageCounter();
            
        } catch (error) {
            console.error('生成エラー:', error);
            showErrorMessage(error.message);
        } finally {
            toggleLoadingState(false);
        }
    }
    
    function toggleLoadingState(isLoading) {
        if (isLoading) {
            resultSection.style.display = 'none';
            loadingSection.style.display = 'block';
        } else {
            loadingSection.style.display = 'none';
        }
    }
    
    function displayResult(koizumiResult) {
        koizumiText.textContent = koizumiResult;
        resultSection.style.display = 'block';
        setupShareButtons(koizumiResult);
    }
});

5. 利用回数カウンター実装

counter.php
// カウンター用テーブル作成
function create_koizumi_counter_table() {
    global $wpdb;
    
    $table_name = $wpdb->prefix . 'koizumi_counter';
    
    $charset_collate = $wpdb->get_charset_collate();
    
    $sql = "CREATE TABLE $table_name (
        id mediumint(9) NOT NULL AUTO_INCREMENT,
        count_value bigint(20) DEFAULT 0,
        last_updated datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (id)
    ) $charset_collate;";
    
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
    
    // 初期値設定
    $wpdb->insert($table_name, ['count_value' => 0]);
}

// カウンター更新
add_action('wp_ajax_koizumi_counter', 'update_koizumi_counter');
add_action('wp_ajax_nopriv_koizumi_counter', 'update_koizumi_counter');

function update_koizumi_counter() {
    global $wpdb;
    
    $table_name = $wpdb->prefix . 'koizumi_counter';
    
    // カウンターを1増加
    $wpdb->query("UPDATE $table_name SET count_value = count_value + 1 WHERE id = 1");
    
    // 現在のカウント数を取得
    $count = $wpdb->get_var("SELECT count_value FROM $table_name WHERE id = 1");
    
    echo $count;
    wp_die();
}

// カウント数取得
add_action('wp_ajax_koizumi_get_count', 'get_koizumi_count');
add_action('wp_ajax_nopriv_koizumi_get_count', 'get_koizumi_count');

function get_koizumi_count() {
    global $wpdb;
    
    $table_name = $wpdb->prefix . 'koizumi_counter';
    $count = $wpdb->get_var("SELECT count_value FROM $table_name WHERE id = 1");
    
    echo $count ?: 0;
    wp_die();
}

工夫したポイント・苦労した点

1. API制限への対応

Gemini APIの無料枠は以下の通りでした(2025年7月31日14時30分時点):

  • gemini-2.0-flash-lite: 200リクエスト/日
  • gemini-2.0-flash: 200リクエスト/日
  • gemini-1.5-flash: 50リクエスト/日

合計450リクエスト/日で、1日100〜200人の利用に対応できています。

2. ユーザビリティの改善

当初は「政治家言い訳メーカー」のように複数の選択肢を用意していましたが、「小泉構文」という明確なコンセプトがあるため、選択肢をなくしてシンプルな仕様に変更しました。

example-buttons.js
// 例文ボタンの実装
const exampleBtns = document.querySelectorAll('.example-btn');
exampleBtns.forEach(btn => {
    btn.addEventListener('click', function() {
        inputText.value = this.getAttribute('data-example');
        inputText.focus();
        
        // 視覚的フィードバック
        this.style.background = '#fd7e14';
        setTimeout(() => {
            this.style.background = '#2c5aa0';
        }, 500);
    });
});

3. エラーハンドリング

API制限に達した場合の分かりやすいメッセージ表示:

error-handling.js
function showApiLimitMessage() {
    const message = `
        🙏 予想以上のご利用をいただき、ありがとうございます!
        無料のAIサービスには1日の利用制限があるため、
        制限に達した場合は数時間後に再度お試しください。
    `;
    alert(message);
}

パフォーマンス最適化

1. レスポンス時間の改善

  • 複数APIキーによる負荷分散
  • タイムアウト処理の実装
  • キャッシュ機能(今後実装予定)

2. フロントエンドの最適化

loading-animation.css
.spinner {
    display: inline-block;
    width: 50px;
    height: 50px;
    border: 5px solid rgba(44, 90, 160, 0.3);
    border-radius: 50%;
    border-top-color: #2c5aa0;
    animation: spin 1s ease-in-out infinite;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

結果・反響

スクリーンショット 2025-07-31 153450.png

数値的成果

  • 利用回数: 13,000回(2ヶ月)
  • 1日平均利用者: 100〜200人
  • SEO効果: 「小泉構文メーカー」でGoogle検索1位獲得

ユーザーからの反応

  • SNSでの共有多数
  • 「思ったよりも精度が高い」という評価
  • リピーター率の高さ

今後の改善予定

1. 技術的改善

  • レスポンス速度の向上
  • キャッシュ機能の実装
  • エラーログの詳細化

2. 機能追加

  • 生成履歴の保存機能
  • お気に入り機能
  • APIの有料プラン検討

まとめ

個人開発したWebサービスが2ヶ月で1万回使われるのは、正直予想外でした。技術的には特別新しいことはしていませんが、以下のポイントが成功要因だったと思います:

  1. シンプルな UI/UX: 余計な機能を削ぎ落として直感的に使える設計
  2. 安定したAPI運用: 複数キーでのフォールバック機能
  3. 適切なエラーハンドリング: ユーザーにストレスを与えない情報提供
  4. 話題性: 時事ネタ + AI という組み合わせ

同じ課題で困っている人の参考になれば幸いです。何か質問があればコメントください!

お仕事や取材等のご依頼は下記HPまでお願いします。

参考リンク

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?