|WP_Error|null $funnel Funnel payload. * @param array|WP_Error|null $status Site status. * @return void */ function citolume_render_overview_head( $funnel, $status ) { $meta = array(); $scanned = ( is_array( $funnel ) && ! empty( $funnel['last_scanned_at'] ) ) ? (int) strtotime( (string) $funnel['last_scanned_at'] . ' UTC' ) : 0; $next = ( is_array( $funnel ) && ! empty( $funnel['next_scheduled_at'] ) ) ? (int) strtotime( (string) $funnel['next_scheduled_at'] . ' UTC' ) : 0; if ( is_array( $status ) ) { // The plan's name is already the pill on the right; here it would be // said twice. Questions read against the plan's allowance instead. $used = (int) ( $status['queries_used'] ?? 0 ); $limit = (int) ( $status['max_queries'] ?? 0 ); $meta[] = $limit > 0 /* translators: 1: tracked questions, 2: how many the plan allows. */ ? sprintf( _n( '%1$d/%2$d question', '%1$d/%2$d questions', $limit, 'citolume' ), $used, $limit ) /* translators: %d: number of tracked questions. */ : sprintf( _n( '%d question', '%d questions', $used, 'citolume' ), $used ); if ( ! empty( $status['scan_frequency'] ) ) { $meta[] = (string) $status['scan_frequency']; } } if ( $scanned ) { /* translators: %s: how long ago the last scan finished. */ $meta[] = sprintf( __( 'scanned %s ago', 'citolume' ), human_time_diff( $scanned ) ); } ?>

|WP_Error|null $status Site status, if connected. * @return void */ function citolume_render_upgrade_control( $status ) { $plan = ( is_array( $status ) && ! empty( $status['plan'] ) ) ? (string) $status['plan'] : 'free'; if ( 'growth' === $plan ) { return; } ?> |WP_Error $funnel Funnel payload. * @return void */ function citolume_render_first_citation_banner( $funnel ) { if ( is_wp_error( $funnel ) || empty( $funnel['first_cited_at'] ) ) { return; } $when = (int) strtotime( (string) $funnel['first_cited_at'] . ' UTC' ); if ( time() - $when > 14 * DAY_IN_SECONDS ) { return; } ?>
|WP_Error $funnel Funnel payload, or the error. * @return void */ function citolume_render_coverage_gaps( $funnel ) { if ( is_wp_error( $funnel ) || empty( $funnel['coverage_gaps'] ) || ! is_array( $funnel['coverage_gaps'] ) ) { return; } ?>

|WP_Error $funnel Funnel payload. * @return void */ function citolume_render_scanning_notice( $funnel ) { if ( is_wp_error( $funnel ) || empty( $funnel['run_in_flight'] ) ) { return; } if ( ! empty( $funnel['status'] ) && 'never_run' !== $funnel['status'] ) { return; } wp_enqueue_script( 'citolume-scan-poll', CITOLUME_URL . 'assets/js/scan-poll.js', array(), citolume_asset_version( 'assets/js/scan-poll.js' ), true ); wp_add_inline_script( 'citolume-scan-poll', 'window.citolumeScanPoll = ' . wp_json_encode( array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'citolume_scan_status' ), ) ) . ';', 'before' ); $total = (int) ( $funnel['total_questions'] ?? 0 ); ?>
! is_wp_error( $funnel ) && ! empty( $funnel['run_in_flight'] ) ) ); } add_action( 'wp_ajax_citolume_scan_status', 'citolume_ajax_scan_status' ); /** * The visibility score — the funnel card's leading section (§3.4, §3.7). * * Rendered inside the funnel card, above the stages, in the stages' own * typography: the design's uppercase micro-label, a large number, a grey * caption. The score leads; the funnel underneath explains it. The full * arithmetic sits in a native expandable, because a score people cannot * interpret is worse than no score. * * Silent when the payload carries no score: an older API, a site that has * not scanned yet, or a run that collected nothing. No score is never a * zero, so nothing renders rather than something misleading. * * @param array|WP_Error $funnel Funnel payload, or the error. * @return void */ function citolume_render_score( $funnel ) { // An API without the field at all (older deploy) renders nothing — // silence, never an invented number. if ( is_wp_error( $funnel ) || ! is_array( $funnel ) || ! array_key_exists( 'score', $funnel ) ) { return; } $score = is_array( $funnel['score'] ?? null ) && isset( $funnel['score']['value'] ) ? $funnel['score'] : null; $scanning = ! empty( $funnel['run_in_flight'] ); $has_scan = ! empty( $funnel['status'] ) && 'never_run' !== $funnel['status']; $parts = $score && isset( $score['parts'] ) && is_array( $score['parts'] ) ? $score['parts'] : null; $version = $score && isset( $score['version'] ) ? (int) $score['version'] : 1; $tone = ''; if ( $score ) { $delta = isset( $score['delta'] ) && null !== $score['delta'] ? (int) $score['delta'] : null; if ( null === $delta ) { $movement = __( 'your first score — movement appears after the next scan', 'citolume' ); } elseif ( 0 === $delta ) { $movement = __( 'no change since the last scan', 'citolume' ); } elseif ( $delta > 0 ) { /* translators: %d: points gained since the previous scan. */ $movement = sprintf( __( '▲ %d since the last scan', 'citolume' ), $delta ); $tone = 'is-up'; } else { /* translators: %d: points lost since the previous scan. */ $movement = sprintf( __( '▼ %d since the last scan', 'citolume' ), absint( $delta ) ); } } elseif ( $scanning && ! $has_scan ) { // The design's overview-scanning state: the bar shows the dash and // says the number is minutes away, never pretending to be zero. $movement = __( 'first scan running now', 'citolume' ); $tone = 'is-scanning'; } elseif ( ! $has_scan ) { $movement = __( 'appears after the first scan', 'citolume' ); } else { $movement = __( 'appears after the next scan', 'citolume' ); } ?>

|WP_Error $funnel Funnel payload, or the error. * @param bool $connected Whether a key is stored. * @param array|WP_Error|null $status Site status, for the samples per question. * @return void */ function citolume_render_funnel( $funnel, $connected, $status = null ) { $reach = citolume_reachability(); $crawled = citolume_get_bot_totals( 30 ); $visits = 0; foreach ( $crawled as $row ) { $visits += (int) $row->hits; } $allowed = (int) $reach['total'] - (int) $reach['blocked']; $has_scan = is_array( $funnel ) && ! empty( $funnel['status'] ) && 'never_run' !== $funnel['status']; $mentioned = $has_scan ? (int) $funnel['mentioned'] : null; $cited = $has_scan ? (int) $funnel['cited'] : null; $total = is_array( $funnel ) ? (int) ( $funnel['total_questions'] ?? 0 ) : 0; // The first scan is running right now: the service stages show live // progress dots, not dashes that read as "nothing will happen". $first_scanning = is_array( $funnel ) && ! empty( $funnel['run_in_flight'] ) && ! $has_scan; // The day the first scan runs, for the Cited caption before any scan — // the platform says "first scan Mon 26 Oct", not a bare "appears later". $next_day = ( is_array( $funnel ) && ! empty( $funnel['next_scheduled_at'] ) ) ? date_i18n( 'D j M', (int) strtotime( (string) $funnel['next_scheduled_at'] . ' UTC' ) ) : ''; $stages = array( 'reachable' => array( 'label' => __( 'Reachable', 'citolume' ), 'value' => sprintf( '%d/%d', $allowed, (int) $reach['total'] ), 'good' => 0 === (int) $reach['blocked'], // The platform's wording, word for word. These captions are the // stage's definition, not commentary on this site's result — see // the header note. 'caption' => ( 0 === (int) $reach['blocked'] ) ? __( 'AI crawlers allowed — none blocked in robots.txt', 'citolume' ) : sprintf( /* translators: %s: comma-separated crawler names. */ __( '%s blocked in robots.txt', 'citolume' ), implode( ', ', (array) ( $reach['blocked_names'] ?? array() ) ) ), ), 'crawled' => array( 'label' => __( 'Crawled', 'citolume' ), 'value' => number_format_i18n( $visits ), 'good' => false, 'caption' => $visits > 0 ? __( 'crawler visits in 30 days, reported by the client on your site', 'citolume' ) : __( 'crawler visits are counted on the site itself', 'citolume' ), ), 'mentioned' => array( 'label' => __( 'Mentioned', 'citolume' ), 'value' => $has_scan ? sprintf( /* translators: 1: questions mentioned on, 2: total questions. */ __( '%1$d of %2$d', 'citolume' ), $mentioned, $total ) : ( $first_scanning ? '…' : '—' ), 'good' => $has_scan && $mentioned > 0, 'checking' => $first_scanning, 'caption' => $has_scan ? __( 'questions where an answer names you without linking', 'citolume' ) : ( $first_scanning ? sprintf( /* translators: %d: number of tracked questions. */ __( 'checking now — asking your %d questions', 'citolume' ), $total ) : __( 'appears after the first scan', 'citolume' ) ), ), 'cited' => array( 'label' => __( 'Cited', 'citolume' ), 'value' => $has_scan ? sprintf( /* translators: 1: questions cited on, 2: total questions. */ __( '%1$d of %2$d', 'citolume' ), $cited, $total ) : ( $first_scanning ? '…' : '—' ), 'good' => $has_scan && $cited > 0, 'checking' => $first_scanning, // One caption whether cited is 3 or 0. It states what the stage // *is*; the old version editorialised at zero ("the stage before // this is where to work"), which is the job of the "one thing to // do this week" card and read as a different product. 'caption' => $has_scan ? __( 'questions where your site is in the sources', 'citolume' ) : ( $first_scanning ? __( 'results in a few minutes', 'citolume' ) : ( $next_day ? sprintf( /* translators: %s: date of the first scan. */ __( 'first scan %s', 'citolume' ), $next_day ) : __( 'appears after the first scan', 'citolume' ) ) ), ), ); ?>
$stage ) : ?>
' . esc_html( date_i18n( get_option( 'date_format' ), $next ) ) . '' ); } else { esc_html_e( 'The first scan will run on schedule; reachability and crawler data are live already.', 'citolume' ); } ?> 1 ) { printf( /* translators: %d: how many times each question is asked per scan. */ esc_html__( '%d asks per question per scan · answers vary, so we report frequency', 'citolume' ), absint( $samples ) ); } ?>
__( 'Gemini', 'citolume' ), 'openai' => __( 'ChatGPT', 'citolume' ), 'perplexity' => __( 'Perplexity', 'citolume' ), 'claude' => __( 'Claude', 'citolume' ), 'ai_overviews' => __( 'AI Overviews', 'citolume' ), 'mock' => __( 'Test engine', 'citolume' ), ); return isset( $labels[ $engine ] ) ? $labels[ $engine ] : $engine; } /** * Who owns your questions — prominent, never buried (§3.6 block 2). * * @param array|WP_Error|null $sources Sources payload. * @param array|WP_Error $funnel Funnel payload, for state. * @return void */ function citolume_render_who_owns( $sources, $funnel ) { // Without a connection there is nothing to say yet — the funnel's foot // already carries the connect nudge; a second card would be a banner. if ( null === $sources || is_wp_error( $sources ) ) { return; } $total_questions = (int) ( $sources['total_questions'] ?? ( is_array( $funnel ) ? (int) ( $funnel['total_questions'] ?? 0 ) : 0 ) ); ?>

|WP_Error|null $status Site status, if connected. * @return void */ function citolume_render_account_card( $status ) { if ( ! is_array( $status ) || ! isset( $status['account_claimed'] ) ) { return; } ?> |WP_Error $funnel Funnel payload. * @param bool $connected Whether a key is stored. * @return void */ function citolume_render_next_step( $funnel, $connected ) { // The platform's heading, not "Next step": it says *when*, which is what // turns a suggestion into something a person schedules. $label = __( 'One thing to do this week', 'citolume' ); // Not connected, or the service is away: the honest next step is local. if ( ! $connected || is_wp_error( $funnel ) || 'never_run' === ( $funnel['status'] ?? '' ) ) { if ( ! get_option( 'citolume_llms_enabled', true ) ) { ?> 0 && 0 === (int) ( $engine['runs_cited'] ?? 0 ) ) { citolume_render_mentioned_step( $label, $query, $engine ); return; } } } $worst = null; $missing = -1; foreach ( $queries as $query ) { $engines = (array) ( $query['engines'] ?? array() ); if ( ! $engines ) { continue; } $absent = 0; foreach ( $engines as $engine ) { if ( (int) ( $engine['runs_cited'] ?? 0 ) === 0 ) { ++$absent; } } if ( $absent > $missing ) { $missing = $absent; $worst = $query; } } if ( null === $worst || $missing < 1 ) { // No per-question data at all — every current question joined after the // last scan. If that scan still says an answer named this site, the gap // is worth pointing at even without naming the question. if ( is_array( $funnel ) && (int) ( $funnel['mentioned'] ?? 0 ) > 0 && 0 === (int) ( $funnel['cited'] ?? 0 ) ) { ?> $query Query row from the results payload. * @param array $engine The engine row doing the mentioning. * @return void */ function citolume_render_mentioned_step( $label, $query, $engine ) { $engine_name = citolume_engine_label( (string) ( $engine['engine'] ?? '' ) ); $competitor = (string) ( ( (array) ( $engine['competitors'] ?? array() ) )[0] ?? '' ); $page = citolume_guess_page_for( (string) $query['text'] ); ?> 3 && ! in_array( $word, array( 'what', 'best', 'does', 'have', 'with', 'this', 'that', 'your', 'how' ), true ); } ); if ( ! $words ) { return 0; } $found = get_posts( array( 'post_type' => array( 'post', 'page' ), 'post_status' => 'publish', 's' => implode( ' ', array_slice( array_values( $words ), 0, 3 ) ), 'posts_per_page' => 1, 'fields' => 'ids', ) ); return $found ? (int) $found[0] : 0; } /** * "Welcome back" after a reinstall reconnected to the kept site (ticket 146). * * Said once, then forgotten. The account is named by a masked address only: * the domain proved this site, not the address typed in the wizard, so the * full address of whoever owns it is not this visitor's to read. * * @return void */ function citolume_render_reconnected_notice() { $back = get_transient( 'citolume_reconnected' ); if ( ! is_array( $back ) ) { return; } delete_transient( 'citolume_reconnected' ); $status = (string) ( $back['owner_status'] ?? 'none' ); $email = (string) ( $back['owner_email'] ?? '' ); $sign_in = '' . esc_html( preg_replace( '#^https?://#', '', citolume_site_url() ) ) . ''; ?>

' . esc_html( $email ) . '', $sign_in // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above. ); ?>

' . esc_html( $email ) . '', $sign_in // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above. ); ?>

' . esc_html( $email ) . '' ); ?>