/** * AJAX: SEO COMPLETO (Contenido + Rank Math + Schema + Imágenes) * Enfoque robusto: * - Gemini devuelve JSON SIMPLE (no JSON-LD completo) * - PHP construye JSON-LD final */ add_action( 'wp_ajax_nv_gemini_generate_full_seo', function() { // Base: permisos + nonce + request disponible + rate-limit $post = nv_gemini_check_ajax_base( 'nv_gemini_generate_save' ); $post_id = $post->ID; $prompt = isset($_POST['prompt']) ? sanitize_textarea_field( wp_unslash($_POST['prompt']) ) : ''; if ( empty( $prompt ) ) { wp_send_json_error([ 'message' => 'Prompt vacío.' ]); } // Contexto (capado para no enviar megas) $existing_title = get_post_meta( $post_id, 'rank_math_title', true ); $page_title = $existing_title ?: get_the_title( $post_id ); $existing_content = wp_strip_all_tags( $post->post_content ); if ( function_exists('mb_substr') ) { $existing_content = mb_substr( $existing_content, 0, 2000 ); } else { $existing_content = substr( $existing_content, 0, 2000 ); } /** * Importante: * - gemini-pro NO soporta JSON mode, así que NO intentamos response_mime_type JSON. [3](https://github.com/WordPress/health-check/blob/trunk/mu-plugin/health-check-troubleshooting-mode.php) * - Pedimos JSON SIMPLE para reducir respuestas vacías. [1](https://cvefeed.io/vuln/detail/CVE-2022-47161) */ $instruction = "Eres un asistente SEO experto para WordPress. Devuelve SOLO un JSON válido (sin texto extra). NO devuelvas JSON-LD completo. Devuelve estas claves: - title: título SEO (máx 60 caracteres) - description: meta descripción (máx 160 caracteres) - focus_keyword: keyword principal (frase corta) - extra_keywords: array de 10 keywords adicionales (strings) - content: HTML simple (sin scripts, sin iframes) - schema_simple: objeto JSON simple (NO JSON-LD) con: - headline - description (máx 160) - author_name - date_published (YYYY-MM-DD) TÍTULO ACTUAL: {$page_title} CONTENIDO ACTUAL (si existe): {$existing_content} PROMPT: {$prompt}"; // Con caché por prompt si existe helper if ( function_exists('nv_gemini_cached_request') ) { $raw = nv_gemini_cached_request( $instruction, 12 ); } else { $raw = nv_gemini_pro_request( $instruction ); } if ( empty( $raw ) ) { // generateContent puede devolver respuesta vacía sin error HTTP en algunos casos. [1](https://cvefeed.io/vuln/detail/CVE-2022-47161) wp_send_json_error([ 'message' => 'No devuelve contenido (API Key, timeout o bloqueo).' ]); } // Extraer JSON robustamente if ( function_exists('nv_extract_json_object') ) { $clean = nv_extract_json_object( $raw ); } else { // fallback simple $raw2 = preg_replace('/^```(?:json)?/i', '', trim($raw)); $raw2 = preg_replace('/```$/', '', trim($raw2)); $start = strpos($raw2, '{'); $end = strrpos($raw2, '}'); $clean = ($start !== false && $end !== false && $end > $start) ? substr($raw2, $start, $end - $start + 1) : ''; } if ( empty($clean) ) { error_log('[NV Gemini] SEO completo: no se pudo extraer JSON. Raw: ' . substr($raw,0,500)); wp_send_json_error([ 'message' => 'Respuesta sin JSON utilizable.' ]); } $json = json_decode( $clean, true ); if ( ! is_array( $json ) ) { error_log('[NV Gemini] SEO completo: JSON inválido. Clean: ' . substr($clean,0,500)); wp_send_json_error([ 'message' => 'La respuesta de Gemini no es JSON válido.' ]); } // ------------------------- // Normalizar campos // ------------------------- $title = sanitize_text_field( $json['title'] ?? '' ); $desc = sanitize_text_field( $json['description'] ?? '' ); $kw = sanitize_text_field( $json['focus_keyword'] ?? '' ); if ( function_exists('mb_substr') ) { if ( $title ) $title = mb_substr( $title, 0, 60 ); if ( $desc ) $desc = mb_substr( $desc, 0, 160 ); } else { if ( $title ) $title = substr( $title, 0, 60 ); if ( $desc ) $desc = substr( $desc, 0, 160 ); } $extra_keywords = ( isset($json['extra_keywords']) && is_array($json['extra_keywords']) ) ? array_values(array_filter(array_map('sanitize_text_field', $json['extra_keywords'] ))) : []; // contenido HTML permitido $content_html = wp_kses_post( $json['content'] ?? '' ); // schema_simple $schema_simple = ( isset($json['schema_simple']) && is_array($json['schema_simple']) ) ? $json['schema_simple'] : []; // ------------------------- // 1) Guardar contenido en post_content // ------------------------- if ( ! empty( $content_html ) ) { wp_update_post([ 'ID' => $post_id, 'post_content' => $content_html, ]); } // ------------------------- // 2) Guardar metas Rank Math (keys oficiales) // ------------------------- if ( $title !== '' ) update_post_meta( $post_id, 'rank_math_title', $title ); // [2](https://onedrive.live.com/?id=fb75e519-2aea-4490-8fea-9dcc9c3edeb9&cid=f68e591688629750&web=1) if ( $desc !== '' ) update_post_meta( $post_id, 'rank_math_description', $desc ); // [2](https://onedrive.live.com/?id=fb75e519-2aea-4490-8fea-9dcc9c3edeb9&cid=f68e591688629750&web=1) if ( $kw !== '' ) update_post_meta( $post_id, 'rank_math_focus_keyword', $kw ); // uso habitual en automatizaciones [4](https://woocommerce.com/document/troubleshooting-using-health-check/) // ------------------------- // 3) Guardar keywords extra (meta propia) // ------------------------- if ( ! empty( $extra_keywords ) ) { update_post_meta( $post_id, 'nv_gemini_extra_keywords', $extra_keywords ); } // ------------------------- // 4) Construir JSON-LD Article en PHP (estable) // ------------------------- $headline = sanitize_text_field( $schema_simple['headline'] ?? ($title ?: $page_title) ); $sd_desc = sanitize_text_field( $schema_simple['description'] ?? $desc ); $author = sanitize_text_field( $schema_simple['author_name'] ?? 'Redacción' ); $date_pub = sanitize_text_field( $schema_simple['date_published'] ?? date('Y-m-d') ); if ( function_exists('mb_substr') ) { $sd_desc = mb_substr( $sd_desc, 0, 160 ); } else { $sd_desc = substr( $sd_desc, 0, 160 ); } if ( ! preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_pub ) ) { $date_pub = date('Y-m-d'); } $schema_ld = [ '@context' => 'https://schema.org', '@type' => 'Article', 'headline' => $headline, 'description' => $sd_desc, 'author' => [ '@type' => 'Person', 'name' => $author, ], 'datePublished' => $date_pub, 'inLanguage' => 'es-ES', 'mainEntityOfPage' => get_permalink( $post_id ), ]; if ( function_exists('nv_gemini_save_schema_raw') ) { nv_gemini_save_schema_raw( $post_id, $schema_ld ); } else { update_post_meta( $post_id, 'nv_gemini_schema_raw', wp_json_encode( $schema_ld ) ); } // ------------------------- // 5) Optimizar imágenes (ALT/caption/desc solo si faltan) // ------------------------- if ( function_exists('nv_gemini_optimize_images_for_post') ) { $seo_title_for_images = $title ?: $page_title; $html_for_images = $content_html ?: $post->post_content; nv_gemini_optimize_images_for_post( $post_id, $seo_title_for_images, $html_for_images ); } wp_send_json_success([ 'message' => 'SEO completo guardado: contenido + Rank Math (title/description/keyword) + schema + optimización de imágenes.' ]); });