Add Google Analytics and Search Console integration

- Implemented Groq_AI_Google_Analytics_Data_Client for fetching GA4 data.
- Created Groq_AI_Google_Search_Console_Client for retrieving Search Console data.
- Added Google OAuth client for authentication with Google APIs.
- Enhanced Groq_AI_Settings_Manager to include Google OAuth settings.
- Introduced term context building methods in Groq_AI_Google_Context_Builder.
- Developed JavaScript functionality for term generation in the admin interface.
- Added methods for generating term prompts and handling responses.
- Improved error handling and response parsing for Google API interactions.
This commit is contained in:
2026-01-16 17:48:34 +00:00
parent 985f7dfbcd
commit 95f7983e70
10 changed files with 2089 additions and 3 deletions

View File

@@ -0,0 +1,182 @@
<?php
class Groq_AI_Google_Analytics_Data_Client {
/** @var Groq_AI_Google_OAuth_Client */
private $oauth;
public function __construct( Groq_AI_Google_OAuth_Client $oauth ) {
$this->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,
];
}
}

View File

@@ -0,0 +1,113 @@
<?php
class Groq_AI_Google_Context_Builder {
/** @var Groq_AI_Google_Search_Console_Client */
private $gsc;
/** @var Groq_AI_Google_Analytics_Data_Client */
private $ga;
public function __construct( Groq_AI_Google_Search_Console_Client $gsc, Groq_AI_Google_Analytics_Data_Client $ga ) {
$this->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 );
}
}

View File

@@ -0,0 +1,101 @@
<?php
class Groq_AI_Google_OAuth_Client {
/**
* @param array $settings
* @return string|WP_Error
*/
public function get_access_token( $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'] ) : '';
$refresh_token = isset( $settings['google_oauth_refresh_token'] ) ? trim( (string) $settings['google_oauth_refresh_token'] ) : '';
if ( '' === $client_id || '' === $client_secret || '' === $refresh_token ) {
return new WP_Error( 'groq_ai_google_oauth_missing', __( 'Google OAuth is niet (volledig) geconfigureerd.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) );
}
$cache_key = 'groq_ai_google_access_token_' . md5( $client_id . '|' . $refresh_token );
$cached = get_transient( $cache_key );
if ( is_string( $cached ) && '' !== $cached ) {
return $cached;
}
$response = wp_remote_post(
'https://oauth2.googleapis.com/token',
[
'timeout' => 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,
];
}
}

View File

@@ -0,0 +1,195 @@
<?php
class Groq_AI_Google_Search_Console_Client {
/** @var Groq_AI_Google_OAuth_Client */
private $oauth;
public function __construct( Groq_AI_Google_OAuth_Client $oauth ) {
$this->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;
}
}

View File

@@ -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 <p>-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 );

View File

@@ -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,