*/
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 = '';
?>
|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();
}
?>
|
|
|
|
|
|
|
Authorization: Bearer <token> |
|
|
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(
'',
$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,
],
];
}
}