*/ private static function get_tabs(): array { $tabs = [ self::TAB_OVERVIEW, self::TAB_SETTINGS, self::TAB_POSITIONS, ]; if ( Release::is_module_public( Release::MODULE_INVENTORY_API ) ) { $tabs[] = self::TAB_API; } if ( Release::is_module_public( Release::MODULE_GSC ) && GscModule::get_instance()->is_enabled() ) { $tabs[] = self::TAB_GSC; } return $tabs; } /** * Handle Settings POST and redirects before any admin HTML is sent. */ public static function handle_load(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } $gsc_redirect = GscAdmin::get_instance()->handle_connect_post(); if ( is_string( $gsc_redirect ) && '' !== $gsc_redirect ) { wp_safe_redirect( $gsc_redirect ); exit; } $redirect = self::process_settings_post(); if ( is_string( $redirect ) && '' !== $redirect ) { wp_safe_redirect( $redirect ); exit; } } public static function render(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } $tab = self::get_active_tab(); $is_connected = GscAdmin::get_instance()->is_connected(); $gsc_enabled = GscModule::get_instance()->is_enabled(); $crossposting_enabled = CrossPostingModule::get_instance()->is_enabled(); $inventory_enabled = InventoryModule::get_instance()->is_enabled(); $seo_adapter = SeoMetaRegistry::get_active(); $multilingual = MultilingualRegistry::get_active(); $inventory_stats = $inventory_enabled ? ( new Repository() )->get_stats() : null; $heading_svg = ''; ?>

>
>
>
>
>
is_enabled() ? self::TAB_GSC : self::TAB_SETTINGS; } return self::TAB_SETTINGS; } // phpcs:enable WordPress.Security.NonceVerification.Recommended return $tab; } private static function render_tab_button( string $id, string $label, string $active_tab ): void { $active = $id === $active_tab; printf( '', esc_attr( $id ), $active ? ' is-active' : '', $active ? 'true' : 'false', $active ? 0 : -1, esc_html( $label ) ); } /** * @param array|null $inventory_stats */ private static function render_overview_tab( bool $is_connected, bool $crossposting_enabled, bool $inventory_enabled, ?array $inventory_stats, $seo_adapter, $multilingual ): void { ?>

get_label(), $multilingual->get_label() ), 'none' !== $seo_adapter->get_id() ? 'live' : 'warn', 'none' !== $seo_adapter->get_id() ? __( 'Adapter active', '4wp-seo-helper' ) : __( 'No SEO plugin detected', '4wp-seo-helper' ) ); self::render_status_card( __( 'TechArticle schema', '4wp-seo-helper' ), __( 'JSON-LD and Gutenberg blocks for technical articles.', '4wp-seo-helper' ), Release::is_module_public( Release::MODULE_TECHARTICLE ) ? 'live' : 'soon', Release::is_module_public( Release::MODULE_TECHARTICLE ) ? __( 'Active in editor & frontend', '4wp-seo-helper' ) : __( 'Planned for a future release', '4wp-seo-helper' ) ); self::render_status_card( __( 'Google Search Console', '4wp-seo-helper' ), __( 'OAuth, property picker, URL inspection, analytics.', '4wp-seo-helper' ), Release::is_module_public( Release::MODULE_GSC ) ? ( ! GscModule::get_instance()->is_enabled() ? 'off' : ( GscAdmin::get_instance()->is_menu_visible() ? 'live' : ( $is_connected ? 'warn' : 'warn' ) ) ) : 'soon', Release::is_module_public( Release::MODULE_GSC ) ? ( ! GscModule::get_instance()->is_enabled() ? __( 'Disabled — enable in Settings', '4wp-seo-helper' ) : ( GscAdmin::get_instance()->is_menu_visible() ? __( 'Ready — open GSC in the sidebar', '4wp-seo-helper' ) : ( $is_connected ? __( 'Choose a property in Search Console tab', '4wp-seo-helper' ) : __( 'Not connected', '4wp-seo-helper' ) ) ) ) : __( 'Planned for a future release', '4wp-seo-helper' ) ); self::render_status_card( __( 'LLMS.txt', '4wp-seo-helper' ), __( 'Machine-readable site index for AI crawlers.', '4wp-seo-helper' ), Release::is_module_public( Release::MODULE_LLMS ) ? 'live' : 'soon', Release::is_module_public( Release::MODULE_LLMS ) ? (string) home_url( '/llms.txt' ) : __( 'Planned for a future release', '4wp-seo-helper' ) ); self::render_status_card( __( 'Cross posting', '4wp-seo-helper' ), __( 'Editor tools for social / syndication drafts.', '4wp-seo-helper' ), Release::is_module_public( Release::MODULE_CROSSPOSTING ) ? ( $crossposting_enabled ? 'live' : 'off' ) : 'soon', Release::is_module_public( Release::MODULE_CROSSPOSTING ) ? ( $crossposting_enabled ? __( 'Enabled', '4wp-seo-helper' ) : __( 'Disabled', '4wp-seo-helper' ) ) : __( 'Planned for a future release', '4wp-seo-helper' ) ); ?>

$provider Provider row from PositionsSettings::providers(). */ private static function render_position_provider_card( array $provider ): void { $id = (string) ( $provider['id'] ?? '' ); $label = (string) ( $provider['label'] ?? '' ); $live = ! empty( $provider['live'] ); $ready = ! empty( $provider['configured'] ); $classes = 'forwp-seo-provider-card'; if ( $ready ) { $classes .= ' is-enabled'; } ?>

$provider Provider row from PositionsSettings::providers(). */ private static function render_position_provider_detail( array $provider, bool $key_set ): void { $id = (string) ( $provider['id'] ?? '' ); $live = ! empty( $provider['live'] ); ?> $provider Provider row. */ private static function render_provider_links( array $provider ): void { $site_url = (string) ( $provider['site_url'] ?? '' ); $site_label = (string) ( $provider['site_label'] ?? $site_url ); $extra = is_array( $provider['extra_links'] ?? null ) ? $provider['extra_links'] : []; if ( '' === $site_url ) { return; } ?> render_connect_section( $gsc_enabled ); } private static function render_ai_assistant_panel(): void { $url = admin_url( 'admin.php?page=' . Menu::AI_PAGE_SLUG ); ?>

__( 'TechArticle schema & blocks', '4wp-seo-helper' ), Release::MODULE_LLMS => __( 'LLMS.txt', '4wp-seo-helper' ), Release::MODULE_CROSSPOSTING => __( 'Cross posting', '4wp-seo-helper' ), ]; $upcoming = array_filter( $modules, static function ( string $label, string $slug ): bool { unset( $label ); return ! Release::is_module_public( $slug ); }, ARRAY_FILTER_USE_BOTH ); if ( empty( $upcoming ) ) { return; } ?>

get_api_token(); $base_url = rest_url( 'forwp-seo-helper/v1/seo-inventory' ); $priority_labels = PriorityLabels::get_all(); if ( '' === $token ) { $token = InventoryModule::get_instance()->regenerate_api_token(); } ?>

$default_label ) : ?>
regenerate_api_token(); $tab = self::TAB_API; $saved = 'token'; } if ( ! empty( $_POST['forwp_seo_save_settings'] ) ) { if ( Release::is_module_public( Release::MODULE_CROSSPOSTING ) ) { $enabled = isset( $_POST['forwp_seo_crossposting_enabled'] ) ? '1' : '0'; update_option( CrossPostingModule::OPTION_ENABLED, $enabled ); } if ( Release::is_module_public( Release::MODULE_INVENTORY_API ) ) { $inventory_enabled = isset( $_POST['forwp_seo_inventory_enabled'] ) ? '1' : '0'; update_option( InventoryModule::OPTION_ENABLED, $inventory_enabled ); } if ( Release::is_module_public( Release::MODULE_GSC ) ) { $gsc_enabled = isset( $_POST['forwp_seo_gsc_enabled'] ) ? '1' : '0'; update_option( GscModule::OPTION_ENABLED, $gsc_enabled ); if ( '0' === $gsc_enabled ) { GscSync::unschedule_cron(); } else { GscSync::sync_cron_state( GscModule::get_instance()->is_cron_enabled() ); } } $tab = self::TAB_SETTINGS; $saved = 'settings'; } if ( ! empty( $_POST['forwp_seo_save_position_providers'] ) || ! empty( $_POST['forwp_seo_test_position_provider'] ) ) { PositionsSettings::save_from_post(); $tab = self::TAB_POSITIONS; $saved = 'positions'; if ( ! empty( $_POST['forwp_seo_test_position_provider'] ) ) { $result = PositionsSettings::test_connection(); set_transient( 'forwp_seo_positions_test_' . get_current_user_id(), $result, 60 ); $saved = 'positions_test'; } } if ( ! empty( $_POST['forwp_seo_save_excluded_post_types'] ) ) { $slugs = isset( $_POST['forwp_seo_excluded_post_types'] ) ? (array) wp_unslash( $_POST['forwp_seo_excluded_post_types'] ) : []; ExcludedPostTypes::set( $slugs ); $tab = self::TAB_SETTINGS; $saved = 'excluded_types'; } if ( ! empty( $_POST['forwp_seo_save_priority_labels'] ) ) { PriorityLabels::save( [ '1' => sanitize_text_field( wp_unslash( (string) ( $_POST['forwp_seo_priority_label_1'] ?? '' ) ) ), '2' => sanitize_text_field( wp_unslash( (string) ( $_POST['forwp_seo_priority_label_2'] ?? '' ) ) ), '3' => sanitize_text_field( wp_unslash( (string) ( $_POST['forwp_seo_priority_label_3'] ?? '' ) ) ), ] ); $tab = self::TAB_API; $saved = 'priority'; } if ( false === $saved ) { return null; } $args = [ 'page' => Menu::SETTINGS_PAGE_SLUG, 'tab' => $tab, 'saved' => $saved, ]; if ( self::TAB_POSITIONS === $tab ) { $open = sanitize_key( wp_unslash( (string) ( $_POST['forwp_seo_open_provider'] ?? PositionsSettings::PROVIDER_SERPER ) ) ); if ( '' !== $open ) { $args['provider'] = $open; } } return add_query_arg( $args, admin_url( 'admin.php' ) ); } private static function render_notices(): void { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only PRG success and OAuth redirect flags. if ( isset( $_GET['saved'] ) ) { $saved = sanitize_key( wp_unslash( $_GET['saved'] ) ); if ( 'token' === $saved ) { echo '

' . esc_html__( 'Sync token regenerated.', '4wp-seo-helper' ) . '

'; } elseif ( 'settings' === $saved ) { echo '

' . esc_html__( 'Settings saved.', '4wp-seo-helper' ) . '

'; } elseif ( 'priority' === $saved ) { echo '

' . esc_html__( 'Priority tiers saved.', '4wp-seo-helper' ) . '

'; } elseif ( 'excluded_types' === $saved ) { echo '

' . esc_html__( 'Excluded post types saved.', '4wp-seo-helper' ) . '

'; } elseif ( 'positions' === $saved ) { echo '

' . esc_html__( 'Position provider saved.', '4wp-seo-helper' ) . '

'; } elseif ( 'positions_test' === $saved ) { $test = get_transient( 'forwp_seo_positions_test_' . get_current_user_id() ); delete_transient( 'forwp_seo_positions_test_' . get_current_user_id() ); if ( is_array( $test ) ) { $ok = ! empty( $test['ok'] ); printf( '

%2$s

', $ok ? 'success' : 'error', esc_html( (string) ( $test['message'] ?? '' ) ) ); } } } if ( Release::is_module_public( Release::MODULE_GSC ) ) { if ( ! empty( $_GET['gsc_error'] ) ) { echo '

' . esc_html( sanitize_text_field( wp_unslash( $_GET['gsc_error'] ) ) ) . '

'; } if ( ! empty( $_GET['gsc_connected'] ) ) { echo '

' . esc_html__( 'Google Search Console connected.', '4wp-seo-helper' ) . '

'; } if ( ! empty( $_GET['gsc_disconnected'] ) ) { echo '

' . esc_html__( 'Google Search Console disconnected.', '4wp-seo-helper' ) . '

'; } if ( isset( $_GET['gsc_saved'] ) ) { $gsc_saved = sanitize_key( wp_unslash( $_GET['gsc_saved'] ) ); if ( 'property' === $gsc_saved ) { echo '

' . esc_html__( 'Search Console property saved.', '4wp-seo-helper' ) . '

'; } elseif ( 'credentials' === $gsc_saved ) { echo '

' . esc_html__( 'Search Console credentials saved.', '4wp-seo-helper' ) . '

'; } elseif ( 'redirect' === $gsc_saved ) { echo '

' . esc_html__( 'OAuth redirect saved.', '4wp-seo-helper' ) . '

'; } } if ( isset( $_GET['data_cleared'] ) && '1' === (string) wp_unslash( $_GET['data_cleared'] ) ) { echo '

' . esc_html__( 'All synced Search Console data was cleared.', '4wp-seo-helper' ) . '

'; } if ( ! empty( $_GET['clear_error'] ) ) { echo '

' . esc_html( sanitize_text_field( wp_unslash( (string) $_GET['clear_error'] ) ) ) . '

'; } } // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Plugin version for admin UI (uppercase, from plugin header). */ public static function get_version_label(): string { $version = defined( 'FORWP_SEO_HELPER_VERSION' ) ? FORWP_SEO_HELPER_VERSION : '0.0.0'; return strtoupper( (string) $version ); } /** * @return array> */ public static function svg_allowed_html(): array { return [ 'svg' => [ 'xmlns' => true, 'viewbox' => true, 'width' => true, 'height' => true, 'fill' => true, 'focusable' => true, 'aria-hidden' => true, ], 'path' => [ 'd' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, ], 'circle' => [ 'cx' => true, 'cy' => true, 'r' => true, 'fill' => true, ], ]; } }