, word_count: int} */ public static function analyze( WP_Post $post ): array { $presentation = Plugin::instance()->presentation( Context::for_post( $post ) ); $title = $presentation->title(); $description = $presentation->description(); $html = (string) $post->post_content; $text = trim( (string) preg_replace( '/\s+/u', ' ', wp_strip_all_tags( $html ) ) ); $words = '' === $text ? 0 : count( preg_split( '/\s+/u', $text ) ?: array() ); // phpcs:ignore Universal.Operators.DisallowShortTernary.Found $keywords = array_values( array_filter( (array) Meta::post( $post->ID, 'focus_keywords' ), 'is_string' ) ); $keyword = mb_strtolower( (string) ( $keywords[0] ?? '' ) ); $checks = array(); $add = static function ( string $id, string $status, string $message ) use ( &$checks ): void { $checks[] = array( 'id' => $id, 'status' => $status, 'message' => $message, ); }; $title_length = mb_strlen( $title ); $add( 'title-length', $title_length >= 30 && $title_length <= 60 ? 'good' : 'ok', /* translators: %d: number of characters. */ sprintf( __( 'The SEO title is %d characters (30–60 is a good range).', 'wpinterface-seo' ), $title_length ) ); $description_length = mb_strlen( $description ); if ( 0 === $description_length ) { $add( 'description', 'bad', __( 'There is no meta description.', 'wpinterface-seo' ) ); } else { $add( 'description', $description_length >= 120 && $description_length <= 160 ? 'good' : 'ok', /* translators: %d: number of characters. */ sprintf( __( 'The meta description is %d characters (120–160 is a good range).', 'wpinterface-seo' ), $description_length ) ); } if ( '' === $keyword ) { $add( 'keyword', 'bad', __( 'No focus keyword is set.', 'wpinterface-seo' ) ); } else { $contains = static fn( string $haystack ): bool => false !== mb_strpos( mb_strtolower( $haystack ), $keyword ); $intro = mb_substr( $text, 0, 600 ); $add( 'keyword-title', $contains( $title ) ? 'good' : 'bad', $contains( $title ) ? __( 'The focus keyword is in the SEO title.', 'wpinterface-seo' ) : __( 'Add the focus keyword to the SEO title.', 'wpinterface-seo' ) ); $add( 'keyword-description', $contains( $description ) ? 'good' : 'ok', $contains( $description ) ? __( 'The focus keyword is in the meta description.', 'wpinterface-seo' ) : __( 'Add the focus keyword to the meta description.', 'wpinterface-seo' ) ); $add( 'keyword-intro', $contains( $intro ) ? 'good' : 'ok', $contains( $intro ) ? __( 'The focus keyword appears at the start of the content.', 'wpinterface-seo' ) : __( 'Use the focus keyword near the start of the content.', 'wpinterface-seo' ) ); // Slugs have no hyphens left, so "pour-over coffee" must match "pour over coffee". $in_slug = false !== mb_strpos( str_replace( array( '-', '_' ), ' ', urldecode( $post->post_name ) ), str_replace( array( '-', '_' ), ' ', $keyword ) ); $add( 'keyword-slug', $in_slug ? 'good' : 'ok', $in_slug ? __( 'The focus keyword is in the URL.', 'wpinterface-seo' ) : __( 'Consider adding the focus keyword to the URL.', 'wpinterface-seo' ) ); } $add( 'length', $words >= 300 ? 'good' : ( $words >= 150 ? 'ok' : 'bad' ), /* translators: %d: number of words. */ sprintf( __( 'The content has %d words (300 or more is recommended).', 'wpinterface-seo' ), $words ) ); $images = substr_count( strtolower( $html ), ']*\balt\s*=\s*["\'][^"\']+["\'])[^>]*>/i', $html ); if ( $images > 0 ) { $add( 'image-alt', $missing_alt ? 'ok' : 'good', $missing_alt /* translators: %d: number of images. */ ? sprintf( __( '%d images have no alt text.', 'wpinterface-seo' ), (int) $missing_alt ) : __( 'All images have alt text.', 'wpinterface-seo' ) ); } $home = (string) wp_parse_url( home_url(), PHP_URL_HOST ); $internal = 0; if ( preg_match_all( '/]*href=["\']([^"\']+)["\']/i', $html, $matches ) ) { foreach ( $matches[1] as $href ) { $host = wp_parse_url( $href, PHP_URL_HOST ); if ( null === $host || 0 === strcasecmp( (string) $host, $home ) ) { ++$internal; } } } $add( 'internal-links', $internal ? 'good' : 'ok', $internal ? __( 'The content links to other pages on the site.', 'wpinterface-seo' ) : __( 'Add links to related pages on your site.', 'wpinterface-seo' ) ); if ( ! $presentation->is_indexable() ) { $add( 'indexable', 'bad', __( 'This page is hidden from search engines (noindex).', 'wpinterface-seo' ) ); } $points = array( 'good' => 100, 'ok' => 50, 'bad' => 0, ); $score = (int) round( array_sum( array_map( static fn( array $check ): int => $points[ $check['status'] ] ?? 0, $checks ) ) / max( 1, count( $checks ) ) ); return array( 'score' => $score, 'checks' => $checks, 'word_count' => $words, ); } }