load_dependencies(); $this->init_hooks(); } /** * Load all class files. */ private function load_dependencies() { $includes = ASGM_PLUGIN_DIR . 'includes/'; // License stub. Class_exists-guarded so the Pro plugin's real License // class (loaded at plugins_loaded priority 5, before this method runs // at priority 10) takes precedence when both plugins are active. // When Pro is not installed, this stub is the only License class and // returns false for everything. require_once $includes . 'class-license-stub.php'; require_once $includes . 'class-account-connection.php'; require_once $includes . 'class-site-profile.php'; // Free classes — always loaded. require_once $includes . 'class-api.php'; require_once $includes . 'class-detector.php'; require_once $includes . 'class-faq-parser.php'; require_once $includes . 'class-schema.php'; require_once $includes . 'class-robots.php'; require_once $includes . 'class-aeo.php'; require_once $includes . 'class-ai-crawlers.php'; require_once $includes . 'class-llms.php'; require_once $includes . 'class-crawler-log.php'; require_once $includes . 'class-dashboard-widget.php'; require_once $includes . 'class-repurpose-ui.php'; require_once $includes . 'class-content-gaps.php'; require_once $includes . 'class-validator.php'; require_once $includes . 'class-conflict.php'; require_once $includes . 'class-ai-plugin.php'; require_once $includes . 'class-ai-headers.php'; require_once $includes . 'class-editor-panel.php'; require_once $includes . 'class-metadata-writer.php'; require_once $includes . 'class-metadata-generator.php'; require_once $includes . 'class-answer-density.php'; require_once $includes . 'class-rendered-page-evaluator.php'; require_once $includes . 'class-link-health.php'; require_once $includes . 'class-content-health.php'; require_once $includes . 'class-crawler-access.php'; require_once $includes . 'class-bulk-meta.php'; require_once $includes . 'class-okf.php'; require_once $includes . 'class-affiliate-badge.php'; require_once $includes . 'class-faq-blocks.php'; require_once $includes . 'class-brand-kit.php'; require_once $includes . 'class-visual-icons.php'; require_once $includes . 'class-visual-templates.php'; require_once $includes . 'class-cta-renderer.php'; require_once $includes . 'class-visual-blocks.php'; require_once $includes . 'class-visual-ai.php'; require_once $includes . 'class-draft-quality.php'; require_once $includes . 'class-earned-source-api.php'; // Pro classes live in the separate `aeo-god-mode-pro` plugin and load // themselves on plugins_loaded priority 5/10. Free does not require any // Pro files. Free code paths that need Pro at runtime use class_exists() // checks (see init_hooks() in this class) so they no-op gracefully when // Pro is not installed. } /** * Initialise WordPress hooks. */ private function init_hooks() { add_action( 'admin_menu', array( $this, 'register_admin_menu' ) ); Account_Connection::init(); // Reassert the public product identity after third-party/older add-ons // have registered their menu filters. This protects mixed-version // updates without knowing anything about a paid tier. add_action( 'admin_menu', array( $this, 'enforce_admin_product_identity' ), PHP_INT_MAX ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) ); add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) ); // Defensive cache-busting: every REST response from this plugin // explicitly tells page caches (LiteSpeed, WP Rocket, W3TC, etc.) // never to cache. Activation also writes path-based excludes, but // this header is the floor in case a customer adds a new cache // plugin after activation. add_filter( 'rest_post_dispatch', array( $this, 'set_rest_no_cache_headers' ), 10, 3 ); add_action( 'admin_init', array( $this, 'maybe_redirect_to_wizard' ) ); add_action( 'admin_notices', array( $this, 'maybe_show_setup_notice' ) ); add_action( 'admin_notices', array( $this, 'maybe_show_pro_migration_notice' ) ); // Front-end hooks — schema, meta, crawler logging. // // Meta stays at priority 1 because it installs a pre_get_document_title // filter, which is only useful before core renders the title tag. // // Schema waits until 9 so Yoast and Rank Math, both at priority 1, have // already published their graph. Our author reference then points at // the Person node that exists on the page instead of one we predicted // from a plugin constant. See EEAT::author_person_id(). add_action( 'wp_head', array( $this, 'render_native_meta' ), 1 ); add_filter( 'pre_get_document_title', array( __NAMESPACE__ . '\\MetadataWriter', 'filter_native_title' ), 20 ); add_action( 'wp_head', array( $this, 'render_frontend_output' ), 9 ); add_action( 'init', array( $this, 'boot_modules' ) ); add_action( 'init', array( $this, 'register_post_meta_fields' ) ); // Daily site-score refresh. Keeps the cached AEO score fresh for the // agency console even when nobody opens the dashboard. add_action( 'init', array( $this, 'schedule_site_score_refresh' ) ); add_action( 'asgm_refresh_site_score', array( $this, 'run_site_score_refresh' ) ); // Bust the site-health transient whenever the inputs that feed it // change. Keeps the dashboard's Setup Details panel in sync after // robots rules edits, settings saves, scan runs, or conflict // resolutions, without waiting on the 5-minute cache. $invalidate = function () { delete_transient( 'asgm_site_health' ); }; foreach ( array( 'asgm_settings', 'asgm_robots_rules', 'asgm_content_gap_results', 'asgm_schema_resolutions' ) as $opt ) { add_action( "update_option_{$opt}", $invalidate ); add_action( "add_option_{$opt}", $invalidate ); } // Schema conflict resolution: when user picks "ours" for a type, strip it from Rank Math / Yoast. // Deferred to plugins_loaded p20 so RANK_MATH_VERSION / WPSEO_VERSION are defined when we check. // (Plugin load order is alphabetical: aeo-god-mode loads before seo-by-rank-math.) add_action( 'plugins_loaded', array( $this, 'register_schema_override_filters' ), 20 ); // GSC background batch inspection via WP Cron. add_action( 'asgm_gsc_batch_inspect', function () { if ( class_exists( 'AISEOGodMode\\GSC' ) ) { $gsc = new \AISEOGodMode\GSC(); $gsc->batch_inspect(); } } ); // Local avatar upload — replaces Gravatar for users with uploaded photos. API::init_avatar_filters(); // Free owns the stable REST contract used by the shared Pro/Growth UI. // The paid repository and matching implementation are supplied by the // Pro companion at runtime, so no paid source is shipped in Free. Earned_Source_API::init(); // Ensure user profile form supports file uploads. add_action( 'user_edit_form_tag', function() { echo ' enctype="multipart/form-data"'; } ); // E-E-A-T user profile fields (Pro only). if ( License::is_pro() ) { add_action( 'show_user_profile', array( 'AISEOGodMode\EEAT', 'show_user_fields' ) ); add_action( 'edit_user_profile', array( 'AISEOGodMode\EEAT', 'show_user_fields' ) ); add_action( 'personal_options_update', array( 'AISEOGodMode\EEAT', 'save_user_fields' ) ); add_action( 'edit_user_profile_update', array( 'AISEOGodMode\EEAT', 'save_user_fields' ) ); } // Plugin action links add_filter( 'plugin_action_links_' . ASGM_PLUGIN_BASENAME, array( $this, 'add_plugin_action_links' ) ); } /** * Add links to the plugin listing on the Plugins page. * * @param array $links Existing links. * @return array Modified links. */ public function add_plugin_action_links( $links ) { $setup_link = '' . __( 'Setup Wizard', 'aeo-god-mode' ) . ''; $docs_url = apply_filters( 'asgm_documentation_url', 'https://aeogodmode.io/docs/' ); $support_url = apply_filters( 'asgm_support_url', 'https://aeogodmode.io/support/' ); $docs_link = '' . __( 'Documentation', 'aeo-god-mode' ) . ''; $support_link = '' . __( 'Support', 'aeo-god-mode' ) . ''; array_unshift( $links, $setup_link, $docs_link, $support_link ); return $links; } /** * Redirect to the Setup Wizard on first activation. * * Runs once after `activate()` sets the transient. Silently does nothing * on bulk activations, AJAX, network admin, or when a user lacks the capability. */ public function maybe_redirect_to_wizard() { if ( ! get_transient( 'asgm_activation_redirect' ) ) { return; } delete_transient( 'asgm_activation_redirect' ); if ( wp_doing_ajax() || is_network_admin() ) { return; } if ( ! current_user_can( 'manage_options' ) ) { return; } wp_safe_redirect( admin_url( 'admin.php?page=aeo-god-mode#/wizard' ) ); exit; } /** * Show a dismissible setup notice until the wizard is completed. * * Suppressed on the plugin's own screen (the React app shows its own wizard UI). */ public function maybe_show_setup_notice() { if ( ! current_user_can( 'manage_options' ) ) { return; } $settings = get_option( 'asgm_settings', array() ); if ( ! empty( $settings['wizard_completed'] ) ) { return; } // Don't double up on our own page. $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( $screen && false !== strpos( $screen->id, 'aeo-god-mode' ) ) { return; } $wizard_url = admin_url( 'admin.php?page=aeo-god-mode#/wizard' ); echo '

' . esc_html__( 'AEO God Mode is ready to set up.', 'aeo-god-mode' ) . ' ' . esc_html__( 'Run the 5-step Setup Wizard to configure AI crawlers, schema, and llms.txt for your site.', 'aeo-god-mode' ) . ' ' . esc_html__( 'Run Setup Wizard', 'aeo-god-mode' ) . '

'; } /** * Register the admin menu page. */ public function register_admin_menu() { add_menu_page( __( 'AEO God Mode', 'aeo-god-mode' ), __( 'AEO', 'aeo-god-mode' ), 'manage_options', 'aeo-god-mode', array( $this, 'render_admin_page' ), 'dashicons-superhero-alt', 81 ); // We do not add submenu pages here. The React App controls all sub-navigation via its own // custom dark-mode sidebar UI. This prevents redundant, confusing sidebars in the WordPress admin. } /** * Keep the AEO God Mode product name stable during mixed-version updates. * * Older Agency add-ons renamed this menu after Free registered it. The * shared plugin owns the menu, so it also owns its final identity. * * @return void */ public function enforce_admin_product_identity() { if ( empty( $GLOBALS['menu'] ) || ! is_array( $GLOBALS['menu'] ) ) { return; } foreach ( $GLOBALS['menu'] as $index => $item ) { if ( isset( $item[2] ) && 'aeo-god-mode' === $item[2] ) { $GLOBALS['menu'][ $index ][0] = __( 'AEO', 'aeo-god-mode' ); if ( isset( $GLOBALS['menu'][ $index ][3] ) ) { $GLOBALS['menu'][ $index ][3] = __( 'AEO God Mode', 'aeo-god-mode' ); } break; } } } /** * Render the React root container. */ public function render_admin_page() { // Since we removed all submenus, the React router depends purely on hash routing. $react_route = '/'; $wizard_url = esc_url( admin_url( 'admin.php?page=aeo-god-mode#/wizard' ) ); // The inner loading state is replaced by React on mount. // It gives reviewers and users something meaningful on the initial paint // and provides a fallback link to the wizard if JS fails to run. ?>

$css_file ) { wp_enqueue_style( 'asgm-admin-css-' . $index, $asset_url . $css_file, array(), ASGM_VERSION ); } // JS. The bundle reads its strings through WordPress's own // wp.i18n, so language packs from translate.wordpress.org reach // the React screens as well as the PHP ones. wp_enqueue_script( 'asgm-admin-app', $asset_url . $entry['file'], array( 'wp-i18n' ), ASGM_VERSION, true ); wp_set_script_translations( 'asgm-admin-app', 'aeo-god-mode', ASGM_PLUGIN_DIR . 'languages' ); // Load the admin bundle as type="module" so its top-level // declarations are module-scoped. Without this, the Vite bundle's // `var _ = ...` (React alias after minification) leaks to window._ // and collides with Underscore.js / Lodash loaded by themes like // Salient or builders like WPBakery, producing // `TypeError: _.useContext is not a function` on the admin page. // The bundle is already self-contained (no runtime ES imports), // so module scoping is purely additive. add_filter( 'script_loader_tag', array( $this, 'admin_app_as_module' ), 10, 3 ); // Pass data to the React app. $license = new License(); $profile = Site_Profile::get(); wp_localize_script( 'asgm-admin-app', 'asgmData', array( 'restUrl' => esc_url_raw( rest_url( 'aeo-god-mode/v1' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'adminUrl' => admin_url(), 'pluginUrl' => ASGM_PLUGIN_URL, 'version' => ASGM_VERSION, 'siteUrl' => get_site_url(), 'siteName' => (string) ( $profile['identity']['business_name'] ?? get_bloginfo( 'name' ) ), 'isPro' => License::is_pro(), 'isProBuild' => License::is_pro_build(), 'plan' => $license->get_plan(), 'pricingUrl' => 'https://aeogodmode.io/pricing', ) ); } } /** * Add type="module" to the admin React bundle