* state is one of ok, warn, bad. */ function novasira_dashboard_visibility(): array { $rows = []; $public = (string) get_option('blog_public') !== '0'; $seo = novasira_dashboard_seo_plugin(); $rows[] = [ 'label' => __('Search engines allowed', 'novasira'), 'state' => $public ? 'ok' : 'bad', 'detail' => $public ? __('The site asks to be indexed.', 'novasira') : __('"Discourage search engines" is ticked, so the whole site asks not to be indexed.', 'novasira'), 'url' => $public ? '' : admin_url('options-reading.php'), ]; $https = wp_parse_url(home_url(), PHP_URL_SCHEME) === 'https'; $rows[] = [ 'label' => __('HTTPS', 'novasira'), 'state' => $https ? 'ok' : 'warn', 'detail' => $https ? __('The site address uses https://.', 'novasira') : __('The site address is http://. Browsers mark it not secure and search engines prefer the https version.', 'novasira'), 'url' => $https ? '' : admin_url('options-general.php'), ]; $structure = (string) get_option('permalink_structure'); if ($structure === '') { $state = 'bad'; $detail = __('Plain permalinks (?p=123) carry no words a search engine can read.', 'novasira'); } elseif (str_contains($structure, '%postname%')) { $state = 'ok'; $detail = sprintf( /* translators: %s: the permalink structure, such as /%postname%/ */ __('Readable addresses: %s', 'novasira'), $structure ); } else { $state = 'warn'; $detail = sprintf( /* translators: %s: the permalink structure */ __('Addresses use %s without the post name.', 'novasira'), $structure ); } $rows[] = [ 'label' => __('Permalinks', 'novasira'), 'state' => $state, 'detail' => $detail, 'url' => $state === 'ok' ? '' : admin_url('options-permalink.php'), ]; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- core's own filter. $core_sitemap = function_exists('wp_sitemaps_get_server') && $public && (bool) apply_filters('wp_sitemaps_enabled', true); $has_sitemap = $core_sitemap || ($seo !== null && $seo !== ''); $rows[] = [ 'label' => __('XML sitemap', 'novasira'), 'state' => $has_sitemap ? 'ok' : 'bad', 'detail' => $core_sitemap ? __('WordPress serves one at /wp-sitemap.xml.', 'novasira') : ($has_sitemap ? sprintf( /* translators: %s: the name of an SEO plugin */ __('Provided by %s.', 'novasira'), (string) $seo ) : __('No sitemap is being served.', 'novasira')), 'url' => $core_sitemap ? home_url('/wp-sitemap.xml') : '', ]; $robots_file = is_readable(ABSPATH . 'robots.txt'); $rows[] = [ 'label' => __('robots.txt', 'novasira'), 'state' => 'ok', 'detail' => $robots_file ? __('A physical file on the server, which overrides WordPress\'s own.', 'novasira') : __('Generated by WordPress.', 'novasira'), 'url' => home_url('/robots.txt'), ]; if ($seo !== null) { $rows[] = [ 'label' => __('SEO plugin', 'novasira'), 'state' => $seo !== '' ? 'ok' : 'warn', 'detail' => $seo !== '' ? $seo : __('None detected, so titles and descriptions fall back to whatever the theme prints.', 'novasira'), 'url' => '', ]; } $tagline = trim((string) get_option('blogdescription')); // Empty is the default on new installs; older ones shipped this English line. $default_tagline = $tagline === '' || $tagline === 'Just another WordPress site'; $rows[] = [ 'label' => __('Tagline', 'novasira'), 'state' => $default_tagline ? 'warn' : 'ok', 'detail' => $default_tagline ? __('Empty or still the WordPress default; many themes print it in the title.', 'novasira') : $tagline, 'url' => $default_tagline ? admin_url('options-general.php') : '', ]; $icon = has_site_icon(); $rows[] = [ 'label' => __('Site icon', 'novasira'), 'state' => $icon ? 'ok' : 'warn', 'detail' => $icon ? __('Set. Search results show it beside the site name.', 'novasira') : __('Not set. Search results show a generic globe instead.', 'novasira'), 'url' => $icon ? '' : admin_url('customize.php?autofocus[section]=title_tagline'), ]; return $rows; } /** * Published, unpublished and private counts for every public content type. * * @return list */ function novasira_dashboard_inventory(): array { $rows = []; foreach (get_post_types(['public' => true], 'objects') as $type) { if ($type->name === 'attachment') { continue; } $counts = wp_count_posts($type->name); $publish = (int) ($counts->publish ?? 0); $draft = (int) ($counts->draft ?? 0) + (int) ($counts->pending ?? 0) + (int) ($counts->future ?? 0); $private = (int) ($counts->private ?? 0); if ($publish + $draft + $private === 0) { continue; } $rows[] = [ 'label' => (string) ($type->labels->name ?? $type->name), 'publish' => $publish, 'draft' => $draft, 'private' => $private, 'url' => admin_url('edit.php?post_type=' . $type->name), ]; } usort($rows, static fn(array $a, array $b): int => $b['publish'] <=> $a['publish']); return $rows; } /** * Terms in each public taxonomy, and how many hold no content at all. * * @return list */ function novasira_dashboard_taxonomies(): array { $rows = []; foreach (get_taxonomies(['public' => true, 'show_ui' => true], 'objects') as $taxonomy) { if ($taxonomy->name === 'post_format') { continue; } $all = wp_count_terms(['taxonomy' => $taxonomy->name, 'hide_empty' => false]); $used = wp_count_terms(['taxonomy' => $taxonomy->name, 'hide_empty' => true]); $all = is_wp_error($all) ? 0 : (int) $all; if ($all === 0) { continue; } $rows[] = [ 'label' => (string) ($taxonomy->labels->name ?? $taxonomy->name), 'terms' => $all, 'empty' => max(0, $all - (is_wp_error($used) ? 0 : (int) $used)), ]; } return $rows; } /** * Who the site is, as a search engine and an agent would first want to know. * * @return list */ function novasira_dashboard_profile(): array { $theme = wp_get_theme(); $rows = [ ['label' => __('Site', 'novasira'), 'value' => (string) get_bloginfo('name')], ['label' => __('Address', 'novasira'), 'value' => (string) wp_parse_url(home_url(), PHP_URL_HOST)], ['label' => __('Language', 'novasira'), 'value' => get_bloginfo('language')], ['label' => __('Time zone', 'novasira'), 'value' => wp_timezone_string()], [ 'label' => __('Theme', 'novasira'), 'value' => (string) $theme->get('Name') . (function_exists('wp_is_block_theme') && wp_is_block_theme() ? ' ' . __('(block theme)', 'novasira') : ''), ], ['label' => __('WordPress', 'novasira'), 'value' => (string) get_bloginfo('version')], ['label' => __('PHP', 'novasira'), 'value' => PHP_VERSION], ]; $front = (string) get_option('show_on_front') === 'page' ? get_the_title((int) get_option('page_on_front')) : __('Latest posts', 'novasira'); $rows[] = ['label' => __('Front page', 'novasira'), 'value' => $front !== '' ? $front : __('Not chosen', 'novasira')]; if (function_exists('novasira_ab_multilingual')) { $multi = novasira_ab_multilingual(); if (!empty($multi['present'])) { $rows[] = [ 'label' => __('Languages', 'novasira'), 'value' => trim((string) ($multi['label'] ?? '') . ' ' . implode(', ', (array) ($multi['languages'] ?? []))), ]; } } if (post_type_exists('product')) { $rows[] = [ 'label' => __('Products', 'novasira'), 'value' => number_format_i18n((int) (wp_count_posts('product')->publish ?? 0)), ]; } if (is_multisite()) { $rows[] = ['label' => __('Network', 'novasira'), 'value' => __('Part of a multisite network', 'novasira')]; } return $rows; } /** A status dot for the visibility list. */ function novasira_dashboard_state_icon(string $state): void { echo ''; } /** * The dashboard. */ function novasira_render_dashboard_page(): void { if (!current_user_can(novasira_read_cap())) { wp_die(esc_html__('You do not have permission to view Novasira.', 'novasira')); } $request = novasira_scan_request(); $results = novasira_scan_all_guarded($request['ok']); $engine = novasira_scan_available(); $areas = novasira_areas(); // Asking whether each tool registered also initialises the Abilities API, // which loads the SEO-plugin and multilingual detectors used below. $abilities = novasira_ability_names(); $ready = array_values(array_filter($abilities, 'novasira_has_ability')); // The site score is the mean of the areas that actually looked at something. // // An area that returned no score is left out of the divisor rather than // counted as zero, and so is one that checked nothing: the engine scores // what was found rather than what was missed, so a scanner that could not // run reports a clean 100 it did not earn. $scores = []; $area_rows = []; $checked = 0; $skipped = 0; $ran_at = 0; $severity = ['critical' => 0, 'high' => 0, 'medium' => 0, 'low' => 0, 'info' => 0]; foreach (novasira_direct_results($results) as $slug => $result) { $area_checked = (int) ($result['checked'] ?? 0); $ran_at = max($ran_at, (int) ($result['ran_at'] ?? 0)); $findings = is_array($result['findings'] ?? null) ? $result['findings'] : []; foreach ($findings as $finding) { $key = novasira_severity((string) ($finding['severity'] ?? ''))['key']; $key = $key === 'notice' ? 'info' : $key; $severity[$key] = ($severity[$key] ?? 0) + 1; } if ($area_checked > 0 && isset($result['score']) && is_int($result['score'])) { $scores[] = (int) $result['score']; $area = novasira_area((string) $slug); $area_rows[] = [ 'slug' => (string) $slug, 'name' => (string) ($area['name'] ?? $slug), 'score' => (int) $result['score'], 'findings' => count($findings), ]; } elseif ($area_checked === 0) { $skipped++; } $checked += $area_checked; } $open = array_sum($severity); $site_score = $scores === [] ? null : (int) round(array_sum($scores) / count($scores)); usort($area_rows, static fn(array $a, array $b): int => $a['score'] <=> $b['score']); $inventory = novasira_dashboard_inventory(); $published = array_sum(array_column($inventory, 'publish')); novasira_header( __('SEO dashboard', 'novasira'), __('What this site knows about its own SEO, and what it would tell an agent.', 'novasira') ); if ($request['asked'] && !$request['ok']) { novasira_echo_notice('error', (string) $request['reason']); } elseif ($request['ok'] && !$engine) { novasira_echo_notice('warning', __('There is no scan engine installed yet, so there was nothing to run.', 'novasira')); } elseif ($request['ok']) { novasira_echo_notice('success', __('Scan finished. The numbers below are from this run.', 'novasira')); } /* ── Health ───────────────────────────────────────────────────────── */ $tone = $site_score === null ? 'none' : novasira_score_tone($site_score); echo '
'; echo '
'; echo ''; echo '
'; echo '

' . esc_html__('Site health', 'novasira') . '

'; echo '

'; if ($scores === []) { esc_html_e('Run a scan to score this site.', 'novasira'); } else { echo esc_html(sprintf( /* translators: %s: a number of areas */ _n('Across %s scored area.', 'Across %s scored areas.', count($scores), 'novasira'), number_format_i18n(count($scores)) )); if ($ran_at > 0) { echo ' ' . esc_html(sprintf( /* translators: %s: a human-readable time difference, such as "5 minutes" */ __('Scanned %s ago.', 'novasira'), human_time_diff($ran_at) )); } } echo '

'; if ($open > 0) { $labels = [ 'critical' => __('Critical', 'novasira'), 'high' => __('High', 'novasira'), 'medium' => __('Medium', 'novasira'), 'low' => __('Low', 'novasira'), 'info' => __('Info', 'novasira'), ]; echo '
    '; foreach ($severity as $key => $count) { echo '
  • ' . '' . esc_html(number_format_i18n($count)) . ' ' . esc_html($labels[$key]) . '
  • '; } echo '
'; } elseif ($scores !== []) { echo '

' . esc_html__('No open findings. Every check that ran is satisfied.', 'novasira') . '

'; } echo '
'; novasira_echo_scan_form('novasira', '', $scores === [] ? __('Run the first scan', 'novasira') : __('Scan again', 'novasira')); echo '
'; echo '
'; echo '
'; novasira_echo_stat( __('Open findings', 'novasira'), $engine && $results !== [] ? number_format_i18n($open) : '—', $severity['critical'] + $severity['high'] > 0 ? sprintf( /* translators: %s: number of critical and high findings */ __('%s critical or high', 'novasira'), number_format_i18n($severity['critical'] + $severity['high']) ) : __('Across every area that ran', 'novasira') ); novasira_echo_stat( __('Published content', 'novasira'), number_format_i18n($published), sprintf( /* translators: %s: number of content types */ _n('Across %s content type', 'Across %s content types', count($inventory), 'novasira'), number_format_i18n(count($inventory)) ) ); novasira_echo_stat( __('Things checked', 'novasira'), $engine && $checked > 0 ? number_format_i18n($checked) : '—', $skipped > 0 ? sprintf( /* translators: %s: a number of areas */ _n('%s area had nothing to check', '%s areas had nothing to check', $skipped, 'novasira'), number_format_i18n($skipped) ) : __('Posts, images, links and settings', 'novasira') ); novasira_echo_stat( __('Agent tools', 'novasira'), sprintf( /* translators: 1: registered tool count, 2: expected tool count */ __('%1$s of %2$s', 'novasira'), number_format_i18n(count($ready)), number_format_i18n(count($abilities)) ), $ready === [] ? __('Nothing registered yet', 'novasira') : __('Ready over MCP', 'novasira') ); echo '
'; echo '
'; /* ── What to fix, and can search engines see it ───────────────────── */ echo '
'; novasira_card_open(__('What to fix first', 'novasira')); if (!$engine) { echo '

' . esc_html__('The scan engine is not installed, so there are no findings yet. Everything else on this screen still reads the site directly.', 'novasira') . '

'; } elseif ($results === []) { echo '

' . esc_html__('No scan has run yet. A scan reads every post, image, link and SEO setting, then lists what to fix, worst first.', 'novasira') . '

'; } else { $worst = novasira_worst_findings(novasira_direct_results($results), 6); if ($worst === []) { echo '

' . esc_html__('Nothing came back. Every check that ran is satisfied with what it found.', 'novasira') . '

'; } else { echo '
    '; foreach ($worst as $finding) { novasira_echo_finding($finding, true); } echo '
'; echo '

' . esc_html(sprintf( /* translators: 1: findings shown, 2: findings in total */ __('Showing %1$s of %2$s.', 'novasira'), number_format_i18n(count($worst)), number_format_i18n($open) )) . ' ' . esc_html__('See every area', 'novasira') . '

'; } } novasira_card_close(); $visibility = novasira_dashboard_visibility(); $vis_ok = count(array_filter($visibility, static fn(array $r): bool => $r['state'] === 'ok')); echo '
'; echo '

' . esc_html__('Search visibility', 'novasira') . '

' . '' . esc_html(sprintf( /* translators: 1: checks passing, 2: checks in total */ __('%1$s of %2$s in order', 'novasira'), number_format_i18n($vis_ok), number_format_i18n(count($visibility)) )) . '
'; echo '
    '; foreach ($visibility as $row) { echo '
  • '; novasira_dashboard_state_icon($row['state']); echo '' . esc_html($row['label']) . '' . '' . esc_html($row['detail']) . ''; if ($row['url'] !== '') { $fix = $row['state'] === 'ok' ? __('View', 'novasira') : __('Fix', 'novasira'); echo '' . esc_html($fix) . ''; } echo '
  • '; } echo '
'; echo '
'; /* ── Where it is weakest, and what it holds ───────────────────────── */ echo '
'; novasira_card_open(__('Scores by area', 'novasira')); if ($area_rows === []) { echo '

' . esc_html__('Each SEO area gets its own score once a scan has run. The weakest appear first.', 'novasira') . '

'; } else { echo ''; if (count($area_rows) > 8) { echo '

' . esc_html(sprintf( /* translators: %s: number of further areas */ __('%s more areas', 'novasira'), number_format_i18n(count($area_rows) - 8) )) . '

'; } } novasira_card_close(); novasira_card_open(__('Content inventory', 'novasira')); if ($inventory === []) { echo '

' . esc_html__('No public content yet.', 'novasira') . '

'; } else { echo '' . '' . '' . '' . '' . ''; foreach ($inventory as $row) { echo '' . '' . '' . ''; } echo '
' . esc_html__('Type', 'novasira') . '' . esc_html__('Published', 'novasira') . '' . esc_html__('Unpublished', 'novasira') . '' . esc_html__('Private', 'novasira') . '
' . esc_html($row['label']) . '' . esc_html(number_format_i18n($row['publish'])) . '' . esc_html(number_format_i18n($row['draft'])) . '' . esc_html(number_format_i18n($row['private'])) . '
'; } $taxonomies = novasira_dashboard_taxonomies(); if ($taxonomies !== []) { echo '
    '; foreach ($taxonomies as $row) { echo '
  • ' . '' . esc_html(number_format_i18n($row['terms'])) . ' ' . esc_html($row['label']); if ($row['empty'] > 0) { echo ' ' . esc_html(sprintf( /* translators: %s: number of terms holding no content */ __('%s empty', 'novasira'), number_format_i18n($row['empty']) )) . ''; } echo '
  • '; } echo '
'; } novasira_card_close(); echo '
'; /* ── Who the site is, and the hand-over ───────────────────────────── */ echo '
'; novasira_card_open(__('Site profile', 'novasira')); echo '
'; foreach (novasira_dashboard_profile() as $row) { echo '
' . esc_html($row['label']) . '
' . esc_html($row['value'] !== '' ? $row['value'] : '—') . '
'; } echo '
'; novasira_card_close(); novasira_card_open(__('Hand it to an agent', 'novasira')); echo '

' . esc_html__('Paste this into an agent connected to this site. It reads the same data this screen does, with the whole method behind it.', 'novasira') . '

'; novasira_echo_prompt(novasira_dashboard_prompt()); if ($ready === []) { echo '

' . esc_html__('No tools are registered yet, so that prompt has nothing to run against.', 'novasira') . ' ' . esc_html__('Check the connection', 'novasira') . '

'; } echo '

' . esc_html__('The areas', 'novasira') . '

'; echo ''; novasira_card_close(); echo '
'; novasira_footer(); }