Claude API + WordPress: Il Setup Completo in PHP
L’articolo precedente ha coperto il perché e il cosa dell’automazione AI su WordPress. Questo articolo è il come. Codice PHP funzionante, pronto da copiare e adattare.
Costruiremo tre cose concrete: un wrapper PHP robusto per Claude API, un sistema di content analysis automatica, e un endpoint REST personalizzato che espone le funzionalità AI alla dashboard WordPress.
Il Wrapper PHP: Versione Production-Ready
Il wrapper base dell’articolo precedente funziona, ma per la produzione serve di più: gestione errori, retry, caching, rate limiting.
<?php
/**
* Claude API Wrapper - Production Ready
* wp-content/mu-plugins/claude-api-pro.php
*/
class Claude_API_Pro {
private string $api_key;
private string $model;
private string $api_url = 'https://api.anthropic.com/v1/messages';
private int $max_retries = 3;
private int $rate_limit_per_hour = 100;
public function __construct(string $model = 'claude-sonnet-4-20250514') {
$this->api_key = defined('CLAUDE_API_KEY') ? CLAUDE_API_KEY : '';
$this->model = $model;
if (empty($this->api_key)) {
error_log('Claude API: chiave API non configurata in wp-config.php');
}
}
public function query(string $prompt, array $options = []): array {
// Rate limiting con transient
$calls = (int) get_transient('claude_api_calls_hour') ?: 0;
if ($calls >= $this->rate_limit_per_hour) {
return ['error' => 'Rate limit raggiunto. Riprova tra un\'ora.'];
}
// Check cache (stessa query = stessa risposta)
$cache_key = 'claude_' . md5($prompt . ($options['system'] ?? ''));
$cached = get_transient($cache_key);
if ($cached !== false && !($options['skip_cache'] ?? false)) {
return $cached;
}
$body = [
'model' => $options['model'] ?? $this->model,
'max_tokens' => $options['max_tokens'] ?? 1024,
'messages' => [
['role' => 'user', 'content' => $prompt]
]
];
if (!empty($options['system'])) {
$body['system'] = $options['system'];
}
// Retry con exponential backoff
$last_error = '';
for ($attempt = 1; $attempt <= $this->max_retries; $attempt++) {
$response = wp_remote_post($this->api_url, [
'timeout' => $options['timeout'] ?? 30,
'headers' => [
'Content-Type' => 'application/json',
'x-api-key' => $this->api_key,
'anthropic-version' => '2023-06-01'
],
'body' => json_encode($body)
]);
if (is_wp_error($response)) {
$last_error = $response->get_error_message();
sleep(pow(2, $attempt)); // 2, 4, 8 secondi
continue;
}
$code = wp_remote_retrieve_response_code($response);
$data = json_decode(wp_remote_retrieve_body($response), true);
if ($code === 200) {
$result = [
'content' => $data['content'][0]['text'] ?? '',
'model' => $data['model'] ?? '',
'input_tokens'=> $data['usage']['input_tokens'] ?? 0,
'output_tokens'=> $data['usage']['output_tokens'] ?? 0,
'cached' => false,
];
// Cache per 1 ora (default)
set_transient($cache_key, $result, $options['cache_ttl'] ?? HOUR_IN_SECONDS);
// Incrementa contatore rate limit
set_transient('claude_api_calls_hour', $calls + 1, HOUR_IN_SECONDS);
// Log per audit
$this->log_call($prompt, $result, $options);
return $result;
}
if ($code === 429) {
// Rate limited da Anthropic, aspetta
$retry_after = (int) wp_remote_retrieve_header($response, 'retry-after') ?: 10;
sleep($retry_after);
continue;
}
$last_error = $data['error']['message'] ?? "HTTP $code";
break; // Non ritentare per errori non-transitori
}
return ['error' => $last_error];
}
private function log_call(string $prompt, array $result, array $options): void {
if (!defined('CLAUDE_API_LOG') || !CLAUDE_API_LOG) return;
$log = [
'timestamp' => current_time('mysql'),
'model' => $result['model'],
'input_tokens' => $result['input_tokens'],
'output_tokens'=> $result['output_tokens'],
'system' => substr($options['system'] ?? '', 0, 100),
'prompt_hash' => md5($prompt),
];
// Salva in custom table o option
$logs = get_option('claude_api_logs', []);
$logs[] = $log;
// Tieni solo gli ultimi 500
if (count($logs) > 500) {
$logs = array_slice($logs, -500);
}
update_option('claude_api_logs', $logs, false);
}
public function get_monthly_cost(): float {
$logs = get_option('claude_api_logs', []);
$month = date('Y-m');
$total_input = 0;
$total_output = 0;
foreach ($logs as $log) {
if (strpos($log['timestamp'], $month) === 0) {
$total_input += $log['input_tokens'];
$total_output += $log['output_tokens'];
}
}
// Prezzi Claude Sonnet (marzo 2026)
return ($total_input / 1000000 * 3) + ($total_output / 1000000 * 15);
}
}
$GLOBALS['claude'] = new Claude_API_Pro();
Aggiungi a wp-config.php:
define('CLAUDE_API_KEY', 'sk-ant-api03-...');
define('CLAUDE_API_LOG', true); // Abilita logging
Caso d’Uso 1: Content Analyzer
Analizza automaticamente ogni post prima della pubblicazione e suggerisce miglioramenti per SEO e GEO.
// Hook su save_post per analisi automatica
add_action('save_post', function(int $post_id, WP_Post $post) {
// Solo per post pubblicati o in revisione
if (!in_array($post->post_status, ['publish', 'pending'])) return;
if (wp_is_post_revision($post_id)) return;
if ($post->post_type !== 'post') return;
// Evita analisi duplicate (una volta al giorno max)
$last_analysis = get_post_meta($post_id, '_ai_analysis_date', true);
if ($last_analysis === date('Y-m-d')) return;
$claude = $GLOBALS['claude'];
$content = wp_strip_all_tags($post->post_content);
$title = $post->post_title;
$prompt = "Analizza questo articolo WordPress per SEO e GEO (Generative Engine Optimization).\n\n"
. "Titolo: {$title}\n\n"
. "Contenuto (prime 2000 parole):\n" . substr($content, 0, 8000) . "\n\n"
. "Fornisci:\n"
. "1. Keyword primaria identificata\n"
. "2. Score GEO stimato (0-100) con motivazione\n"
. "3. Numero di statistiche con fonte trovate\n"
. "4. Presenza di tabelle comparative (sì/no)\n"
. "5. Le 3 migliorie più impattanti per la citabilità AI\n"
. "Rispondi in formato JSON.";
$result = $claude->query($prompt, [
'system' => 'Sei un SEO/GEO specialist. Rispondi solo in JSON valido.',
'max_tokens' => 1024
]);
if (!isset($result['error'])) {
update_post_meta($post_id, '_ai_content_analysis', $result['content']);
update_post_meta($post_id, '_ai_analysis_date', date('Y-m-d'));
}
}, 10, 2);
Caso d’Uso 2: Endpoint REST per la Dashboard
Esponi le funzionalità AI come endpoint REST, così puoi usarle dalla dashboard WordPress o da un frontend React/Next.js.
add_action('rest_api_init', function() {
register_rest_route('ai/v1', '/analyze', [
'methods' => 'POST',
'callback' => function(WP_REST_Request $request) {
$claude = $GLOBALS['claude'];
$content = $request->get_param('content');
$type = $request->get_param('type') ?: 'general';
if (empty($content)) {
return new WP_Error('no_content', 'Contenuto richiesto', ['status' => 400]);
}
$prompts = [
'seo' => "Analizza questo testo per SEO: keyword, meta description suggerita, heading ottimizzati.\n\n{$content}",
'geo' => "Analizza questo testo per GEO (citabilità AI): statistiche presenti, fonti citate, struttura.\n\n{$content}",
'security' => "Analizza questo log WordPress per problemi di sicurezza:\n\n{$content}",
'general' => "Analizza questo contenuto WordPress e fornisci raccomandazioni:\n\n{$content}",
];
$result = $claude->query(
$prompts[$type] ?? $prompts['general'],
['max_tokens' => 2048]
);
return rest_ensure_response($result);
},
'permission_callback' => function() {
return current_user_can('edit_posts');
}
]);
// Endpoint per i costi
register_rest_route('ai/v1', '/stats', [
'methods' => 'GET',
'callback' => function() {
$claude = $GLOBALS['claude'];
return rest_ensure_response([
'monthly_cost' => round($claude->get_monthly_cost(), 4),
'calls_today' => (int) get_transient('claude_api_calls_hour') ?: 0,
]);
},
'permission_callback' => function() {
return current_user_can('manage_options');
}
]);
});
Ora puoi chiamare l’AI dalla dashboard WordPress o da qualsiasi client:
# Analisi GEO di un testo
curl -X POST https://tuosito.com/wp-json/ai/v1/analyze \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_APP_PASSWORD" \
-d '{"content": "Il tuo testo qui...", "type": "geo"}'
# Statistiche costi
curl https://tuosito.com/wp-json/ai/v1/stats \
-H "Authorization: Bearer YOUR_APP_PASSWORD"
Caso d’Uso 3: Smart Excerpt Generator
WordPress genera excerpt troncando le prime 55 parole. Risultato: excerpt inutili. L’AI genera un riassunto reale ottimizzato per schema markup e meta description.
add_filter('wp_insert_post_data', function(array $data): array {
// Solo se l'excerpt è vuoto
if (!empty($data['post_excerpt'])) return $data;
if ($data['post_status'] !== 'publish') return $data;
if ($data['post_type'] !== 'post') return $data;
$content = wp_strip_all_tags($data['post_content']);
if (strlen($content) < 200) return $data;
$claude = $GLOBALS['claude'];
$result = $claude->query(
"Genera una meta description di esattamente 150-160 caratteri per questo articolo. "
. "Deve contenere la keyword principale, un beneficio specifico, e invogliare al click.\n\n"
. "Titolo: {$data['post_title']}\n\n"
. "Contenuto (inizio): " . substr($content, 0, 3000),
[
'system' => 'Rispondi SOLO con il testo della meta description. Niente virgolette, niente prefissi.',
'max_tokens' => 100
]
);
if (!isset($result['error']) && !empty($result['content'])) {
$data['post_excerpt'] = sanitize_text_field($result['content']);
}
return $data;
});
Performance e Ottimizzazione
Le chiamate API aggiungono latenza. Ecco come minimizzarla:
- Cache aggressiva: il wrapper usa transient WordPress. Stessa domanda = stessa risposta per 1 ora (configurabile). Per analisi di contenuto, il TTL può essere 24 ore
- Async con Action Scheduler: per task non urgenti (report mensili, analisi batch), usa Action Scheduler di WooCommerce o
wp_schedule_single_event(). L’analisi avviene in background - Prompt engineering: prompt più corti = meno token = meno costo e meno latenza. Un prompt ben scritto di 200 parole batte uno generico di 1000
- Modello giusto per il task: Claude Haiku per task semplici (excerpt, classificazione). Claude Sonnet per analisi complesse. Non serve Opus per generare una meta description
FAQ
Questo codice funziona con WordPress multisite?
Sì. I mu-plugin sono caricati su tutti i siti della rete. La API key in wp-config.php è globale. Se vuoi rate limit separati per ogni sito nella rete, cambia la chiave del transient aggiungendo get_current_blog_id().
Posso usare GPT-4 invece di Claude?
Sì, con modifiche minime. Cambia l’endpoint (https://api.openai.com/v1/chat/completions), il formato degli header (Authorization: Bearer) e la struttura della risposta. La logica di caching, rate limiting e retry resta identica.
C’è un rischio di sicurezza nell’inviare contenuti all’API?
Il contenuto del blog è pubblico, quindi il rischio è basso. Non inviare mai: credenziali, dati personali dei clienti, informazioni finanziarie. Per i log di errore, sanitizza prima rimuovendo path assoluti e IP. Anthropic non usa le chiamate API per il training (policy dichiarata).
Quanto spazio occupano i log?
Il wrapper conserva gli ultimi 500 log come option WordPress. Circa 50-100KB. Trascurabile. Se vuoi più storico, sposta i log in una custom table con cleanup automatico.