';
echo esc_html__(
'AI Snippet SEO Helper works as an integration for Rank Math SEO and requires the Rank Math SEO plugin to be installed and active.',
'ai-snippet-seo-helper'
);
echo '
';
}
}
// Check if Rank Math SEO is active
include_once ABSPATH . 'wp-admin/includes/plugin.php';
if (
! function_exists( 'is_plugin_active' )
|| ! is_plugin_active( 'seo-by-rank-math/rank-math.php' )
) {
add_action( 'admin_notices', 'aish_rank_math_required_notice' );
// Do not register metaboxes, bulk actions or other helpers if Rank Math is not active.
return;
}
}
/* Simple helpers (kept separate to avoid PRO collisions) */
if ( ! function_exists('aish_model_cost') ) {
function aish_model_cost(string $model): int {
$m = strtolower(trim($model));
if ($m === 'gpt-4o') return 5;
if ($m === 'gpt-4.1') return 3;
if ($m === 'o3') return 12;
return 1; // gpt-4-turbo
}
}
if ( ! function_exists('aish_status_remaining') ) {
function aish_status_remaining(array $st): int {
if (isset($st['remaining'])) return (int)$st['remaining'];
if (isset($st['remaining_total'])) return (int)$st['remaining_total'];
if (isset($st['global_remaining'])) return (int)$st['global_remaining'];
if (isset($st['plan_limit_total'], $st['usage_total']))
return max(0, (int)$st['plan_limit_total'] - (int)$st['usage_total']);
if (isset($st['plan_limit'], $st['usage']))
return max(0, (int)$st['plan_limit'] - (int)$st['usage']);
return 0;
}
}
/* Token-info fallback shim */
if ( ! function_exists('aish_get_token_status') ) {
function aish_get_token_status(string $site_token, string $plugin = 'ai-snippet-seo-helper'): array {
$resp = wp_remote_post( home_url('/wp-json/rankpilotai/v1/token-info'), [
'headers' => ['Content-Type' => 'application/json'],
'body' => wp_json_encode([
'site_token' => $site_token,
'plugin' => $plugin,
]),
'timeout' => 15,
]);
if ( is_wp_error($resp) || wp_remote_retrieve_response_code($resp) !== 200 ) {
return ['remaining' => 0, 'error' => 'token-info'];
}
return json_decode( wp_remote_retrieve_body($resp), true ) ?: ['remaining'=>0];
}
}
/* ADMIN ASSETS (post editor + list/bulk) */
function aish_enqueue_admin_assets( $hook ) {
$force = ! empty( $_GET['aish_job'] );
if ( ! $force && ! in_array( $hook, [ 'post.php', 'post-new.php', 'edit.php' ], true ) ) {
return;
}
wp_enqueue_style(
'aish-admin-css',
AISH_URL . 'admin/assets/css/admin.css',
[],
AISH_VERSION
);
wp_enqueue_script(
'aish-admin-js',
AISH_URL . 'admin/assets/js/admin.js',
[ 'jquery' ],
AISH_VERSION,
true
);
$opts = get_option( AISH_OPT_KEY, [] );
wp_localize_script(
'aish-admin-js',
'AISH_Ajax',
[
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'aish_nonce' ),
'site_token' => $opts['site_token'] ?? '',
'rest_url' => home_url( '/wp-json/rankpilotai/v1' ),
'home' => home_url( '/' ),
]
);
}
add_action( 'admin_enqueue_scripts', 'aish_enqueue_admin_assets' );
add_action( 'wp_ajax_aish_bulk_tick', 'aish_ajax_bulk_tick' );
add_action( 'wp_ajax_aish_bulk_cancel', 'aish_ajax_bulk_cancel' );
/* OPTIONAL Rank Math metabox button (kept commented intentionally) */
// add_action( 'rank_math/metabox/before', 'aish_add_rm_button' );
// function aish_add_rm_button( $metabox ) {
// if ( $metabox->id !== 'rank_math_metabox' ) return;
// echo '';
// echo ' Generate Snippet with AI ';
// echo ' ';
// echo '
';
// }
/* SINGLE POST AJAX */
function aish_ajax_generate_single() {
check_ajax_referer( 'aish_nonce', 'nonce' );
$post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0;
if ( ! $post_id ) {
wp_send_json_error( [ 'msg' => 'ID missing' ] );
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
wp_send_json_error( [ 'msg' => 'Insufficient permissions.' ] );
}
try {
$data = aish_generate_snippet_for_post( $post_id, true );
if ( isset( $data['score'] ) ) {
$data['score'] = (int) $data['score'];
}
wp_send_json_success( $data );
} catch ( Exception $e ) {
wp_send_json_error( [ 'msg' => $e->getMessage() ] );
}
}
add_action( 'wp_ajax_aish_generate_single', 'aish_ajax_generate_single' );
/* Bulk job storage (transient-backed) */
function aish_job_key($id){ return 'aish_job_'.$id; }
function aish_get_job($id){ return get_transient(aish_job_key($id)); }
function aish_save_job($id,$data){ set_transient(aish_job_key($id), $data, HOUR_IN_SECONDS); }
function aish_delete_job($id){ delete_transient(aish_job_key($id)); }
/* Create bulk job descriptor */
function aish_create_bulk_job(array $ids, string $site_token, string $model, array $opts){
$job_id = wp_generate_password(12, false, false);
aish_save_job($job_id, [
'id' => $job_id,
'ids' => array_values($ids),
'i' => 0,
'total' => count($ids),
's' => 0,
'f' => 0,
'flags' => ['zerotok'=>false,'insuff'=>false],
'site_token' => $site_token,
'model' => strtolower($model ?: 'gpt-4-turbo'),
'opts' => $opts,
]);
return $job_id;
}
/* AJAX: process one item per tick */
function aish_ajax_bulk_tick(){
check_ajax_referer('aish_nonce','nonce');
if ( ! current_user_can('edit_posts') ) {
wp_send_json_error(['msg'=>'Unauthorized']);
}
$jid = sanitize_text_field($_POST['job'] ?? '');
if ( ! $jid ) wp_send_json_error(['msg'=>'Missing job id']);
$job = aish_get_job($jid);
if ( ! is_array($job) ) wp_send_json_error(['msg'=>'Job not found']);
if ( $job['i'] >= $job['total'] ) {
$args = [
'aish_bsuccess' => (int)$job['s'],
'aish_bfailed' => (int)$job['f'],
];
if ( !empty($job['flags']['zerotok']) ) $args['aish_bzerotok']=1;
if ( !empty($job['flags']['insuff']) ) $args['aish_binsufficient']=1;
aish_delete_job($jid);
$base = wp_get_referer();
if ( ! $base ) $base = admin_url('edit.php');
$redir = add_query_arg($args, remove_query_arg('aish_job', $base));
wp_send_json_success(['done'=>true,'redirect'=>$redir]);
}
$id = (int)$job['ids'][ $job['i'] ];
$opts = $job['opts'];
$site = $job['site_token'];
$mdl = $job['model'];
$item = aish_prepare_snippet_item_for_post( $id, $opts );
$ok=false; $err=''; $zerotok=false; $insuff=false;
try{
$res = aish_call_rankpilot_api_bulk( $site, [$item], $mdl, $opts );
$list = $res['items'] ?? [];
if ( array_keys($list) === range(0, count($list)-1) ){
$row = $list[0] ?? [];
} else {
$row = $list[$id] ?? [];
}
if ( ! empty($row['error']) ){
$txt = (string)$row['error'];
if ( stripos($txt,'No tokens left') !== false ) $zerotok = true;
if ( stripos($txt,'Not enough tokens') !== false ) $insuff = true;
$err = $txt;
} else {
$ok = aish_apply_snippet_result_for_post( $id, $row, $opts );
}
} catch (\Exception $ex){
$err = $ex->getMessage();
if ( stripos($err,'No tokens left') !== false ) $zerotok = true;
if ( stripos($err,'Not enough tokens') !== false ) $insuff = true;
}
$job['i']++;
if ($ok) $job['s']++; else $job['f']++;
if ($zerotok) $job['flags']['zerotok'] = true;
if ($insuff) $job['flags']['insuff'] = true;
aish_save_job($jid,$job);
wp_send_json_success([
'done' => false,
'i' => (int)$job['i'],
'total' => (int)$job['total'],
's' => (int)$job['s'],
'f' => (int)$job['f'],
'flags' => $job['flags'],
'lastId' => $id,
'error' => $ok ? '' : $err,
]);
}
/* AJAX: cancel bulk job */
function aish_ajax_bulk_cancel(){
check_ajax_referer('aish_nonce','nonce');
if ( ! current_user_can('edit_posts') ) {
wp_send_json_error(['msg'=>'Unauthorized']);
}
$jid = sanitize_text_field($_POST['job'] ?? '');
if ($jid) aish_delete_job($jid);
wp_send_json_success();
}
/* MAIN GENERATOR (single post) */
function aish_generate_snippet_for_post( int $post_id, bool $return = false ) {
$opts = get_option( AISH_OPT_KEY, [] );
if ( ! isset( $opts['auto_slug'] ) ) $opts['auto_slug'] = 'no';
$site_token = $opts['site_token'] ?? '';
if ( ! $site_token ) {
throw new Exception( 'No site token.' );
}
$title = get_the_title( $post_id );
$content = wp_strip_all_tags( get_post_field( 'post_content', $post_id ) );
$excerpt = wp_trim_words( $content, 50, '…' );
/* Focus Keyword fallback */
$focus_kw = get_post_meta( $post_id, 'rank_math_focus_keyword', true );
if ( $opts['auto_slug'] !== 'yes' && empty( $focus_kw ) ) {
$slug_now = get_post_field( 'post_name', $post_id );
$focus_kw = preg_replace( '/[-_]+/', ' ', $slug_now );
}
$payload = [
'context_type' => 'post',
'post_title' => $title,
'post_excerpt' => $excerpt,
'post_type' => get_post_type( $post_id ),
'focus_keyword' => $focus_kw,
'model' => $opts['model_choice'] ?? 'gpt-4-turbo',
'custom_prompt' => trim( (string) ( $opts['custom_prompt'] ?? '' ) ),
'domain' => parse_url( home_url(), PHP_URL_HOST ),
'plugin' => 'ai-snippet-seo-helper',
];
$payload['slug'] = get_post_field( 'post_name', $post_id );
$payload['auto_slug'] = ( $opts['auto_slug'] ?? 'no' ) === 'yes';
$plugin_slug = 'ai-snippet-seo-helper';
$model = strtolower($opts['model_choice'] ?? 'gpt-4-turbo');
$need = aish_model_cost($model);
$status = function_exists('aish_check_token_status')
? aish_check_token_status($site_token)
: aish_get_token_status($site_token, $plugin_slug);
$remaining = aish_status_remaining($status);
if ($remaining <= 0) {
throw new Exception('No tokens left. Please upgrade.');
}
if ($remaining < $need) {
throw new Exception('Not enough tokens for the selected model');
}
$res = aish_call_rankpilot_api( $site_token, $payload );
$kw = $res['keyword'] ?? '';
$ti = $res['title'] ?? '';
$desc = $res['description'] ?? '';
$slug = $res['slug'] ?? '';
/* Apply Rank Math fields */
update_post_meta( $post_id, 'rank_math_focus_keyword', $focus_kw ?: $kw );
if ( $ti ) update_post_meta( $post_id, 'rank_math_title', $ti );
if ( $desc ) update_post_meta( $post_id, 'rank_math_description', $desc );
/* Optional slug update */
$auto = $opts['auto_slug'] ?? 'no';
if ( $auto === 'yes' && $slug ) {
$san = sanitize_title( $slug );
$current_slug = get_post_field( 'post_name', $post_id );
if ( $san && $san !== $current_slug ) {
add_post_meta( $post_id, '_wp_old_slug', $current_slug );
wp_update_post( [ 'ID' => $post_id, 'post_name' => $san ] );
update_post_meta( $post_id, '_edit_last', get_current_user_id() );
}
}
/* Score */
$final_slug = get_post_field( 'post_name', $post_id );
$used_kw = $focus_kw ?: $kw;
$sc = aish_calc_score( $used_kw, $ti, $desc, $final_slug );
update_post_meta( $post_id, '_aish_snippet_score', $sc['score'] );
update_post_meta( $post_id, '_aish_score', $sc['score'] );
if ( $return ) {
return [
'keyword' => $kw,
'title' => $ti,
'desc' => $desc,
'slug' => $final_slug,
'score' => $sc['score'],
];
}
}
/* BULK-ACTION SUPPORT */
function aish_register_bulk_actions() {
$pts = get_post_types( [ 'public' => true ], 'names' );
foreach ( $pts as $pt ) {
add_filter( "bulk_actions-edit-{$pt}", function( $acts ) {
$acts['aish_generate_ai'] = 'Generate with AI Snippet SEO Helper';
return $acts;
} );
add_filter( "handle_bulk_actions-edit-{$pt}", function( $redirect, $action, $post_ids ) {
if ( $action !== 'aish_generate_ai' ) return $redirect;
$opts = get_option( AISH_OPT_KEY, [] );
$site_token = $opts['site_token'] ?? '';
if ( ! $site_token ) {
return add_query_arg( 'aish_berror', 'notoken', $redirect );
}
$model_sel = strtolower( $opts['model_choice'] ?? 'gpt-4-turbo' );
$ids = [];
foreach ( (array) $post_ids as $pid ) {
$pid = (int) $pid;
if ( $pid > 0 && current_user_can('edit_post', $pid) ) $ids[] = $pid;
}
if ( empty($ids) ) return $redirect;
$st = aish_check_token_status( $site_token );
if ( isset($st['error']) ) {
return add_query_arg( 'aish_berror', 'tokencheck', $redirect );
}
$remaining = aish_status_remaining( $st );
$need_each = aish_model_cost( $model_sel );
$need_all = $need_each * count($ids);
if ( $remaining <= 0 ) {
return add_query_arg(['aish_bsuccess'=>0,'aish_bfailed'=>count($ids),'aish_bzerotok'=>1], $redirect);
}
if ( $remaining < $need_all ) {
return add_query_arg(['aish_bsuccess'=>0,'aish_bfailed'=>count($ids),'aish_binsufficient'=>1], $redirect);
}
$job_id = aish_create_bulk_job( $ids, $site_token, $model_sel, $opts );
return add_query_arg(['aish_job'=>$job_id], $redirect);
}, 10, 3 );
}
}
add_action( 'admin_init', 'aish_register_bulk_actions' );
/* ADMIN NOTICES (bulk progress + results) */
add_action('admin_notices', function (){
if ( ! empty($_GET['aish_job']) ) {
$jid = sanitize_text_field($_GET['aish_job']);
echo ''
. '
AI Snippet SEO Helper: Running bulk job… '
. 'starting
'
. '
Cancel
'
. '
';
}
if ( isset($_GET['aish_berror']) && $_GET['aish_berror'] === 'notoken' ) {
echo 'AI Snippet SEO Helper: No Site Key configured.
';
}
if ( ! empty($_GET['aish_bzerotok']) ) {
echo 'AI Snippet SEO Helper: Failed – no tokens left. '
. 'Buy Tokens to continue.
';
}
if ( ! empty($_GET['aish_binsufficient']) ) {
echo 'AI Snippet SEO Helper: Not enough tokens for the selected model. '
. 'Please switch to a cheaper model (e.g. GPT-4 Turbo) from '
. 'SEO Settings '
. 'or Buy Tokens .
';
}
if ( isset($_GET['aish_bsuccess']) || isset($_GET['aish_bfailed']) ) {
$s = (int)($_GET['aish_bsuccess'] ?? 0);
$f = (int)($_GET['aish_bfailed'] ?? 0);
if ( $s > 0 ) {
echo 'AI Snippet SEO Helper: Generated for ' . intval($s) . ' item(s).
';
}
if ( $f > 0 && empty($_GET['aish_bzerotok']) ) {
echo 'AI Snippet SEO Helper: Failed for ' . intval($f) . ' item(s).
';
}
}
});
/* PLUGIN LIST: Settings link */
add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), function ( $links ) {
$url = admin_url( 'admin.php?page=ai-snippet-seo-helper' );
$label = esc_html__( 'Settings', 'ai-snippet-seo-helper' );
array_unshift(
$links,
'' . $label . ' '
);
return $links;
} );
/* MENU */
add_action( 'admin_menu', function () {
add_menu_page(
'AI Snippet SEO Helper',
'AI Snippet SEO Helper',
'manage_options',
'ai-snippet-seo-helper',
'aish_render_settings_page',
plugin_dir_url(__FILE__) . 'admin/assets/img/ai-snippet-seo-helper.svg',
25
);
} );
/* SIDE METABOX: button + live score badge */
add_action( 'add_meta_boxes', function () {
foreach ( get_post_types( ['public' => true], 'names' ) as $pt ) {
add_meta_box(
'aish_seo_helper',
'AI Snippet SEO Helper',
function ( $post ) { ?>
Generate with AI Snippet SEO Helper
ID, '_aish_snippet_score', true );
if ($raw === '' || $raw === null) {
$raw = get_post_meta( $post->ID, '_aish_score', true );
}
$sc = (int)$raw;
$col = ($sc >= 100) ? '#2980b9' : (($sc >= 66) ? '#f39c12' : '#e74c3c');
?>
SEO Snippet Score: / 100
= 100 ) ? '#2980b9' : ( ( $sc >= 66 ) ? '#f39c12' : '#e74c3c' );
$style = sprintf(
'display:inline-block;padding:2px 6px;color:#fff;border-radius:3px;background:%s',
$colr
);
echo ''
. intval( $sc ) . ' / 100 ';
}, 10, 2 );
}
/* LIST FILTERS: FK and Score buckets */
add_action('restrict_manage_posts', function () {
$screen = get_current_screen();
if ($screen->base !== 'edit') return;
$val = isset($_GET['aish_filter']) ? sanitize_text_field($_GET['aish_filter']) : '';
echo ''
. 'AI Snippet SEO Helper '
. 'Has Focus Keyword '
. 'Missing Focus Keyword '
. 'Score: Poor (0/33) '
. 'Score: Fair (66/100) '
. 'Score: Excellent (100/100) '
. ' ';
});
add_action('pre_get_posts', function($q){
if (!is_admin() || !$q->is_main_query()) return;
$f = isset($_GET['aish_filter']) ? sanitize_text_field($_GET['aish_filter']) : '';
if (!$f) return;
$score_keys = ['_aish_snippet_score','_aish_score'];
if ($f === 'has_kw') {
$q->set('meta_query', [[
'key' => 'rank_math_focus_keyword',
'value' => '',
'compare' => '!=',
]]);
return;
}
if ($f === 'missing_kw') {
$q->set('meta_query', [
'relation' => 'OR',
[ 'key' => 'rank_math_focus_keyword', 'compare' => 'NOT EXISTS' ],
[ 'key' => 'rank_math_focus_keyword', 'value' => '', 'compare' => '=' ],
]);
return;
}
if ($f === 'score_poor') {
$vals = [0,33];
$mq = ['relation'=>'OR'];
foreach ($score_keys as $k) {
$mq[] = ['key'=>$k,'value'=>$vals,'compare'=>'IN','type'=>'NUMERIC'];
}
$q->set('meta_query', $mq);
return;
}
if ($f === 'score_fair' || $f === 'score_excellent') {
$target = ($f === 'score_fair') ? 66 : 100;
$mq = ['relation'=>'OR'];
foreach ($score_keys as $k) {
$mq[] = ['key'=>$k,'value'=>$target,'compare'=>'=','type'=>'NUMERIC'];
}
$q->set('meta_query', $mq);
return;
}
});