, 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; } }