Template data. */ public static function data( string $page ): array { $settings = Options::all(); $data = [ 'page' => $page, 'title' => self::title( $page ), 'version' => ACID_AEO_VERSION, 'settings' => $settings, 'admin_url' => admin_url( 'admin.php' ), 'rest_url' => rest_url( Controller::REST_NAMESPACE . '/' ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'actions' => [], ]; if ( 'dashboard' === $page ) { $data['audit'] = Runner::run( false ); $data['environment'] = Environment::detect(); $data['file_urls'] = SiteData::file_urls(); $data['files'] = self::file_list(); $data['modules'] = self::module_list(); $data['stats'] = Admin::citation_stats( 30 ); $data['ring'] = self::ring( (int) ( $data['audit']['score'] ?? 0 ) ); $data['band_label'] = Admin::band_label( (string) ( $data['audit']['band'] ?? '' ) ); $data['fixes'] = self::top_fixes( $data['audit'] ); $data['spark'] = self::sparkline( $data['stats']['timeline'] ); $data['statuses'] = self::status_labels(); $data['actions'] = [ [ 'label' => __( 'Run setup again', 'acid-aeo' ), 'url' => admin_url( 'admin.php?page=acid-aeo-wizard' ), 'primary' => false, ], ]; } if ( 'settings' === $page ) { $data['tabs'] = FieldsUi::tabs(); $data['environment'] = Environment::detect(); $data['defaults'] = Options::defaults(); $data['file_urls'] = SiteData::file_urls(); } if ( 'citations' === $page ) { $data['export_url'] = add_query_arg( '_wpnonce', $data['nonce'], $data['rest_url'] . 'citations/export' ); } if ( 'tools' === $page ) { $data['file_urls'] = SiteData::file_urls(); $data['files'] = self::file_list(); $data['key_url'] = self::indexnow_key_url(); } return $data; } /** * Renders the fields of one settings tab. * * The tab table is data; what belongs on each tab is not, because three of * them carry something the schema cannot express — a preset that decides * what the crawler table inherits, the Content-Signal line those two add up * to, and the key URL an IndexNow key has to be published at. * * @param string $tab Tab id, e.g. `policy`. * @param array $data Template data. * @return void */ public static function render_tab( string $tab, array $data ): void { $settings = is_array( $data['settings'] ?? null ) ? $data['settings'] : []; switch ( $tab ) { case 'identity': Fields::render_path( 'general.site_type', $settings ); Fields::render_path( 'general.language', $settings ); Fields::render_section( 'identity', $settings ); break; case 'policy': Fields::render_path( 'policy.enabled', $settings ); Fields::render_path( 'policy.preset', $settings ); self::content_signal_summary(); Fields::render_path( 'policy.content_signal', $settings ); Fields::render_path( 'policy.bots', $settings ); Fields::render_path( 'policy.disallow_paths', $settings ); Fields::render_path( 'policy.license', $settings ); Fields::render_path( 'policy.license_url', $settings ); Fields::render_path( 'policy.permissions_contact', $settings ); Fields::render_path( 'policy.noai_meta', $settings ); break; case 'llms': Fields::render_section( 'llms', $settings ); Fields::render_path( 'offerings', $settings ); Fields::render_path( 'faq', $settings ); break; case 'files': foreach ( [ 'humans', 'security', 'ai_txt', 'ai_context' ] as $section ) { self::subsection( $section, $settings ); } break; case 'head': self::subsection( 'head', $settings ); self::subsection( 'headers', $settings ); break; case 'indexnow': Fields::render_section( 'indexnow', $settings ); self::key_url_row(); break; case 'advanced': Fields::render_path( 'general.agency_credit', $settings ); Fields::render_path( 'general.remove_data', $settings ); self::tools_shortcut(); break; default: Fields::render_section( $tab, $settings ); break; } } /** * The files the dashboard and the tools screen list. * * @return array> Each file: `key`, `label`, `url`, `preview`, `enabled`. */ public static function file_list(): array { $urls = SiteData::file_urls(); $table = [ 'llms' => [ __( 'llms.txt', 'acid-aeo' ), 'llms.enabled' ], 'llms_full' => [ __( 'llms-full.txt', 'acid-aeo' ), 'llms.full_enabled' ], 'humans' => [ __( 'humans.txt', 'acid-aeo' ), 'humans.enabled' ], 'ai_txt' => [ __( 'ai.txt', 'acid-aeo' ), 'ai_txt.enabled' ], 'ai_context' => [ __( 'ai-context.json', 'acid-aeo' ), 'ai_context.enabled' ], 'security' => [ __( 'security.txt', 'acid-aeo' ), 'security.enabled' ], 'sitemap' => [ __( 'Sitemap', 'acid-aeo' ), '' ], 'feed' => [ __( 'Feed', 'acid-aeo' ), '' ], ]; $files = []; foreach ( $table as $key => $meta ) { $files[] = [ 'key' => $key, 'label' => $meta[0], 'url' => (string) ( $urls[ $key ] ?? '' ), 'preview' => in_array( $key, Preview::FILES, true ), 'enabled' => '' === $meta[1] || (bool) Options::get( $meta[1], true ), ]; } $files[] = [ 'key' => 'robots', 'label' => __( 'robots.txt', 'acid-aeo' ), 'url' => home_url( '/robots.txt' ), 'preview' => true, 'enabled' => (bool) Options::get( 'policy.enabled', true ), ]; return $files; } /** * The booted modules, with the settings section that switches each on. * * @return array> Each module: `slug`, `title`, `enabled`. */ public static function module_list(): array { $modules = []; foreach ( Admin::module_titles() as $slug => $title ) { $modules[] = [ 'slug' => $slug, 'title' => $title, 'enabled' => (bool) Options::get( $slug . '.enabled', true ), ]; } return $modules; } /** * The word shown on an audit status pill. * * @return array Status key => translated label. */ public static function status_labels(): array { return [ 'pass' => __( 'Pass', 'acid-aeo' ), 'warn' => __( 'Warning', 'acid-aeo' ), 'fail' => __( 'Fail', 'acid-aeo' ), 'skip' => __( 'Skipped', 'acid-aeo' ), ]; } /** * The first five checks that are not a pass. * * @param array $audit Audit result. * @return array> Checks. */ public static function top_fixes( array $audit ): array { $checks = is_array( $audit['checks'] ?? null ) ? $audit['checks'] : []; $fixes = []; foreach ( $checks as $check ) { if ( ! is_array( $check ) || 'pass' === (string) ( $check['status'] ?? '' ) ) { continue; } $fixes[] = $check; if ( count( $fixes ) >= 5 ) { break; } } return $fixes; } /** * The stroke geometry of the score ring. * * @param int $score Score, 0–100. * @return array Keys `radius`, `circumference` and `dash`. */ public static function ring( int $score ): array { $radius = 52.0; $circumference = round( 2 * M_PI * $radius, 2 ); $filled = round( max( 0.0, min( 100.0, (float) $score ) ) / 100 * $circumference, 2 ); return [ 'radius' => $radius, 'circumference' => $circumference, 'dash' => $filled . ' ' . $circumference, ]; } /** * Two polylines drawn from the citation timeline. * * @param mixed $timeline Timeline rows of `day`, `crawl` and `referral`. * @return array Keys `width`, `height`, `max`, `crawl` and `referral`. */ public static function sparkline( $timeline ): array { $rows = is_array( $timeline ) ? array_values( $timeline ) : []; $width = 300.0; $height = 48.0; $count = count( $rows ); $max = 1; foreach ( $rows as $row ) { $max = max( $max, (int) ( $row['crawl'] ?? 0 ), (int) ( $row['referral'] ?? 0 ) ); } $series = [ 'crawl' => [], 'referral' => [], ]; foreach ( $rows as $index => $row ) { $x = $count > 1 ? round( $index / ( $count - 1 ) * $width, 2 ) : 0.0; foreach ( array_keys( $series ) as $key ) { $y = round( $height - ( (int) ( $row[ $key ] ?? 0 ) / $max * $height ), 2 ); $series[ $key ][] = $x . ',' . $y; } } return [ 'width' => $width, 'height' => $height, 'max' => $max, 'crawl' => implode( ' ', $series['crawl'] ), 'referral' => implode( ' ', $series['referral'] ), ]; } /** * The URL the IndexNow key file is published at. * * @return string Absolute URL, or an empty string when the module is unavailable. */ private static function indexnow_key_url(): string { $module = Controller::module( 'indexnow', Modules\IndexNow::class ); if ( ! $module instanceof Modules\IndexNow ) { return ''; } return (string) $module->key_url(); } /** * The heading of one screen. * * @param string $page Template name. * @return string Translated title. */ private static function title( string $page ): string { $titles = [ 'dashboard' => __( 'Dashboard', 'acid-aeo' ), 'settings' => __( 'Settings', 'acid-aeo' ), 'citations' => __( 'AI Citations', 'acid-aeo' ), 'tools' => __( 'Tools', 'acid-aeo' ), ]; return $titles[ $page ] ?? __( 'Acid AEO', 'acid-aeo' ); } /** * One settings section inside a titled block. * * @param string $section Section name. * @param array $settings Settings array. * @return void */ private static function subsection( string $section, array $settings ): void { $titles = [ 'humans' => __( 'humans.txt', 'acid-aeo' ), 'security' => __( 'security.txt', 'acid-aeo' ), 'ai_txt' => __( 'ai.txt', 'acid-aeo' ), 'ai_context' => __( 'ai-context.json', 'acid-aeo' ), 'head' => __( 'Head tags', 'acid-aeo' ), 'headers' => __( 'HTTP headers', 'acid-aeo' ), ]; printf( '

%s

', esc_html( $titles[ $section ] ?? $section ) ); Fields::render_section( $section, $settings ); echo '
'; } /** * The Content-Signal line the preset and the custom values add up to. * * @return void */ private static function content_signal_summary(): void { printf( '

%1$s %2$s

', esc_html__( 'Published Content-Signal:', 'acid-aeo' ), esc_html( SiteData::content_signal_string() ) ); } /** * The read-only row showing where the IndexNow key is published. * * @return void */ private static function key_url_row(): void { $url = self::indexnow_key_url(); if ( '' === $url ) { return; } printf( '

%1$s %3$s

', esc_html__( 'Key file:', 'acid-aeo' ), esc_url( $url ), esc_html( $url ), esc_html__( 'Copy', 'acid-aeo' ) ); } /** * The link from the Advanced tab to export and import. * * @return void */ private static function tools_shortcut(): void { printf( '

%1$s %3$s

', esc_html__( 'Export or import every setting as JSON on the Tools screen.', 'acid-aeo' ), esc_url( admin_url( 'admin.php?page=acid-aeo-tools' ) ), esc_html__( 'Open Tools', 'acid-aeo' ) ); } }