includes/class-foo-bar.php * and Evoka_Admin_Foo => admin/class-foo.php. */ spl_autoload_register( function ( $class_name ) { if ( strpos( $class_name, 'Evoka' ) !== 0 ) { return; } $relative = strtolower( substr( $class_name, strlen( 'Evoka_' ) ) ); $relative = str_replace( '_', '-', $relative ); // Admin classes (Evoka_Admin_*) live under admin/; everything else under includes/. if ( strpos( $relative, 'admin-' ) === 0 ) { $path = EVOKA_DIR . 'admin/class-' . $relative . '.php'; } else { $path = EVOKA_DIR . 'includes/class-' . $relative . '.php'; } if ( is_readable( $path ) ) { require_once $path; } } ); /** * Main plugin container. Wires everything together on plugins_loaded. */ final class Evoka_Plugin { /** * Singleton instance. * * @var Evoka_Plugin|null */ private static $instance = null; /** * Boot the plugin (singleton). * * @return Evoka_Plugin */ public static function instance() { if ( null === self::$instance ) { self::$instance = new self(); self::$instance->init(); } return self::$instance; } /** * Hook everything up. */ private function init() { // Front-end: emit structured data + manage AI-crawler signals. $schema = new Evoka_Schema_Generator(); add_action( 'wp_head', array( $schema, 'output' ), 20 ); $crawler = new Evoka_Crawler_Controls(); add_filter( 'robots_txt', array( $crawler, 'filter_robots_txt' ), 20, 2 ); $llms = new Evoka_Llms_Txt(); $llms->register(); // Passive AI crawler/referral logging. $visibility = new Evoka_Visibility_Log(); $visibility->register(); // Scheduled weekly re-audit + tracked-citation checks. $cron = new Evoka_Cron(); $cron->register(); // Admin UI (also owns the client-ready report screen). if ( is_admin() ) { $admin = new Evoka_Admin_Ui(); $admin->register(); } // Translations: WordPress.org-hosted plugins get them loaded // automatically for the "evoka" text domain (WP 4.6+). } /** * Activation: set defaults and flush rewrites so llms.txt resolves. * * On multisite network activation this runs for the current site only; * other sites fall back to default settings (evoka_get_setting()) and * add the llms.txt rule on their first request (Evoka_Llms_Txt::maybe_flush_rules()). */ public static function activate() { $defaults = array( 'schema_enabled' => 1, 'llms_txt_enabled' => 1, 'crawler_policy' => 'allow', // One of: allow, track, block. 'byok_provider' => '', // One of: '' (none), openai, perplexity. // NOTE: the API key is stored in a separate option and is the user's own key. ); if ( false === get_option( 'evoka_settings' ) ) { add_option( 'evoka_settings', $defaults ); } // Make sure the llms.txt rewrite endpoint exists before flushing. ( new Evoka_Llms_Txt() )->add_rewrite(); flush_rewrite_rules(); // The visibility-log table only matters once the site is Pro, but // creating it now avoids a lazy-create on the first logged pageview. ( new Evoka_Visibility_Log() )->install(); } /** * Deactivation: clear the scheduled audit and remove the llms.txt rule - * on every site when network-deactivated on multisite. * * @param bool $network_wide Whether the plugin is being network-deactivated. */ public static function deactivate( $network_wide = false ) { if ( is_multisite() && $network_wide ) { foreach ( get_sites( array( 'fields' => 'ids', 'number' => 0, ) ) as $blog_id ) { switch_to_blog( $blog_id ); self::deactivate_site(); restore_current_blog(); } return; } self::deactivate_site(); } /** * Per-site deactivation cleanup. */ private static function deactivate_site() { wp_clear_scheduled_hook( 'evoka_scheduled_audit' ); Evoka_Llms_Txt::remove_stored_rule(); } } register_activation_hook( __FILE__, array( 'Evoka_Plugin', 'activate' ) ); register_deactivation_hook( __FILE__, array( 'Evoka_Plugin', 'deactivate' ) ); add_action( 'plugins_loaded', array( 'Evoka_Plugin', 'instance' ) );