*/
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, ); } }