Initial commit: PRAutoBlogger v0.1.0 with frontend posts widget

This commit is contained in:
Terence Ong
2026-04-10 23:33:42 +07:00
parent 68c6f67bcf
commit 88d674d679
65 changed files with 2178 additions and 1084 deletions

View File

@@ -4,12 +4,12 @@ declare(strict_types=1);
/**
* Displays admin notices for onboarding, errors, and budget warnings.
*
* Triggered by: Autoblogger::register_admin_hooks() on `admin_notices`.
* Dependencies: Autoblogger_Cost_Tracker (for budget warnings).
* Triggered by: PRAutoBlogger::register_admin_hooks() on `admin_notices`.
* Dependencies: PRAutoBlogger_Cost_Tracker (for budget warnings).
*
* @see class-autoblogger.php — Registers the hook.
* @see class-prautoblogger.php — Registers the hook.
*/
class Autoblogger_Admin_Notices {
class PRAutoBlogger_Admin_Notices {
/**
* Display relevant admin notices.
@@ -32,16 +32,16 @@ class Autoblogger_Admin_Notices {
* @return void
*/
private function check_api_key_notice(): void {
$api_key = get_option( 'autoblogger_openrouter_api_key', '' );
$api_key = get_option( 'prautoblogger_openrouter_api_key', '' );
if ( '' !== $api_key ) {
return;
}
printf(
'<div class="notice notice-warning is-dismissible"><p>%s <a href="%s">%s</a></p></div>',
esc_html__( 'AutoBlogger: OpenRouter API key is not configured. Content generation is disabled.', 'autoblogger' ),
esc_url( admin_url( 'admin.php?page=autoblogger-settings' ) ),
esc_html__( 'Configure now', 'autoblogger' )
esc_html__( 'PRAutoBlogger: OpenRouter API key is not configured. Content generation is disabled.', 'prautoblogger' ),
esc_url( admin_url( 'admin.php?page=prautoblogger-settings' ) ),
esc_html__( 'Configure now', 'prautoblogger' )
);
}
@@ -51,20 +51,20 @@ class Autoblogger_Admin_Notices {
* @return void
*/
private function check_budget_notice(): void {
$cost_tracker = new Autoblogger_Cost_Tracker();
$cost_tracker = new PRAutoBlogger_Cost_Tracker();
$utilization = $cost_tracker->get_budget_utilization();
if ( $utilization >= 100.0 ) {
printf(
'<div class="notice notice-error"><p>%s</p></div>',
esc_html__( 'AutoBlogger: Monthly API budget EXCEEDED. Content generation is paused until next month or budget increase.', 'autoblogger' )
esc_html__( 'PRAutoBlogger: Monthly API budget EXCEEDED. Content generation is paused until next month or budget increase.', 'prautoblogger' )
);
} elseif ( $utilization >= 80.0 ) {
printf(
'<div class="notice notice-warning"><p>%s</p></div>',
esc_html( sprintf(
/* translators: %s: budget utilization percentage */
__( 'AutoBlogger: Monthly API budget is at %.0f%%. Consider increasing the budget or reducing daily article count.', 'autoblogger' ),
__( 'PRAutoBlogger: Monthly API budget is at %.0f%%. Consider increasing the budget or reducing daily article count.', 'prautoblogger' ),
$utilization
) )
);
@@ -77,20 +77,20 @@ class Autoblogger_Admin_Notices {
* @return void
*/
private function check_subreddit_notice(): void {
$subreddits = json_decode( get_option( 'autoblogger_target_subreddits', '[]' ), true );
$subreddits = json_decode( get_option( 'prautoblogger_target_subreddits', '[]' ), true );
if ( ! empty( $subreddits ) ) {
return;
}
// Only show on our settings page.
$screen = get_current_screen();
if ( ! $screen || 'toplevel_page_autoblogger-settings' !== $screen->id ) {
if ( ! $screen || 'toplevel_page_prautoblogger-settings' !== $screen->id ) {
return;
}
printf(
'<div class="notice notice-info is-dismissible"><p>%s</p></div>',
esc_html__( 'AutoBlogger: No subreddits configured. Add target subreddits in Source Configuration to start collecting data.', 'autoblogger' )
esc_html__( 'PRAutoBlogger: No subreddits configured. Add target subreddits in Source Configuration to start collecting data.', 'prautoblogger' )
);
}
}

View File

@@ -2,28 +2,28 @@
declare(strict_types=1);
/**
* Registers and renders the main AutoBlogger settings page in wp-admin.
* Registers and renders the main PRAutoBlogger settings page in wp-admin.
*
* Uses the WordPress Settings API for option registration, sanitization, and rendering.
* Settings are defined as a declarative array — adding a new setting is one array entry.
*
* Triggered by: Autoblogger::register_admin_hooks() registers menu and settings hooks.
* Dependencies: Autoblogger_Encryption (for API key storage), Autoblogger_Settings_Fields.
* Triggered by: PRAutoBlogger::register_admin_hooks() registers menu and settings hooks.
* Dependencies: PRAutoBlogger_Encryption (for API key storage), PRAutoBlogger_Settings_Fields.
*
* @see class-autoblogger.php — Registers hooks that call this class.
* @see class-prautoblogger.php — Registers hooks that call this class.
* @see class-settings-fields.php — Defines all settings fields and sections.
* @see CONVENTIONS.md — "How To: Add a New Admin Setting".
*/
class Autoblogger_Admin_Page {
class PRAutoBlogger_Admin_Page {
private const PAGE_SLUG = 'autoblogger-settings';
private const OPTION_GROUP = 'autoblogger_settings_group';
private const PAGE_SLUG = 'prautoblogger-settings';
private const OPTION_GROUP = 'prautoblogger_settings_group';
/** Register the top-level AutoBlogger menu item. */
/** Register the top-level PRAutoBlogger menu item. */
public function on_register_menu(): void {
add_menu_page(
__( 'AutoBlogger', 'autoblogger' ),
__( 'AutoBlogger', 'autoblogger' ),
__( 'PRAutoBlogger', 'prautoblogger' ),
__( 'PRAutoBlogger', 'prautoblogger' ),
'manage_options',
self::PAGE_SLUG,
[ $this, 'render_page' ],
@@ -34,15 +34,15 @@ class Autoblogger_Admin_Page {
/** Register all settings with the WordPress Settings API. */
public function on_register_settings(): void {
$sections = Autoblogger_Settings_Fields::get_sections();
$fields = Autoblogger_Settings_Fields::get_fields();
$sections = PRAutoBlogger_Settings_Fields::get_sections();
$fields = PRAutoBlogger_Settings_Fields::get_fields();
foreach ( $sections as $section_id => $section ) {
add_settings_section( $section_id, $section['title'], '__return_empty_string', self::PAGE_SLUG );
}
foreach ( $fields as $field ) {
$section = $field['section'] ?? 'autoblogger_api';
$section = $field['section'] ?? 'prautoblogger_api';
register_setting( self::OPTION_GROUP, $field['id'], [
'type' => $field['wp_type'] ?? 'string',
'sanitize_callback' => [ $this, 'sanitize_field' ],
@@ -52,30 +52,30 @@ class Autoblogger_Admin_Page {
}
}
/** Enqueue admin CSS and JS on all AutoBlogger admin pages. */
/** Enqueue admin CSS and JS on all PRAutoBlogger admin pages. */
public function on_enqueue_assets( string $hook_suffix ): void {
$pages = [
'toplevel_page_' . self::PAGE_SLUG,
'autoblogger_page_autoblogger-metrics',
'autoblogger_page_autoblogger-review-queue',
'autoblogger_page_autoblogger-logs',
'prautoblogger_page_prautoblogger-metrics',
'prautoblogger_page_prautoblogger-review-queue',
'prautoblogger_page_prautoblogger-logs',
];
if ( ! in_array( $hook_suffix, $pages, true ) ) {
return;
}
wp_enqueue_style( 'autoblogger-admin', AUTOBLOGGER_PLUGIN_URL . 'assets/css/admin.css', [], AUTOBLOGGER_VERSION );
wp_enqueue_script( 'autoblogger-admin', AUTOBLOGGER_PLUGIN_URL . 'assets/js/admin.js', [ 'jquery' ], AUTOBLOGGER_VERSION, true );
wp_enqueue_style( 'prautoblogger-admin', PRAUTOBLOGGER_PLUGIN_URL . 'assets/css/admin.css', [], PRAUTOBLOGGER_VERSION );
wp_enqueue_script( 'prautoblogger-admin', PRAUTOBLOGGER_PLUGIN_URL . 'assets/js/admin.js', [ 'jquery' ], PRAUTOBLOGGER_VERSION, true );
wp_localize_script( 'autoblogger-admin', 'autobloggerAdmin', [
wp_localize_script( 'prautoblogger-admin', 'prautobloggerAdmin', [
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'generateNonce' => wp_create_nonce( 'autoblogger_generate_now' ),
'testNonce' => wp_create_nonce( 'autoblogger_test_connection' ),
'reviewNonce' => wp_create_nonce( 'autoblogger_review_queue' ),
'generatingText' => __( 'Generating...', 'autoblogger' ),
'generateText' => __( 'Generate Now', 'autoblogger' ),
'testingText' => __( 'Testing...', 'autoblogger' ),
'testText' => __( 'Test Connections', 'autoblogger' ),
'generateNonce' => wp_create_nonce( 'prautoblogger_generate_now' ),
'testNonce' => wp_create_nonce( 'prautoblogger_test_connection' ),
'reviewNonce' => wp_create_nonce( 'prautoblogger_review_queue' ),
'generatingText' => __( 'Generating...', 'prautoblogger' ),
'generateText' => __( 'Generate Now', 'prautoblogger' ),
'testingText' => __( 'Testing...', 'prautoblogger' ),
'testText' => __( 'Test Connections', 'prautoblogger' ),
] );
}
@@ -84,9 +84,9 @@ class Autoblogger_Admin_Page {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$sections = Autoblogger_Settings_Fields::get_sections();
$fields = Autoblogger_Settings_Fields::get_fields();
include AUTOBLOGGER_PLUGIN_DIR . 'templates/admin/settings-page.php';
$sections = PRAutoBlogger_Settings_Fields::get_sections();
$fields = PRAutoBlogger_Settings_Fields::get_fields();
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/settings-page.php';
}
/**
@@ -102,7 +102,7 @@ class Autoblogger_Admin_Page {
$value = get_option( $args['id'], $default );
if ( 'password' === $type && '' !== $value ) {
$decrypted = Autoblogger_Encryption::decrypt( $value );
$decrypted = PRAutoBlogger_Encryption::decrypt( $value );
$value = '' !== $decrypted ? '••••••••' : '';
}
@@ -120,7 +120,7 @@ class Autoblogger_Admin_Page {
case 'password':
printf( '<input type="password" id="%s" name="%s" value="" class="ab-input" placeholder="%s" autocomplete="off" />',
$id, $id, '' !== $value ? esc_attr__( 'Saved (enter new value to change)', 'autoblogger' ) : '' );
$id, $id, '' !== $value ? esc_attr__( 'Saved (enter new value to change)', 'prautoblogger' ) : '' );
break;
case 'textarea':
@@ -152,11 +152,11 @@ class Autoblogger_Admin_Page {
break;
case 'author_select':
wp_dropdown_users( [ 'name' => $id, 'id' => $id, 'selected' => absint( $value ), 'show_option_none' => __( '— Auto (first admin) —', 'autoblogger' ), 'option_none_value' => '0', 'class' => 'ab-select' ] );
wp_dropdown_users( [ 'name' => $id, 'id' => $id, 'selected' => absint( $value ), 'show_option_none' => __( '— Auto (first admin) —', 'prautoblogger' ), 'option_none_value' => '0', 'class' => 'ab-select' ] );
break;
case 'category_select':
wp_dropdown_categories( [ 'name' => $id, 'id' => $id, 'selected' => absint( $value ), 'show_option_none' => __( '— Auto-assign by type —', 'autoblogger' ), 'option_none_value' => '0', 'class' => 'ab-select', 'hide_empty' => false ] );
wp_dropdown_categories( [ 'name' => $id, 'id' => $id, 'selected' => absint( $value ), 'show_option_none' => __( '— Auto-assign by type —', 'prautoblogger' ), 'option_none_value' => '0', 'class' => 'ab-select', 'hide_empty' => false ] );
break;
}
@@ -181,13 +181,13 @@ class Autoblogger_Admin_Page {
$option_name = substr( $filter, strlen( 'sanitize_option_' ) );
}
$encrypted = [ 'autoblogger_openrouter_api_key', 'autoblogger_reddit_client_secret', 'autoblogger_ga4_credentials_json' ];
$encrypted = [ 'prautoblogger_openrouter_api_key', 'prautoblogger_reddit_client_secret', 'prautoblogger_ga4_credentials_json' ];
if ( in_array( $option_name, $encrypted, true ) ) {
if ( '' === $value ) { return get_option( $option_name, '' ); }
return Autoblogger_Encryption::encrypt( sanitize_text_field( $value ) );
return PRAutoBlogger_Encryption::encrypt( sanitize_text_field( $value ) );
}
$json_fields = [ 'autoblogger_target_subreddits', 'autoblogger_topic_exclusions', 'autoblogger_enabled_sources' ];
$json_fields = [ 'prautoblogger_target_subreddits', 'prautoblogger_topic_exclusions', 'prautoblogger_enabled_sources' ];
if ( in_array( $option_name, $json_fields, true ) ) {
if ( is_array( $value ) ) { return wp_json_encode( array_values( array_map( 'sanitize_text_field', $value ) ) ); }
if ( is_string( $value ) && '[' !== substr( trim( $value ), 0, 1 ) ) {
@@ -197,7 +197,7 @@ class Autoblogger_Admin_Page {
return sanitize_text_field( $value );
}
$numeric = [ 'autoblogger_daily_article_target', 'autoblogger_monthly_budget_usd', 'autoblogger_min_word_count', 'autoblogger_max_word_count', 'autoblogger_default_author', 'autoblogger_default_category' ];
$numeric = [ 'prautoblogger_daily_article_target', 'prautoblogger_monthly_budget_usd', 'prautoblogger_min_word_count', 'prautoblogger_max_word_count', 'prautoblogger_default_author', 'prautoblogger_default_category' ];
if ( in_array( $option_name, $numeric, true ) ) {
return is_numeric( $value ) ? $value : 0;
}

View File

@@ -2,17 +2,17 @@
declare(strict_types=1);
/**
* WordPress Dashboard widget showing AutoBlogger generation status at a glance.
* WordPress Dashboard widget showing PRAutoBlogger generation status at a glance.
*
* Displays: last generation time, articles generated today, monthly cost,
* and budget utilization.
*
* Triggered by: Could be registered via wp_dashboard_setup hook.
* Dependencies: Autoblogger_Cost_Tracker.
* Dependencies: PRAutoBlogger_Cost_Tracker.
*
* @see class-autoblogger.php — Can register this on wp_dashboard_setup.
* @see class-prautoblogger.php — Can register this on wp_dashboard_setup.
*/
class Autoblogger_Dashboard_Widget {
class PRAutoBlogger_Dashboard_Widget {
/**
* Register the dashboard widget.
@@ -25,8 +25,8 @@ class Autoblogger_Dashboard_Widget {
}
wp_add_dashboard_widget(
'autoblogger_status',
__( 'AutoBlogger Status', 'autoblogger' ),
'prautoblogger_status',
__( 'PRAutoBlogger Status', 'prautoblogger' ),
[ $this, 'render_widget' ]
);
}
@@ -37,15 +37,15 @@ class Autoblogger_Dashboard_Widget {
* @return void
*/
public function render_widget(): void {
$cost_tracker = new Autoblogger_Cost_Tracker();
$cost_tracker = new PRAutoBlogger_Cost_Tracker();
$monthly_spend = $cost_tracker->get_monthly_spend();
$budget = (float) get_option( 'autoblogger_monthly_budget_usd', 50.00 );
$budget = (float) get_option( 'prautoblogger_monthly_budget_usd', 50.00 );
$utilization = $cost_tracker->get_budget_utilization();
$next_run = wp_next_scheduled( 'autoblogger_daily_generation' );
$next_run = wp_next_scheduled( 'prautoblogger_daily_generation' );
printf( '<p><strong>%s</strong> $%.2f / $%.2f (%.0f%%)</p>',
esc_html__( 'Monthly Spend:', 'autoblogger' ),
esc_html__( 'Monthly Spend:', 'prautoblogger' ),
$monthly_spend,
$budget,
$utilization
@@ -53,15 +53,15 @@ class Autoblogger_Dashboard_Widget {
if ( false !== $next_run ) {
printf( '<p><strong>%s</strong> %s</p>',
esc_html__( 'Next Generation:', 'autoblogger' ),
esc_html__( 'Next Generation:', 'prautoblogger' ),
esc_html( wp_date( 'M j, Y g:i A', $next_run ) )
);
}
printf(
'<p><a href="%s" class="button button-secondary">%s</a></p>',
esc_url( admin_url( 'admin.php?page=autoblogger-metrics' ) ),
esc_html__( 'View Full Metrics', 'autoblogger' )
esc_url( admin_url( 'admin.php?page=prautoblogger-metrics' ) ),
esc_html__( 'View Full Metrics', 'prautoblogger' )
);
}
}

View File

@@ -6,29 +6,29 @@ declare(strict_types=1);
*
* Provides level filtering (error/warning/info/debug), text search,
* pagination, and a one-click "Clear Old Logs" action. Reads from the
* ab_event_log table via Autoblogger_Logger::query().
* prab_event_log table via PRAutoBlogger_Logger::query().
*
* Triggered by: Autoblogger::register_admin_hooks() on `admin_menu`.
* Dependencies: Autoblogger_Logger.
* Triggered by: PRAutoBlogger::register_admin_hooks() on `admin_menu`.
* Dependencies: PRAutoBlogger_Logger.
*
* @see core/class-logger.php — Writes log entries and provides query().
* @see templates/admin/log-viewer.php — Renders the HTML.
* @see class-autoblogger.php — Registers the AJAX clear action.
* @see class-prautoblogger.php — Registers the AJAX clear action.
*/
class Autoblogger_Log_Viewer {
class PRAutoBlogger_Log_Viewer {
/**
* Register the log viewer submenu page under AutoBlogger.
* Register the log viewer submenu page under PRAutoBlogger.
*
* @return void
*/
public function on_register_menu(): void {
add_submenu_page(
'autoblogger-settings',
__( 'Activity Log', 'autoblogger' ),
__( 'Activity Log', 'autoblogger' ),
'prautoblogger-settings',
__( 'Activity Log', 'prautoblogger' ),
__( 'Activity Log', 'prautoblogger' ),
'manage_options',
'autoblogger-logs',
'prautoblogger-logs',
[ $this, 'render_page' ]
);
}
@@ -47,36 +47,36 @@ class Autoblogger_Log_Viewer {
$search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
$paged = isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1;
$result = Autoblogger_Logger::query( $level, $search, $paged, 50 );
$result = PRAutoBlogger_Logger::query( $level, $search, $paged, 50 );
$rows = $result['rows'];
$total = $result['total'];
$total_pages = (int) ceil( $total / 50 );
include AUTOBLOGGER_PLUGIN_DIR . 'templates/admin/log-viewer.php';
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/log-viewer.php';
}
/**
* AJAX handler: clear logs older than 30 days.
*
* Side effects: deletes rows from ab_event_log.
* Side effects: deletes rows from prab_event_log.
*
* @return void
*/
public function on_ajax_clear_logs(): void {
check_ajax_referer( 'autoblogger_clear_logs', 'nonce' );
check_ajax_referer( 'prautoblogger_clear_logs', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'autoblogger' ) ], 403 );
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'prautoblogger' ) ], 403 );
return;
}
$days = isset( $_POST['days'] ) ? absint( $_POST['days'] ) : 30;
$deleted = Autoblogger_Logger::prune( $days );
$deleted = PRAutoBlogger_Logger::prune( $days );
wp_send_json_success( [
'message' => sprintf(
/* translators: %d: number of entries deleted */
__( '%d log entries cleared.', 'autoblogger' ),
__( '%d log entries cleared.', 'prautoblogger' ),
$deleted
),
] );

View File

@@ -7,26 +7,26 @@ declare(strict_types=1);
* Shows: monthly spend, daily cost chart, spend by pipeline stage, budget
* utilization, top/bottom performing posts, and recent generation logs.
*
* Triggered by: Autoblogger::register_admin_hooks() on `admin_menu`.
* Dependencies: Autoblogger_Cost_Tracker.
* Triggered by: PRAutoBlogger::register_admin_hooks() on `admin_menu`.
* Dependencies: PRAutoBlogger_Cost_Tracker.
*
* @see class-autoblogger.php — Registers the hook.
* @see class-prautoblogger.php — Registers the hook.
* @see core/class-cost-tracker.php — Provides cost data.
*/
class Autoblogger_Metrics_Page {
class PRAutoBlogger_Metrics_Page {
/**
* Register the metrics submenu page under AutoBlogger.
* Register the metrics submenu page under PRAutoBlogger.
*
* @return void
*/
public function on_register_menu(): void {
add_submenu_page(
'autoblogger-settings',
__( 'Metrics & Costs', 'autoblogger' ),
__( 'Metrics & Costs', 'autoblogger' ),
'prautoblogger-settings',
__( 'Metrics & Costs', 'prautoblogger' ),
__( 'Metrics & Costs', 'prautoblogger' ),
'manage_options',
'autoblogger-metrics',
'prautoblogger-metrics',
[ $this, 'render_page' ]
);
}
@@ -41,6 +41,6 @@ class Autoblogger_Metrics_Page {
return;
}
include AUTOBLOGGER_PLUGIN_DIR . 'templates/admin/metrics-page.php';
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/metrics-page.php';
}
}

View File

@@ -2,18 +2,18 @@
declare(strict_types=1);
/**
* Adds a metabox to posts showing AutoBlogger generation metadata.
* Adds a metabox to posts showing PRAutoBlogger generation metadata.
*
* Only appears on posts that were generated by AutoBlogger (checked via
* _autoblogger_generated post meta).
* Only appears on posts that were generated by PRAutoBlogger (checked via
* _prautoblogger_generated post meta).
*
* Triggered by: Autoblogger::register_admin_hooks() on `add_meta_boxes`.
* Triggered by: PRAutoBlogger::register_admin_hooks() on `add_meta_boxes`.
* Dependencies: WordPress post meta API.
*
* @see class-autoblogger.php — Registers the hook.
* @see class-prautoblogger.php — Registers the hook.
* @see core/class-publisher.php — Writes the metadata this class displays.
*/
class Autoblogger_Post_Metabox {
class PRAutoBlogger_Post_Metabox {
/**
* Register the metabox for the post editor.
@@ -27,13 +27,13 @@ class Autoblogger_Post_Metabox {
}
global $post;
if ( ! $post || '1' !== get_post_meta( $post->ID, '_autoblogger_generated', true ) ) {
if ( ! $post || '1' !== get_post_meta( $post->ID, '_prautoblogger_generated', true ) ) {
return;
}
add_meta_box(
'autoblogger_generation_info',
__( 'AutoBlogger Generation Info', 'autoblogger' ),
'prautoblogger_generation_info',
__( 'PRAutoBlogger Generation Info', 'prautoblogger' ),
[ $this, 'render_metabox' ],
'post',
'side',
@@ -49,6 +49,6 @@ class Autoblogger_Post_Metabox {
* @return void
*/
public function render_metabox( \WP_Post $post ): void {
include AUTOBLOGGER_PLUGIN_DIR . 'templates/admin/metabox-generation-info.php';
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/metabox-generation-info.php';
}
}

View File

@@ -7,27 +7,27 @@ declare(strict_types=1);
* Provides approve (publish), edit (opens WP editor), and reject (trash)
* actions on each draft. Supports bulk approve/reject via checkboxes.
*
* Triggered by: Autoblogger::register_admin_hooks() on `admin_menu`.
* Triggered by: PRAutoBlogger::register_admin_hooks() on `admin_menu`.
* Dependencies: WordPress WP_Query, post meta API.
*
* @see class-autoblogger.php — Registers the hook and AJAX handlers.
* @see class-prautoblogger.php — Registers the hook and AJAX handlers.
* @see core/class-publisher.php — Creates the drafts this page displays.
* @see templates/admin/review-queue.php — Renders the HTML.
*/
class Autoblogger_Review_Queue {
class PRAutoBlogger_Review_Queue {
/**
* Register the review queue submenu page under AutoBlogger.
* Register the review queue submenu page under PRAutoBlogger.
*
* @return void
*/
public function on_register_menu(): void {
$hook = add_submenu_page(
'autoblogger-settings',
__( 'Review Queue', 'autoblogger' ),
__( 'Review Queue', 'autoblogger' ),
'prautoblogger-settings',
__( 'Review Queue', 'prautoblogger' ),
__( 'Review Queue', 'prautoblogger' ),
'manage_options',
'autoblogger-review-queue',
'prautoblogger-review-queue',
[ $this, 'render_page' ]
);
@@ -49,7 +49,7 @@ class Autoblogger_Review_Queue {
$paged = isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1;
$query = $this->get_pending_posts( $paged );
include AUTOBLOGGER_PLUGIN_DIR . 'templates/admin/review-queue.php';
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/review-queue.php';
}
/**
@@ -67,7 +67,7 @@ class Autoblogger_Review_Queue {
'paged' => $paged,
'meta_query' => [
[
'key' => '_autoblogger_generated',
'key' => '_prautoblogger_generated',
'value' => '1',
],
],
@@ -87,19 +87,19 @@ class Autoblogger_Review_Queue {
* @return void
*/
public function on_handle_bulk_actions(): void {
if ( ! isset( $_POST['autoblogger_bulk_action'] ) ) {
if ( ! isset( $_POST['prautoblogger_bulk_action'] ) ) {
return;
}
check_admin_referer( 'autoblogger_review_queue_bulk', 'autoblogger_review_nonce' );
check_admin_referer( 'prautoblogger_review_queue_bulk', 'prautoblogger_review_nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$action = sanitize_text_field( wp_unslash( $_POST['autoblogger_bulk_action'] ) );
$post_ids = isset( $_POST['autoblogger_post_ids'] ) && is_array( $_POST['autoblogger_post_ids'] )
? array_map( 'absint', $_POST['autoblogger_post_ids'] )
$action = sanitize_text_field( wp_unslash( $_POST['prautoblogger_bulk_action'] ) );
$post_ids = isset( $_POST['prautoblogger_post_ids'] ) && is_array( $_POST['prautoblogger_post_ids'] )
? array_map( 'absint', $_POST['prautoblogger_post_ids'] )
: [];
if ( empty( $post_ids ) ) {
@@ -115,7 +115,7 @@ class Autoblogger_Review_Queue {
if ( 'approve' === $action ) {
$result = wp_update_post( [ 'ID' => $post_id, 'post_status' => 'publish' ], true );
if ( ! is_wp_error( $result ) ) {
update_post_meta( $post_id, '_autoblogger_approved_at', gmdate( 'c' ) );
update_post_meta( $post_id, '_prautoblogger_approved_at', gmdate( 'c' ) );
$count++;
}
} elseif ( 'reject' === $action ) {
@@ -128,9 +128,9 @@ class Autoblogger_Review_Queue {
$redirect = add_query_arg(
[
'page' => 'autoblogger-review-queue',
'autoblogger_bulk_done' => $count,
'autoblogger_bulk_type' => $action,
'page' => 'prautoblogger-review-queue',
'prautoblogger_bulk_done' => $count,
'prautoblogger_bulk_type' => $action,
],
admin_url( 'admin.php' )
);
@@ -147,16 +147,16 @@ class Autoblogger_Review_Queue {
* @return void
*/
public function on_ajax_approve_post(): void {
check_ajax_referer( 'autoblogger_review_queue', 'nonce' );
check_ajax_referer( 'prautoblogger_review_queue', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'autoblogger' ) ], 403 );
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'prautoblogger' ) ], 403 );
return;
}
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
if ( $post_id <= 0 ) {
wp_send_json_error( [ 'message' => __( 'Invalid post ID.', 'autoblogger' ) ] );
wp_send_json_error( [ 'message' => __( 'Invalid post ID.', 'prautoblogger' ) ] );
return;
}
@@ -167,8 +167,8 @@ class Autoblogger_Review_Queue {
return;
}
update_post_meta( $post_id, '_autoblogger_approved_at', gmdate( 'c' ) );
wp_send_json_success( [ 'message' => __( 'Post published.', 'autoblogger' ), 'post_id' => $post_id ] );
update_post_meta( $post_id, '_prautoblogger_approved_at', gmdate( 'c' ) );
wp_send_json_success( [ 'message' => __( 'Post published.', 'prautoblogger' ), 'post_id' => $post_id ] );
}
/**
@@ -179,25 +179,25 @@ class Autoblogger_Review_Queue {
* @return void
*/
public function on_ajax_reject_post(): void {
check_ajax_referer( 'autoblogger_review_queue', 'nonce' );
check_ajax_referer( 'prautoblogger_review_queue', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'autoblogger' ) ], 403 );
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'prautoblogger' ) ], 403 );
return;
}
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
if ( $post_id <= 0 ) {
wp_send_json_error( [ 'message' => __( 'Invalid post ID.', 'autoblogger' ) ] );
wp_send_json_error( [ 'message' => __( 'Invalid post ID.', 'prautoblogger' ) ] );
return;
}
$result = wp_trash_post( $post_id );
if ( false === $result || null === $result ) {
wp_send_json_error( [ 'message' => __( 'Failed to reject post.', 'autoblogger' ) ] );
wp_send_json_error( [ 'message' => __( 'Failed to reject post.', 'prautoblogger' ) ] );
return;
}
wp_send_json_success( [ 'message' => __( 'Post rejected.', 'autoblogger' ), 'post_id' => $post_id ] );
wp_send_json_success( [ 'message' => __( 'Post rejected.', 'prautoblogger' ), 'post_id' => $post_id ] );
}
}

View File

@@ -2,18 +2,18 @@
declare(strict_types=1);
/**
* Declarative settings fields and sections for the AutoBlogger admin page.
* Declarative settings fields and sections for the PRAutoBlogger admin page.
*
* Centralizes all field and section definitions in one place, making it trivial
* to add new settings (just add one array entry). Decoupled from page rendering logic.
*
* Triggered by: Autoblogger_Admin_Page::on_register_settings() calls static methods here.
* Triggered by: PRAutoBlogger_Admin_Page::on_register_settings() calls static methods here.
* Dependencies: None — contains only data definitions and static methods.
*
* @see admin/class-admin-page.php — Calls get_sections() and get_fields() to register settings.
* @see CONVENTIONS.md — "How To: Add a New Admin Setting".
*/
class Autoblogger_Settings_Fields {
class PRAutoBlogger_Settings_Fields {
/**
* Get all settings sections. Each section maps to a tab in the admin UI.
@@ -22,40 +22,40 @@ class Autoblogger_Settings_Fields {
*/
public static function get_sections(): array {
return [
'autoblogger_api' => [
'title' => __( 'API Keys', 'autoblogger' ),
'prautoblogger_api' => [
'title' => __( 'API Keys', 'prautoblogger' ),
'icon' => 'dashicons-admin-network',
'description' => __( 'Connect your external services. Keys are encrypted at rest.', 'autoblogger' ),
'description' => __( 'Connect your external services. Keys are encrypted at rest.', 'prautoblogger' ),
],
'autoblogger_models' => [
'title' => __( 'AI Models', 'autoblogger' ),
'prautoblogger_models' => [
'title' => __( 'AI Models', 'prautoblogger' ),
'icon' => 'dashicons-superhero-alt',
'description' => __( 'Choose which models power each stage of the pipeline.', 'autoblogger' ),
'description' => __( 'Choose which models power each stage of the pipeline.', 'prautoblogger' ),
],
'autoblogger_content' => [
'title' => __( 'Content', 'autoblogger' ),
'prautoblogger_content' => [
'title' => __( 'Content', 'prautoblogger' ),
'icon' => 'dashicons-edit-large',
'description' => __( 'Control tone, length, pipeline mode, and topic guardrails.', 'autoblogger' ),
'description' => __( 'Control tone, length, pipeline mode, and topic guardrails.', 'prautoblogger' ),
],
'autoblogger_sources' => [
'title' => __( 'Sources', 'autoblogger' ),
'prautoblogger_sources' => [
'title' => __( 'Sources', 'prautoblogger' ),
'icon' => 'dashicons-rss',
'description' => __( 'Configure where AutoBlogger finds trending topics.', 'autoblogger' ),
'description' => __( 'Configure where PRAutoBlogger finds trending topics.', 'prautoblogger' ),
],
'autoblogger_schedule' => [
'title' => __( 'Schedule & Budget', 'autoblogger' ),
'prautoblogger_schedule' => [
'title' => __( 'Schedule & Budget', 'prautoblogger' ),
'icon' => 'dashicons-calendar-alt',
'description' => __( 'Set daily generation schedule, volume, and spending limits.', 'autoblogger' ),
'description' => __( 'Set daily generation schedule, volume, and spending limits.', 'prautoblogger' ),
],
'autoblogger_publishing' => [
'title' => __( 'Publishing', 'autoblogger' ),
'prautoblogger_publishing' => [
'title' => __( 'Publishing', 'prautoblogger' ),
'icon' => 'dashicons-megaphone',
'description' => __( 'Control how generated content is published.', 'autoblogger' ),
'description' => __( 'Control how generated content is published.', 'prautoblogger' ),
],
'autoblogger_analytics' => [
'title' => __( 'Analytics', 'autoblogger' ),
'prautoblogger_analytics' => [
'title' => __( 'Analytics', 'prautoblogger' ),
'icon' => 'dashicons-chart-area',
'description' => __( 'Connect Google Analytics 4 for post performance scoring.', 'autoblogger' ),
'description' => __( 'Connect Google Analytics 4 for post performance scoring.', 'prautoblogger' ),
],
];
}
@@ -69,222 +69,222 @@ class Autoblogger_Settings_Fields {
return [
// ── API Keys ────────────────────────────────────────────────
[
'id' => 'autoblogger_openrouter_api_key',
'label' => __( 'OpenRouter API Key', 'autoblogger' ),
'id' => 'prautoblogger_openrouter_api_key',
'label' => __( 'OpenRouter API Key', 'prautoblogger' ),
'type' => 'password',
'section' => 'autoblogger_api',
'description' => __( 'Get your key at openrouter.ai/keys', 'autoblogger' ),
'section' => 'prautoblogger_api',
'description' => __( 'Get your key at openrouter.ai/keys', 'prautoblogger' ),
'icon' => '🔑',
],
[
'id' => 'autoblogger_reddit_client_id',
'label' => __( 'Reddit Client ID', 'autoblogger' ),
'id' => 'prautoblogger_reddit_client_id',
'label' => __( 'Reddit Client ID', 'prautoblogger' ),
'type' => 'text',
'section' => 'autoblogger_api',
'description' => __( 'Create a script app at reddit.com/prefs/apps', 'autoblogger' ),
'section' => 'prautoblogger_api',
'description' => __( 'Create a script app at reddit.com/prefs/apps', 'prautoblogger' ),
'icon' => '🤖',
],
[
'id' => 'autoblogger_reddit_client_secret',
'label' => __( 'Reddit Client Secret', 'autoblogger' ),
'id' => 'prautoblogger_reddit_client_secret',
'label' => __( 'Reddit Client Secret', 'prautoblogger' ),
'type' => 'password',
'section' => 'autoblogger_api',
'section' => 'prautoblogger_api',
],
// ── AI Models ───────────────────────────────────────────────
[
'id' => 'autoblogger_analysis_model',
'label' => __( 'Analysis Model', 'autoblogger' ),
'id' => 'prautoblogger_analysis_model',
'label' => __( 'Analysis Model', 'prautoblogger' ),
'type' => 'text',
'section' => 'autoblogger_models',
'default' => AUTOBLOGGER_DEFAULT_ANALYSIS_MODEL,
'description' => __( 'Used for source analysis. Pick a cheap, fast model.', 'autoblogger' ),
'badge' => __( 'Low cost', 'autoblogger' ),
'section' => 'prautoblogger_models',
'default' => PRAUTOBLOGGER_DEFAULT_ANALYSIS_MODEL,
'description' => __( 'Used for source analysis. Pick a cheap, fast model.', 'prautoblogger' ),
'badge' => __( 'Low cost', 'prautoblogger' ),
],
[
'id' => 'autoblogger_writing_model',
'label' => __( 'Writing Model', 'autoblogger' ),
'id' => 'prautoblogger_writing_model',
'label' => __( 'Writing Model', 'prautoblogger' ),
'type' => 'text',
'section' => 'autoblogger_models',
'default' => AUTOBLOGGER_DEFAULT_WRITING_MODEL,
'description' => __( 'Used for article generation. Quality matters here.', 'autoblogger' ),
'badge' => __( 'Quality', 'autoblogger' ),
'section' => 'prautoblogger_models',
'default' => PRAUTOBLOGGER_DEFAULT_WRITING_MODEL,
'description' => __( 'Used for article generation. Quality matters here.', 'prautoblogger' ),
'badge' => __( 'Quality', 'prautoblogger' ),
],
[
'id' => 'autoblogger_editor_model',
'label' => __( 'Editor Model', 'autoblogger' ),
'id' => 'prautoblogger_editor_model',
'label' => __( 'Editor Model', 'prautoblogger' ),
'type' => 'text',
'section' => 'autoblogger_models',
'default' => AUTOBLOGGER_DEFAULT_EDITOR_MODEL,
'description' => __( 'Used for the chief editor review pass.', 'autoblogger' ),
'badge' => __( 'Quality', 'autoblogger' ),
'section' => 'prautoblogger_models',
'default' => PRAUTOBLOGGER_DEFAULT_EDITOR_MODEL,
'description' => __( 'Used for the chief editor review pass.', 'prautoblogger' ),
'badge' => __( 'Quality', 'prautoblogger' ),
],
// ── Content ─────────────────────────────────────────────────
[
'id' => 'autoblogger_niche_description',
'label' => __( 'Niche Description', 'autoblogger' ),
'id' => 'prautoblogger_niche_description',
'label' => __( 'Niche Description', 'prautoblogger' ),
'type' => 'textarea',
'section' => 'autoblogger_content',
'description' => __( 'Describe your site\'s niche. Guides content analysis and generation.', 'autoblogger' ),
'section' => 'prautoblogger_content',
'description' => __( 'Describe your site\'s niche. Guides content analysis and generation.', 'prautoblogger' ),
],
[
'id' => 'autoblogger_tone',
'label' => __( 'Content Tone', 'autoblogger' ),
'id' => 'prautoblogger_tone',
'label' => __( 'Content Tone', 'prautoblogger' ),
'type' => 'select',
'section' => 'autoblogger_content',
'section' => 'prautoblogger_content',
'default' => 'informational',
'options' => [
'informational' => __( 'Informational', 'autoblogger' ),
'conversational' => __( 'Conversational', 'autoblogger' ),
'professional' => __( 'Professional', 'autoblogger' ),
'casual' => __( 'Casual', 'autoblogger' ),
'authoritative' => __( 'Authoritative', 'autoblogger' ),
'informational' => __( 'Informational', 'prautoblogger' ),
'conversational' => __( 'Conversational', 'prautoblogger' ),
'professional' => __( 'Professional', 'prautoblogger' ),
'casual' => __( 'Casual', 'prautoblogger' ),
'authoritative' => __( 'Authoritative', 'prautoblogger' ),
],
],
[
'id' => 'autoblogger_writing_pipeline',
'label' => __( 'Writing Pipeline', 'autoblogger' ),
'id' => 'prautoblogger_writing_pipeline',
'label' => __( 'Writing Pipeline', 'prautoblogger' ),
'type' => 'select',
'section' => 'autoblogger_content',
'section' => 'prautoblogger_content',
'default' => 'multi_step',
'options' => [
'multi_step' => __( 'Multi-step (outline → draft → polish)', 'autoblogger' ),
'single_pass' => __( 'Single-pass (one LLM call)', 'autoblogger' ),
'multi_step' => __( 'Multi-step (outline → draft → polish)', 'prautoblogger' ),
'single_pass' => __( 'Single-pass (one LLM call)', 'prautoblogger' ),
],
],
[
'id' => 'autoblogger_min_word_count',
'label' => __( 'Min Word Count', 'autoblogger' ),
'id' => 'prautoblogger_min_word_count',
'label' => __( 'Min Word Count', 'prautoblogger' ),
'type' => 'number',
'section' => 'autoblogger_content',
'section' => 'prautoblogger_content',
'default' => 800,
'min' => 200,
],
[
'id' => 'autoblogger_max_word_count',
'label' => __( 'Max Word Count', 'autoblogger' ),
'id' => 'prautoblogger_max_word_count',
'label' => __( 'Max Word Count', 'prautoblogger' ),
'type' => 'number',
'section' => 'autoblogger_content',
'section' => 'prautoblogger_content',
'default' => 2000,
'min' => 500,
],
[
'id' => 'autoblogger_topic_exclusions',
'label' => __( 'Topic Exclusions', 'autoblogger' ),
'id' => 'prautoblogger_topic_exclusions',
'label' => __( 'Topic Exclusions', 'prautoblogger' ),
'type' => 'textarea',
'section' => 'autoblogger_content',
'description' => __( 'Comma-separated topics to never write about.', 'autoblogger' ),
'section' => 'prautoblogger_content',
'description' => __( 'Comma-separated topics to never write about.', 'prautoblogger' ),
],
// ── Sources ─────────────────────────────────────────────────
[
'id' => 'autoblogger_target_subreddits',
'label' => __( 'Target Subreddits', 'autoblogger' ),
'id' => 'prautoblogger_target_subreddits',
'label' => __( 'Target Subreddits', 'prautoblogger' ),
'type' => 'textarea',
'section' => 'autoblogger_sources',
'description' => __( 'Comma-separated, without r/. E.g.: peptides, Nootropics, biohackers', 'autoblogger' ),
'section' => 'prautoblogger_sources',
'description' => __( 'Comma-separated, without r/. E.g.: peptides, Nootropics, biohackers', 'prautoblogger' ),
],
[
'id' => 'autoblogger_enabled_sources',
'label' => __( 'Enabled Sources', 'autoblogger' ),
'id' => 'prautoblogger_enabled_sources',
'label' => __( 'Enabled Sources', 'prautoblogger' ),
'type' => 'checkboxes',
'section' => 'autoblogger_sources',
'section' => 'prautoblogger_sources',
'options' => [
'reddit' => __( 'Reddit', 'autoblogger' ),
'tiktok' => __( 'TikTok (coming soon)', 'autoblogger' ),
'instagram' => __( 'Instagram (coming soon)', 'autoblogger' ),
'youtube' => __( 'YouTube (coming soon)', 'autoblogger' ),
'reddit' => __( 'Reddit', 'prautoblogger' ),
'tiktok' => __( 'TikTok (coming soon)', 'prautoblogger' ),
'instagram' => __( 'Instagram (coming soon)', 'prautoblogger' ),
'youtube' => __( 'YouTube (coming soon)', 'prautoblogger' ),
],
'default' => '["reddit"]',
'description' => __( 'Select which platforms to monitor for topics.', 'autoblogger' ),
'description' => __( 'Select which platforms to monitor for topics.', 'prautoblogger' ),
],
// ── Schedule & Budget ───────────────────────────────────────
[
'id' => 'autoblogger_daily_article_target',
'label' => __( 'Articles per Day', 'autoblogger' ),
'id' => 'prautoblogger_daily_article_target',
'label' => __( 'Articles per Day', 'prautoblogger' ),
'type' => 'number',
'section' => 'autoblogger_schedule',
'section' => 'prautoblogger_schedule',
'default' => 1,
'min' => 1,
'max' => 10,
],
[
'id' => 'autoblogger_schedule_time',
'label' => __( 'Generation Time', 'autoblogger' ),
'id' => 'prautoblogger_schedule_time',
'label' => __( 'Generation Time', 'prautoblogger' ),
'type' => 'time',
'section' => 'autoblogger_schedule',
'section' => 'prautoblogger_schedule',
'default' => '03:00',
'description' => __( 'Daily generation runs at this time (server timezone).', 'autoblogger' ),
'description' => __( 'Daily generation runs at this time (server timezone).', 'prautoblogger' ),
],
[
'id' => 'autoblogger_monthly_budget_usd',
'label' => __( 'Monthly Budget (USD)', 'autoblogger' ),
'id' => 'prautoblogger_monthly_budget_usd',
'label' => __( 'Monthly Budget (USD)', 'prautoblogger' ),
'type' => 'number',
'section' => 'autoblogger_schedule',
'section' => 'prautoblogger_schedule',
'default' => 50,
'min' => 0,
'step' => '0.01',
'description' => __( 'Hard stop when reached. 0 = unlimited.', 'autoblogger' ),
'description' => __( 'Hard stop when reached. 0 = unlimited.', 'prautoblogger' ),
],
// ── Publishing ──────────────────────────────────────────────
[
'id' => 'autoblogger_auto_publish',
'label' => __( 'Auto-Publish', 'autoblogger' ),
'id' => 'prautoblogger_auto_publish',
'label' => __( 'Auto-Publish', 'prautoblogger' ),
'type' => 'toggle',
'section' => 'autoblogger_publishing',
'section' => 'prautoblogger_publishing',
'default' => '0',
'description' => __( 'Automatically publish editor-approved posts. When off, all posts go to the Review Queue as drafts.', 'autoblogger' ),
'description' => __( 'Automatically publish editor-approved posts. When off, all posts go to the Review Queue as drafts.', 'prautoblogger' ),
],
[
'id' => 'autoblogger_default_author',
'label' => __( 'Default Author', 'autoblogger' ),
'id' => 'prautoblogger_default_author',
'label' => __( 'Default Author', 'prautoblogger' ),
'type' => 'author_select',
'section' => 'autoblogger_publishing',
'section' => 'prautoblogger_publishing',
'default' => 0,
'description' => __( 'WordPress user assigned as author of generated posts.', 'autoblogger' ),
'description' => __( 'WordPress user assigned as author of generated posts.', 'prautoblogger' ),
],
[
'id' => 'autoblogger_default_category',
'label' => __( 'Default Category', 'autoblogger' ),
'id' => 'prautoblogger_default_category',
'label' => __( 'Default Category', 'prautoblogger' ),
'type' => 'category_select',
'section' => 'autoblogger_publishing',
'section' => 'prautoblogger_publishing',
'default' => 0,
'description' => __( 'Fallback category for generated posts.', 'autoblogger' ),
'description' => __( 'Fallback category for generated posts.', 'prautoblogger' ),
],
// ── Logging ─────────────────────────────────────────────────
[
'id' => 'autoblogger_log_level',
'label' => __( 'Log Level', 'autoblogger' ),
'id' => 'prautoblogger_log_level',
'label' => __( 'Log Level', 'prautoblogger' ),
'type' => 'select',
'section' => 'autoblogger_publishing',
'section' => 'prautoblogger_publishing',
'default' => 'info',
'options' => [
'error' => __( 'Error — only failures', 'autoblogger' ),
'warning' => __( 'Warning — errors + warnings', 'autoblogger' ),
'info' => __( 'Info — key events (recommended)', 'autoblogger' ),
'debug' => __( 'Debug — everything (verbose)', 'autoblogger' ),
'error' => __( 'Error — only failures', 'prautoblogger' ),
'warning' => __( 'Warning — errors + warnings', 'prautoblogger' ),
'info' => __( 'Info — key events (recommended)', 'prautoblogger' ),
'debug' => __( 'Debug — everything (verbose)', 'prautoblogger' ),
],
'description' => __( 'Controls detail level in the Activity Log.', 'autoblogger' ),
'description' => __( 'Controls detail level in the Activity Log.', 'prautoblogger' ),
],
// ── Analytics ───────────────────────────────────────────────
[
'id' => 'autoblogger_ga4_property_id',
'label' => __( 'GA4 Property ID', 'autoblogger' ),
'id' => 'prautoblogger_ga4_property_id',
'label' => __( 'GA4 Property ID', 'prautoblogger' ),
'type' => 'text',
'section' => 'autoblogger_analytics',
'description' => __( 'Format: properties/XXXXXXXXX. Leave blank to skip GA4.', 'autoblogger' ),
'section' => 'prautoblogger_analytics',
'description' => __( 'Format: properties/XXXXXXXXX. Leave blank to skip GA4.', 'prautoblogger' ),
],
[
'id' => 'autoblogger_ga4_credentials_json',
'label' => __( 'Service Account JSON', 'autoblogger' ),
'id' => 'prautoblogger_ga4_credentials_json',
'label' => __( 'Service Account JSON', 'prautoblogger' ),
'type' => 'password',
'section' => 'autoblogger_analytics',
'description' => __( 'Paste the full JSON key file for a service account with Analytics read access.', 'autoblogger' ),
'section' => 'prautoblogger_analytics',
'description' => __( 'Paste the full JSON key file for a service account with Analytics read access.', 'prautoblogger' ),
],
];
}