diff --git a/assets/js/term-admin.js b/assets/js/term-admin.js
new file mode 100644
index 0000000..89efeac
--- /dev/null
+++ b/assets/js/term-admin.js
@@ -0,0 +1,102 @@
+(function () {
+ if (!window.GroqAITermGenerator) {
+ return;
+ }
+
+ const form = document.getElementById('groq-ai-term-form');
+ if (!form) {
+ return;
+ }
+
+ const promptField = document.getElementById('groq-ai-term-prompt');
+ const outputField = document.getElementById('groq-ai-term-generated');
+ const rawField = document.getElementById('groq-ai-term-raw');
+ const statusField = document.getElementById('groq-ai-term-status');
+ const applyButton = document.getElementById('groq-ai-term-apply');
+ const includeTopProducts = document.getElementById('groq-ai-term-include-top-products');
+ const topProductsLimit = document.getElementById('groq-ai-term-top-products-limit');
+
+ function setStatus(message, type) {
+ if (!statusField) {
+ return;
+ }
+ statusField.textContent = message || '';
+ statusField.setAttribute('data-status', type || '');
+ }
+
+ function setLoading(isLoading) {
+ form.classList.toggle('is-loading', !!isLoading);
+ const buttons = form.querySelectorAll('button, input[type="submit"]');
+ buttons.forEach((btn) => {
+ btn.disabled = !!isLoading;
+ });
+ }
+
+ function buildPayload(prompt) {
+ const payload = new URLSearchParams();
+ payload.append('action', 'groq_ai_generate_term_text');
+ payload.append('nonce', GroqAITermGenerator.nonce);
+ payload.append('taxonomy', GroqAITermGenerator.taxonomy);
+ payload.append('term_id', GroqAITermGenerator.termId);
+ payload.append('prompt', prompt);
+ payload.append('include_top_products', includeTopProducts && includeTopProducts.checked ? '1' : '0');
+ payload.append('top_products_limit', topProductsLimit ? String(topProductsLimit.value || '') : '10');
+ return payload;
+ }
+
+ if (applyButton) {
+ applyButton.addEventListener('click', () => {
+ const descriptionField = document.getElementById('description');
+ if (!descriptionField || !outputField) {
+ return;
+ }
+ descriptionField.value = outputField.value || '';
+ setStatus('Tekst ingevuld in het beschrijving-veld. Vergeet niet op "Opslaan" te klikken.', 'success');
+ });
+ }
+
+ form.addEventListener('submit', (event) => {
+ event.preventDefault();
+ const prompt = promptField ? (promptField.value || '').trim() : '';
+ if (!prompt) {
+ setStatus('Vul eerst een prompt in.', 'error');
+ return;
+ }
+
+ setLoading(true);
+ setStatus('AI is bezig met schrijven...', 'loading');
+ if (rawField) {
+ rawField.textContent = '';
+ }
+
+ fetch(GroqAITermGenerator.ajaxUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
+ },
+ body: buildPayload(prompt).toString(),
+ })
+ .then((response) => response.json())
+ .then((json) => {
+ if (!json.success) {
+ const errorMessage = json.data && json.data.message ? json.data.message : 'Onbekende fout';
+ throw new Error(errorMessage);
+ }
+
+ if (outputField) {
+ outputField.value = (json.data && json.data.description ? json.data.description : '').trim();
+ }
+ if (rawField) {
+ rawField.textContent = (json.data && json.data.raw ? String(json.data.raw) : '').trim();
+ }
+
+ setStatus('Tekst gegenereerd. Je kunt hem toepassen en opslaan.', 'success');
+ })
+ .catch((error) => {
+ setStatus(error && error.message ? error.message : 'Er ging iets mis bij het genereren.', 'error');
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ });
+})();
diff --git a/groq-ai-product-text.php b/groq-ai-product-text.php
index 8332188..e139ced 100644
--- a/groq-ai-product-text.php
+++ b/groq-ai-product-text.php
@@ -2,7 +2,7 @@
/**
* Plugin Name: SitiAI Product Teksten
* Description: Genereer productteksten met diverse AI-aanbieders rechtstreeks vanuit WooCommerce.
- * Version: 1.3.0
+ * Version: 1.4.0
* Author: SitiAI
* Text Domain: siti-ai-product-content-generator
* Domain Path: /languages
@@ -54,6 +54,10 @@ require_once __DIR__ . '/includes/Services/Settings/class-groq-ai-settings-manag
require_once __DIR__ . '/includes/Services/Prompt/class-groq-ai-prompt-builder.php';
require_once __DIR__ . '/includes/Services/Conversations/class-groq-ai-conversation-manager.php';
require_once __DIR__ . '/includes/Services/Logging/class-groq-ai-generation-logger.php';
+require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-oauth-client.php';
+require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-search-console-client.php';
+require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-analytics-data-client.php';
+require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-context-builder.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-settings-page.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-logs-table.php';
require_once __DIR__ . '/includes/Admin/class-groq-ai-product-ui.php';
@@ -108,6 +112,7 @@ final class Groq_AI_Product_Text_Plugin {
add_action( 'init', [ $this, 'load_textdomain' ] );
add_action( 'plugins_loaded', [ $this, 'maybe_create_logs_table' ] );
add_action( 'load-plugins.php', [ $this, 'maybe_deactivate_if_woocommerce_missing' ] );
+ add_filter( 'groq_ai_term_google_context', [ $this, 'inject_google_term_context' ], 10, 3 );
}
public function load_textdomain() {
@@ -179,10 +184,47 @@ final class Groq_AI_Product_Text_Plugin {
}
);
+ $this->container->set(
+ 'google_oauth_client',
+ function () {
+ return new Groq_AI_Google_OAuth_Client();
+ }
+ );
+
+ $this->container->set(
+ 'gsc_client',
+ function ( Groq_AI_Service_Container $container ) {
+ return new Groq_AI_Google_Search_Console_Client( $container->get( 'google_oauth_client' ) );
+ }
+ );
+
+ $this->container->set(
+ 'ga_client',
+ function ( Groq_AI_Service_Container $container ) {
+ return new Groq_AI_Google_Analytics_Data_Client( $container->get( 'google_oauth_client' ) );
+ }
+ );
+
+ $this->container->set(
+ 'google_context_builder',
+ function ( Groq_AI_Service_Container $container ) {
+ return new Groq_AI_Google_Context_Builder( $container->get( 'gsc_client' ), $container->get( 'ga_client' ) );
+ }
+ );
+
// Instantiate controller immediately so hooks are registered.
$this->container->get( 'ajax_controller' );
}
+ public function inject_google_term_context( $existing, $term, $settings ) {
+ $builder = $this->container->get( 'google_context_builder' );
+ if ( ! $builder ) {
+ return (string) $existing;
+ }
+
+ return $builder->build_term_google_context( $existing, $term, $settings );
+ }
+
public function get_option_key() {
return self::OPTION_KEY;
}
diff --git a/includes/Admin/class-groq-ai-settings-page.php b/includes/Admin/class-groq-ai-settings-page.php
index ff3ef15..f0fc26b 100644
--- a/includes/Admin/class-groq-ai-settings-page.php
+++ b/includes/Admin/class-groq-ai-settings-page.php
@@ -3,6 +3,7 @@
class Groq_AI_Product_Text_Settings_Page {
private $plugin;
private $provider_manager;
+ private $brand_taxonomy = null;
public function __construct( $plugin, Groq_AI_Provider_Manager $provider_manager ) {
$this->plugin = $plugin;
@@ -12,6 +13,11 @@ class Groq_AI_Product_Text_Settings_Page {
add_action( 'admin_init', [ $this, 'register_settings' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_settings_assets' ] );
add_action( 'admin_head', [ $this, 'hide_menu_links' ] );
+ add_action( 'admin_post_groq_ai_google_oauth_start', [ $this, 'handle_google_oauth_start' ] );
+ add_action( 'admin_post_groq_ai_google_oauth_callback', [ $this, 'handle_google_oauth_callback' ] );
+ add_action( 'admin_post_groq_ai_google_oauth_disconnect', [ $this, 'handle_google_oauth_disconnect' ] );
+ add_action( 'admin_post_groq_ai_save_term_content', [ $this, 'handle_save_term_content' ] );
+ add_action( 'admin_post_groq_ai_google_test_connection', [ $this, 'handle_google_test_connection' ] );
}
public function register_settings_pages() {
@@ -23,6 +29,33 @@ class Groq_AI_Product_Text_Settings_Page {
[ $this, 'render_settings_page' ]
);
+ add_submenu_page(
+ 'options-general.php',
+ __( 'Siti AI Categorie teksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ __( 'Siti AI Categorieën', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ 'manage_options',
+ 'groq-ai-product-text-categories',
+ [ $this, 'render_categories_overview_page' ]
+ );
+
+ add_submenu_page(
+ 'options-general.php',
+ __( 'Siti AI Merk teksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ __( 'Siti AI Merken', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ 'manage_options',
+ 'groq-ai-product-text-brands',
+ [ $this, 'render_brands_overview_page' ]
+ );
+
+ add_submenu_page(
+ 'options-general.php',
+ __( 'Siti AI Term tekst', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ __( 'Siti AI Term tekst', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ 'manage_options',
+ 'groq-ai-product-text-term',
+ [ $this, 'render_term_generator_page' ]
+ );
+
add_submenu_page(
'options-general.php',
__( 'Siti AI Modules', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
@@ -60,13 +93,338 @@ class Groq_AI_Product_Text_Settings_Page {
brand_taxonomy ) {
+ return $this->brand_taxonomy;
+ }
+
+ $candidates = [
+ 'product_brand',
+ 'pwb-brand',
+ 'yith_product_brand',
+ 'berocket_brand',
+ ];
+
+ // Attribute-taxonomy fallback (vaak pa_brand).
+ if ( taxonomy_exists( 'pa_brand' ) ) {
+ array_unshift( $candidates, 'pa_brand' );
+ }
+
+ $candidates = apply_filters( 'groq_ai_brand_taxonomy_candidates', $candidates );
+ $found = '';
+ foreach ( $candidates as $tax ) {
+ $tax = sanitize_key( (string) $tax );
+ if ( $tax && taxonomy_exists( $tax ) ) {
+ $found = $tax;
+ break;
+ }
+ }
+
+ $found = apply_filters( 'groq_ai_brand_taxonomy', $found );
+ $this->brand_taxonomy = sanitize_key( (string) $found );
+ return $this->brand_taxonomy;
+ }
+
+ private function get_term_page_url( $taxonomy, $term_id ) {
+ return add_query_arg(
+ [
+ 'page' => 'groq-ai-product-text-term',
+ 'taxonomy' => sanitize_key( (string) $taxonomy ),
+ 'term_id' => absint( $term_id ),
+ ],
+ admin_url( 'options-general.php' )
+ );
+ }
+
+ public function render_categories_overview_page() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ return;
+ }
+
+ $terms = get_terms(
+ [
+ 'taxonomy' => 'product_cat',
+ 'hide_empty' => false,
+ 'number' => 0,
+ ]
+ );
+ if ( is_wp_error( $terms ) ) {
+ $terms = [];
+ }
+ ?>
+
+
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+
+ |
+
+
+ get_term_page_url( 'product_cat', $term->term_id );
+ $words = $this->count_words( $term->description );
+ $count = isset( $term->count ) ? absint( $term->count ) : 0;
+ ?>
+
+ |
+ name ); ?>
+ |
+ slug ); ?> |
+ |
+ |
+
+
+
+
+
+
+ detect_brand_taxonomy();
+ if ( '' === $taxonomy ) {
+ ?>
+
+ $taxonomy,
+ 'hide_empty' => false,
+ 'number' => 0,
+ ]
+ );
+ if ( is_wp_error( $terms ) ) {
+ $terms = [];
+ }
+ ?>
+
+
+
+
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+
+ |
+
+
+ get_term_page_url( $taxonomy, $term->term_id );
+ $words = $this->count_words( $term->description );
+ $count = isset( $term->count ) ? absint( $term->count ) : 0;
+ ?>
+
+ |
+ name ); ?>
+ |
+ slug ); ?> |
+ |
+ |
+
+
+
+
+
+
+
+
+
+
+ count_words( $term->description );
+ $meta_prompt = get_term_meta( $term_id, 'groq_ai_term_custom_prompt', true );
+ $default_prompt = (string) $meta_prompt;
+ if ( '' === trim( $default_prompt ) ) {
+ $default_prompt = __( 'Schrijf een SEO-vriendelijke categorieomschrijving in het Nederlands. Gebruik duidelijke tussenkoppen en -tags. Voeg geen prijsinformatie toe.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
+ }
+ ?>
+
+
+ : name ); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ get_settings_page_url() );
+ exit;
+ }
+
+ $result = wp_update_term(
+ $term_id,
+ $taxonomy,
+ [
+ 'description' => $description,
+ ]
+ );
+
+ if ( ! is_wp_error( $result ) ) {
+ update_term_meta( $term_id, 'groq_ai_term_custom_prompt', $custom_prompt );
+ }
+
+ wp_safe_redirect( $this->get_term_page_url( $taxonomy, $term_id ) );
+ exit;
+ }
+
public function register_settings() {
register_setting( 'groq_ai_product_text_group', $this->plugin->get_option_key(), [ $this->plugin, 'sanitize_settings' ] );
@@ -77,6 +435,13 @@ class Groq_AI_Product_Text_Settings_Page {
'groq-ai-product-text'
);
+ add_settings_section(
+ 'groq_ai_product_text_google',
+ __( 'Google koppeling (OAuth)', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ '__return_false',
+ 'groq-ai-product-text'
+ );
+
add_settings_field(
'groq_ai_provider',
__( 'AI-aanbieder', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
@@ -93,6 +458,54 @@ class Groq_AI_Product_Text_Settings_Page {
'groq_ai_product_text_general'
);
+ add_settings_field(
+ 'groq_ai_google_oauth_client_id',
+ __( 'Google Client ID', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ [ $this, 'render_google_oauth_client_id_field' ],
+ 'groq-ai-product-text',
+ 'groq_ai_product_text_google'
+ );
+
+ add_settings_field(
+ 'groq_ai_google_oauth_client_secret',
+ __( 'Google Client secret', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ [ $this, 'render_google_oauth_client_secret_field' ],
+ 'groq-ai-product-text',
+ 'groq_ai_product_text_google'
+ );
+
+ add_settings_field(
+ 'groq_ai_google_oauth_status',
+ __( 'Google status', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ [ $this, 'render_google_oauth_status_field' ],
+ 'groq-ai-product-text',
+ 'groq_ai_product_text_google'
+ );
+
+ add_settings_field(
+ 'groq_ai_google_gsc_site_url',
+ __( 'Search Console site URL', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ [ $this, 'render_google_gsc_site_url_field' ],
+ 'groq-ai-product-text',
+ 'groq_ai_product_text_google'
+ );
+
+ add_settings_field(
+ 'groq_ai_google_ga4_property_id',
+ __( 'GA4 property ID', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ [ $this, 'render_google_ga4_property_id_field' ],
+ 'groq-ai-product-text',
+ 'groq_ai_product_text_google'
+ );
+
+ add_settings_field(
+ 'groq_ai_google_context_toggles',
+ __( 'Google data gebruiken', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ [ $this, 'render_google_context_toggles_field' ],
+ 'groq-ai-product-text',
+ 'groq_ai_product_text_google'
+ );
+
foreach ( $this->provider_manager->get_providers() as $provider ) {
add_settings_field(
'groq_ai_api_key_' . $provider->get_key(),
@@ -223,6 +636,7 @@ class Groq_AI_Product_Text_Settings_Page {
?>
+ render_google_oauth_admin_notice(); ?>
@@ -246,6 +660,521 @@ class Groq_AI_Product_Text_Settings_Page {
+
+ plugin->get_settings();
+ $value = isset( $settings['google_oauth_client_id'] ) ? $settings['google_oauth_client_id'] : '';
+ ?>
+
+
+
+
+ plugin->get_settings();
+ $value = isset( $settings['google_oauth_client_secret'] ) ? $settings['google_oauth_client_secret'] : '';
+ ?>
+
+
+
+
+ plugin->get_settings();
+ $connected = ! empty( $settings['google_oauth_refresh_token'] );
+ $email = isset( $settings['google_oauth_connected_email'] ) ? $settings['google_oauth_connected_email'] : '';
+ $connected_at = isset( $settings['google_oauth_connected_at'] ) ? absint( $settings['google_oauth_connected_at'] ) : 0;
+ $redirect_uri = $this->get_google_oauth_redirect_uri();
+
+ $start_url = wp_nonce_url(
+ admin_url( 'admin-post.php?action=groq_ai_google_oauth_start' ),
+ 'groq_ai_google_oauth_start',
+ '_wpnonce'
+ );
+
+ $disconnect_url = wp_nonce_url(
+ admin_url( 'admin-post.php?action=groq_ai_google_oauth_disconnect' ),
+ 'groq_ai_google_oauth_disconnect',
+ '_wpnonce'
+ );
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ —
+
+
+ ()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ plugin->get_settings();
+ $messages = [];
+ $status = 'success';
+
+ $oauth = new Groq_AI_Google_OAuth_Client();
+ $token = $oauth->get_access_token( $settings );
+ if ( is_wp_error( $token ) ) {
+ $status = 'error';
+ $messages[] = sprintf( __( 'OAuth: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $token->get_error_message() );
+ } else {
+ $messages[] = __( 'OAuth: OK (access token opgehaald).', GROQ_AI_PRODUCT_TEXT_DOMAIN );
+ $info = $oauth->get_access_token_info( $token );
+ if ( is_array( $info ) ) {
+ $scope = isset( $info['scope'] ) ? trim( (string) $info['scope'] ) : '';
+ if ( '' !== $scope ) {
+ $messages[] = sprintf( __( 'OAuth scopes: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $scope );
+ if ( false === strpos( $scope, 'https://www.googleapis.com/auth/webmasters' ) ) {
+ $messages[] = __( 'Tip: je access token mist Search Console scope. Klik op "Opnieuw verbinden" zodat je toestemming opnieuw wordt gevraagd.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
+ }
+ }
+ }
+ }
+
+ $range_days = 7;
+ $end_date = gmdate( 'Y-m-d' );
+ $start_date = gmdate( 'Y-m-d', time() - ( $range_days * DAY_IN_SECONDS ) );
+
+ if ( 'error' !== $status && ! empty( $settings['google_enable_gsc'] ) ) {
+ $gsc = new Groq_AI_Google_Search_Console_Client( $oauth );
+ $sites = $gsc->list_sites( $settings );
+ if ( is_wp_error( $sites ) ) {
+ $status = 'error';
+ $messages[] = sprintf( __( 'Search Console: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $sites->get_error_message() );
+ } else {
+ $count = is_array( $sites ) ? count( $sites ) : 0;
+ $messages[] = sprintf( __( 'Search Console: OK (%d properties zichtbaar).', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $count );
+ $site_url = isset( $settings['google_gsc_site_url'] ) ? trim( (string) $settings['google_gsc_site_url'] ) : '';
+ if ( '' !== $site_url && is_array( $sites ) && ! in_array( $site_url, $sites, true ) ) {
+ $messages[] = __( 'Let op: de ingestelde site URL is niet gevonden in jouw zichtbare GSC properties.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
+ }
+ }
+ }
+
+ if ( 'error' !== $status && ! empty( $settings['google_enable_ga'] ) ) {
+ $property_id = isset( $settings['google_ga4_property_id'] ) ? trim( (string) $settings['google_ga4_property_id'] ) : '';
+ if ( '' !== $property_id ) {
+ $ga = new Groq_AI_Google_Analytics_Data_Client( $oauth );
+ $stats = $ga->get_property_sessions_summary( $settings, $property_id, $start_date, $end_date );
+ if ( is_wp_error( $stats ) ) {
+ $status = 'error';
+ $messages[] = sprintf( __( 'Analytics: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $stats->get_error_message() );
+ } else {
+ $sessions = isset( $stats['sessions'] ) ? absint( $stats['sessions'] ) : 0;
+ $messages[] = sprintf( __( 'Analytics: OK (sessies laatste %1$d dagen: ~%2$d).', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $range_days, $sessions );
+ }
+ } else {
+ $messages[] = __( 'Analytics: overgeslagen (GA4 property ID niet ingevuld).', GROQ_AI_PRODUCT_TEXT_DOMAIN );
+ }
+ }
+
+ $url = add_query_arg(
+ [
+ 'groq_ai_google_oauth' => $status,
+ 'groq_ai_google_oauth_message' => implode( ' ', $messages ),
+ ],
+ $this->get_settings_page_url()
+ );
+ wp_safe_redirect( $url );
+ exit;
+ }
+
+ public function render_google_gsc_site_url_field() {
+ $settings = $this->plugin->get_settings();
+ $value = isset( $settings['google_gsc_site_url'] ) ? (string) $settings['google_gsc_site_url'] : '';
+ ?>
+
+
+
+
+ plugin->get_settings();
+ $value = isset( $settings['google_ga4_property_id'] ) ? (string) $settings['google_ga4_property_id'] : '';
+ ?>
+
+
+
+
+ plugin->get_settings();
+ $gsc = ! empty( $settings['google_enable_gsc'] );
+ $ga = ! empty( $settings['google_enable_ga'] );
+ ?>
+
+
+ plugin->get_settings();
+ $client_id = isset( $settings['google_oauth_client_id'] ) ? trim( (string) $settings['google_oauth_client_id'] ) : '';
+ $client_secret = isset( $settings['google_oauth_client_secret'] ) ? trim( (string) $settings['google_oauth_client_secret'] ) : '';
+
+ if ( '' === $client_id || '' === $client_secret ) {
+ $url = add_query_arg(
+ [
+ 'groq_ai_google_oauth' => 'error',
+ 'groq_ai_google_oauth_message' => __( 'Vul eerst Google Client ID en Client secret in.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ ],
+ $this->get_settings_page_url()
+ );
+ wp_safe_redirect( $url );
+ exit;
+ }
+
+ $state = wp_generate_password( 32, false, false );
+ set_transient( $this->get_google_oauth_state_key(), $state, 10 * MINUTE_IN_SECONDS );
+
+ $scope = implode( ' ', $this->get_google_oauth_scopes() );
+ $redirect_uri = $this->get_google_oauth_redirect_uri();
+
+ $auth_url = add_query_arg(
+ [
+ 'client_id' => $client_id,
+ 'redirect_uri' => $redirect_uri,
+ 'response_type' => 'code',
+ 'access_type' => 'offline',
+ 'prompt' => 'consent',
+ 'include_granted_scopes' => 'true',
+ 'scope' => $scope,
+ 'state' => $state,
+ ],
+ 'https://accounts.google.com/o/oauth2/v2/auth'
+ );
+ $auth_url = esc_url_raw( $auth_url );
+ $parsed = wp_parse_url( $auth_url );
+ $host = isset( $parsed['host'] ) ? strtolower( (string) $parsed['host'] ) : '';
+ if ( 'accounts.google.com' !== $host ) {
+ $url = add_query_arg(
+ [
+ 'groq_ai_google_oauth' => 'error',
+ 'groq_ai_google_oauth_message' => __( 'OAuth URL ongeldig. Controleer plugin instellingen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ ],
+ $this->get_settings_page_url()
+ );
+ wp_safe_redirect( $url );
+ exit;
+ }
+
+ // Let op: wp_safe_redirect staat standaard geen externe hosts toe en valt dan terug naar /wp-admin.
+ wp_redirect( $auth_url );
+ exit;
+ }
+
+ public function handle_google_oauth_callback() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'Geen toestemming.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
+ }
+
+ $expected_state = get_transient( $this->get_google_oauth_state_key() );
+ delete_transient( $this->get_google_oauth_state_key() );
+
+ $state = isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : '';
+ $code = isset( $_GET['code'] ) ? sanitize_text_field( wp_unslash( $_GET['code'] ) ) : '';
+ $error = isset( $_GET['error'] ) ? sanitize_text_field( wp_unslash( $_GET['error'] ) ) : '';
+
+ if ( '' !== $error ) {
+ $url = add_query_arg(
+ [
+ 'groq_ai_google_oauth' => 'error',
+ 'groq_ai_google_oauth_message' => sprintf( __( 'Google OAuth error: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $error ),
+ ],
+ $this->get_settings_page_url()
+ );
+ wp_safe_redirect( $url );
+ exit;
+ }
+
+ if ( empty( $expected_state ) || '' === $state || $state !== $expected_state ) {
+ $url = add_query_arg(
+ [
+ 'groq_ai_google_oauth' => 'error',
+ 'groq_ai_google_oauth_message' => __( 'Ongeldige OAuth state. Probeer opnieuw te verbinden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ ],
+ $this->get_settings_page_url()
+ );
+ wp_safe_redirect( $url );
+ exit;
+ }
+
+ if ( '' === $code ) {
+ $url = add_query_arg(
+ [
+ 'groq_ai_google_oauth' => 'error',
+ 'groq_ai_google_oauth_message' => __( 'Geen OAuth code ontvangen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ ],
+ $this->get_settings_page_url()
+ );
+ wp_safe_redirect( $url );
+ exit;
+ }
+
+ $settings = $this->plugin->get_settings();
+ $client_id = isset( $settings['google_oauth_client_id'] ) ? trim( (string) $settings['google_oauth_client_id'] ) : '';
+ $client_secret = isset( $settings['google_oauth_client_secret'] ) ? trim( (string) $settings['google_oauth_client_secret'] ) : '';
+ $redirect_uri = $this->get_google_oauth_redirect_uri();
+
+ if ( '' === $client_id || '' === $client_secret ) {
+ $url = add_query_arg(
+ [
+ 'groq_ai_google_oauth' => 'error',
+ 'groq_ai_google_oauth_message' => __( 'Client ID/secret ontbreken. Sla eerst de instellingen op.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ ],
+ $this->get_settings_page_url()
+ );
+ wp_safe_redirect( $url );
+ exit;
+ }
+
+ $token_response = wp_remote_post(
+ 'https://oauth2.googleapis.com/token',
+ [
+ 'timeout' => 20,
+ 'headers' => [
+ 'Content-Type' => 'application/x-www-form-urlencoded',
+ ],
+ 'body' => [
+ 'code' => $code,
+ 'client_id' => $client_id,
+ 'client_secret' => $client_secret,
+ 'redirect_uri' => $redirect_uri,
+ 'grant_type' => 'authorization_code',
+ ],
+ ]
+ );
+
+ if ( is_wp_error( $token_response ) ) {
+ $url = add_query_arg(
+ [
+ 'groq_ai_google_oauth' => 'error',
+ 'groq_ai_google_oauth_message' => $token_response->get_error_message(),
+ ],
+ $this->get_settings_page_url()
+ );
+ wp_safe_redirect( $url );
+ exit;
+ }
+
+ $status_code = wp_remote_retrieve_response_code( $token_response );
+ $body = wp_remote_retrieve_body( $token_response );
+ $data = json_decode( (string) $body, true );
+
+ if ( 200 !== $status_code || ! is_array( $data ) ) {
+ $url = add_query_arg(
+ [
+ 'groq_ai_google_oauth' => 'error',
+ 'groq_ai_google_oauth_message' => __( 'Token exchange mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ ],
+ $this->get_settings_page_url()
+ );
+ wp_safe_redirect( $url );
+ exit;
+ }
+
+ $access_token = isset( $data['access_token'] ) ? sanitize_text_field( (string) $data['access_token'] ) : '';
+ $refresh_token = isset( $data['refresh_token'] ) ? sanitize_text_field( (string) $data['refresh_token'] ) : '';
+
+ if ( '' === $refresh_token ) {
+ $refresh_token = isset( $settings['google_oauth_refresh_token'] ) ? sanitize_text_field( (string) $settings['google_oauth_refresh_token'] ) : '';
+ }
+
+ $connected_email = '';
+ if ( '' !== $access_token ) {
+ $userinfo_response = wp_remote_get(
+ 'https://openidconnect.googleapis.com/v1/userinfo',
+ [
+ 'timeout' => 20,
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . $access_token,
+ ],
+ ]
+ );
+ if ( ! is_wp_error( $userinfo_response ) && 200 === wp_remote_retrieve_response_code( $userinfo_response ) ) {
+ $userinfo_body = wp_remote_retrieve_body( $userinfo_response );
+ $userinfo_data = json_decode( (string) $userinfo_body, true );
+ if ( is_array( $userinfo_data ) && ! empty( $userinfo_data['email'] ) ) {
+ $connected_email = sanitize_email( (string) $userinfo_data['email'] );
+ }
+ }
+ }
+
+ $options = get_option( $this->plugin->get_option_key(), [] );
+ if ( ! is_array( $options ) ) {
+ $options = [];
+ }
+
+ $options['google_oauth_refresh_token'] = $refresh_token;
+ $options['google_oauth_connected_email'] = $connected_email;
+ $options['google_oauth_connected_at'] = time();
+ update_option( $this->plugin->get_option_key(), $options );
+
+ $url = add_query_arg(
+ [
+ 'groq_ai_google_oauth' => 'success',
+ 'groq_ai_google_oauth_message' => __( 'Google succesvol verbonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ ],
+ $this->get_settings_page_url()
+ );
+ wp_safe_redirect( $url );
+ exit;
+ }
+
+ public function handle_google_oauth_disconnect() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'Geen toestemming.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
+ }
+
+ check_admin_referer( 'groq_ai_google_oauth_disconnect' );
+
+ $options = get_option( $this->plugin->get_option_key(), [] );
+ if ( ! is_array( $options ) ) {
+ $options = [];
+ }
+
+ $options['google_oauth_refresh_token'] = '';
+ $options['google_oauth_connected_email'] = '';
+ $options['google_oauth_connected_at'] = 0;
+ update_option( $this->plugin->get_option_key(), $options );
+
+ $url = add_query_arg(
+ [
+ 'groq_ai_google_oauth' => 'success',
+ 'groq_ai_google_oauth_message' => __( 'Google koppeling verwijderd.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ ],
+ $this->get_settings_page_url()
+ );
+ wp_safe_redirect( $url );
+ exit;
+ }
+
public function render_modules_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
@@ -648,7 +1577,14 @@ class Groq_AI_Product_Text_Settings_Page {
}
public function enqueue_settings_assets( $hook ) {
- if ( ! in_array( $hook, [ 'settings_page_groq-ai-product-text', 'settings_page_groq-ai-product-text-modules', 'settings_page_groq-ai-product-text-prompts' ], true ) ) {
+ if ( ! in_array( $hook, [
+ 'settings_page_groq-ai-product-text',
+ 'settings_page_groq-ai-product-text-modules',
+ 'settings_page_groq-ai-product-text-prompts',
+ 'settings_page_groq-ai-product-text-categories',
+ 'settings_page_groq-ai-product-text-brands',
+ 'settings_page_groq-ai-product-text-term',
+ ], true ) ) {
return;
}
@@ -674,6 +1610,29 @@ class Groq_AI_Product_Text_Settings_Page {
true
);
+ if ( 'settings_page_groq-ai-product-text-term' === $hook ) {
+ wp_enqueue_script(
+ 'groq-ai-term-admin',
+ plugins_url( 'assets/js/term-admin.js', GROQ_AI_PRODUCT_TEXT_FILE ),
+ [],
+ GROQ_AI_PRODUCT_TEXT_VERSION,
+ true
+ );
+
+ $taxonomy = isset( $_GET['taxonomy'] ) ? sanitize_key( wp_unslash( $_GET['taxonomy'] ) ) : '';
+ $term_id = isset( $_GET['term_id'] ) ? absint( $_GET['term_id'] ) : 0;
+ wp_localize_script(
+ 'groq-ai-term-admin',
+ 'GroqAITermGenerator',
+ [
+ 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
+ 'nonce' => wp_create_nonce( 'groq_ai_generate_term' ),
+ 'taxonomy' => $taxonomy,
+ 'termId' => $term_id,
+ ]
+ );
+ }
+
$current_settings = $this->plugin->get_settings();
$data = [
'optionKey' => $this->plugin->get_option_key(),
diff --git a/includes/Core/class-groq-ai-ajax-controller.php b/includes/Core/class-groq-ai-ajax-controller.php
index 26726a0..40ff885 100644
--- a/includes/Core/class-groq-ai-ajax-controller.php
+++ b/includes/Core/class-groq-ai-ajax-controller.php
@@ -9,6 +9,108 @@ class Groq_AI_Ajax_Controller {
add_action( 'wp_ajax_groq_ai_generate_text', [ $this, 'handle_generate_text' ] );
add_action( 'wp_ajax_groq_ai_refresh_models', [ $this, 'handle_refresh_models' ] );
+ add_action( 'wp_ajax_groq_ai_generate_term_text', [ $this, 'handle_generate_term_text' ] );
+ }
+
+ public function handle_generate_term_text() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error( [ 'message' => __( 'Je hebt geen toestemming voor deze actie.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 403 );
+ }
+
+ check_ajax_referer( 'groq_ai_generate_term', 'nonce' );
+
+ $prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['prompt'] ) ) : '';
+ $taxonomy = isset( $_POST['taxonomy'] ) ? sanitize_key( wp_unslash( $_POST['taxonomy'] ) ) : '';
+ $term_id = isset( $_POST['term_id'] ) ? absint( $_POST['term_id'] ) : 0;
+ $include_top_products = ! empty( $_POST['include_top_products'] );
+ $top_products_limit = isset( $_POST['top_products_limit'] ) ? absint( $_POST['top_products_limit'] ) : 10;
+ $top_products_limit = max( 1, min( 25, $top_products_limit ) );
+
+ if ( '' === $prompt || '' === $taxonomy || ! taxonomy_exists( $taxonomy ) || ! $term_id ) {
+ wp_send_json_error( [ 'message' => __( 'Prompt, taxonomy en term_id zijn verplicht.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 400 );
+ }
+
+ $term = get_term( $term_id, $taxonomy );
+ if ( ! $term || is_wp_error( $term ) ) {
+ wp_send_json_error( [ 'message' => __( 'Term niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 404 );
+ }
+
+ $settings = $this->plugin->get_settings();
+ $provider_manager = $this->plugin->get_provider_manager();
+ $provider_key = $settings['provider'];
+ $provider = $provider_manager->get_provider( $provider_key );
+
+ if ( ! $provider ) {
+ $provider = $provider_manager->get_provider( 'groq' );
+ $provider_key = 'groq';
+ }
+
+ $conversation_id = $this->plugin->get_conversation_manager()->ensure_id( $provider_key, $settings['store_context'] );
+ $prompt_builder = $this->plugin->get_prompt_builder();
+ $system_prompt = method_exists( $prompt_builder, 'build_term_system_prompt' )
+ ? $prompt_builder->build_term_system_prompt( $settings, $conversation_id, $term )
+ : $prompt_builder->build_system_prompt( $settings, $conversation_id );
+
+ $model = $this->plugin->get_selected_model( $provider, $settings );
+ $context_block = '';
+ if ( method_exists( $prompt_builder, 'build_term_context_block' ) ) {
+ $context_block = $prompt_builder->build_term_context_block(
+ $term,
+ [
+ 'include_top_products' => $include_top_products,
+ 'top_products_limit' => $top_products_limit,
+ ],
+ $settings
+ );
+ }
+ $prompt_with_context = method_exists( $prompt_builder, 'prepend_term_context_to_prompt' )
+ ? $prompt_builder->prepend_term_context_to_prompt( $prompt, $context_block )
+ : $prompt_builder->prepend_context_to_prompt( $prompt, $context_block );
+
+ $response_format = null;
+ $use_response_format = $this->plugin->should_use_response_format( $provider, $settings );
+ if ( $use_response_format && method_exists( $prompt_builder, 'get_term_response_format_definition' ) ) {
+ $response_format = $prompt_builder->get_term_response_format_definition( $settings );
+ $final_prompt = $prompt_with_context;
+ } elseif ( method_exists( $prompt_builder, 'append_term_response_instructions' ) ) {
+ $final_prompt = $prompt_builder->append_term_response_instructions( $prompt_with_context, $settings );
+ } else {
+ $final_prompt = $prompt_builder->append_response_instructions( $prompt_with_context, $settings );
+ }
+
+ $result = $provider->generate_content(
+ [
+ 'prompt' => $final_prompt,
+ 'system_prompt' => $system_prompt,
+ 'model' => $model,
+ 'settings' => $settings,
+ 'temperature' => 0.7,
+ 'conversation_id' => $conversation_id,
+ 'response_format' => $response_format,
+ ]
+ );
+
+ if ( is_wp_error( $result ) ) {
+ wp_send_json_error( [ 'message' => $result->get_error_message() ], 500 );
+ }
+
+ $response_text = $this->extract_content_text( $result );
+ $parsed = null;
+ if ( method_exists( $prompt_builder, 'parse_term_structured_response' ) ) {
+ $parsed = $prompt_builder->parse_term_structured_response( $response_text, $settings );
+ }
+ if ( ! is_array( $parsed ) ) {
+ $parsed = [
+ 'description' => trim( (string) $response_text ),
+ ];
+ }
+
+ wp_send_json_success(
+ [
+ 'description' => isset( $parsed['description'] ) ? $parsed['description'] : '',
+ 'raw' => $response_text,
+ ]
+ );
}
public function handle_generate_text() {
diff --git a/includes/Services/Google/class-groq-ai-google-analytics-data-client.php b/includes/Services/Google/class-groq-ai-google-analytics-data-client.php
new file mode 100644
index 0000000..590dcc6
--- /dev/null
+++ b/includes/Services/Google/class-groq-ai-google-analytics-data-client.php
@@ -0,0 +1,182 @@
+oauth = $oauth;
+ }
+
+ /**
+ * Simple connectivity check for GA4 Data API.
+ *
+ * @param array $settings
+ * @param string $property_id
+ * @param string $start_date
+ * @param string $end_date
+ * @return array|WP_Error
+ */
+ public function get_property_sessions_summary( $settings, $property_id, $start_date, $end_date ) {
+ $property_id = trim( (string) $property_id );
+ if ( '' === $property_id ) {
+ return new WP_Error( 'groq_ai_ga_missing', __( 'GA4 property ID ontbreekt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
+ }
+
+ $token = $this->oauth->get_access_token( $settings );
+ if ( is_wp_error( $token ) ) {
+ return $token;
+ }
+
+ $endpoint = 'https://analyticsdata.googleapis.com/v1beta/properties/' . rawurlencode( $property_id ) . ':runReport';
+ $body = [
+ 'dateRanges' => [
+ [
+ 'startDate' => $start_date,
+ 'endDate' => $end_date,
+ ],
+ ],
+ 'metrics' => [
+ [ 'name' => 'sessions' ],
+ [ 'name' => 'engagedSessions' ],
+ ],
+ 'limit' => 1,
+ ];
+
+ $response = wp_remote_post(
+ $endpoint,
+ [
+ 'timeout' => 20,
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . $token,
+ 'Content-Type' => 'application/json',
+ ],
+ 'body' => wp_json_encode( $body ),
+ ]
+ );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ $status_code = wp_remote_retrieve_response_code( $response );
+ $raw_body = wp_remote_retrieve_body( $response );
+ $data = json_decode( (string) $raw_body, true );
+
+ if ( 200 !== $status_code || ! is_array( $data ) ) {
+ return new WP_Error( 'groq_ai_ga_error', __( 'GA4 Data API call mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
+ }
+
+ $rows = isset( $data['rows'] ) && is_array( $data['rows'] ) ? $data['rows'] : [];
+ $sessions = 0;
+ $engaged = 0;
+ foreach ( $rows as $row ) {
+ $metric_values = isset( $row['metricValues'] ) && is_array( $row['metricValues'] ) ? $row['metricValues'] : [];
+ if ( isset( $metric_values[0]['value'] ) ) {
+ $sessions += absint( $metric_values[0]['value'] );
+ }
+ if ( isset( $metric_values[1]['value'] ) ) {
+ $engaged += absint( $metric_values[1]['value'] );
+ }
+ }
+
+ return [
+ 'sessions' => $sessions,
+ 'engagedSessions' => $engaged,
+ ];
+ }
+
+ /**
+ * Returns approximate GA4 sessions for a landing page path.
+ *
+ * @param array $settings
+ * @param string $property_id
+ * @param string $page_path e.g. /product-category/foo/
+ * @param string $start_date YYYY-MM-DD
+ * @param string $end_date YYYY-MM-DD
+ * @return array|WP_Error
+ */
+ public function get_sessions_for_landing_page_path( $settings, $property_id, $page_path, $start_date, $end_date ) {
+ $property_id = trim( (string) $property_id );
+ $page_path = trim( (string) $page_path );
+
+ if ( '' === $property_id || '' === $page_path ) {
+ return new WP_Error( 'groq_ai_ga_missing', __( 'GA4 property ID of page path ontbreekt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
+ }
+
+ $token = $this->oauth->get_access_token( $settings );
+ if ( is_wp_error( $token ) ) {
+ return $token;
+ }
+
+ $endpoint = 'https://analyticsdata.googleapis.com/v1beta/properties/' . rawurlencode( $property_id ) . ':runReport';
+
+ $body = [
+ 'dateRanges' => [
+ [
+ 'startDate' => $start_date,
+ 'endDate' => $end_date,
+ ],
+ ],
+ 'dimensions' => [
+ [ 'name' => 'landingPagePlusQueryString' ],
+ ],
+ 'metrics' => [
+ [ 'name' => 'sessions' ],
+ [ 'name' => 'engagedSessions' ],
+ ],
+ 'dimensionFilter' => [
+ 'filter' => [
+ 'fieldName' => 'landingPagePlusQueryString',
+ 'stringFilter' => [
+ 'matchType' => 'CONTAINS',
+ 'value' => $page_path,
+ ],
+ ],
+ ],
+ 'limit' => 5,
+ ];
+
+ $response = wp_remote_post(
+ $endpoint,
+ [
+ 'timeout' => 20,
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . $token,
+ 'Content-Type' => 'application/json',
+ ],
+ 'body' => wp_json_encode( $body ),
+ ]
+ );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ $status_code = wp_remote_retrieve_response_code( $response );
+ $raw_body = wp_remote_retrieve_body( $response );
+ $data = json_decode( (string) $raw_body, true );
+
+ if ( 200 !== $status_code || ! is_array( $data ) ) {
+ return new WP_Error( 'groq_ai_ga_error', __( 'GA4 Data API call mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
+ }
+
+ $rows = isset( $data['rows'] ) && is_array( $data['rows'] ) ? $data['rows'] : [];
+ $sessions = 0;
+ $engaged = 0;
+ foreach ( $rows as $row ) {
+ $metric_values = isset( $row['metricValues'] ) && is_array( $row['metricValues'] ) ? $row['metricValues'] : [];
+ if ( isset( $metric_values[0]['value'] ) ) {
+ $sessions += absint( $metric_values[0]['value'] );
+ }
+ if ( isset( $metric_values[1]['value'] ) ) {
+ $engaged += absint( $metric_values[1]['value'] );
+ }
+ }
+
+ return [
+ 'sessions' => $sessions,
+ 'engagedSessions' => $engaged,
+ ];
+ }
+}
diff --git a/includes/Services/Google/class-groq-ai-google-context-builder.php b/includes/Services/Google/class-groq-ai-google-context-builder.php
new file mode 100644
index 0000000..0a515b2
--- /dev/null
+++ b/includes/Services/Google/class-groq-ai-google-context-builder.php
@@ -0,0 +1,113 @@
+gsc = $gsc;
+ $this->ga = $ga;
+ }
+
+ /**
+ * @param string $existing
+ * @param WP_Term $term
+ * @param array $settings
+ * @return string
+ */
+ public function build_term_google_context( $existing, $term, $settings ) {
+ if ( ! $term || ! is_object( $term ) ) {
+ return (string) $existing;
+ }
+
+ $enabled_gsc = ! empty( $settings['google_enable_gsc'] );
+ $enabled_ga = ! empty( $settings['google_enable_ga'] );
+
+ if ( ! $enabled_gsc && ! $enabled_ga ) {
+ return (string) $existing;
+ }
+
+ $term_id = isset( $term->term_id ) ? absint( $term->term_id ) : 0;
+ $taxonomy = isset( $term->taxonomy ) ? sanitize_key( (string) $term->taxonomy ) : '';
+
+ $range_days = 28;
+ $end_date = gmdate( 'Y-m-d' );
+ $start_date = gmdate( 'Y-m-d', time() - ( $range_days * DAY_IN_SECONDS ) );
+
+ $term_link = get_term_link( $term );
+ if ( is_wp_error( $term_link ) ) {
+ $term_link = '';
+ }
+
+ $page_path = '';
+ if ( is_string( $term_link ) && '' !== $term_link ) {
+ $parts = wp_parse_url( $term_link );
+ if ( is_array( $parts ) && isset( $parts['path'] ) ) {
+ $page_path = (string) $parts['path'];
+ }
+ }
+
+ $cache_key = 'groq_ai_google_term_ctx_' . md5( $taxonomy . '|' . $term_id . '|' . $start_date . '|' . $end_date );
+ $cached = get_transient( $cache_key );
+ if ( is_string( $cached ) && '' !== $cached ) {
+ return trim( (string) $existing . "\n\n" . $cached );
+ }
+
+ $lines = [];
+ $lines[] = sprintf(
+ /* translators: %d: days */
+ __( 'Google data (laatste %d dagen):', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ $range_days
+ );
+
+ if ( $enabled_gsc ) {
+ $site_url = isset( $settings['google_gsc_site_url'] ) ? trim( (string) $settings['google_gsc_site_url'] ) : '';
+ if ( '' !== $site_url && '' !== $term_link ) {
+ $queries = $this->gsc->get_top_queries_for_page( $settings, $site_url, $term_link, $start_date, $end_date, 10 );
+ if ( is_wp_error( $queries ) ) {
+ $lines[] = __( 'Search Console: kon queries niet ophalen.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
+ } elseif ( empty( $queries ) ) {
+ $lines[] = __( 'Search Console: geen query data gevonden voor deze pagina.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
+ } else {
+ $lines[] = __( 'Search Console top zoekopdrachten (query → clicks/impr):', GROQ_AI_PRODUCT_TEXT_DOMAIN );
+ foreach ( $queries as $row ) {
+ $q = isset( $row['query'] ) ? (string) $row['query'] : '';
+ $c = isset( $row['clicks'] ) ? (float) $row['clicks'] : 0.0;
+ $i = isset( $row['impressions'] ) ? (float) $row['impressions'] : 0.0;
+ if ( '' === $q ) {
+ continue;
+ }
+ $lines[] = sprintf( '- %s → %d/%d', $q, (int) round( $c ), (int) round( $i ) );
+ }
+ }
+ }
+ }
+
+ if ( $enabled_ga ) {
+ $property_id = isset( $settings['google_ga4_property_id'] ) ? trim( (string) $settings['google_ga4_property_id'] ) : '';
+ if ( '' !== $property_id && '' !== $page_path ) {
+ $stats = $this->ga->get_sessions_for_landing_page_path( $settings, $property_id, $page_path, $start_date, $end_date );
+ if ( is_wp_error( $stats ) ) {
+ $lines[] = __( 'Analytics: kon sessies niet ophalen.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
+ } else {
+ $sessions = isset( $stats['sessions'] ) ? absint( $stats['sessions'] ) : 0;
+ $engaged = isset( $stats['engagedSessions'] ) ? absint( $stats['engagedSessions'] ) : 0;
+ $lines[] = sprintf( __( 'Analytics (GA4): sessies ~%1$d, engaged sessies ~%2$d', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $sessions, $engaged );
+ }
+ }
+ }
+
+ // If we only have the header, skip.
+ if ( count( $lines ) <= 1 ) {
+ return (string) $existing;
+ }
+
+ $context = implode( "\n", $lines );
+ set_transient( $cache_key, $context, 15 * MINUTE_IN_SECONDS );
+
+ return trim( (string) $existing . "\n\n" . $context );
+ }
+}
diff --git a/includes/Services/Google/class-groq-ai-google-oauth-client.php b/includes/Services/Google/class-groq-ai-google-oauth-client.php
new file mode 100644
index 0000000..5bbdad7
--- /dev/null
+++ b/includes/Services/Google/class-groq-ai-google-oauth-client.php
@@ -0,0 +1,101 @@
+ 20,
+ 'headers' => [
+ 'Content-Type' => 'application/x-www-form-urlencoded',
+ ],
+ 'body' => [
+ 'client_id' => $client_id,
+ 'client_secret' => $client_secret,
+ 'refresh_token' => $refresh_token,
+ 'grant_type' => 'refresh_token',
+ ],
+ ]
+ );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ $status_code = wp_remote_retrieve_response_code( $response );
+ $body = wp_remote_retrieve_body( $response );
+ $data = json_decode( (string) $body, true );
+
+ if ( 200 !== $status_code || ! is_array( $data ) ) {
+ return new WP_Error( 'groq_ai_google_oauth_refresh_failed', __( 'Google token refresh mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
+ }
+
+ $access_token = isset( $data['access_token'] ) ? sanitize_text_field( (string) $data['access_token'] ) : '';
+ $expires_in = isset( $data['expires_in'] ) ? absint( $data['expires_in'] ) : 0;
+
+ if ( '' === $access_token ) {
+ return new WP_Error( 'groq_ai_google_oauth_refresh_failed', __( 'Geen access token ontvangen van Google.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
+ }
+
+ $ttl = max( 60, $expires_in - 60 );
+ set_transient( $cache_key, $access_token, $ttl );
+
+ return $access_token;
+ }
+
+ /**
+ * Diagnostics helper: returns scopes for a given access token.
+ *
+ * @param string $access_token
+ * @return array|WP_Error { 'scope' => string, 'expires_in' => int }
+ */
+ public function get_access_token_info( $access_token ) {
+ $access_token = trim( (string) $access_token );
+ if ( '' === $access_token ) {
+ return new WP_Error( 'groq_ai_google_tokeninfo_missing', __( 'Geen access token om te inspecteren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
+ }
+
+ $response = wp_remote_get(
+ add_query_arg( [ 'access_token' => $access_token ], 'https://oauth2.googleapis.com/tokeninfo' ),
+ [ 'timeout' => 15 ]
+ );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ $status_code = wp_remote_retrieve_response_code( $response );
+ $body = wp_remote_retrieve_body( $response );
+ $data = json_decode( (string) $body, true );
+
+ if ( 200 !== $status_code || ! is_array( $data ) ) {
+ return new WP_Error( 'groq_ai_google_tokeninfo_failed', __( 'Kon tokeninfo niet ophalen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
+ }
+
+ $scope = isset( $data['scope'] ) ? trim( (string) $data['scope'] ) : '';
+ $expires_in = isset( $data['expires_in'] ) ? absint( $data['expires_in'] ) : 0;
+
+ return [
+ 'scope' => $scope,
+ 'expires_in' => $expires_in,
+ ];
+ }
+}
diff --git a/includes/Services/Google/class-groq-ai-google-search-console-client.php b/includes/Services/Google/class-groq-ai-google-search-console-client.php
new file mode 100644
index 0000000..0d69f3e
--- /dev/null
+++ b/includes/Services/Google/class-groq-ai-google-search-console-client.php
@@ -0,0 +1,195 @@
+oauth = $oauth;
+ }
+
+ /**
+ * @param int $status_code
+ * @param string $raw_body
+ * @return WP_Error
+ */
+ private function build_http_error( $status_code, $raw_body ) {
+ $status_code = absint( $status_code );
+ $raw_body = (string) $raw_body;
+
+ $message = __( 'Search Console API call mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
+ $details = '';
+
+ $data = json_decode( $raw_body, true );
+ if ( is_array( $data ) ) {
+ // Google APIs often respond with: { error: { code, message, status, details/errors } }
+ $err = isset( $data['error'] ) && is_array( $data['error'] ) ? $data['error'] : [];
+ $google_message = isset( $err['message'] ) ? trim( (string) $err['message'] ) : '';
+ $google_status = isset( $err['status'] ) ? trim( (string) $err['status'] ) : '';
+ if ( '' !== $google_status || '' !== $google_message ) {
+ $details = trim( $google_status . ( $google_status && $google_message ? ': ' : '' ) . $google_message );
+ }
+ }
+
+ if ( '' !== $details ) {
+ $message = sprintf(
+ /* translators: 1: HTTP status, 2: details */
+ __( 'Search Console API call mislukt (HTTP %1$d): %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ $status_code,
+ $details
+ );
+ } else {
+ $message = sprintf(
+ /* translators: %d: HTTP status */
+ __( 'Search Console API call mislukt (HTTP %d).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ $status_code
+ );
+ }
+
+ return new WP_Error( 'groq_ai_gsc_error', $message );
+ }
+
+ /**
+ * @param array $settings
+ * @return array|WP_Error Array of siteUrl strings.
+ */
+ public function list_sites( $settings ) {
+ $token = $this->oauth->get_access_token( $settings );
+ if ( is_wp_error( $token ) ) {
+ return $token;
+ }
+
+ $response = wp_remote_get(
+ 'https://searchconsole.googleapis.com/webmasters/v3/sites',
+ [
+ 'timeout' => 20,
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . $token,
+ 'Accept' => 'application/json',
+ ],
+ ]
+ );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ $status_code = wp_remote_retrieve_response_code( $response );
+ $raw_body = wp_remote_retrieve_body( $response );
+ $data = json_decode( (string) $raw_body, true );
+
+ if ( 200 !== $status_code || ! is_array( $data ) ) {
+ return $this->build_http_error( $status_code, $raw_body );
+ }
+
+ $entries = isset( $data['siteEntry'] ) && is_array( $data['siteEntry'] ) ? $data['siteEntry'] : [];
+ $sites = [];
+ foreach ( $entries as $entry ) {
+ if ( ! is_array( $entry ) ) {
+ continue;
+ }
+ $site_url = isset( $entry['siteUrl'] ) ? trim( (string) $entry['siteUrl'] ) : '';
+ if ( '' !== $site_url ) {
+ $sites[] = $site_url;
+ }
+ }
+
+ $sites = array_values( array_unique( $sites ) );
+ sort( $sites, SORT_NATURAL | SORT_FLAG_CASE );
+
+ return $sites;
+ }
+
+ /**
+ * @param array $settings
+ * @param string $site_url
+ * @param string $page_url
+ * @param string $start_date YYYY-MM-DD
+ * @param string $end_date YYYY-MM-DD
+ * @param int $limit
+ * @return array|WP_Error
+ */
+ public function get_top_queries_for_page( $settings, $site_url, $page_url, $start_date, $end_date, $limit = 10 ) {
+ $site_url = trim( (string) $site_url );
+ $page_url = trim( (string) $page_url );
+ $limit = max( 1, min( 25, absint( $limit ) ) );
+
+ if ( '' === $site_url || '' === $page_url ) {
+ return new WP_Error( 'groq_ai_gsc_missing', __( 'Search Console site URL of pagina URL ontbreekt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
+ }
+
+ $token = $this->oauth->get_access_token( $settings );
+ if ( is_wp_error( $token ) ) {
+ return $token;
+ }
+
+ $endpoint = 'https://searchconsole.googleapis.com/webmasters/v3/sites/' . rawurlencode( $site_url ) . '/searchAnalytics/query';
+
+ $body = [
+ 'startDate' => $start_date,
+ 'endDate' => $end_date,
+ 'dimensions' => [ 'query' ],
+ 'rowLimit' => $limit,
+ 'dimensionFilterGroups' => [
+ [
+ 'filters' => [
+ [
+ 'dimension' => 'page',
+ 'operator' => 'equals',
+ 'expression' => $page_url,
+ ],
+ ],
+ ],
+ ],
+ 'aggregationType' => 'auto',
+ ];
+
+ $response = wp_remote_post(
+ $endpoint,
+ [
+ 'timeout' => 20,
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . $token,
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json',
+ ],
+ 'body' => wp_json_encode( $body ),
+ ]
+ );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ $status_code = wp_remote_retrieve_response_code( $response );
+ $raw_body = wp_remote_retrieve_body( $response );
+ $data = json_decode( (string) $raw_body, true );
+
+ if ( 200 !== $status_code || ! is_array( $data ) ) {
+ return $this->build_http_error( $status_code, $raw_body );
+ }
+
+ $rows = isset( $data['rows'] ) && is_array( $data['rows'] ) ? $data['rows'] : [];
+ $result = [];
+
+ foreach ( $rows as $row ) {
+ if ( ! is_array( $row ) ) {
+ continue;
+ }
+ $keys = isset( $row['keys'] ) && is_array( $row['keys'] ) ? $row['keys'] : [];
+ $query = isset( $keys[0] ) ? sanitize_text_field( (string) $keys[0] ) : '';
+ if ( '' === $query ) {
+ continue;
+ }
+ $result[] = [
+ 'query' => $query,
+ 'clicks' => isset( $row['clicks'] ) ? (float) $row['clicks'] : 0.0,
+ 'impressions' => isset( $row['impressions'] ) ? (float) $row['impressions'] : 0.0,
+ 'ctr' => isset( $row['ctr'] ) ? (float) $row['ctr'] : 0.0,
+ 'position' => isset( $row['position'] ) ? (float) $row['position'] : 0.0,
+ ];
+ }
+
+ return $result;
+ }
+}
diff --git a/includes/Services/Prompt/class-groq-ai-prompt-builder.php b/includes/Services/Prompt/class-groq-ai-prompt-builder.php
index 7a511e7..568e8d5 100644
--- a/includes/Services/Prompt/class-groq-ai-prompt-builder.php
+++ b/includes/Services/Prompt/class-groq-ai-prompt-builder.php
@@ -29,6 +29,32 @@ class Groq_AI_Prompt_Builder {
);
}
+ public function build_term_system_prompt( $settings, $conversation_id, $term ) {
+ $context = isset( $settings['store_context'] ) ? trim( $settings['store_context'] ) : '';
+ $term_name = is_object( $term ) && isset( $term->name ) ? (string) $term->name : '';
+ $base_instruction = __( 'Je bent een copywriter voor een WooCommerce winkel en schrijft SEO-vriendelijke categorie- en merkpagina teksten.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
+
+ if ( $context ) {
+ $base_instruction = sprintf(
+ __( 'Je bent een copywriter voor een WooCommerce winkel. Gebruik de volgende winkelcontext indien beschikbaar: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ $context
+ );
+ }
+
+ if ( '' !== $term_name ) {
+ $base_instruction .= ' ' . sprintf(
+ __( 'Je schrijft nu voor de term: %s.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ $term_name
+ );
+ }
+
+ return sprintf(
+ __( 'Conversatie-ID: %1$s. %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ $conversation_id,
+ $base_instruction
+ );
+ }
+
public function append_response_instructions( $prompt, $settings ) {
$instructions = (string) ( $this->get_structured_response_instructions( $settings ) ?? '' );
$prompt = trim( (string) $prompt );
@@ -232,6 +258,243 @@ class Groq_AI_Prompt_Builder {
return $intro . "\n" . $context . "\n\n" . $prompt;
}
+ public function build_term_context_block( $term, $options = [], $settings = null ) {
+ if ( ! $term || ! is_object( $term ) ) {
+ return '';
+ }
+
+ $taxonomy = isset( $term->taxonomy ) ? sanitize_key( (string) $term->taxonomy ) : '';
+ $term_id = isset( $term->term_id ) ? absint( $term->term_id ) : 0;
+ if ( '' === $taxonomy || ! $term_id ) {
+ return '';
+ }
+
+ $include_top_products = ! empty( $options['include_top_products'] );
+ $top_products_limit = isset( $options['top_products_limit'] ) ? absint( $options['top_products_limit'] ) : 10;
+ $top_products_limit = max( 1, min( 25, $top_products_limit ) );
+
+ $parts = [];
+ $parts[] = sprintf( __( 'Term: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), wp_strip_all_tags( (string) $term->name ) );
+ if ( isset( $term->slug ) && '' !== (string) $term->slug ) {
+ $parts[] = sprintf( __( 'Slug: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), sanitize_title( (string) $term->slug ) );
+ }
+ if ( isset( $term->count ) ) {
+ $parts[] = sprintf( __( 'Aantal producten: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), (string) absint( $term->count ) );
+ }
+ if ( isset( $term->description ) && '' !== trim( (string) $term->description ) ) {
+ $parts[] = sprintf( __( 'Huidige omschrijving: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), wp_strip_all_tags( (string) $term->description ) );
+ }
+
+ if ( $include_top_products ) {
+ $top_products = $this->get_top_products_for_term( $taxonomy, $term_id, $top_products_limit );
+ if ( ! empty( $top_products ) ) {
+ $lines = [];
+ foreach ( $top_products as $product_row ) {
+ $lines[] = sprintf( '- %s', $product_row );
+ }
+ $parts[] = __( 'Top verkochte producten (indicatief):', GROQ_AI_PRODUCT_TEXT_DOMAIN ) . "\n" . implode( "\n", $lines );
+ }
+ }
+
+ $google_context = apply_filters( 'groq_ai_term_google_context', '', $term, $settings );
+ $google_context = trim( (string) $google_context );
+ if ( '' !== $google_context ) {
+ $parts[] = $google_context;
+ }
+
+ return implode( "\n\n", array_filter( $parts ) );
+ }
+
+ public function prepend_term_context_to_prompt( $prompt, $context ) {
+ $context = trim( (string) $context );
+ if ( '' === $context ) {
+ return $prompt;
+ }
+ $intro = __( 'Gebruik de volgende categorie/term-context bij het schrijven:', GROQ_AI_PRODUCT_TEXT_DOMAIN );
+ return $intro . "\n" . $context . "\n\n" . $prompt;
+ }
+
+ public function get_term_response_format_definition( $settings = null ) {
+ $rankmath_enabled = $this->settings_manager->is_module_enabled( 'rankmath', $settings );
+ $keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings );
+ $title_pixels = $this->settings_manager->get_rankmath_meta_title_pixel_limit( $settings );
+ $desc_pixels = $this->settings_manager->get_rankmath_meta_description_pixel_limit( $settings );
+
+ $properties = [
+ 'description' => [
+ 'type' => 'string',
+ 'description' => __( 'HTML-omschrijving voor de categorie/term met paragrafen en eventueel lijstjes.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ 'minLength' => 20,
+ ],
+ ];
+
+ if ( $rankmath_enabled ) {
+ $properties['meta_title'] = [
+ 'type' => 'string',
+ 'description' => sprintf(
+ __( 'SEO-meta title (max. %1$d tekens en %2$d pixels).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ 60,
+ $title_pixels
+ ),
+ 'maxLength' => 120,
+ ];
+ $properties['meta_description'] = [
+ 'type' => 'string',
+ 'description' => sprintf(
+ __( 'SEO-meta description (max. %1$d tekens en %2$d pixels).', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ 160,
+ $desc_pixels
+ ),
+ 'maxLength' => 320,
+ ];
+ $properties['focus_keywords'] = [
+ 'type' => 'array',
+ 'description' => __( 'Lijst met korte zoekwoorden zonder hashtags of extra tekst.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ 'maxItems' => max( 1, $keyword_limit ),
+ 'items' => [
+ 'type' => 'string',
+ 'minLength' => 1,
+ ],
+ ];
+ }
+
+ $schema = [
+ 'type' => 'object',
+ 'properties' => $properties,
+ 'required' => [ 'description' ],
+ 'additionalProperties' => false,
+ ];
+
+ return [
+ 'type' => 'json_schema',
+ 'json_schema' => [
+ 'name' => 'groq_ai_term_text',
+ 'schema' => $schema,
+ ],
+ ];
+ }
+
+ public function append_term_response_instructions( $prompt, $settings ) {
+ $instructions = (string) ( $this->get_term_structured_response_instructions( $settings ) ?? '' );
+ $prompt = trim( (string) $prompt );
+ if ( '' === $instructions ) {
+ return $prompt;
+ }
+ if ( false !== strpos( $prompt, $instructions ) ) {
+ return $prompt;
+ }
+ return $prompt . "\n\n" . $instructions;
+ }
+
+ public function parse_term_structured_response( $raw, $settings = null ) {
+ if ( empty( $raw ) ) {
+ return new WP_Error( 'groq_ai_empty_response', __( 'Geen data ontvangen van de AI.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
+ }
+
+ $clean = trim( (string) $raw );
+ if ( preg_match( '/```(?:json)?\s*(.*?)```/is', $clean, $matches ) ) {
+ $clean = trim( $matches[1] );
+ }
+
+ $decoded = json_decode( $clean, true );
+ if ( ! is_array( $decoded ) ) {
+ // Fallback: treat as plain text.
+ return [
+ 'description' => trim( (string) $raw ),
+ ];
+ }
+
+ $description = isset( $decoded['description'] ) ? trim( (string) $decoded['description'] ) : '';
+ if ( '' === $description ) {
+ return new WP_Error( 'groq_ai_parse_error', __( 'De AI-respons bevatte geen description veld.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
+ }
+
+ $result = [
+ 'description' => $description,
+ ];
+
+ if ( isset( $decoded['meta_title'] ) ) {
+ $result['meta_title'] = $this->truncate_meta_field( (string) $decoded['meta_title'], 60 );
+ }
+ if ( isset( $decoded['meta_description'] ) ) {
+ $result['meta_description'] = $this->truncate_meta_field( (string) $decoded['meta_description'], 160 );
+ }
+ if ( isset( $decoded['focus_keywords'] ) ) {
+ if ( is_array( $decoded['focus_keywords'] ) ) {
+ $keywords = [];
+ foreach ( $decoded['focus_keywords'] as $kw ) {
+ $kw = trim( (string) $kw );
+ if ( '' !== $kw ) {
+ $keywords[] = $kw;
+ }
+ }
+ $keywords = array_values( array_unique( $keywords ) );
+ $result['focus_keywords'] = implode( ', ', $keywords );
+ }
+ }
+
+ return $result;
+ }
+
+ private function get_term_structured_response_instructions( $settings = null ) {
+ $schema_parts = [
+ '"description":"..."',
+ ];
+
+ $rankmath_enabled = $this->settings_manager->is_module_enabled( 'rankmath', $settings );
+ if ( $rankmath_enabled ) {
+ $schema_parts[] = '"meta_title":"..."';
+ $schema_parts[] = '"meta_description":"..."';
+ $schema_parts[] = '"focus_keywords":["...","..."]';
+ }
+
+ $json_structure = '{' . implode( ',', $schema_parts ) . '}';
+
+ $instruction = sprintf(
+ __( 'Geef ALLEEN een geldig JSON-object terug met deze structuur: %s. Gebruik dubbele aanhalingstekens, geen Markdown of extra tekst. Gebruik \n voor regeleinden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ),
+ $json_structure
+ );
+
+ $instruction .= ' ' . __( 'Zorg dat description geldige HTML bevat (gebruik minimaal -tags en waar relevant lijstjes of benadrukking). Voeg geen extra tekst buiten het JSON-object toe.', GROQ_AI_PRODUCT_TEXT_DOMAIN );
+ return $instruction;
+ }
+
+ private function get_top_products_for_term( $taxonomy, $term_id, $limit = 10 ) {
+ $taxonomy = sanitize_key( (string) $taxonomy );
+ $term_id = absint( $term_id );
+ $limit = max( 1, min( 25, absint( $limit ) ) );
+
+ $query = new WP_Query(
+ [
+ 'post_type' => 'product',
+ 'post_status' => 'publish',
+ 'posts_per_page' => $limit,
+ 'no_found_rows' => true,
+ 'meta_key' => 'total_sales',
+ 'orderby' => 'meta_value_num',
+ 'order' => 'DESC',
+ 'tax_query' => [
+ [
+ 'taxonomy' => $taxonomy,
+ 'field' => 'term_id',
+ 'terms' => [ $term_id ],
+ ],
+ ],
+ ]
+ );
+
+ $rows = [];
+ if ( $query->have_posts() ) {
+ foreach ( $query->posts as $post ) {
+ $title = isset( $post->post_title ) ? wp_strip_all_tags( (string) $post->post_title ) : '';
+ $rows[] = $title;
+ }
+ }
+ wp_reset_postdata();
+
+ return array_values( array_filter( $rows ) );
+ }
+
public function get_response_format_definition( $settings = null ) {
$rankmath_enabled = $this->settings_manager->is_module_enabled( 'rankmath', $settings );
$keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings );
diff --git a/includes/Services/Settings/class-groq-ai-settings-manager.php b/includes/Services/Settings/class-groq-ai-settings-manager.php
index c1532c1..b02a2b7 100644
--- a/includes/Services/Settings/class-groq-ai-settings-manager.php
+++ b/includes/Services/Settings/class-groq-ai-settings-manager.php
@@ -35,6 +35,15 @@ class Groq_AI_Settings_Manager {
'groq_api_key' => '',
'openai_api_key' => '',
'google_api_key' => '',
+ 'google_oauth_client_id' => '',
+ 'google_oauth_client_secret' => '',
+ 'google_oauth_refresh_token' => '',
+ 'google_oauth_connected_email' => '',
+ 'google_oauth_connected_at' => 0,
+ 'google_enable_gsc' => true,
+ 'google_enable_ga' => true,
+ 'google_gsc_site_url' => '',
+ 'google_ga4_property_id' => '',
'context_fields' => $this->get_default_context_fields(),
'modules' => $this->get_default_modules_settings(),
'image_context_mode' => 'url',
@@ -81,6 +90,15 @@ class Groq_AI_Settings_Manager {
'groq_api_key' => '',
'openai_api_key' => '',
'google_api_key' => '',
+ 'google_oauth_client_id' => '',
+ 'google_oauth_client_secret' => '',
+ 'google_oauth_refresh_token' => '',
+ 'google_oauth_connected_email' => '',
+ 'google_oauth_connected_at' => 0,
+ 'google_enable_gsc' => true,
+ 'google_enable_ga' => true,
+ 'google_gsc_site_url' => '',
+ 'google_ga4_property_id' => '',
'context_fields' => $this->get_default_context_fields(),
'modules' => $this->get_default_modules_settings(),
'image_context_mode' => 'url',
@@ -127,6 +145,15 @@ class Groq_AI_Settings_Manager {
'groq_api_key' => sanitize_text_field( $input['groq_api_key'] ),
'openai_api_key' => sanitize_text_field( $input['openai_api_key'] ),
'google_api_key' => sanitize_text_field( $input['google_api_key'] ),
+ 'google_oauth_client_id' => sanitize_text_field( $input['google_oauth_client_id'] ),
+ 'google_oauth_client_secret' => sanitize_text_field( $input['google_oauth_client_secret'] ),
+ 'google_oauth_refresh_token' => sanitize_text_field( $input['google_oauth_refresh_token'] ),
+ 'google_oauth_connected_email' => sanitize_text_field( $input['google_oauth_connected_email'] ),
+ 'google_oauth_connected_at' => absint( $input['google_oauth_connected_at'] ),
+ 'google_enable_gsc' => ! empty( $raw_input['google_enable_gsc'] ),
+ 'google_enable_ga' => ! empty( $raw_input['google_enable_ga'] ),
+ 'google_gsc_site_url' => esc_url_raw( (string) $input['google_gsc_site_url'] ),
+ 'google_ga4_property_id' => sanitize_text_field( (string) $input['google_ga4_property_id'] ),
'response_format_compat' => ! empty( $raw_input['response_format_compat'] ),
'image_context_mode' => $image_mode,
'image_context_limit' => $image_limit,