__('Published pages', 'crownify'), 'value' => number_format_i18n((int) $audit['total']), ]; $rows[] = [ 'label' => __('Missing a meta description', 'crownify'), 'value' => number_format_i18n((int) $audit['missingDescription']), 'tone' => $audit['missingDescription'] > 0 ? 'warn' : 'good', ]; $rows[] = [ 'label' => __('Missing a featured image', 'crownify'), 'value' => number_format_i18n((int) $audit['missingImage']), 'tone' => $audit['missingImage'] > 0 ? 'warn' : 'good', ]; if ($audit['longTitles'] > 0) { $rows[] = [ 'label' => __('Titles long enough to be cut off', 'crownify'), 'value' => number_format_i18n((int) $audit['longTitles']), 'tone' => 'warn', ]; } $crawlers = self::crawlers($days); $broken = self::brokenUrls(); if (null !== $crawlers) { $rows[] = [ 'label' => __('AI crawler requests', 'crownify'), 'value' => number_format_i18n($crawlers['hits']), 'tone' => $crawlers['hits'] > 0 ? 'good' : 'warn', ]; if ('' !== $crawlers['top']) { $rows[] = [ 'label' => __('Read your site most', 'crownify'), 'value' => $crawlers['top'], ]; } } if ($broken > 0) { $rows[] = [ 'label' => __('URLs returning 404', 'crownify'), 'value' => number_format_i18n($broken), 'tone' => 'warn', ]; } /* translators: 1: site name, 2: number of days covered. */ $subject = _n('[%1$s] Your SEO summary for the last %2$s day', '[%1$s] Your SEO summary for the last %2$s days', $days, 'crownify'); Mailer::send(sprintf($subject, get_bloginfo('name'), number_format_i18n($days)), __('How your site looks to search and AI', 'crownify'), self::intro($audit, $crawlers), $rows, ['label' => __('Open the Crownify dashboard', 'crownify'), 'url' => admin_url('admin.php?page=rankify')], self::footnote($crawlers)); return true; } /** * Leads with whichever number the reader can actually act on. */ protected static function intro(array $audit, $crawlers) { if ($audit['missingDescription'] > 0) { /* translators: %s: number of pages with no meta description. */ $notice = _n('%s published page has nothing written for search engines or AI assistants to quote. That one is the fastest thing to fix.', '%s published pages have nothing written for search engines or AI assistants to quote. Those are the fastest thing to fix.', (int) $audit['missingDescription'], 'crownify'); return sprintf($notice, '' . number_format_i18n((int) $audit['missingDescription']) . ''); } if (null !== $crawlers && 0 === $crawlers['hits']) { return __('Every published page has a description, which is the hard part. No AI crawler reached the site in this period though. Worth checking that none of them are blocked.', 'crownify'); } return __('Every published page has a description and assistants are reading the site. Nothing here needs your attention.', 'crownify'); } protected static function footnote($crawlers) { if (null === $crawlers || !$crawlers['cached']) { return ''; } return __('A page cache is active on this site. Requests served straight from the cache never reach Crownify, so the crawler count is a floor rather than a total.', 'crownify'); } /** * @return array{hits:int,top:string,cached:bool}|null Null when the crawler log is off. */ protected static function crawlers($days) { if (!class_exists('\DaftPlug\Rankify\Features\Geo\CrawlerLog')) { return null; } if ('on' !== Utils::getSetting('aeoCitationTracking')) { return null; } $totals = \DaftPlug\Rankify\Features\Geo\CrawlerLog::totals($days); $bots = \DaftPlug\Rankify\Features\Geo\CrawlerLog::byBot($days); $top = ''; if (!empty($bots)) { $first = reset($bots); $top = isset($first['source']) ? $first['source'] : ''; } return [ 'hits' => isset($totals['hits']) ? (int) $totals['hits'] : 0, 'top' => $top, 'cached' => \DaftPlug\Rankify\Features\Geo\CrawlerLog::pageCacheActive(), ]; } protected static function brokenUrls() { if (!class_exists('\DaftPlug\Rankify\Features\Redirects\Log')) { return 0; } return (int) \DaftPlug\Rankify\Features\Redirects\Log::count(); } }