*/ public static function analyze( array $input ): array { $keyphrase = trim( (string) ( $input['keyphrase'] ?? '' ) ); $seo_title = trim( (string) ( $input['seo_title'] ?? '' ) ); $description = trim( (string) ( $input['description'] ?? '' ) ); $post_title = trim( (string) ( $input['post_title'] ?? '' ) ); $content = (string) ( $input['content'] ?? '' ); $slug = trim( (string) ( $input['slug'] ?? '' ) ); // The title shown in SERPs: the custom SEO title, else the post title. $effective_title = '' !== $seo_title ? $seo_title : $post_title; $checks = array(); $checks[] = self::keyphrase_check( 'keyphrase_in_title', __( 'Focus keyphrase in the SEO title', 'acez-seo' ), $keyphrase, $effective_title, __( 'Add the phrase to the title — it\'s the single strongest signal.', 'acez-seo' ) ); $checks[] = self::keyphrase_check( 'keyphrase_in_description', __( 'Focus keyphrase in the meta description', 'acez-seo' ), $keyphrase, $description, __( 'A description containing the phrase gets bolded in results — it raises CTR.', 'acez-seo' ) ); $checks[] = self::keyphrase_check( 'keyphrase_in_h1', __( 'Focus keyphrase in the heading (post title)', 'acez-seo' ), $keyphrase, $post_title, __( 'The post title is usually the page\'s H1 heading.', 'acez-seo' ) ); $checks[] = self::keyphrase_check( 'keyphrase_in_first_paragraph', __( 'Focus keyphrase in the first paragraph', 'acez-seo' ), $keyphrase, self::first_paragraph( $content ), __( 'Confirm the topic right away — for both the reader and the search engine.', 'acez-seo' ) ); $checks[] = self::slug_check( $keyphrase, $slug ); $checks[] = self::length_check( 'title_length', __( 'SEO title length', 'acez-seo' ), $effective_title, self::TITLE_MIN, self::TITLE_MAX ); $checks[] = self::length_check( 'description_length', __( 'Meta description length', 'acez-seo' ), $description, self::DESC_MIN, self::DESC_MAX ); $checks[] = self::word_count_check( $content ); $checks[] = self::image_alt_check( $content ); return $checks; } /** * @return array{id: string, label: string, status: string, detail: string} */ private static function keyphrase_check( string $id, string $label, string $keyphrase, string $haystack, string $hint ): array { if ( '' === $keyphrase ) { return self::check( $id, $label, 'neutral', __( 'Set a focus keyphrase to enable this check.', 'acez-seo' ) ); } $found = '' !== $haystack && false !== mb_stripos( wp_strip_all_tags( $haystack ), $keyphrase ); return self::check( $id, $label, $found ? 'good' : 'warn', $found ? __( 'The phrase is present (literal match).', 'acez-seo' ) : $hint ); } /** * @return array{id: string, label: string, status: string, detail: string} */ private static function slug_check( string $keyphrase, string $slug ): array { $label = __( 'Focus keyphrase in the URL (slug)', 'acez-seo' ); if ( '' === $keyphrase ) { return self::check( 'keyphrase_in_slug', $label, 'neutral', __( 'Set a focus keyphrase to enable this check.', 'acez-seo' ) ); } if ( '' === $slug ) { return self::check( 'keyphrase_in_slug', $label, 'neutral', __( 'The slug will be created when the post is saved.', 'acez-seo' ) ); } $found = str_contains( $slug, sanitize_title( $keyphrase ) ); return self::check( 'keyphrase_in_slug', $label, $found ? 'good' : 'warn', $found ? __( 'The phrase is present (literal match).', 'acez-seo' ) : __( 'A short URL containing the phrase is more readable in results.', 'acez-seo' ) ); } /** * @return array{id: string, label: string, status: string, detail: string} */ private static function length_check( string $id, string $label, string $text, int $min, int $max ): array { $length = mb_strlen( wp_strip_all_tags( $text ) ); if ( 0 === $length ) { return self::check( $id, $label, 'neutral', __( 'The field is empty — a template/fallback will be used.', 'acez-seo' ) ); } if ( $length >= $min && $length <= $max ) { return self::check( $id, $label, 'good', sprintf( /* translators: 1: current length, 2: min, 3: max */ __( '%1$d characters — within the recommended range of %2$d–%3$d.', 'acez-seo' ), $length, $min, $max ) ); } return self::check( $id, $label, 'warn', sprintf( $length < $min /* translators: 1: current length, 2: min, 3: max */ ? __( '%1$d characters — too short (recommended %2$d–%3$d).', 'acez-seo' ) /* translators: 1: current length, 2: min, 3: max */ : __( '%1$d characters — too long, the result may get truncated (recommended %2$d–%3$d).', 'acez-seo' ), $length, $min, $max ) ); } /** * @return array{id: string, label: string, status: string, detail: string} */ private static function word_count_check( string $content ): array { $label = __( 'Content length', 'acez-seo' ); $words = self::word_count( $content ); if ( 0 === $words ) { return self::check( 'word_count', $label, 'neutral', __( 'No content to analyze.', 'acez-seo' ) ); } if ( $words >= self::WORDS_MIN ) { return self::check( 'word_count', $label, 'good', sprintf( /* translators: %d: word count */ __( '%d words — enough to cover the topic thoroughly.', 'acez-seo' ), $words ) ); } return self::check( 'word_count', $label, 'warn', sprintf( /* translators: 1: word count, 2: recommended minimum */ __( '%1$d words — short content rarely covers a topic thoroughly (recommended min. %2$d).', 'acez-seo' ), $words, self::WORDS_MIN ) ); } /** * @return array{id: string, label: string, status: string, detail: string} */ private static function image_alt_check( string $content ): array { $label = __( 'Image alt texts', 'acez-seo' ); preg_match_all( '/]*>/i', $content, $matches ); $images = $matches[0]; if ( array() === $images ) { return self::check( 'image_alts', $label, 'neutral', __( 'No images in the content.', 'acez-seo' ) ); } $missing = 0; foreach ( $images as $tag ) { if ( 1 !== preg_match( '/\balt\s*=\s*("[^"]+"|\'[^\']+\')/i', $tag ) ) { $missing++; } } if ( 0 === $missing ) { return self::check( 'image_alts', $label, 'good', sprintf( /* translators: %d: image count */ __( 'All images (%d) have alt text.', 'acez-seo' ), count( $images ) ) ); } return self::check( 'image_alts', $label, 'warn', sprintf( /* translators: 1: images without alt, 2: total images */ __( '%1$d of %2$d images have no alt text.', 'acez-seo' ), $missing, count( $images ) ) ); } private static function first_paragraph( string $content ): string { if ( 1 === preg_match( '/]*>(.*?)<\/p>/is', $content, $m ) ) { return (string) $m[1]; } // No markup (plain text / classic textarea) — first 40 words. $words = preg_split( '/\s+/u', trim( wp_strip_all_tags( $content ) ), 41 ) ?: array(); return implode( ' ', array_slice( $words, 0, 40 ) ); } private static function word_count( string $content ): int { $text = trim( wp_strip_all_tags( $content ) ); if ( '' === $text ) { return 0; } return count( (array) preg_split( '/\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY ) ); } /** * @return array{id: string, label: string, status: string, detail: string} */ private static function check( string $id, string $label, string $status, string $detail ): array { return array( 'id' => $id, 'label' => $label, 'status' => $status, 'detail' => $detail, ); } }