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

'; 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 ''; echo ''; echo ''; echo ''; echo ''; } echo '
PageTypeSEO titleDescriptionKeyphraseScore
' . esc_html( get_the_title( $p ) ?: '(no title)' ) . '' . ( in_array( 'noindex', $ctx['robots'], true ) ? ' (noindex)' : '' ) . '' . esc_html( $p->post_type ) . '' . esc_html( sxseo_len( $ctx['title'] ) ) . ' chars' . ( $ctx['title_auto'] ? ' auto' : '' ) . '' . esc_html( sxseo_len( $ctx['desc'] ) ) . ' chars' . ( $ctx['desc_auto'] ? ' auto' : '' ) . '' . esc_html( $ctx['focus'] ) . ( $ctx['focus_auto'] ? ' auto' : '' ) . '' . esc_html( $score ) . '
'; 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 '

Sentrixa SEO settings

'; echo ''; wp_nonce_field( 'sxseo_settings' ); echo ''; foreach ( $tabs[ $cur ][1] as $f ) { list( $key, $label, $type, $help ) = $f; $val = $s[ $key ]; echo ''; } echo '
'; if ( 'check' === $type ) { echo ''; } elseif ( 'select' === $type ) { echo ''; } elseif ( 'textarea' === $type ) { echo ''; } else { echo ''; } if ( $help ) { echo '

' . esc_html( $help ) . '

'; } echo '
'; submit_button( 'Save settings' ); 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 '

Redirects

'; echo '
'; wp_nonce_field( 'sxseo_redir' ); $pre = isset( $_GET['from'] ) ? sanitize_text_field( wp_unslash( $_GET['from'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- prefills a form echo ' → '; submit_button( 'Add redirect', 'primary', 'submit', false ); echo '
'; foreach ( $rows as $r ) { echo ''; } if ( ! $rows ) { echo ''; } echo '
FromToTypeHitsSource
' . esc_html( $r->source ?: '/' ) . '' . esc_html( $r->target ) . '' . (int) $r->type . '' . (int) $r->hits . '' . ( $r->auto ? 'automatic' : 'manual' ) . 'Delete
No redirects yet. They are created automatically when you change a page address.
'; } 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 ''; foreach ( $rows as $r ) { echo ''; } if ( ! $rows ) { echo ''; } echo '
AddressHitsLast seenCame from
' . esc_html( $r->path ) . '' . (int) $r->hits . '' . esc_html( human_time_diff( strtotime( $r->last_seen . ' UTC' ) ) ) . ' ago' . esc_html( $r->referrer ) . 'Redirect
No broken links recorded.
'; } 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.' ); } }