0 ) { $share = round( ( $pages[0]['pc'] - $pages[0]['c'] ) / $lost * 100 ); $focus = (string) $pages[0]['key']; } $insights = array( sprintf( /* translators: 1: percentage, 2: clicks before, 3: clicks now. */ __( 'The site lost %1$s of its Google clicks (%2$s → %3$s).', 'turtlereport' ), Wording::pct_abs( Wording::val( $d, array( 'change' ) ) ), Wording::num( Wording::val( $prev, array( 'clicks' ) ) ), Wording::num( Wording::val( $now, array( 'clicks' ) ) ) ), self::pattern_text( $sig, false ), ); if ( $sig['ctr_also'] ) { $insights[] = self::ctr_also_text( $sig ); } if ( '' !== $focus && $share >= 40 ) { $insights[] = sprintf( /* translators: 1: page name, 2: percentage. */ __( '%1$s accounts for %2$s of the lost clicks, so that is the place to start.', 'turtlereport' ), Wording::page_name( $focus ), $share . '%' ); } elseif ( count( $pages ) >= 3 ) { $insights[] = __( 'The loss is spread across several pages rather than one, which points to something site-wide (rankings, demand or indexing) more than to one page.', 'turtlereport' ); } $target = '' !== $focus && $share >= 40 ? $focus : ''; $checks = self::pattern_checks( $sig, $target ); if ( '' !== $target ) { /* translators: %s: page name. */ array_unshift( $checks, sprintf( __( 'Start with %s and review which search queries lost it the most clicks.', 'turtlereport' ), Wording::page_ref( $target ) ) ); } return array( 'kind' => __( 'Fewer Google visits', 'turtlereport' ), 'headline' => sprintf( /* translators: %s: percentage. */ __( 'Google clicks down %s', 'turtlereport' ), Wording::pct_abs( Wording::val( $d, array( 'change' ) ) ) ), 'url' => Wording::site_url( $site ), 'path' => __( 'All pages', 'turtlereport' ), /* translators: %s: percentage. */ 'title' => sprintf( __( 'Google clicks down %s', 'turtlereport' ), Wording::pct_abs( Wording::val( $d, array( 'change' ) ) ) ), /* translators: 1: clicks before, 2: clicks now. */ 'summary' => sprintf( __( '%1$s → %2$s clicks, last 28 days vs the 28 days before.', 'turtlereport' ), Wording::num( Wording::val( $prev, array( 'clicks' ) ) ), Wording::num( Wording::val( $now, array( 'clicks' ) ) ) ), 'metrics' => self::search_metrics( $now, $prev, $sig ), 'insights' => Wording::with_confidence( $insights, self::search_confidence( $prev ) ), 'next' => '' !== $target /* translators: %s: page name. */ ? sprintf( __( 'Start with %s and review which search queries lost clicks.', 'turtlereport' ), Wording::page_ref( $target ) ) : self::pattern_next( $sig, '' ), 'checks' => $checks, 'confidence' => self::search_confidence( $prev ), 'facts' => self::search_facts( $now, $prev ), 'tables' => array_values( array_filter( array( self::row_table( __( 'Pages that lost the most clicks', 'turtlereport' ), __( 'Page', 'turtlereport' ), $pages, true ), self::row_table( __( 'Keywords behind the drop', 'turtlereport' ), __( 'Keyword', 'turtlereport' ), $sig['rows'], false ), ) ) ), ); } /** * One important page losing clicks. * * @param array $d Data. * @return array */ public static function page( array $d ) { $row = isset( $d['row'] ) ? (array) $d['row'] : array(); $url = (string) Wording::val( $d, array( 'page' ), '' ); $now = array( 'clicks' => Wording::val( $row, array( 'c' ) ), 'impr' => Wording::val( $row, array( 'i' ) ), 'pos' => Wording::val( $row, array( 'i' ) ) ? Wording::val( $row, array( 'pos' ) ) : null, ); $prev = array( 'clicks' => Wording::val( $row, array( 'pc' ) ), 'impr' => Wording::val( $row, array( 'pi' ) ), 'pos' => Wording::val( $row, array( 'pi' ) ) ? Wording::val( $row, array( 'ppos' ) ) : null, ); $sig = self::signals( $d, $now, $prev ); $leads = (int) Wording::val( $d, array( 'leads' ) ); $insights = array( sprintf( /* translators: 1: percentage, 2: clicks before, 3: clicks now. */ __( 'This page lost %1$s of its Google clicks (%2$s → %3$s).', 'turtlereport' ), Wording::pct_abs( Wording::val( $d, array( 'change' ) ) ), Wording::num( $prev['clicks'] ), Wording::num( $now['clicks'] ) ), self::pattern_text( $sig, true ), ); if ( $sig['ctr_also'] ) { $insights[] = self::ctr_also_text( $sig ); } $site_change = Wording::val( $d, array( 'site_change' ), null ); if ( null !== $site_change && $site_change > -10 ) { $insights[] = $leads > 0 /* translators: %d: number of leads. */ ? sprintf( _n( 'The rest of the site held up, so this is specific to this page, which also brought %d lead before.', 'The rest of the site held up, so this is specific to this page, which also brought %d leads before.', $leads, 'turtlereport' ), $leads ) : __( 'The rest of the site held up, so this is specific to this page.', 'turtlereport' ); } $checks = self::pattern_checks( $sig, $url ); if ( $leads >= 3 ) { $checks[] = __( 'It also brought leads: check that its form or call button still works.', 'turtlereport' ); } return array( 'kind' => __( 'Fewer Google visits to a page', 'turtlereport' ), 'headline' => self::page_title( $url ), 'url' => $url, 'path' => Wording::path( $url ), /* translators: 1: page name, 2: percentage. */ 'title' => sprintf( __( '%1$s: clicks down %2$s', 'turtlereport' ), self::page_title( $url ), Wording::pct_abs( Wording::val( $d, array( 'change' ) ) ) ), /* translators: 1: page path, 2: clicks before, 3: clicks now. */ 'summary' => sprintf( __( '%1$s · %2$s → %3$s clicks, last 28 days vs the 28 days before.', 'turtlereport' ), Wording::path( $url ), Wording::num( $prev['clicks'] ), Wording::num( $now['clicks'] ) ), 'metrics' => self::search_metrics( $now, $prev, $sig ), 'insights' => Wording::with_confidence( $insights, self::search_confidence( $prev ) ), 'next' => self::pattern_next( $sig, $url ), 'checks' => $checks, 'confidence' => self::search_confidence( $prev ), 'facts' => self::search_facts( $now, $prev ), 'tables' => array_filter( array( self::row_table( __( 'Keywords behind the drop', 'turtlereport' ), __( 'Keyword', 'turtlereport' ), $sig['rows'], false ) ) ), ); } /** * Signals stored with the alert, or worked out again from its totals * when keyword detail is unavailable. * * @param array $d Data. * @param array $now Now. * @param array $prev Before. * @return array */ private static function signals( array $d, array $now, array $prev ) { if ( isset( $d['signals'] ) && is_array( $d['signals'] ) && isset( $d['signals']['pattern'] ) ) { return $d['signals']; } $rows = isset( $d['queries'] ) && is_array( $d['queries'] ) ? $d['queries'] : array(); return Signals::search( array( 'clicks' => Wording::val( $now, array( 'clicks' ) ), 'impr' => Wording::val( $now, array( 'impr' ) ), 'pos' => Wording::val( $now, array( 'pos' ), null ), ), array( 'clicks' => Wording::val( $prev, array( 'clicks' ) ), 'impr' => Wording::val( $prev, array( 'impr' ) ), 'pos' => Wording::val( $prev, array( 'pos' ), null ), ), $rows ); } /** * What the data suggests, for a pattern. * * @param array $s Signals. * @param bool $page About one page rather than the site. * @return string */ private static function pattern_text( array $s, $page ) { $q = self::lead( $s ); $pp = self::pos_from( $s, 'prev' ); $pn = self::pos_from( $s, 'now' ); switch ( $s['pattern'] ) { case 'ranking': $text = sprintf( /* translators: 1: position before, 2: position now. */ $page ? __( 'Its average position dropped from %1$s to %2$s, so it shows lower in Google\'s results than before.', 'turtlereport' ) : __( 'The average position dropped from %1$s to %2$s, so the site shows lower in Google\'s results than before.', 'turtlereport' ), $pp, $pn ); if ( $s['slipped'] ) { $k = $s['slipped'][0]; $text .= ' ' . sprintf( /* translators: 1: keyword, 2: position before, 3: position now. */ __( 'The clearest example is “%1$s” (position %2$s → %3$s).', 'turtlereport' ), $k['key'], Wording::pos( $k['ppos'] ), Wording::pos( $k['pos'] ) ); } return $text; case 'lost_searches': $text = sprintf( /* translators: 1: impressions change, 2: position before, 3: position now. */ $page ? __( 'Impressions fell %1$s even though the average position improved (%2$s → %3$s). That combination usually means it stopped appearing for searches where it ranked lower: losing those low positions lifts the average while visibility shrinks. This doesn\'t look like a simple ranking drop.', 'turtlereport' ) : __( 'Impressions fell %1$s even though the average position improved (%2$s → %3$s). That combination usually means the site stopped appearing for searches where it ranked lower: losing those low positions lifts the average while visibility shrinks. This doesn\'t look like a simple ranking drop.', 'turtlereport' ), Wording::pct_abs( (float) $s['di'] ), $pp, $pn ); if ( $s['vanished']['count'] ) { $text .= ' ' . sprintf( /* translators: 1: number of searches, 2: impressions. */ _n( 'It no longer appears at all for %1$d search it used to show for (%2$s impressions before).', 'It no longer appears at all for %1$d searches it used to show for (%2$s impressions before).', (int) $s['vanished']['count'], 'turtlereport' ), (int) $s['vanished']['count'], Wording::num( $s['vanished']['impr'] ) ); } return $text . self::top_text( $s ); case 'demand': $text = sprintf( /* translators: 1: impressions change, 2: position before, 3: position now. */ $page ? __( 'Impressions fell %1$s while its average position held (%2$s → %3$s): it ranks about the same but is shown for fewer searches. Either fewer people searched for its topics (often seasonal), or it dropped out of some searches.', 'turtlereport' ) : __( 'Impressions fell %1$s while the average position held (%2$s → %3$s): the site ranks about the same but is shown for fewer searches. Either fewer people searched for its topics (often seasonal), or it dropped out of some searches.', 'turtlereport' ), Wording::pct_abs( (float) $s['di'] ), $pp, $pn ); if ( ! $s['reliable'] ) { $text = sprintf( /* translators: %s: impressions change. */ __( 'Impressions fell %s: Google showed it for fewer searches. There are too few impressions to judge its position reliably.', 'turtlereport' ), Wording::pct_abs( (float) $s['di'] ) ); } return $text . self::top_text( $s ); case 'snippet': return sprintf( /* translators: 1: impressions change, 2: CTR before, 3: CTR now. */ __( 'Impressions held up (%1$s) but the click-through rate fell from %2$s to %3$s: people see it as often but click less. That points to the search result itself: a less appealing title or description, or new features such as AI Overviews, ads or maps above it.', 'turtlereport' ), Wording::signed( (float) $s['di'] ), Wording::pct( $s['pctr'] ), Wording::pct( $s['ctr'] ) ) . ( '' !== $q ? ' ' . sprintf( /* translators: %s: keyword. */ __( 'The biggest example is “%s”.', 'turtlereport' ), $q ) : '' ); } return __( 'Clicks, impressions and position each moved a little; no single change explains the drop. The keyword details show where the clicks went.', 'turtlereport' ); } /** * "The biggest loss is …" for impression patterns. * * @param array $s Signals. * @return string */ private static function top_text( array $s ) { if ( '' === self::lead( $s ) ) { return $s['top'] ? ' ' . __( 'The loss is spread across many searches rather than one or two.', 'turtlereport' ) : ''; } $t = $s['top']; return ' ' . sprintf( /* translators: 1: keyword, 2: impressions before, 3: impressions now, 4: share of lost impressions. */ __( 'The biggest loss is “%1$s” (%2$s → %3$s impressions, %4$s of the impressions lost).', 'turtlereport' ), $t['key'], Wording::num( $t['pi'] ), Wording::num( $t['i'] ), (int) $s['top_share'] . '%' ); } /** * The keyword that explains a real part of the loss (15%+), or ''. * * @param array $s Signals. * @return string */ private static function lead( array $s ) { return $s['top'] && (int) $s['top_share'] >= 15 ? (string) $s['top']['key'] : ''; } /** * CTR fell as well. * * @param array $s Signals. * @return string */ private static function ctr_also_text( array $s ) { return sprintf( /* translators: 1: CTR before, 2: CTR now. */ __( 'The click-through rate also fell (%1$s → %2$s), so even where it still appears, fewer people click.', 'turtlereport' ), Wording::pct( $s['pctr'] ), Wording::pct( $s['ctr'] ) ); } /** * The next checks for a pattern, naming the page and keyword. * * @param array $s Signals. * @param string $target Page URL, or '' for the site. * @return string[] */ private static function pattern_checks( array $s, $target ) { $q = self::lead( $s ); $where = '' !== $target ? Wording::page_ref( $target ) : __( 'the pages that lost clicks', 'turtlereport' ); $it = '' !== $target ? Wording::page_ref( $target ) : __( 'the site', 'turtlereport' ); switch ( $s['pattern'] ) { case 'ranking': $k = $s['slipped'] ? (string) $s['slipped'][0]['key'] : $q; return array( '' !== $k /* translators: 1: keyword, 2: page name. */ ? sprintf( __( 'Search “%1$s” in a private browser window and see which pages now rank above %2$s.', 'turtlereport' ), $k, $where ) /* translators: %s: page name. */ : sprintf( __( 'Search the main queries in a private browser window and see which pages now rank above %s.', 'turtlereport' ), $where ), /* translators: %s: page name. */ sprintf( __( 'Review recent changes to %s (title, headings, content, links, redirects) and compare it with the pages now ranking above it.', 'turtlereport' ), $where ), '' !== $target /* translators: %s: page name. */ ? sprintf( __( 'Check that Google still indexes %s and treats it as the main version of the page.', 'turtlereport' ), $where ) : __( 'Check Search Console for new manual actions, security issues or page errors.', 'turtlereport' ), ); case 'lost_searches': return array( '' !== $q /* translators: 1: "filter by this page", 2: keyword. */ ? sprintf( __( 'Compare the search performance of %1$s with the previous period to see which search queries it lost. The biggest is “%2$s”.', 'turtlereport' ), $it, $q ) /* translators: %s: page name. */ : sprintf( __( 'Compare the search performance of %s with the previous period to see which search queries it lost.', 'turtlereport' ), $it ), '' !== $q /* translators: %s: keyword. */ ? sprintf( __( 'Search “%s” and check whether another page on the same site now shows instead. Two pages aiming at one search can push each other out.', 'turtlereport' ), $q ) : __( 'Check whether another page on the same site now shows for those searches. Two pages aiming at one search can push each other out.', 'turtlereport' ), $s['ctr_also'] /* translators: %s: page name. */ ? sprintf( __( 'Check that %s still covers those searches in its title, headings and content, and review how it looks in Google, since fewer people click it too.', 'turtlereport' ), $it ) /* translators: %s: page name. */ : sprintf( __( 'Check that Google still indexes %s and that it still covers those searches in its title, headings and content. Look for anything removed or merged.', 'turtlereport' ), $it ), ); case 'demand': return array( '' !== $q /* translators: %s: keyword. */ ? sprintf( __( 'Compare the same weeks last year to see whether searches for “%s” always dip at this time.', 'turtlereport' ), $q ) : __( 'Compare the same weeks last year to see whether searches always dip at this time.', 'turtlereport' ), '' !== $q /* translators: %s: keyword. */ ? sprintf( __( 'Check Google Trends for “%s” to see whether interest fell everywhere, not just for this site.', 'turtlereport' ), $q ) : __( 'Check Google Trends for the main queries to see whether interest fell everywhere, not just for this site.', 'turtlereport' ), /* translators: %s: page name. */ sprintf( __( 'If demand didn\'t fall, check which search queries %s stopped appearing for.', 'turtlereport' ), $it ), ); case 'snippet': return array( '' !== $q /* translators: %s: keyword. */ ? sprintf( __( 'Search “%s” and note what now sits above the result: AI Overviews, ads, a map, videos or richer competitor results.', 'turtlereport' ), $q ) : __( 'Search the main queries and note what now sits above the result: AI Overviews, ads, a map, videos or richer competitor results.', 'turtlereport' ), /* translators: %s: page name. */ sprintf( __( 'Review the title and description Google shows for %s. Does it match the search and give a reason to click (price, location, offer)?', 'turtlereport' ), $where ), __( 'Check whether Google rewrote the title or shows an old date or price, and update the page title and description if so.', 'turtlereport' ), ); } return array( '' !== $q /* translators: %s: keyword. */ ? sprintf( __( 'Start with “%s”, the search query that lost the most clicks.', 'turtlereport' ), $q ) : __( 'Start with the search queries that lost the most clicks.', 'turtlereport' ), '' !== $target /* translators: %s: page name. */ ? sprintf( __( 'Check that Google still indexes %s and can show it.', 'turtlereport' ), $where ) : __( 'Check in Search Console whether fewer of the site\'s pages are indexed.', 'turtlereport' ), __( 'Compare the same weeks last year to rule out a seasonal dip.', 'turtlereport' ), ); } /** * One short next step for a pattern, for the issue list. The full checks stay in "Review issue". * * @param array $s Signals. * @param string $target Page URL, or '' for the site. * @return string */ private static function pattern_next( array $s, $target ) { $q = self::lead( $s ); $where = '' !== $target ? Wording::page_ref( $target ) : __( 'the site', 'turtlereport' ); switch ( $s['pattern'] ) { case 'ranking': $k = $s['slipped'] ? (string) $s['slipped'][0]['key'] : $q; return '' !== $k /* translators: 1: keyword, 2: page name. */ ? sprintf( __( 'Search “%1$s” and see which pages now rank above %2$s.', 'turtlereport' ), $k, $where ) /* translators: %s: page name. */ : sprintf( __( 'See which pages now rank above %s for its main keywords.', 'turtlereport' ), $where ); case 'lost_searches': /* translators: %s: page name. */ return sprintf( __( 'In Search Console, list the searches %s stopped appearing for.', 'turtlereport' ), $where ); case 'demand': return '' !== $q /* translators: %s: keyword. */ ? sprintf( __( 'Check whether searches for “%s” fell everywhere (Google Trends, same weeks last year).', 'turtlereport' ), $q ) : __( 'Check whether searches fell everywhere (Google Trends, same weeks last year).', 'turtlereport' ); case 'snippet': return '' !== $q /* translators: 1: keyword, 2: page name. */ ? sprintf( __( 'Search “%1$s” and check what now sits above %2$s in the results.', 'turtlereport' ), $q, $where ) /* translators: %s: page name. */ : sprintf( __( 'Check what now sits above %s in the search results.', 'turtlereport' ), $where ); } return '' !== $q /* translators: %s: keyword. */ ? sprintf( __( 'Start with “%s”, the keyword that lost the most clicks.', 'turtlereport' ), $q ) : __( 'Start with the keywords that lost the most clicks.', 'turtlereport' ); } /** * Clicks, impressions, CTR and position tiles. * * @param array $now Now. * @param array $prev Before. * @param array $s Signals. * @return array[] */ private static function search_metrics( array $now, array $prev, array $s ) { $dpos = $s['dpos']; if ( null === $dpos ) { $pos_change = __( 'Too few impressions to judge', 'turtlereport' ); $pos_tone = 'flat'; } elseif ( abs( $dpos ) < 0.5 ) { $pos_change = __( 'About the same', 'turtlereport' ); $pos_tone = 'flat'; } elseif ( $dpos < 0 ) { /* translators: %s: number of places. */ $pos_change = sprintf( __( 'Improved %s', 'turtlereport' ), number_format_i18n( abs( $dpos ), 1 ) ); $pos_tone = 'good'; } else { /* translators: %s: number of places. */ $pos_change = sprintf( __( 'Dropped %s', 'turtlereport' ), number_format_i18n( $dpos, 1 ) ); $pos_tone = 'bad'; } return array( Wording::metric( __( 'Google clicks', 'turtlereport' ), Wording::num( Wording::val( $now, array( 'clicks' ) ) ), $s['dc'] ), Wording::metric( __( 'Impressions', 'turtlereport' ), Wording::num( Wording::val( $now, array( 'impr' ) ) ), $s['di'] ), array( 'label' => __( 'CTR', 'turtlereport' ), 'value' => Wording::pct( $s['ctr'] ), 'change' => Wording::pts( $s['dpts'] ), 'tone' => abs( $s['dpts'] ) < 0.1 ? 'flat' : ( $s['dpts'] < 0 ? 'bad' : 'good' ), ), array( 'label' => __( 'Avg. position', 'turtlereport' ), 'value' => Wording::pos( Wording::val( $now, array( 'pos' ), null ) ), 'change' => $pos_change, 'tone' => $pos_tone, ), ); } /** * How much data stands behind a Search Console alert. * * @param array $prev Before. * @return array{level:string,text:string} */ private static function search_confidence( array $prev ) { $clicks = (float) Wording::val( $prev, array( 'clicks' ) ); $impr = (float) Wording::val( $prev, array( 'impr' ) ); $level = $impr >= 10000 ? 'strong' : Signals::confidence( $clicks, array( 300, 50 ) ); if ( 'early' === $level && $impr >= 2000 ) { // Few clicks but plenty of impressions: the visibility change is solid. $level = 'moderate'; } switch ( $level ) { case 'strong': /* translators: 1: clicks, 2: impressions. */ $text = sprintf( __( 'Based on %1$s clicks and %2$s impressions in the 28 days before: enough data to rely on.', 'turtlereport' ), Wording::num( $clicks ), Wording::num( $impr ) ); break; case 'moderate': /* translators: 1: clicks, 2: impressions. */ $text = sprintf( __( 'Based on %1$s clicks and %2$s impressions in the 28 days before: a clear change, but confirm it in the details.', 'turtlereport' ), Wording::num( $clicks ), Wording::num( $impr ) ); break; default: /* translators: %s: clicks. */ $text = sprintf( __( 'Based on only %s clicks in the 28 days before. Small numbers swing a lot, so treat this as an early signal.', 'turtlereport' ), Wording::num( $clicks ) ); } return array( 'level' => $level, 'text' => $text, ); } /** * No clicks at all. * * @param array $d Data. * @param Profile $site Site. * @return array */ public static function no_clicks( array $d, Profile $site ) { $prev = Wording::val( $d, array( 'prev', 'clicks' ) ); $impr = Wording::val( $d, array( 'now', 'impr' ) ); return array( 'kind' => __( 'No Google visits', 'turtlereport' ), 'headline' => __( 'No Google clicks in the last 28 days', 'turtlereport' ), 'url' => Wording::site_url( $site ), 'path' => __( 'All pages', 'turtlereport' ), 'title' => __( 'No Google clicks in the last 28 days', 'turtlereport' ), /* translators: %s: number of clicks. */ 'summary' => sprintf( __( 'Down from %s clicks in the 28 days before.', 'turtlereport' ), Wording::num( $prev ) ), 'metrics' => array( Wording::metric( __( 'Google clicks', 'turtlereport' ), '0', -100 ), Wording::metric( __( 'Impressions', 'turtlereport' ), Wording::num( $impr ), Rules::change( $impr, Wording::val( $d, array( 'prev', 'impr' ) ) ) ), ), 'insights' => array( /* translators: %s: clicks. */ sprintf( __( 'Search Console recorded no clicks at all, after %s in the 28 days before.', 'turtlereport' ), Wording::num( $prev ) ), $impr > 0 /* translators: %s: impressions. */ ? sprintf( __( 'Google still showed the site %s times but nobody clicked, which is unusual: check how the result looks in Google.', 'turtlereport' ), Wording::num( $impr ) ) : __( 'There were no impressions either: Google didn\'t show the site at all, which points to the site being blocked, removed from the index or offline rather than to rankings.', 'turtlereport' ), ), 'next' => $impr > 0 ? __( 'Search for the site in Google and check how its result looks now.', 'turtlereport' ) : __( 'Check the site loads and isn\'t blocked from Google (noindex or robots.txt).', 'turtlereport' ), 'checks' => array( __( 'Open the website and check it loads for visitors: no maintenance page, error or login wall.', 'turtlereport' ), __( 'Check that the site isn\'t blocked from Google (robots.txt or a "noindex" setting), especially after an update, move or new plugin.', 'turtlereport' ), __( 'Check Search Console for manual actions or security issues. If the site moved domain or to https, check the connected property is the new one.', 'turtlereport' ), ), 'facts' => array( Wording::fact( __( 'Google clicks', 'turtlereport' ), $prev, 0 ), Wording::fact( __( 'Impressions', 'turtlereport' ), Wording::val( $d, array( 'prev', 'impr' ) ), $impr ), ), ); } /** * Keywords that slipped. * * @param array $d Data. * @param Profile $site Site. * @return array */ public static function rankings( array $d, Profile $site ) { $count = (int) Wording::val( $d, array( 'count' ) ); $lost = Wording::val( $d, array( 'lost' ) ); $kw = isset( $d['keywords'] ) ? (array) $d['keywords'] : array(); $rows = array(); $drop = 0.0; foreach ( $kw as $row ) { $rows[] = array( (string) $row['key'], Wording::pos( $row['ppos'] ), Wording::pos( $row['pos'] ), Wording::num( $row['pc'] ), Wording::num( $row['c'] ) ); $drop += $row['pos'] - $row['ppos']; } $avg = $kw ? $drop / count( $kw ) : 0; $first = $kw ? $kw[0] : null; $insights = array( sprintf( /* translators: 1: number of keywords, 2: clicks. */ _n( '%1$d keyword that brought clicks moved down 3 or more places, costing about %2$s clicks.', '%1$d keywords that brought clicks moved down 3 or more places, costing about %2$s clicks.', $count, 'turtlereport' ), $count, Wording::num( $lost ) ), __( 'The site\'s total clicks haven\'t dropped enough to show it yet, so this is an early warning: rankings usually move before traffic does.', 'turtlereport' ), ); if ( $first ) { $insights[] = sprintf( /* translators: 1: keyword, 2: position before, 3: position now. */ __( 'The biggest is “%1$s” (position %2$s → %3$s).', 'turtlereport' ), $first['key'], Wording::pos( $first['ppos'] ), Wording::pos( $first['pos'] ) ); } return array( 'kind' => __( 'Lower Google rankings', 'turtlereport' ), /* translators: %d: number of keywords. */ 'headline' => sprintf( _n( '%d search query lost visibility', '%d search queries lost visibility', $count, 'turtlereport' ), $count ), 'url' => Wording::site_url( $site ), 'path' => __( 'Several pages', 'turtlereport' ), /* translators: %d: number of keywords. */ 'title' => sprintf( _n( '%d search query lost visibility', '%d search queries lost visibility', $count, 'turtlereport' ), $count ), 'summary' => $lost > 0 /* translators: %s: number of clicks. */ ? sprintf( __( 'About %s clicks lost so far; the site\'s total clicks have held up.', 'turtlereport' ), Wording::num( $lost ) ) : __( 'Positions fell, though clicks haven\'t dropped much yet.', 'turtlereport' ), 'metrics' => array( array( 'label' => __( 'Keywords slipped', 'turtlereport' ), 'value' => number_format_i18n( $count ), 'change' => __( '3+ places each', 'turtlereport' ), 'tone' => 'bad', ), array( 'label' => __( 'Clicks lost', 'turtlereport' ), 'value' => Wording::num( $lost ), 'change' => __( 'on these keywords', 'turtlereport' ), 'tone' => 'bad', ), array( 'label' => __( 'Average drop', 'turtlereport' ), /* translators: %s: number of places. */ 'value' => sprintf( __( '%s places', 'turtlereport' ), number_format_i18n( $avg, 1 ) ), 'change' => '', 'tone' => 'bad', ), ), 'insights' => $insights, 'next' => $first /* translators: %s: keyword. */ ? sprintf( __( 'Search “%s” and see which pages now rank above the site.', 'turtlereport' ), $first['key'] ) : __( 'Search the keywords that slipped and see which pages now rank above the site.', 'turtlereport' ), 'checks' => array( $first /* translators: %s: keyword. */ ? sprintf( __( 'Search “%s” in a private browser window and see which pages now rank above the site, and what they offer.', 'turtlereport' ), $first['key'] ) : __( 'Search these queries in a private browser window and see which pages now rank above the site.', 'turtlereport' ), __( 'Find the page that ranks for each query and review what changed on it recently.', 'turtlereport' ), __( 'If several of these queries lead to the same page, improve that page first: it has the most to gain.', 'turtlereport' ), ), 'confidence' => array( 'level' => $lost >= 100 ? 'strong' : 'moderate', 'text' => __( 'Positions come from Search Console and only count keywords that brought clicks or many impressions before.', 'turtlereport' ), ), 'tables' => $rows ? array( array( 'title' => __( 'Keywords that moved down', 'turtlereport' ), 'cols' => array( __( 'Keyword', 'turtlereport' ), __( 'Position before', 'turtlereport' ), __( 'Position now', 'turtlereport' ), __( 'Clicks before', 'turtlereport' ), __( 'Clicks now', 'turtlereport' ) ), 'rows' => $rows, ), ) : array(), ); } /** * Average position before or now, from signals' rows-free totals. * * @param array $s Signals. * @param string $when prev|now. * @return string */ private static function pos_from( array $s, $when ) { return isset( $s[ $when . '_pos' ] ) ? Wording::pos( $s[ $when . '_pos' ] ) : '–'; } /** * Clicks, impressions, CTR and position, before and now. * * @param array $now Now. * @param array $prev Before. * @return array[] */ private static function search_facts( array $now, array $prev ) { $ctr = Wording::val( $now, array( 'impr' ) ) > 0 ? Wording::val( $now, array( 'clicks' ) ) / Wording::val( $now, array( 'impr' ) ) : 0; $pctr = Wording::val( $prev, array( 'impr' ) ) > 0 ? Wording::val( $prev, array( 'clicks' ) ) / Wording::val( $prev, array( 'impr' ) ) : 0; return array( Wording::fact( __( 'Google clicks', 'turtlereport' ), Wording::val( $prev, array( 'clicks' ) ), Wording::val( $now, array( 'clicks' ) ) ), Wording::fact( __( 'Impressions', 'turtlereport' ), Wording::val( $prev, array( 'impr' ) ), Wording::val( $now, array( 'impr' ) ) ), Wording::fact( __( 'Click-through rate', 'turtlereport' ), $pctr, $ctr, 'ratio' ), Wording::fact( __( 'Average position', 'turtlereport' ), Wording::val( $prev, array( 'pos' ), null ), Wording::val( $now, array( 'pos' ), null ), 'position' ), ); } /** * A table of joined Search Console rows. * * @param string $title Title. * @param string $col First column name. * @param array[] $rows Rows with key, c, pc, i, pi, pos, ppos. * @param bool $is_page Whether keys are page URLs. * @return array|null */ private static function row_table( $title, $col, array $rows, $is_page ) { if ( ! $rows ) { return null; } $out = array(); foreach ( $rows as $row ) { $out[] = array( $is_page ? Wording::path( (string) $row['key'] ) : (string) $row['key'], Wording::num( $row['pc'] ) . ' → ' . Wording::num( $row['c'] ), Wording::change_text( $row['c'], $row['pc'] ), Wording::num( $row['pi'] ) . ' → ' . Wording::num( $row['i'] ), $row['pi'] && $row['i'] ? Wording::pos( $row['ppos'] ) . ' → ' . Wording::pos( $row['pos'] ) : ( $row['pi'] ? __( 'no longer shown', 'turtlereport' ) : __( 'new', 'turtlereport' ) ), ); } return array( 'title' => $title, 'cols' => array( $col, __( 'Clicks', 'turtlereport' ), __( 'Change', 'turtlereport' ), __( 'Impressions', 'turtlereport' ), __( 'Position', 'turtlereport' ) ), 'rows' => $out, ); } /** * A page name for a heading: /water-blasting-cost/ → "Water blasting cost page". * * @param string $url URL or path. * @return string */ public static function page_title( $url ) { $path = (string) strtok( Wording::path( (string) $url ), '?' ); $bits = array_values( array_filter( explode( '/', $path ), static function ( $part ) { return '' !== $part; } ) ); if ( ! $bits ) { return __( 'Home page', 'turtlereport' ); } $slug = preg_replace( '/\.(html?|php|aspx?)$/i', '', rawurldecode( (string) end( $bits ) ) ); $name = trim( preg_replace( '/[-_]+/', ' ', $slug ) ); if ( '' === $name ) { return $path; } /* translators: %s: page name made from its address, e.g. "Pricing". */ return sprintf( _x( '%s page', 'page title derived from a URL', 'turtlereport' ), Text::upper( Text::substr( $name, 0, 1 ) ) . Text::substr( $name, 1 ) ); } }