Report' );
return $l;
} );
}
public static function menu() {
add_menu_page( 'Sentrixa SEO', 'Sentrixa SEO', 'edit_posts', 'sentrixa-seo', array( __CLASS__, 'page_overview' ), 'dashicons-chart-area', 58 );
add_submenu_page( 'sentrixa-seo', 'Overview', 'Overview', 'edit_posts', 'sentrixa-seo', array( __CLASS__, 'page_overview' ) );
add_submenu_page( 'sentrixa-seo', 'Audit', 'Audit', 'edit_posts', 'sentrixa-seo-audit', array( __CLASS__, 'page_audit' ) );
add_submenu_page( 'sentrixa-seo', 'Settings', 'Settings', 'manage_options', 'sentrixa-seo-settings', array( __CLASS__, 'page_settings' ) );
add_submenu_page( 'sentrixa-seo', 'Redirects', 'Redirects', 'manage_options', 'sentrixa-seo-redirects', array( __CLASS__, 'page_redirects' ) );
add_submenu_page( 'sentrixa-seo', '404 monitor', '404 monitor', 'manage_options', 'sentrixa-seo-404', array( __CLASS__, 'page_404' ) );
add_submenu_page( 'sentrixa-seo', 'Tools', 'Tools', 'manage_options', 'sentrixa-seo-tools', array( __CLASS__, 'page_tools' ) );
}
public static function notices() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$screen = get_current_screen();
if ( $screen && 'plugins' === $screen->id && SXSEO_Health::other_seo_plugins() ) {
echo '
Sentrixa SEO: another SEO plugin is active (' . esc_html( implode( ', ', SXSEO_Health::other_seo_plugins() ) ) . '). Import its data under Sentrixa SEO → Tools , then deactivate it to avoid duplicate tags.
';
}
if ( isset( $_GET['sxseo_msg'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
echo '' . esc_html( sanitize_text_field( wp_unslash( $_GET['sxseo_msg'] ) ) ) . '
'; // phpcs:ignore WordPress.Security.NonceVerification
}
}
private static function back( $page, $msg ) {
wp_safe_redirect( add_query_arg( array( 'page' => $page, 'sxseo_msg' => rawurlencode( $msg ) ), admin_url( 'admin.php' ) ) );
exit;
}
private static function guard( $nonce ) {
if ( ! current_user_can( 'manage_options' ) || ! isset( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), $nonce ) ) {
wp_die( 'Not allowed.', 403 );
}
}
// ------------------------------------------------------------- overview
public static function page_overview() {
echo 'Sentrixa SEO ';
echo '
Everything is optimised automatically. This is the status of your site right now.
';
echo '
';
SXSEO_Health::render_widget();
echo '
Site checks ';
foreach ( SXSEO_Health::site_checks() as $c ) {
$icon = 'good' === $c['status'] ? '✓' : ( 'bad' === $c['status'] ? '✕' : '!' );
echo '' . esc_html( $icon ) . ' ' . ( $c['link'] && 'good' !== $c['status'] ? '' . esc_html( $c['msg'] ) . ' ' : esc_html( $c['msg'] ) ) . ' ';
}
echo ' sitemap.xml · robots.txt · llms.txt
';
echo '
';
}
// ---------------------------------------------------------------- audit
public static function page_audit() {
$paged = isset( $_GET['paged'] ) ? max( 1, absint( wp_unslash( $_GET['paged'] ) ) ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only pagination
$q = new WP_Query( array( 'post_type' => sxseo_post_types(), 'post_status' => 'publish', 'posts_per_page' => 25, 'paged' => $paged, 'orderby' => 'title', 'order' => 'ASC' ) );
echo 'SEO audit Page Type SEO title Description Keyphrase Score ';
foreach ( $q->posts as $p ) {
$ctx = SXSEO_Context::for_post( $p );
$score = get_post_meta( $p->ID, '_sxseo_score', true );
if ( '' === $score ) {
$score = SXSEO_Analysis::for_post( $p )['score'];
}
$cls = $score >= 80 ? 'good' : ( $score >= 60 ? 'ok' : 'bad' );
echo '' . esc_html( get_the_title( $p ) ?: '(no title)' ) . ' ' . ( in_array( 'noindex', $ctx['robots'], true ) ? ' (noindex) ' : '' ) . '' . esc_html( $p->post_type ) . ' ';
echo '' . esc_html( sxseo_len( $ctx['title'] ) ) . ' chars' . ( $ctx['title_auto'] ? ' auto ' : '' ) . ' ';
echo '' . esc_html( sxseo_len( $ctx['desc'] ) ) . ' chars' . ( $ctx['desc_auto'] ? ' auto ' : '' ) . ' ';
echo '' . esc_html( $ctx['focus'] ) . ( $ctx['focus_auto'] ? ' auto ' : '' ) . ' ';
echo '' . esc_html( $score ) . ' ';
}
echo '
';
echo wp_kses_post( (string) paginate_links( array( 'base' => add_query_arg( 'paged', '%#%' ), 'format' => '', 'current' => $paged, 'total' => max( 1, $q->max_num_pages ) ) ) );
echo '
';
}
// ------------------------------------------------------------- settings
private static function tabs() {
return array(
'general' => array( 'Titles & descriptions', array(
array( 'sep', 'Title separator', 'text', 'Placed between parts of the title, e.g. –, | or ·' ),
array( 'title_home', 'Home page title', 'text', 'Variables: %sitename% %tagline% %sep%' ),
array( 'home_description', 'Home page description', 'textarea', 'Blank = your tagline' ),
array( 'title_post', 'Post title', 'text', '%title% %sep% %sitename% %category% %author%' ),
array( 'title_page', 'Page title', 'text', '' ),
array( 'title_term', 'Category / tag archive title', 'text', '%term%' ),
array( 'title_author', 'Author archive title', 'text', '%author%' ),
array( 'title_archive', 'Other archives', 'text', '%archive%' ),
array( 'title_search', 'Search results', 'text', '%searchphrase%' ),
array( 'title_404', 'Page not found', 'text', '' ),
array( 'description_len', 'Automatic description length', 'text', '155 is ideal (100–320).' ),
) ),
'business' => array( 'Business', array(
array( 'org_type', 'Type', 'select', 'Local business enables map and opening-hours features.', array( 'Organization' => 'Organisation', 'LocalBusiness' => 'Local business', 'ProfessionalService' => 'Professional service', 'Store' => 'Shop', 'Restaurant' => 'Restaurant', 'EducationalOrganization' => 'School / education', 'NGO' => 'Charity / NGO', 'Person' => 'Person / personal brand' ) ),
array( 'org_name', 'Business name', 'text', '' ),
array( 'org_legal_name', 'Legal name', 'text', 'e.g. Your Company Ltd (optional).' ),
array( 'org_description', 'Business description', 'textarea', 'One or two sentences about what you do. Used in structured data.' ),
array( 'org_knows_about', 'Topics you are expert in', 'text', 'Comma separated, e.g. Penetration testing, ISO 27001.' ),
array( 'org_services', 'Services', 'textarea', 'One per line. Listed in structured data so search engines understand what you offer.' ),
array( 'org_logo', 'Logo URL', 'url', 'Square, at least 112×112px.' ),
array( 'org_phone', 'Phone', 'text', '' ),
array( 'org_email', 'Email', 'text', '' ),
array( 'org_street', 'Street address', 'text', '' ),
array( 'org_city', 'Town / city', 'text', '' ),
array( 'org_region', 'County / region', 'text', '' ),
array( 'org_postcode', 'Postcode', 'text', '' ),
array( 'org_country', 'Country code', 'text', 'e.g. GB' ),
array( 'org_lat', 'Latitude', 'text', 'For maps and local search.' ),
array( 'org_lng', 'Longitude', 'text', '' ),
array( 'org_hours', 'Opening hours', 'textarea', 'One per line, e.g. Mo-Fr 09:00-17:30' ),
array( 'org_area', 'Areas served', 'text', 'Comma separated towns or counties.' ),
array( 'org_price_range', 'Price range', 'text', 'e.g. ££' ),
array( 'org_profiles', 'Social profile URLs', 'textarea', 'One per line: Facebook, LinkedIn, X, Instagram, YouTube…' ),
array( 'article_type', 'Article type for posts', 'select', '', array( 'BlogPosting' => 'Blog post', 'Article' => 'Article', 'NewsArticle' => 'News article' ) ),
) ),
'social' => array( 'Social', array(
array( 'og', 'Open Graph (Facebook, LinkedIn, WhatsApp…)', 'check', '' ),
array( 'twitter_card', 'X / Twitter card', 'select', '', array( 'summary_large_image' => 'Large image', 'summary' => 'Small image', 'none' => 'Off' ) ),
array( 'twitter_site', 'X / Twitter handle', 'text', '@yourbrand' ),
array( 'fb_app_id', 'Facebook App ID', 'text', 'Optional.' ),
array( 'default_image', 'Default social image URL', 'url', 'Used when a page has no image. 1200×630 is ideal.' ),
) ),
'legacy' => array( 'Legacy & extra tags', array(
array( 'meta_keywords', 'Keywords meta tag', 'check', 'Google ignores it; some other engines still read it.' ),
array( 'meta_author', 'Author meta tag', 'check', '' ),
array( 'meta_language', 'Language meta tags', 'check', '' ),
array( 'meta_copyright', 'Copyright meta tag', 'check', '' ),
array( 'meta_legacy', 'Classic tags (revisit-after, rating, distribution…)', 'check', 'Harmless; read by older crawlers.' ),
array( 'dublin_core', 'Dublin Core tags', 'check', 'Used by libraries, government and academic indexes.' ),
array( 'geo_meta', 'Geo tags (needs latitude/longitude)', 'check', '' ),
array( 'hreflang', 'Language alternate link', 'check', '' ),
array( 'cleanup_head', 'Remove WordPress clutter from the head', 'check', 'Generator, RSD, wlwmanifest, shortlink.' ),
) ),
'index' => array( 'Indexing & discovery', array(
array( 'sitemap', 'XML sitemap', 'check', '/sitemap.xml' ),
array( 'sitemap_images', 'Include images in the sitemap', 'check', '' ),
array( 'indexnow', 'IndexNow instant notification', 'check', 'Tells Bing, Yandex and others the moment you publish.' ),
array( 'llms_txt', 'llms.txt for AI assistants', 'check', '/llms.txt' ),
array( 'ai_policy', 'AI training crawlers', 'select', 'Search and answer engines can always read your site; this only affects model-training bots.', array( 'allow' => 'Allow', 'block_training' => 'Block (GPTBot, CCBot, Google-Extended, ClaudeBot…)' ) ),
array( 'noindex_search', 'Hide search results pages', 'check', '' ),
array( 'noindex_date', 'Hide date archives', 'check', '' ),
array( 'noindex_author', 'Hide author archives', 'select', '', array( 'auto' => 'Automatic (single-author sites)', '1' => 'Always', '0' => 'Never' ) ),
array( 'attachment_redirect', 'Redirect attachment pages to their post', 'check', '' ),
array( 'robots_max', 'Allow large image previews and full snippets', 'check', '' ),
array( 'feeds_noindex', 'Keep RSS feeds out of results', 'check', '' ),
array( 'robots_txt_extras', 'Block search-result URLs in robots.txt', 'check', '' ),
array( 'auto_redirects', 'Redirect automatically when an address changes', 'check', '' ),
array( 'smart_404', 'Recover old URLs by matching page names', 'check', '' ),
array( 'log_404', 'Log broken links', 'check', '' ),
array( 'auto_alt', 'Fill in missing image alt text', 'check', '' ),
) ),
'verify' => array( 'Verification', array(
array( 'v_google', 'Google Search Console', 'text', 'Just the code from the verification tag.' ),
array( 'v_bing', 'Bing Webmaster Tools', 'text', '' ),
array( 'v_yandex', 'Yandex', 'text', '' ),
array( 'v_baidu', 'Baidu', 'text', '' ),
array( 'v_pinterest', 'Pinterest', 'text', '' ),
array( 'v_facebook', 'Facebook domain', 'text', '' ),
) ),
);
}
public static function page_settings() {
$tabs = self::tabs();
if ( SXSEO_Edition::is_pro() ) { // updates come from WordPress.org in the free edition
$tabs['index'][1][] = array( 'auto_update', 'Keep Sentrixa SEO up to date automatically', 'check', 'Recommended: search engines change, so the plugin does too.' );
}
$cur = isset( $_GET['tab'] ) && isset( $tabs[ $_GET['tab'] ] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general'; // phpcs:ignore WordPress.Security.NonceVerification
$s = SXSEO_Settings::all();
echo '';
}
public static function save_settings() {
check_admin_referer( 'sxseo_settings' );
self::guard( 'sxseo_settings' );
$in = isset( $_POST['s'] ) ? (array) wp_unslash( $_POST['s'] ) : array();
SXSEO_Settings::save( $in );
SXSEO_Sitemap::add_rewrites();
flush_rewrite_rules( false );
$tab = isset( $_POST['tab'] ) ? sanitize_key( wp_unslash( $_POST['tab'] ) ) : 'general';
wp_safe_redirect( admin_url( 'admin.php?page=sentrixa-seo-settings&tab=' . $tab . '&sxseo_msg=' . rawurlencode( 'Settings saved.' ) ) );
exit;
}
// ------------------------------------------------------------ redirects
public static function page_redirects() {
global $wpdb;
$rows = $wpdb->get_results( 'SELECT * FROM ' . SXSEO_Redirects::table() . ' ORDER BY id DESC LIMIT 300' ); // phpcs:ignore WordPress.DB
echo '';
}
public static function redirect_add() {
check_admin_referer( 'sxseo_redir' );
self::guard( 'sxseo_redir' );
$ok = SXSEO_Redirects::add( sanitize_text_field( wp_unslash( $_POST['source'] ?? '' ) ), sanitize_text_field( wp_unslash( $_POST['target'] ?? '' ) ), isset( $_POST['type'] ) ? absint( wp_unslash( $_POST['type'] ) ) : 301 );
self::back( 'sentrixa-seo-redirects', $ok ? 'Redirect added.' : 'Could not add that redirect (empty, or it points to itself).' );
}
public static function redirect_del() {
check_admin_referer( 'sxseo_redir' );
self::guard( 'sxseo_redir' );
global $wpdb;
$wpdb->delete( SXSEO_Redirects::table(), array( 'id' => isset( $_GET['id'] ) ? absint( wp_unslash( $_GET['id'] ) ) : 0 ) );
wp_cache_flush();
self::back( 'sentrixa-seo-redirects', 'Redirect deleted.' );
}
public static function page_404() {
global $wpdb;
$rows = $wpdb->get_results( 'SELECT * FROM ' . SXSEO_Redirects::log_table() . ' ORDER BY hits DESC, last_seen DESC LIMIT 200' ); // phpcs:ignore WordPress.DB
echo '404 monitor Addresses visitors and search engines asked for that do not exist. Redirect the valuable ones so their traffic and rankings are not lost.
';
echo '
Clear log
';
echo '
Address Hits Last seen Came from ';
foreach ( $rows as $r ) {
echo '' . esc_html( $r->path ) . ' ' . (int) $r->hits . ' ' . esc_html( human_time_diff( strtotime( $r->last_seen . ' UTC' ) ) ) . ' ago ' . esc_html( $r->referrer ) . ' Redirect ';
}
if ( ! $rows ) {
echo 'No broken links recorded. ';
}
echo '
';
}
public static function clear_404() {
self::guard( 'sxseo_404' );
global $wpdb;
$wpdb->query( 'TRUNCATE TABLE ' . SXSEO_Redirects::log_table() ); // phpcs:ignore WordPress.DB
self::back( 'sentrixa-seo-404', '404 log cleared.' );
}
// ---------------------------------------------------------------- tools
public static function page_tools() {
$found = SXSEO_Importer::detect();
echo 'Tools Import from another SEO plugin ';
if ( $found ) {
echo '
Found SEO data from: ' . esc_html( implode( ', ', array_map( function ( $k, $v ) {
return "$k ($v pages)";
}, array_keys( $found ), $found ) ) ) . ' . Only blank Sentrixa SEO fields are filled; nothing is overwritten.
';
echo '
';
wp_nonce_field( 'sxseo_import' );
submit_button( 'Import now', 'primary', 'submit', false );
echo '';
} else {
echo '
No data from Yoast SEO, Rank Math or All in One SEO was found.
';
}
echo '
Addresses ';
echo '
Add [sxseo_breadcrumbs] to a page or template for visible breadcrumbs matching the structured data.
';
}
public static function do_import() {
self::guard( 'sxseo_import' );
$done = SXSEO_Importer::run();
self::back( 'sentrixa-seo-tools', $done ? 'Imported ' . array_sum( $done ) . ' values.' : 'Nothing to import.' );
}
}