get_bloginfo( 'name' ), 'geistm_organization_tagline' => get_bloginfo( 'description' ), 'geistm_content_license' => 'cc-by-4.0', 'geistm_allow_quoting' => '1', 'geistm_allow_training' => '0', 'geistm_sector' => '', 'geistm_enable_seo_tags' => '1', 'geistm_enable_llms_txt' => '1', 'geistm_enable_llms_full' => '0', 'geistm_llms_posts_limit' => '10', 'geistm_llms_full_posts_limit' => '50', 'geistm_llms_pages_limit' => '30', 'geistm_llms_include_excerpts' => '1', // Adnet — disabled by default. 'geistm_adnet_enabled' => '0', 'geistm_adnet_host' => 'https://adnet.geistm.com', 'geistm_adnet_auto_enabled' => '0', 'geistm_adnet_auto_format' => 'banner', 'geistm_adnet_auto_paragraph' => '3', 'geistm_adnet_auto_max' => '1', 'geistm_adnet_auto_pages' => '0', 'geistm_adnet_declined' => '0', 'geistm_adnet_placement_choice' => '', ); foreach ( $defaults as $key => $value ) { if ( false === get_option( $key ) ) { update_option( $key, $value ); } } // Register rewrite rules and flush. geistm_ai_discovery_register_rewrites(); flush_rewrite_rules(); // Clear any cached data. geistm_ai_discovery_clear_all_caches(); // The wallet is NOT auto-created here: creating it is a deliberate // onboarding step (its ID stamps content, its Credit receives payouts — // the publisher should know it exists and back it up). A wallet from a // previous install survives deactivate/reactivate untouched. update_option( 'geistm_ai_discovery_version', GEISTM_AI_DISCOVERY_VERSION ); } register_activation_hook( __FILE__, 'geistm_ai_discovery_activate' ); /** * Deactivation: clean up rewrite rules. */ function geistm_ai_discovery_deactivate() { flush_rewrite_rules(); } register_deactivation_hook( __FILE__, 'geistm_ai_discovery_deactivate' ); /** * Upgrade routine — runs once per version change. * * Replacing the plugin via zip upload does not re-fire the activation hook, * so schema/option migrations live here. Hooked on init at priority 20 so * rewrites are registered before the flush. */ function geistm_ai_discovery_maybe_upgrade() { $installed = get_option( 'geistm_ai_discovery_version', '0' ); if ( $installed === GEISTM_AI_DISCOVERY_VERSION ) { return; } // ── Legacy lineage (>= 2.0.0) ─────────────────────────────────────── // The plugin renumbered to 1.x for the Adnet by GeistM release; the // forked lineage shipped as 2.x, with interim 3.x builds before the // renumber. Anything >= 2.0.0 is a legacy install: clean up the removed // features and bring the signature up to the current conventions. if ( '0' !== $installed && version_compare( $installed, '2.0.0', '>=' ) ) { global $wpdb; // Migrate the legacy sector option before deleting it. $legacy_sector = get_option( 'geistm_organization_sector', '' ); if ( ! empty( $legacy_sector ) && '' === get_option( 'geistm_sector', '' ) ) { update_option( 'geistm_sector', $legacy_sector ); } // Options behind features removed from the legacy lineage (content // endpoint, WebMCP, metrics, licensing, registry). $stale_options = array( 'geistm_webmcp_enabled', 'geistm_enable_knowledge', 'geistm_enable_feed', 'geistm_enable_content', 'geistm_content_include_pages', 'geistm_content_include_posts', 'geistm_content_include_custom_types', 'geistm_content_include_media', 'geistm_content_include_full_text', 'geistm_content_posts_limit', 'geistm_content_media_limit', 'geistm_owner_identity', 'geistm_registry_consent', 'geistm_setup_log', 'geistm_authorization_signature', 'geistm_account_status', 'geistm_account_tier', 'geistm_organization_sector', ); foreach ( $stale_options as $option ) { delete_option( $option ); } $stale_transients = array( 'geistm_knowledge_cache', 'geistm_feed_cache', 'geistm_content_cache', 'geistm_license_status', 'geistm_update_check', 'geistm_proxy_usage', 'geistm_proxy_error', ); foreach ( $stale_transients as $transient ) { delete_transient( $transient ); } // Metrics module removed — drop its table and cron. wp_clear_scheduled_hook( 'geistm_metrics_prune' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}geistm_ai_metrics" ); if ( version_compare( $installed, '3.0.0', '<' ) ) { // 2.x snapshots advertise removed endpoints — discard entirely // and drop the removed rewrite rules. delete_option( 'geistm_signed_manifest' ); flush_rewrite_rules(); } if ( version_compare( $installed, '3.2.2', '<' ) ) { // Signature conventions changed after these builds (canonical // key-sorted hashes, digitalName default) — ask for a re-sign. update_option( 'geistm_manifest_needs_signing', '1', false ); } // Legacy builds stored a user-supplied Anthropic API key; generation // is now served by the GeistM AI service — remove the stored secret. delete_option( 'geistm_ai_api_key' ); } // 1.4.0 removed the vestigial "Authorizer Wallet" card — the address was // stored but never published or verified. Organization identity lives in // the spec fields (digitalName / identityContract) on the Identity tab. delete_option( 'geistm_plugin_wallet' ); // 1.5.0 removed the in-plugin balance lookup (it queried public RPC and // price endpoints on admin page load) — drop its cache. delete_transient( 'geistm_credit_balance' ); update_option( 'geistm_ai_discovery_version', GEISTM_AI_DISCOVERY_VERSION ); } add_action( 'init', 'geistm_ai_discovery_maybe_upgrade', 20 ); /** * Register custom rewrite rules — v1.2 URL hierarchy under /.well-known/ai/. */ function geistm_ai_discovery_register_rewrites() { // v1.2: Everything under /.well-known/ai/ hierarchy (RFC 8615). add_rewrite_rule( '\.well-known/ai/?$', 'index.php?geistm_endpoint=discovery', 'top' ); add_rewrite_rule( '\.well-known/ai/policies/?$', 'index.php?geistm_endpoint=policies', 'top' ); // llms.txt and llms-full.txt. add_rewrite_rule( 'llms\.txt$', 'index.php?geistm_endpoint=llms_txt', 'top' ); add_rewrite_rule( 'llms-full\.txt$', 'index.php?geistm_endpoint=llms_full_txt', 'top' ); // Prevent WordPress from redirecting .well-known/ai URLs. add_filter( 'redirect_canonical', 'geistm_ai_discovery_prevent_redirect', 10, 2 ); } add_action( 'init', 'geistm_ai_discovery_register_rewrites' ); /** * Prevent WordPress canonical redirects for our endpoints. * * @param string $redirect_url The redirect URL. * @param string $requested_url The requested URL. * @return string|false The redirect URL or false to cancel. */ function geistm_ai_discovery_prevent_redirect( $redirect_url, $requested_url ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Required by WordPress filter signature. if ( get_query_var( 'geistm_endpoint' ) ) { return false; } return $redirect_url; } /** * Register query vars. * * @param array $vars Existing query vars. * @return array Modified query vars. */ function geistm_ai_discovery_query_vars( $vars ) { $vars[] = 'geistm_endpoint'; return $vars; } add_filter( 'query_vars', 'geistm_ai_discovery_query_vars' ); /** * Serve plain text with standard headers. * * @param string $content The text content to serve. * @param int $cache_seconds Cache duration in seconds. */ function geistm_ai_discovery_serve_text( $content, $cache_seconds = 3600 ) { header( 'Content-Type: text/plain; charset=utf-8' ); header( 'X-Robots-Tag: noindex' ); header( 'Access-Control-Allow-Origin: *' ); header( 'Cache-Control: public, max-age=' . intval( $cache_seconds ) ); header( 'X-GeistM-AI-Discovery: ' . GEISTM_AI_DISCOVERY_VERSION ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Plain text output, not HTML context. echo $content; exit; } /** * Serve JSON with standard headers. * * @param array $data The data to encode as JSON. * @param int $cache_seconds Cache duration in seconds. */ function geistm_ai_discovery_serve_json( $data, $cache_seconds = 3600 ) { header( 'Content-Type: application/json; charset=utf-8' ); header( 'Access-Control-Allow-Origin: *' ); header( 'Access-Control-Allow-Methods: GET, OPTIONS' ); header( 'Access-Control-Allow-Headers: Content-Type' ); header( 'Cache-Control: public, max-age=' . intval( $cache_seconds ) ); header( 'X-GeistM-AI-Discovery: ' . GEISTM_AI_DISCOVERY_VERSION ); echo wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); exit; } /** * Handle all /.well-known/ai/* requests. */ function geistm_ai_discovery_template_redirect() { $endpoint = get_query_var( 'geistm_endpoint' ); if ( ! $endpoint ) { return; } switch ( $endpoint ) { case 'discovery': $generator = new Geistm_Ai_Json(); geistm_ai_discovery_serve_json( $generator->generate() ); break; case 'policies': $policies = new Geistm_Policies(); geistm_ai_discovery_serve_json( $policies->generate() ); break; case 'llms_txt': if ( '1' !== get_option( 'geistm_enable_llms_txt', '1' ) ) { status_header( 404 ); exit; } $generator = new Geistm_Llms_Txt(); geistm_ai_discovery_serve_text( $generator->generate() ); break; case 'llms_full_txt': if ( '1' !== get_option( 'geistm_enable_llms_full', '0' ) ) { status_header( 404 ); exit; } $generator = new Geistm_Llms_Txt(); geistm_ai_discovery_serve_text( $generator->generate_full() ); break; } } add_action( 'template_redirect', 'geistm_ai_discovery_template_redirect' ); /** * Add and optional SEO meta tags to HTML
. */ function geistm_ai_discovery_wp_head() { // AI Discovery link tag (always). $url = home_url( '/.well-known/ai' ); echo '' . "\n"; // llms.txt link tags. if ( '1' === get_option( 'geistm_enable_llms_txt', '1' ) ) { echo '' . "\n"; } if ( '1' === get_option( 'geistm_enable_llms_full', '0' ) ) { echo '' . "\n"; } // SEO tags: meta description, OpenGraph, JSON-LD. // Only output if enabled AND no known SEO plugin is active. if ( '1' !== get_option( 'geistm_enable_seo_tags', '1' ) ) { return; } $seo_plugins = array( 'wordpress-seo/wp-seo.php', 'seo-by-rank-math/rank-math.php', 'all-in-one-seo-pack/all_in_one_seo_pack.php', 'wp-seopress/seopress.php', ); foreach ( $seo_plugins as $plugin ) { if ( is_plugin_active( $plugin ) ) { return; // Let the SEO plugin handle meta tags. } } $org_name = get_option( 'geistm_organization_name', get_bloginfo( 'name' ) ); $description = get_option( 'geistm_ai_summary', '' ); if ( empty( $description ) ) { $description = get_option( 'geistm_organization_tagline', get_bloginfo( 'description' ) ); } // Meta description (all pages). if ( ! empty( $description ) ) { echo '' . "\n"; } // OpenGraph + JSON-LD (front page only to avoid duplication on every page). if ( is_front_page() || is_home() ) { echo '' . "\n"; if ( ! empty( $description ) ) { echo '' . "\n"; } echo '' . "\n"; echo '' . "\n"; $site_icon_id = get_option( 'site_icon' ); if ( $site_icon_id ) { $icon_url = wp_get_attachment_image_url( $site_icon_id, 'full' ); if ( $icon_url ) { echo '' . "\n"; } } // JSON-LD Organization schema. $jsonld = array( '@context' => 'https://schema.org', '@type' => 'Organization', 'name' => $org_name, 'url' => home_url( '/' ), ); if ( ! empty( $description ) ) { $jsonld['description'] = wp_strip_all_tags( $description ); } $sector = get_option( 'geistm_sector', '' ); if ( empty( $sector ) ) { $sector = get_option( 'geistm_organization_sector', '' ); } if ( ! empty( $sector ) ) { $jsonld['knowsAbout'] = array_map( 'trim', explode( ',', $sector ) ); } $contact_email = get_option( 'geistm_contact_email', get_option( 'admin_email', '' ) ); if ( ! empty( $contact_email ) ) { $jsonld['contactPoint'] = array( '@type' => 'ContactPoint', 'email' => $contact_email, ); } if ( $site_icon_id ) { $icon_url = wp_get_attachment_image_url( $site_icon_id, 'full' ); if ( $icon_url ) { $jsonld['logo'] = $icon_url; } } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_json_encode output is safe for script context when not using JSON_UNESCAPED_SLASHES. echo '' . "\n"; } } add_action( 'wp_head', 'geistm_ai_discovery_wp_head' ); /** * Add Link HTTP header to all responses. */ function geistm_ai_discovery_send_headers() { if ( headers_sent() ) { return; } $url = home_url( '/.well-known/ai' ); header( 'Link: <' . esc_url( $url ) . '>; rel="ai-discovery"; type="application/json"' ); } add_action( 'send_headers', 'geistm_ai_discovery_send_headers' ); /** * Enqueue frontend scripts. */ function geistm_ai_discovery_enqueue_scripts() { // Adnet client — the slim publisher script served by the Adnet factory // (adnet.geistm.com, or a publisher's first-party _adnet.' . esc_html__( 'If this site has enabled Adnet advertising, the following applies to visitors:', 'geistm-adnet' ) . '
'; $content .= '' . sprintf( /* translators: %s: URL of the Adnet privacy policy. */ esc_html__( 'Adnet privacy policy: %s', 'geistm-adnet' ), 'https://adnet.geistm.com/privacy' ) . '
'; wp_add_privacy_policy_content( 'GeistM Adnet', wp_kses_post( $content ) ); } add_action( 'admin_init', 'geistm_ai_discovery_privacy_policy_content' ); /** * Load admin settings page. */ if ( is_admin() ) { require_once GEISTM_AI_DISCOVERY_DIR . 'admin/class-geistm-admin.php'; new Geistm_Admin(); }