0.25, 'structure' => 0.25, 'crawl' => 0.20, 'authority' => 0.20, 'freshness' => 0.10, ); } /** Category metadata: label + tag colors (per design tokens). */ public static function categories() { return array( 'schema' => array( 'label' => 'Structured Data', 'color' => '#5b50e6', 'bg' => '#ecebfd' ), 'structure' => array( 'label' => 'Content Structure', 'color' => '#0d9488', 'bg' => '#dcf5f1' ), 'crawl' => array( 'label' => 'Crawlability', 'color' => '#2563eb', 'bg' => '#e3edfe' ), 'authority' => array( 'label' => 'Authority', 'color' => '#c026d3', 'bg' => '#f9e6fc' ), 'freshness' => array( 'label' => 'Freshness', 'color' => '#d97706', 'bg' => '#fdf0e0' ), ); } /** The 12 AI crawlers the plugin can allow / block. */ public static function bot_meta() { return array( array( 'id' => 'GPTBot', 'owner' => 'OpenAI', 'purpose' => 'ChatGPT' ), array( 'id' => 'OAI-SearchBot', 'owner' => 'OpenAI', 'purpose' => 'ChatGPT Search' ), array( 'id' => 'ClaudeBot', 'owner' => 'Anthropic', 'purpose' => 'Claude' ), array( 'id' => 'PerplexityBot', 'owner' => 'Perplexity', 'purpose' => 'Perplexity' ), array( 'id' => 'Google-Extended', 'owner' => 'Google', 'purpose' => 'Gemini & AI Overviews' ), array( 'id' => 'Bingbot', 'owner' => 'Microsoft', 'purpose' => 'Copilot' ), array( 'id' => 'Applebot-Extended', 'owner' => 'Apple', 'purpose' => 'Apple Intelligence' ), array( 'id' => 'Amazonbot', 'owner' => 'Amazon', 'purpose' => 'Alexa' ), array( 'id' => 'CCBot', 'owner' => 'Common Crawl', 'purpose' => 'Open training set' ), array( 'id' => 'Bytespider', 'owner' => 'ByteDance', 'purpose' => 'Doubao AI' ), array( 'id' => 'Meta-ExternalAgent', 'owner' => 'Meta', 'purpose' => 'Meta AI' ), array( 'id' => 'DuckAssistBot', 'owner' => 'DuckDuckGo', 'purpose' => 'DuckAssist' ), ); } /** Default allowed-crawler map. */ public static function default_bots() { return array( 'GPTBot' => true, 'OAI-SearchBot' => true, 'ClaudeBot' => true, 'PerplexityBot' => true, 'Google-Extended' => true, 'Bingbot' => true, 'Applebot-Extended' => true, 'Amazonbot' => true, 'CCBot' => false, 'Bytespider' => false, 'Meta-ExternalAgent' => false, 'DuckAssistBot' => true, ); } /** * Hosts that identify a human visitor arriving from an AI assistant. * Keyed by the display source; values are case-insensitive host substrings. */ public static function ai_referrers() { return array( 'ChatGPT' => array( 'chatgpt.com', 'chat.openai.com' ), 'Perplexity' => array( 'perplexity.ai' ), 'Gemini' => array( 'gemini.google.com' ), 'Claude' => array( 'claude.ai' ), 'Copilot' => array( 'copilot.microsoft.com' ), 'You.com' => array( 'you.com' ), 'Poe' => array( 'poe.com' ), ); } /** Default settings. */ public static function default_settings() { return array( 'schema' => 'complementary', // complementary | full 'demo_mode' => false, // default OFF: scan real posts. true = use seed dataset. 'include_drafts' => false, // also scan/analyze draft & pending posts. 'check_links' => false, // OFF by default: opt-in outbound-link verification (makes external HTTP requests). 'amazon_enabled' => false, // Master switch for the Amazon link-verification module. 'amazon' => array( // Amazon PA-API 5.0 credentials (secret stored encrypted). 'access' => '', 'secret' => '', 'partner' => '', ), 'keys' => array( 'openai' => '', 'anthropic' => '', 'google' => '', 'perplexity' => '', 'grok' => '', ), 'frequency' => 'daily', // hourly | twicedaily | daily | weekly 'visibility_freq' => 'weekly', // daily | weekly 'last_scan' => '', 'last_scan_ts' => 0, 'competitor_setup' => false, 'report_email' => '', 'reports_enabled' => false, 'report_freq' => 'Monthly', 'alerts_enabled' => true, 'score_drop_threshold' => 5, 'ollama_enabled' => false, 'ollama_url' => 'http://host.docker.internal:11434', 'ollama_model' => 'llama3.2', 'models' => array( 'openai' => 'gpt-4o-mini', 'perplexity' => 'sonar', 'google' => 'gemini-2.0-flash', 'anthropic' => 'claude-3-5-haiku-latest', 'grok' => 'grok-3', ), ); } /** Score trend history (5 prior months; current month is computed live). */ public static function default_history() { return array( 54, 57, 56, 61, 65 ); } /** AI citation tracking demo data: position per [ChatGPT, Perplexity, Gemini, Claude, Grok, Ollama] or null. */ public static function citations() { return array( array( 'q' => 'best cooling pillow 2026', 'e' => array( null, 2, 4, null, 3, null ) ), array( 'q' => 'memory foam vs latex pillow', 'e' => array( 3, 1, null, 5, 2, 4 ) ), array( 'q' => 'best pillow for neck pain', 'e' => array( null, null, null, null, null, null ) ), array( 'q' => 'side sleeper pillow recommendation', 'e' => array( 2, 3, 2, 4, 3, null ) ), array( 'q' => 'is the casper pillow worth it', 'e' => array( 1, 2, null, 3, null, 2 ) ), array( 'q' => 'down vs down alternative pillow', 'e' => array( null, 4, null, null, 5, null ) ), ); } /** Competitor share-of-voice demo data. */ public static function competitors() { return array( array( 'name' => 'Sleep Foundation', 'share' => 34 ), array( 'name' => 'Wirecutter', 'share' => 22 ), array( 'name' => 'Sleepopolis', 'share' => 15 ), array( 'name' => 'Pillow Fair', 'share' => 11, 'me' => true ), array( 'name' => "Tom's Guide", 'share' => 10 ), array( 'name' => 'Other sites', 'share' => 8 ), ); } /** Recent AI crawler hits demo data. */ public static function bot_log() { return array( array( 'bot' => 'PerplexityBot', 'page' => '/best-cooling-pillows', 'time' => '4 min ago', 'ok' => true ), array( 'bot' => 'GPTBot', 'page' => '/memory-foam-vs-latex', 'time' => '12 min ago', 'ok' => true ), array( 'bot' => 'ClaudeBot', 'page' => '/side-sleeper-guide', 'time' => '31 min ago', 'ok' => true ), array( 'bot' => 'Bytespider', 'page' => '/neck-pain-pillows', 'time' => '48 min ago', 'ok' => false ), array( 'bot' => 'Google-Extended', 'page' => '/casper-original-review', 'time' => '1 hr ago', 'ok' => true ), array( 'bot' => 'CCBot', 'page' => '/pillow-loft-guide', 'time' => '2 hr ago', 'ok' => false ), array( 'bot' => 'GPTBot', 'page' => '/down-vs-alternative', 'time' => '3 hr ago', 'ok' => true ), ); } /** The 7 canonical demo pages with sub-scores and issues. */ public static function seed_pages() { return array( array( 'id' => 'p1', 'title' => 'Best Cooling Pillows for Hot Sleepers (2026)', 'url' => '/best-cooling-pillows', 'updated' => '8 days ago', 'stale' => false, 'sub' => array( 'schema' => 40, 'structure' => 55, 'crawl' => 80, 'authority' => 50, 'freshness' => 72 ), 'issues' => array( array( 'id' => 'p1a', 'cat' => 'schema', 'sev' => 'high', 'gain' => 35, 'title' => 'No ItemList or Review schema', 'fixLabel' => 'Add schema', 'desc' => 'This is a product roundup but emits no structured data — AI engines can’t parse which pillows you recommend or their ratings.', 'after' => 'JSON-LD: ItemList · 7 × Review — validated ✓' ), array( 'id' => 'p1b', 'cat' => 'structure', 'sev' => 'high', 'gain' => 15, 'title' => 'No quick-answer summary', 'fixLabel' => 'Insert block', 'desc' => 'There is no concise answer block near the top. AI models favor a 40–60 word direct answer they can lift verbatim.', 'after' => 'Quick-answer block inserted above the fold (52 words)' ), array( 'id' => 'p1c', 'cat' => 'structure', 'sev' => 'med', 'gain' => 12, 'title' => 'Comparison is an image, not an HTML table', 'fixLabel' => 'Convert table', 'desc' => 'Your spec comparison is a JPG. Crawlers cannot read pricing, loft or firmness from pixels.', 'after' => 'HTML table with 5 columns × 7 rows — machine-readable' ), array( 'id' => 'p1d', 'cat' => 'authority', 'sev' => 'high', 'gain' => 25, 'title' => 'No author bio or Person schema', 'fixLabel' => 'Add author box', 'desc' => 'No byline, credentials or Person markup. E-E-A-T signals are a strong driver of AI citation.', 'after' => 'Author box + Person schema (sleep writer, 6 yrs) added' ), array( 'id' => 'p1e', 'cat' => 'crawl', 'sev' => 'low', 'gain' => 10, 'title' => 'Not listed in llms.txt', 'fixLabel' => 'Add to llms.txt', 'desc' => 'This high-value page is missing from your llms.txt index, lowering its discovery priority.', 'after' => 'Added to llms.txt under "Buying guides"' ), ), ), array( 'id' => 'p2', 'title' => 'Memory Foam vs Latex Pillows: Which Is Right?', 'url' => '/memory-foam-vs-latex', 'updated' => '11 months ago', 'stale' => true, 'sub' => array( 'schema' => 60, 'structure' => 65, 'crawl' => 85, 'authority' => 60, 'freshness' => 50 ), 'issues' => array( array( 'id' => 'p2a', 'cat' => 'freshness', 'sev' => 'high', 'gain' => 20, 'title' => 'Content is 11 months old', 'fixLabel' => 'Refresh', 'desc' => 'Last meaningful update was 11 months ago. Update the date and refresh 2026 model references.', 'after' => 'Republished · dateModified set to today' ), array( 'id' => 'p2b', 'cat' => 'structure', 'sev' => 'med', 'gain' => 12, 'title' => 'No side-by-side comparison table', 'fixLabel' => 'Add table', 'desc' => 'A direct comparison table strongly boosts pickup for "X vs Y" queries.', 'after' => 'HTML table: Memory foam vs Latex — 9 attributes' ), array( 'id' => 'p2c', 'cat' => 'authority', 'sev' => 'med', 'gain' => 15, 'title' => 'No sources cited', 'fixLabel' => 'Add sources', 'desc' => 'No outbound citations or methodology. Adding referenced studies raises trust signals.', 'after' => '3 referenced sources + testing methodology added' ), ), ), array( 'id' => 'p3', 'title' => 'Best Pillows for Neck Pain', 'url' => '/neck-pain-pillows', 'updated' => '16 months ago', 'stale' => true, 'sub' => array( 'schema' => 35, 'structure' => 48, 'crawl' => 78, 'authority' => 45, 'freshness' => 40 ), 'issues' => array( array( 'id' => 'p3a', 'cat' => 'schema', 'sev' => 'high', 'gain' => 30, 'title' => 'No ItemList / Review schema', 'fixLabel' => 'Add schema', 'desc' => 'A health-adjacent roundup with zero structured data — the biggest gap on the site.', 'after' => 'JSON-LD: ItemList · 6 × Review — added & validated' ), array( 'id' => 'p3b', 'cat' => 'structure', 'sev' => 'high', 'gain' => 18, 'title' => 'No FAQ section', 'fixLabel' => 'Generate FAQ', 'desc' => 'Neck-pain queries are highly question-driven. A FAQ block with FAQPage schema captures them.', 'after' => 'FAQ section (5 Q&A) + FAQPage schema added' ), array( 'id' => 'p3c', 'cat' => 'structure', 'sev' => 'med', 'gain' => 10, 'title' => 'No quick-answer summary', 'fixLabel' => 'Insert block', 'desc' => 'Add a concise answer near the top for AI lift.', 'after' => 'Quick-answer block inserted (48 words)' ), array( 'id' => 'p3d', 'cat' => 'authority', 'sev' => 'high', 'gain' => 22, 'title' => 'No author or medical reviewer', 'fixLabel' => 'Add reviewer', 'desc' => 'Health content needs visible expertise. Add an author plus a reviewer with Person schema.', 'after' => 'Author + medical reviewer credentials added' ), array( 'id' => 'p3e', 'cat' => 'freshness', 'sev' => 'high', 'gain' => 25, 'title' => 'Content is 16 months old', 'fixLabel' => 'Refresh', 'desc' => 'Oldest page on the site. Refresh content and dateModified.', 'after' => 'Republished · dateModified set to today' ), ), ), array( 'id' => 'p4', 'title' => 'The Complete Side Sleeper Pillow Guide', 'url' => '/side-sleeper-guide', 'updated' => '3 weeks ago', 'stale' => false, 'sub' => array( 'schema' => 70, 'structure' => 75, 'crawl' => 88, 'authority' => 65, 'freshness' => 80 ), 'issues' => array( array( 'id' => 'p4a', 'cat' => 'authority', 'sev' => 'med', 'gain' => 12, 'title' => 'No testing methodology section', 'fixLabel' => 'Add methodology', 'desc' => 'You rank pillows but don’t explain how. A short methodology block lifts trust.', 'after' => '"How we tested" section added' ), array( 'id' => 'p4b', 'cat' => 'schema', 'sev' => 'low', 'gain' => 10, 'title' => 'Missing FAQ schema', 'fixLabel' => 'Add schema', 'desc' => 'You have a FAQ section but no FAQPage markup wrapping it.', 'after' => 'FAQPage schema attached to existing FAQ' ), ), ), array( 'id' => 'p5', 'title' => 'Casper Original Pillow Review', 'url' => '/casper-original-review', 'updated' => '5 days ago', 'stale' => false, 'sub' => array( 'schema' => 80, 'structure' => 78, 'crawl' => 90, 'authority' => 72, 'freshness' => 85 ), 'issues' => array( array( 'id' => 'p5a', 'cat' => 'structure', 'sev' => 'low', 'gain' => 8, 'title' => 'No pros / cons block', 'fixLabel' => 'Add block', 'desc' => 'A structured pros/cons list is highly liftable for review queries.', 'after' => 'Pros / cons block added (4 + 3)' ), array( 'id' => 'p5b', 'cat' => 'authority', 'sev' => 'med', 'gain' => 10, 'title' => 'Reviewer credentials not shown', 'fixLabel' => 'Add credentials', 'desc' => 'Surface who tested the pillow and for how long.', 'after' => 'Reviewer credentials + tested-for duration shown' ), ), ), array( 'id' => 'p6', 'title' => 'Down vs Down-Alternative Pillows', 'url' => '/down-vs-alternative', 'updated' => '2 months ago', 'stale' => false, 'sub' => array( 'schema' => 55, 'structure' => 60, 'crawl' => 82, 'authority' => 55, 'freshness' => 62 ), 'issues' => array( array( 'id' => 'p6a', 'cat' => 'schema', 'sev' => 'high', 'gain' => 20, 'title' => 'No Review schema', 'fixLabel' => 'Add schema', 'desc' => 'Comparison verdicts have no structured rating for AI to cite.', 'after' => 'Review schema with ratings added' ), array( 'id' => 'p6b', 'cat' => 'structure', 'sev' => 'med', 'gain' => 12, 'title' => 'No comparison table', 'fixLabel' => 'Add table', 'desc' => 'Add an HTML comparison table for the two fill types.', 'after' => 'HTML table: Down vs Alternative — 8 attributes' ), array( 'id' => 'p6c', 'cat' => 'crawl', 'sev' => 'low', 'gain' => 8, 'title' => 'Not listed in llms.txt', 'fixLabel' => 'Add to llms.txt', 'desc' => 'Missing from the llms.txt index.', 'after' => 'Added to llms.txt' ), ), ), array( 'id' => 'p7', 'title' => 'How to Choose the Right Pillow Loft', 'url' => '/pillow-loft-guide', 'updated' => '6 weeks ago', 'stale' => false, 'sub' => array( 'schema' => 50, 'structure' => 70, 'crawl' => 85, 'authority' => 58, 'freshness' => 68 ), 'issues' => array( array( 'id' => 'p7a', 'cat' => 'schema', 'sev' => 'high', 'gain' => 18, 'title' => 'No HowTo or FAQ schema', 'fixLabel' => 'Add schema', 'desc' => 'A how-to guide with no HowTo markup. Add it so steps surface in AI answers.', 'after' => 'HowTo schema (4 steps) + FAQPage added' ), array( 'id' => 'p7b', 'cat' => 'freshness', 'sev' => 'med', 'gain' => 12, 'title' => 'Could use a content refresh', 'fixLabel' => 'Refresh', 'desc' => 'Update examples and dateModified to stay current.', 'after' => 'Refreshed · dateModified updated' ), ), ), ); } /** Get the active page dataset (scanned or seeded). */ public static function get_pages() { $settings = self::get_settings(); // Demo mode ON: always show the bundled sample dataset. if ( ! empty( $settings['demo_mode'] ) ) { return self::seed_pages(); } // Demo mode OFF (default): surface only real scanned posts (ids like // "wp123"). Never the bundled demo seed (ids "p1"…"p7") — so a fresh // install shows an empty "run a scan" state instead of fake pages. $pages = get_option( AICITARE_OPT_PAGES ); if ( ! is_array( $pages ) ) { return array(); } return array_values( array_filter( $pages, function ( $p ) { return isset( $p['id'] ) && 0 === strpos( (string) $p['id'], 'wp' ); } ) ); } public static function get_fixed() { $f = get_option( AICITARE_OPT_FIXED ); return is_array( $f ) ? $f : array(); } public static function get_bots() { $b = get_option( AICITARE_OPT_BOTS ); return ( is_array( $b ) && ! empty( $b ) ) ? $b : self::default_bots(); } public static function get_settings() { $defaults = self::default_settings(); $s = get_option( AICITARE_OPT_SETTINGS ); if ( ! is_array( $s ) || empty( $s ) ) { return $defaults; } $s = wp_parse_args( $s, $defaults ); // Deep-merge the keys sub-array so newly added providers (e.g. Grok) // appear on installs that saved settings before the provider existed. $s['keys'] = wp_parse_args( is_array( $s['keys'] ) ? $s['keys'] : array(), $defaults['keys'] ); $s['models'] = wp_parse_args( ( isset( $s['models'] ) && is_array( $s['models'] ) ) ? $s['models'] : array(), $defaults['models'] ); $s['amazon'] = wp_parse_args( ( isset( $s['amazon'] ) && is_array( $s['amazon'] ) ) ? $s['amazon'] : array(), $defaults['amazon'] ); return $s; } /** Decrypted API keys for server-side use (never sent to the browser). */ public static function get_keys_plain() { $keys = self::get_settings()['keys']; $out = array(); foreach ( $keys as $k => $v ) { $out[ $k ] = AICitare_Crypto::decrypt( $v ); } return $out; } public static function get_history() { $settings = self::get_settings(); if ( ! empty( $settings['demo_mode'] ) ) { return self::default_history(); } // A score trend is only meaningful once real posts have been scanned. // This also hides any leftover demo-era history on upgraded installs. if ( ! self::get_pages() ) { return array(); } $h = get_option( AICITARE_OPT_HISTORY ); return ( is_array( $h ) && ! empty( $h ) ) ? $h : array(); } }