, stats:array }
*/
public static function run( array $in ) {
$in += array( 'template_page' => false, 'title' => '', 'desc' => '', 'slug' => '', 'focus' => '', 'content' => '', 'post_title' => '', 'noindex' => false, 'type' => 'post', 'has_image' => false );
$html = (string) $in['content'];
$text = sxseo_plain( $html );
$words = sxseo_words( $text );
$wc = count( $words );
$focus = trim( sxseo_lower( $in['focus'] ) );
$checks = array();
$add = function ( $id, $status, $msg, $weight = 1 ) use ( &$checks ) {
$checks[] = array( 'id' => $id, 'status' => $status, 'msg' => $msg, 'weight' => $weight );
};
if ( $in['noindex'] ) {
$add( 'noindex', 'bad', 'This page is set to noindex, so search engines will not list it.', 3 );
}
// Title ---------------------------------------------------------------------------------
$tl = sxseo_len( $in['title'] );
if ( 0 === $tl ) {
$add( 'title_len', 'bad', 'There is no SEO title.', 2 );
} elseif ( $tl < 20 ) {
$add( 'title_len', 'ok', "The SEO title is short ($tl characters). Aim for 30–60 so it uses the space in results.", 2 );
} elseif ( $tl > 62 ) {
$add( 'title_len', 'ok', "The SEO title is long ($tl characters) and will be cut off in results. Aim for 30–60.", 2 );
} else {
$add( 'title_len', 'good', "The SEO title is a good length ($tl characters).", 2 );
}
// Description -----------------------------------------------------------------------------
$dl = sxseo_len( $in['desc'] );
if ( 0 === $dl ) {
$add( 'desc_len', 'bad', 'There is no meta description.', 2 );
} elseif ( $dl < 90 ) {
$add( 'desc_len', 'ok', "The meta description is short ($dl characters). Aim for 120–160.", 2 );
} elseif ( $dl > 165 ) {
$add( 'desc_len', 'ok', "The meta description is long ($dl characters) and will be cut off. Aim for 120–160.", 2 );
} else {
$add( 'desc_len', 'good', "The meta description is a good length ($dl characters).", 2 );
}
// Content length --------------------------------------------------------------------------
$min = 'page' === $in['type'] ? 200 : 300;
if ( $in['template_page'] ) {
$add( 'content_len', 'good', 'This page is built from its theme template, so its text is not analysed here.', 1 );
$wc = 0; // skip the text-based checks below
} elseif ( $wc >= $min * 2 ) {
$add( 'content_len', 'good', "Plenty of content ($wc words).", 2 );
} elseif ( $wc >= $min ) {
$add( 'content_len', 'good', "Enough content ($wc words). Longer, genuinely useful pages tend to rank better.", 2 );
} elseif ( $wc >= $min / 2 ) {
$add( 'content_len', 'ok', "The content is thin ($wc words). Aim for at least $min words.", 2 );
} else {
$add( 'content_len', 'bad', "Very little content ($wc words). Pages this short rarely rank. Aim for at least $min words.", 2 );
}
// Keyphrase --------------------------------------------------------------------------------
if ( '' === $focus ) {
$add( 'focus_set', 'bad', 'No focus keyphrase. Choose the phrase people would search for to find this page.', 2 );
} else {
$add( 'focus_set', 'good', ! empty( $in['focus_auto'] ) ? 'The focus keyphrase "' . $focus . '" was chosen automatically from the title. Set your own if people search for something different.' : 'A focus keyphrase is set.', 1 );
$has = function ( $haystack ) use ( $focus ) {
return self::contains( $haystack, $focus );
};
$add( 'focus_title', $has( $in['title'] ) ? 'good' : 'bad', $has( $in['title'] ) ? 'The keyphrase is in the SEO title.' : 'The keyphrase is not in the SEO title.', 2 );
$add( 'focus_desc', $has( $in['desc'] ) ? 'good' : 'ok', $has( $in['desc'] ) ? 'The keyphrase is in the meta description.' : 'Add the keyphrase to the meta description.', 1 );
$add( 'focus_slug', $has( str_replace( '-', ' ', $in['slug'] ) ) ? 'good' : 'ok', $has( str_replace( '-', ' ', $in['slug'] ) ) ? 'The keyphrase is in the URL.' : 'The keyphrase is not in the URL.', 1 );
$first = sxseo_sub( $text, 0, max( 200, (int) ( sxseo_len( $text ) * 0.15 ) ) );
$add( 'focus_intro', $has( $first ) ? 'good' : 'ok', $has( $first ) ? 'The keyphrase appears in the opening.' : 'Use the keyphrase in the first paragraph.', 1 );
$heads = '';
if ( preg_match_all( '#]*>(.*?)#is', $html, $m ) ) {
$heads = implode( ' ', $m[1] );
}
$add( 'focus_heading', $has( sxseo_plain( $heads ) ) ? 'good' : 'ok', $has( sxseo_plain( $heads ) ) ? 'The keyphrase is in a subheading.' : 'Use the keyphrase (or a close variation) in at least one subheading.', 1 );
if ( $wc >= 100 && empty( $in['focus_auto'] ) ) {
$count = self::count_phrase( $text, $focus );
$density = round( 100 * $count * max( 1, count( sxseo_words( $focus ) ) ) / max( 1, $wc ), 1 );
if ( 0 === $count ) {
$add( 'focus_density', 'bad', 'The keyphrase never appears in the text.', 2 );
} elseif ( $density > 3.5 ) {
$add( 'focus_density', 'bad', "The keyphrase is overused ($density%). Search engines penalise stuffing.", 2 );
} elseif ( $density < 0.4 ) {
$add( 'focus_density', 'ok', "The keyphrase appears only $count time(s) ($density%). Use it a little more, naturally.", 1 );
} else {
$add( 'focus_density', 'good', "Keyphrase density is healthy ($density%).", 1 );
}
}
}
// Links --------------------------------------------------------------------------------------
$internal = 0;
$external = 0;
if ( preg_match_all( '#]*href=["\']([^"\']+)["\']#i', $html, $m ) ) {
foreach ( $m[1] as $href ) {
if ( preg_match( '#^(mailto:|tel:|\#|javascript:)#i', $href ) ) {
continue;
}
if ( 0 === strpos( $href, '/' ) && 0 !== strpos( $href, '//' ) || false !== stripos( $href, (string) sxseo_home_host() ) ) {
$internal++;
} elseif ( preg_match( '#^(https?:)?//#i', $href ) ) {
$external++;
}
}
}
if ( $wc >= 150 ) {
$add( 'links_int', $internal > 0 ? 'good' : 'ok', $internal > 0 ? "Links to other pages on your site ($internal)." : 'No links to other pages on your site. Internal links help search engines find and rank your content.', 1 );
$add( 'links_ext', $external > 0 ? 'good' : 'ok', $external > 0 ? "Links out to other sites ($external)." : 'No outbound links. Linking to a trusted source adds credibility.', 1 );
}
// Images ----------------------------------------------------------------------------------------
$imgs = 0;
$noalt = 0;
if ( preg_match_all( '#
]*>#i', $html, $m ) ) {
foreach ( $m[0] as $tag ) {
$imgs++;
if ( ! preg_match( '#\balt=["\'][^"\']+["\']#i', $tag ) ) {
$noalt++;
}
}
}
if ( $imgs || $in['has_image'] ) {
$add( 'img', 'good', 'The page has an image.', 1 );
} else {
$add( 'img', 'ok', 'No images. A relevant image improves engagement and social sharing.', 1 );
}
if ( $imgs ) {
$add( 'img_alt', 0 === $noalt ? 'good' : 'ok', 0 === $noalt ? 'All images have alt text.' : "$noalt image(s) have no alt text (Sentrixa SEO fills these in automatically on the page).", 1 );
}
// Headings ------------------------------------------------------------------------------------
if ( preg_match( '#]#i', $html ) ) {
$add( 'h1', 'ok', 'The content contains its own H1. The page title is already the H1; use H2 and below inside the content.', 1 );
}
if ( $wc >= 300 ) {
$add( 'subheadings', preg_match( '#]#i', $html ) ? 'good' : 'ok', preg_match( '#]#i', $html ) ? 'The content is broken up with subheadings.' : 'Long text with no subheadings is hard to scan. Add H2 subheadings.', 1 );
}
// URL ---------------------------------------------------------------------------------------------
if ( '' !== $in['slug'] ) {
$sl = sxseo_len( $in['slug'] );
$add( 'slug', $sl <= 75 ? 'good' : 'ok', $sl <= 75 ? 'The URL is a sensible length.' : 'The URL is very long. Shorter URLs are easier to share and rank slightly better.', 1 );
}
// Readability -----------------------------------------------------------------------------------
if ( $wc >= 120 ) {
$sentences = preg_split( '/(?<=[.!?])\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY );
$sc = max( 1, count( $sentences ) );
$syll = 0;
foreach ( $words as $w ) {
$syll += max( 1, preg_match_all( '/[aeiouy]+/i', $w ) - ( preg_match( '/[^aeiouy]e$/i', $w ) ? 1 : 0 ) );
}
$flesch = 206.835 - 1.015 * ( $wc / $sc ) - 84.6 * ( $syll / $wc );
$long = 0;
foreach ( $sentences as $s ) {
if ( count( sxseo_words( $s ) ) > 25 ) {
$long++;
}
}
$longpct = round( 100 * $long / $sc );
$flesch = (int) round( $flesch );
if ( $flesch >= 60 ) {
$add( 'readability', 'good', "Easy to read (Flesch score $flesch).", 1 );
} elseif ( $flesch >= 40 ) {
$add( 'readability', 'ok', "Fairly hard to read (Flesch score $flesch). Shorter sentences and simpler words help.", 1 );
} else {
$add( 'readability', 'ok', "Hard to read (Flesch score $flesch). Shorten sentences and simplify wording.", 1 );
}
$add( 'sentences', $longpct <= 25 ? 'good' : 'ok', $longpct <= 25 ? 'Sentence length is varied and manageable.' : "$longpct% of sentences are over 25 words. Break some up.", 1 );
}
if ( ! empty( $in['focus_auto'] ) ) { // an automatic keyphrase is derived from the title, so checking the title against it proves nothing
$checks = array_values( array_filter( $checks, function ( $c ) {
return ! in_array( $c['id'], array( 'focus_title', 'focus_desc', 'focus_slug', 'focus_intro', 'focus_heading', 'focus_density' ), true );
} ) );
}
if ( $in['template_page'] ) { // text-level checks are meaningless for a page whose body comes from the theme
$checks = array_values( array_filter( $checks, function ( $c ) {
return ! in_array( $c['id'], array( 'focus_intro', 'focus_heading', 'img', 'subheadings' ), true );
} ) );
}
// Score ------------------------------------------------------------------------------------------
$got = 0;
$max = 0;
foreach ( $checks as $c ) {
$max += $c['weight'];
$got += $c['weight'] * ( 'good' === $c['status'] ? 1 : ( 'ok' === $c['status'] ? 0.5 : 0 ) );
}
return array(
'score' => $max ? (int) round( 100 * $got / $max ) : 0,
'checks' => $checks,
'stats' => array( 'words' => $wc, 'internal' => $internal, 'external' => $external, 'images' => $imgs ),
);
}
private static function norm( $s ) {
$s = sxseo_lower( wp_strip_all_tags( (string) $s ) );
$s = preg_replace( '/[^\p{L}\p{N}\s]+/u', ' ', $s );
return trim( preg_replace( '/\s+/u', ' ', $s ) );
}
private static function stem( $w ) {
return preg_replace( '/(ing|ed|es|s)$/u', '', $w );
}
/** Phrase present as written, or every one of its words present (singular/plural tolerant). */
private static function contains( $haystack, $phrase ) {
$h = ' ' . self::norm( $haystack ) . ' ';
$p = self::norm( $phrase );
if ( '' === $p ) {
return false;
}
if ( false !== strpos( $h, ' ' . $p . ' ' ) ) {
return true;
}
$hay = array_map( array( __CLASS__, 'stem' ), explode( ' ', trim( $h ) ) );
foreach ( explode( ' ', $p ) as $w ) {
if ( ! in_array( self::stem( $w ), $hay, true ) ) {
return false;
}
}
return true;
}
private static function count_phrase( $text, $phrase ) {
$t = ' ' . self::norm( $text ) . ' ';
$p = ' ' . self::norm( $phrase ) . ' ';
return $p === ' ' ? 0 : substr_count( $t, $p );
}
/** Analyse a saved post using its effective values; caches the score in post meta. */
public static function for_post( $post, $force = false ) {
$post = get_post( $post );
$ctx = SXSEO_Context::for_post( $post );
$res = self::run(
array(
'title' => $ctx['title'],
'desc' => $ctx['desc'],
'slug' => $post->post_name,
'focus' => $ctx['focus'],
'focus_auto' => $ctx['focus_auto'],
'content' => apply_filters( 'the_content', $post->post_content ),
'post_title' => $post->post_title,
'noindex' => in_array( 'noindex', $ctx['robots'], true ) && 'publish' === $post->post_status,
'template_page' => '' === trim( $post->post_content ),
'type' => $post->post_type,
'has_image' => (bool) get_post_thumbnail_id( $post ),
)
);
update_post_meta( $post->ID, '_sxseo_score', $res['score'] );
return $res;
}
}