'

'.__('Good: You have power words in your title.', 'siteseo').'

', 'impact' => 'good', 'title'=>'Power Word in title' ]; } else{ $analyzes['power_words'] = [ 'desc' => '

'.__('Consider adding power words to your title for better impact.', 'siteseo').'

', 'impact' => 'low', 'title'=> __('Power Word in title', 'siteseo') ]; } return $analyzes; } static function analyze_title_number($analyzes, $data){ if(!empty($data['number_found'])){ $analyzes['number_found'] = [ /* translators: %s is the number found in the title */ 'desc' => '

'.sprintf(__('Good: Your title contains the number "%s".', 'siteseo'), $data['number_found']).'

', 'impact' => 'good', 'title' => 'Number in title' ]; } else{ $analyzes['number_found'] = [ 'desc' => '

'.__('Consider adding a number to your title to improve CTR.', 'siteseo').'

', 'impact' => 'low', 'title' => 'Number in title' ]; } return $analyzes; } static function analyze_passive_voice($analyzes, $data){ //$analyzes['title'] ='Passive Voice'; if(empty($data['passive_voice'])){ $analyzes['passive_voice'] = [ 'desc' => '

'.__('Great: No passive voice detected.', 'siteseo').'

', 'impact' => 'good', 'title' => 'Passive Voice' ]; } else { $percentage = round(($data['passive_voice']['passive_sentences'] * 100) / $data['passive_voice']['total_sentences']); if($percentage <= 10){ $analyzes['passive_voice'] = [ /* translators: %s is the number found in the title */ 'desc' => '

'.sprintf(__('Good: Only %s%% of sentences use passive voice.', 'siteseo'), $percentage).'

', 'impact' => 'good', 'title' => 'Passive Voice' ]; } elseif($percentage < 20){ $analyzes['passive_voice'] = [ /* translators: %s is the percentage of sentences using passive voice */ 'desc' => '

'.sprintf(__('Okay: %s%% of sentences use passive voice. Try to reduce it.', 'siteseo'), $percentage).'

', 'impact' => 'medium', 'title' => 'Passive Voice' ]; } else{ $analyzes['passive_voice'] = [ /* translators: %s is the percentage of sentences using passive voice */ 'desc' => '

'.sprintf(__('High: %s%% of sentences use passive voice. Consider revising.', 'siteseo'), $percentage).'

', 'impact' => 'high', 'title' => 'Passive Voice' ]; } } return $analyzes; } static function analyze_paragraph_length($analyzes, $data){ if(empty($data['paragraph_length']) || $data['paragraph_length'] < 150){ $analyzes['paragraph_length'] = [ 'desc' => '

Good: Your paragraphs are concise.

', 'impact' => 'good', 'title' => 'Paragraph Length' ]; } else{ $analyzes['paragraph_length'] = [ /* translators: %s is the current paragraph length in words */ 'desc' => '

'.sprintf(__('Consider reducing paragraph length. Current length: %s words.', 'siteseo'), $data['paragraph_length']) . '

', 'impact' => 'low', 'title' => 'Paragraph Length' ]; } return $analyzes; } static function render_readibility($analyzes, $analysis_data, $echo = true){ $acceptable_svg = [ 'svg' => [ 'role' => true, 'aria-hidden' => true, 'focusable' => true, 'width' => true, 'height' => true, 'viewbox' => true, 'version' => true, 'xmlns' => true, 'fill' => true, ], 'path' => ['fill' => true, 'd' => true] ]; if(!empty($analyzes)){ $order = ['1' => 'high', '2' => 'medium', '3' => 'low', '4' => 'good']; usort($analyzes, function ($a, $b) use ($order) { return array_search($a['impact'], $order) - array_search($b['impact'], $order); }); // Define SVG icons $high_icon = ''; $medium_icon = ''; $good_icon = ''; // Generate HTML $html = '
'; foreach($analyzes as $key => $value){ $impact_icon = ''; switch ($value['impact']) { case 'high': $impact_icon = $high_icon; break; case 'medium': case 'low': $impact_icon = $medium_icon; break; case 'good': $impact_icon = $good_icon; break; } $html .= '
'; $html .= '
'; $html .= '
' . /* translators: %s represents the degree of severity */ ''. sprintf(esc_html__('Degree of severity: %s','siteseo'), esc_html($value['impact'])).'' . esc_html($value['title']) . '
'; $html .= '
'; $html .= '
' . wp_kses_post($value['desc']) . '
'; $html .= '
'; } $html .= '
'; if($echo){ $allowed_html = array_merge(wp_kses_allowed_html('post'), $acceptable_svg); echo wp_kses($html, $allowed_html); return; } return $html; } } // Analaysis static function display_seo_analysis($post){ $seo_analysis = self::perform_seo_analysis($post); echo '
'; if(!empty($seo_analysis)){ // grp usort($seo_analysis['checks'], function ($a, $b) { $order = ['error' => 0, 'warning' => 1, 'good' => 2]; $a_status_class = isset($a['status_class']) ? $a['status_class'] : ''; $b_status_class = isset($b['status_class']) ? $b['status_class'] : ''; $a_order = isset($order[$a_status_class]) ? $order[$a_status_class] : 3; $b_order = isset($order[$b_status_class]) ? $order[$b_status_class] : 3; return $a_order - $b_order; }); echo '
'; // counts logic if(!empty($seo_analysis['error_count'])){ echo ''.esc_html($seo_analysis['error_count']). ' Errors'; } if(!empty($seo_analysis['warning_count'])){ echo ''.esc_html($seo_analysis['warning_count']). ' Warnings'; } if(!empty($seo_analysis['good_count'])){ echo ''.esc_html($seo_analysis['good_count']). ' Good'; } echo '
'; // A triangle with exclamation in it. $medium_icon_svg = ''; // A check inside a solid circle $good_icon = ''; $high_icon_svg = ''; $allowed_svg_tags = ['svg' => ['xmlns' => true, 'viewbox' => true, 'width' => true, 'height' => true, 'class' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true], 'path' => ['d' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true]]; foreach($seo_analysis['checks'] as $check){ echo '
'; if(isset($check['label'])){ echo'
'; if(isset($check['status_class'])){ $impact_icon = ''; switch($check['status_class']){ case 'good': $impact_icon = $good_icon; break; case 'warning': $impact_icon = $medium_icon_svg; break; case 'error': $impact_icon = $high_icon_svg; break; } echo '
'. /* translators: %s represents the degree of severity */ sprintf(esc_html__('Degree of severity: %s','siteseo'), esc_html($check['status_class'])).''; } echo esc_html($check['label']).'
'; } if(isset($check['details'])){ echo ''; } echo '
'; } echo '
'; } } static function perform_seo_analysis($post){ $content = $post->post_content; $title = $post->post_title; $title = !empty(get_post_meta($post->ID, '_siteseo_titles_title',true)) ? get_post_meta($post->ID, '_siteseo_titles_title', true) : $title; $permalink = get_permalink($post->ID); $keywords = get_post_meta($post->ID, '_siteseo_analysis_target_kw', true); $meta_desc = get_post_meta($post->ID, '_siteseo_titles_desc', true); if(empty($meta_desc)){ $meta_desc = ''; } $analysis = [ 'good_count' => 0, 'warning_count' => 0, 'error_count' => 0, 'checks' => [] ]; $canonical_check = self::check_canonical_url($permalink); $analysis['checks'][] = $canonical_check; self::update_analysis_score($analysis, $canonical_check); $word_count_check = self::check_word_count($content); $analysis['checks'][] = $word_count_check; self::update_analysis_score($analysis, $word_count_check); $keywords_density_check = self::check_keywords_density($content, $keywords); $analysis['checks'][] = $keywords_density_check; self::update_analysis_score($analysis, $keywords_density_check); $meta_title_check = self::check_meta_title($title); $analysis['checks'][] = $meta_title_check; self::update_analysis_score($analysis, $meta_title_check); $meta_description_check = self::check_meta_description($content, $meta_desc); $analysis['checks'][] = $meta_description_check; self::update_analysis_score($analysis, $meta_description_check); $image_alt_check = self::check_image_alt_texts($content); $analysis['checks'][] = $image_alt_check; self::update_analysis_score($analysis, $image_alt_check); $links_outbound_check = self::analyze_outbound_links($content); $analysis['checks'][] = $links_outbound_check; self::update_analysis_score($analysis, $links_outbound_check); $links_internal_check = self::analyze_internal_links($content); $analysis['checks'][] = $links_internal_check; self::update_analysis_score($analysis, $links_internal_check); $headings_check = self::check_headings($content); $analysis['checks'][] = $headings_check; self::update_analysis_score($analysis, $headings_check); $social_tags_check = self::check_social_meta_tags($post); $analysis['checks'][] = $social_tags_check; self::update_analysis_score($analysis, $social_tags_check); $structured_data_check = self::check_structured_data($post); $analysis['checks'][] = $structured_data_check; self::update_analysis_score($analysis, $structured_data_check); $permalink_keywords_check = self::check_keywords_in_permalink($permalink, $keywords); $analysis['checks'][] = $permalink_keywords_check; self::update_analysis_score($analysis, $permalink_keywords_check); $meta_robots_check = self::check_meta_robots($post); $analysis['checks'][] = $meta_robots_check; self::update_analysis_score($analysis, $meta_robots_check); $last_modified_check = self::check_last_modified_date($post); $analysis['checks'][] = $last_modified_check; self::update_analysis_score($analysis, $last_modified_check); $nofollow_links_check = self::analyze_nofollow_links($content); $analysis['checks'][] = $nofollow_links_check; self::update_analysis_score($analysis, $nofollow_links_check); $readability_data = []; $readability_data = self::analyze_readability($content, $title); update_post_meta($post->ID, '_siteseo_readibility_data', $readability_data); $analysis['checks'][] = $readability_data; return $analysis; } static function update_analysis_score(&$analysis, $check){ switch($check['status']){ case 'Good': $analysis['good_count']++; break; case 'Warning': $analysis['warning_count']++; break; case 'Error': $analysis['error_count']++; break; } } static function check_canonical_url($permalink){ $response = wp_remote_get($permalink); if(is_wp_error($response)){ return [ 'label' => 'Canonical URL', 'status' => 'Error', 'status_class' => 'error', 'details' => '

' . __('Unable to check canonical URL.', 'siteseo') . '

' ]; } $content = wp_remote_retrieve_body($response); preg_match_all('/]+rel=[\'"](canonical)[\'"][^>]+href=[\'"]([^\'"]+)[\'"][^>]*>/i', $content, $matches); $canonical_urls = !empty($matches[2]) ? array_unique($matches[2]) : []; $count = count($canonical_urls); $details = ''; $status = 'Warning'; $details .= '

'. __('A canonical URL is required by search engines to handle duplicate content.', 'siteseo') .'

'; if($count > 0){ $details .= '

' . sprintf( /* translators: %s represents the degree of severity */ _n( 'We found %s canonical URL in your source code. Below, the list:', 'We found %s canonical URLs in your source code. Below, the list:', $count, 'siteseo' ), number_format_i18n($count) ) . '

'; $details .= ''; if($count > 1){ $status = 'Error'; $details .= '

' . __('You must fix this. Canonical URL duplication is bad for SEO.', 'siteseo') . '

'; } else{ $status = 'Good'; } } else{ if(get_post_meta(get_the_ID(), '_siteseo_robots_index', true)){ $status = 'Good'; $details .= '

' . __('This page doesn\'t have any canonical URL because your post is set to noindex. This is normal.', 'siteseo') . '

'; } else{ $details .= '

' . __('This page doesn\'t have any canonical URL.', 'siteseo') . '

'; } } return [ 'label' => 'Canonical URL', 'status' => $status, 'status_class' => strtolower($status), 'details' => $details ]; } static function check_word_count($content){ $word_count = str_word_count(wp_strip_all_tags($content)); $unique_words = count(array_unique(str_word_count(wp_strip_all_tags($content), 1))); $details = ''; if($word_count != 0){ $details = '

'. __('Word count isn\'t a direct ranking factor, but it\'s important for your content to be high-quality, relevant, and unique. To meet these criteria, your article should include a sufficient number of paragraphs to ensure adequate word count.', 'siteseo') .'

'; $details .= ''; } if($word_count == 0){ $status = 'Error'; $details .= '

' . __('No content? Try adding a few more paragraphs.!', 'siteseo') . '

'; } elseif($word_count > 300){ $status = 'Good'; $details .= '
  • ' . __('Your content contains over 300 words, which meets the minimum requirement for a post', 'siteseo') .'
  • '; } else{ $status = 'Error'; $details .= '
  • ' . __('Your content is too brief. Consider adding a few more paragraphs!', 'siteseo') .'
  • '; } return [ 'label' => __('Word Count', 'siteseo'), 'status' => $status, 'status_class' => strtolower($status), 'details' => $details ]; } static function check_keywords_density($content, $keywords){ $content = strtolower(wp_strip_all_tags($content)); $keywords = array_filter(explode(',', trim($keywords))); $content_words = str_word_count($content, 1); $count_words = count($content_words); $details = ''; if(empty($keywords) || empty($content_words)){ $details .= '

    ' . __('We couldn\'t calculate the keyword density. This may be because you haven\'t added any content or your target keywords are missing from the post.', 'siteseo') . '

    '; return [ 'label' => __('Keyword Density', 'siteseo'), 'status' => 'Error', 'status_class' => 'error', 'details' => $details ]; } // calulate $density_details = []; $all_density = []; foreach($keywords as $keyword){ $keyword_occurrence = 0; $keyword = strtolower(trim($keyword)); // If keyword has multiple words if(str_word_count($keyword) > 1){ $pattern = '/\b' . preg_quote($keyword, '/') . '\b/i'; preg_match_all($pattern, $content, $matches); $keyword_occurrence = count($matches[0]); } else { $keyword_occurrence = array_count_values($content_words)[$keyword] ?? 0; } // Calculate density as percentage $kw_density = ($keyword_occurrence * str_word_count($keyword) * 100)/$count_words; $all_density[] = number_format($kw_density, 2); $density_details[] = /* translators: %s represents the degree of severity */ sprintf( '', sprintf( /* translators: %s represents a keyword density of */ esc_html__('%1$s was found %2$d times in your content, a keyword density of %3$s%%', 'siteseo'), $keyword, $keyword_occurrence, number_format($kw_density, 2) ) ); } $details .= implode('', $density_details); $details .= '

    '. sprintf( /* translators: %s represents the keywords stuffing */ __('Find out more about keywords stuffing', 'siteseo'), 'https://www.youtube.com/watch?v=Rk4qgQdp2UA' ) . '

    '; if(empty($all_density)){ return [ 'label' => __('Keyword Density', 'siteseo'), 'status' => 'Error', 'status_class' => 'error', 'details' => $details ]; } //avg density $avg_density = array_sum($all_density)/count($all_density); $status = $avg_density > 1 ? 'Good' : ($avg_density > 0.5 ? 'Warning' : 'Error'); return [ 'label' => __('Keyword Density', 'siteseo'), 'status' => $status, 'status_class' => strtolower($status), 'details' => $details ]; } static function check_meta_title($title, $keywords = []){ $details = ''; $status = 'Good'; $status_class = 'good'; $title = \SiteSEO\TitlesMetas::replace_variables($title); // length if(empty($title)){ $details .= '

    ' . __('A custom title has not been set for this post. If the global meta title works for you, you can disregard this recommendation.', 'siteseo') . '

    '; $status = 'Warning'; $status_class = 'warning'; } elseif(strlen($title) > 60){ $details .= '

    ' . __('Your custom title is too lengthy.', 'siteseo') . '

    '; $status = 'Warning'; $status_class = 'warning'; } elseif(strlen($title) >= 10 && strlen($title) <= 60){ $details .= '

    ' . __('The length of your title is appropriate.', 'siteseo') . '

    '; } else{ $details .= '

    ' . __('Your custom title is too short.', 'siteseo') . '

    '; $status = 'Warning'; $status_class = 'warning'; } if(!empty($keywords)){ $keyword_counts = []; foreach($keywords as $kw_name){ $kw_count = substr_count(strtolower($title), strtolower($kw_name)); if($kw_count > 0){ $keyword_counts[] = $kw_count; /* translators: %s represents the degree of severity */ $details .= ''; } } if(!empty($keyword_counts)){ $details .= '

    '.__('The target keywords are included in the Meta Title', 'siteseo').'

    '; } else { $details .= '

    '.__('None of your target keywords are present in the Meta Title.', 'siteseo').'

    '; $status = $status === 'Good' ? 'Warning' : $status; $status_class = $status_class === 'good' ? 'warning' : $status_class; } } return [ 'label' => __('Meta Title', 'siteseo'), 'status' => $status, 'status_class' => $status_class, 'details' => $details ]; } static function check_meta_description($content, $meta_description = '', $keywords = []){ $details = ''; $status = 'Good'; $status_class = 'good'; $desc = !empty($meta_description) ? $meta_description : wp_trim_words($content, 20); $description = \SiteSEO\TitlesMetas::replace_variables($desc); // desc length if(empty($meta_description)){ $details .= '

    ' . __('A custom meta description has not been set for this post. If the global meta description works for you, you can ignore this recommendation.', 'siteseo') . '

    '; $status = 'Warning'; $status_class = 'warning'; } elseif(strlen($description) > 160){ $details .= '

    ' . __('Your custom meta description is too lengthy', 'siteseo') .'

    '; $status = 'Warning'; $status_class = 'warning'; } elseif(strlen($description) >= 50 && strlen($description) <= 160){ $details .= '

    '. __('The length of your meta description is appropriate.', 'siteseo').'

    '; } if(!empty($keywords)){ $keyword_counts = []; foreach($keywords as $kw_name){ $kw_count = substr_count(strtolower($description), strtolower($kw_name)); if($kw_count > 0){ $keyword_counts[] = $kw_count; $details .= '
  • ' . /* translators: %s represents the key word count */ sprintf(esc_html__('%1$s was found %2$d times.', 'siteseo'), $kw_name, $kw_count) . '
  • '; } } if(!empty($keyword_counts)){ $details .= '

    '. __('The target keywords are included in the Meta description.', 'siteseo') . '

    '; } else{ $details .= '

    '. __('None of your target keywords are included in the Meta description.', 'siteseo') . '

    '; $status = $status === 'Good' ? 'Warning' : $status; $status_class = $status_class === 'good' ? 'warning' : $status_class; } } return [ 'label' => 'Meta Description', 'status' => $status, 'status_class' => $status_class, 'details' => $details ]; } static function check_image_alt_texts($content){ $result = [ 'label' => __('Alternative texts of images', 'siteseo'), 'status' => 'Good', 'status_class' => 'good', 'details' => '' ]; if(empty($content)){ $result['status'] = 'Warning'; $result['status_class'] = 'warning'; $result['details'] = '

    ' . __('No content found to analyze. Please add some content to check for images and alt texts.', 'siteseo') . '

    '; return $result; } preg_match_all('/]+src=[\'"]([^\'"]+)[\'"][^>]*>/i', $content, $image_matches); preg_match_all('/]+alt=[\'"]([^\'"]+)[\'"][^>]*>/i', $content, $alt_matches); $images_count = count($image_matches[0]); $alt_text_count = count(array_filter($alt_matches[1], 'strlen')); if($images_count === 0){ $result['status'] = 'Warning'; $result['status_class'] = 'warning'; $result['details'] = '

    ' . __('We couldn\'t find any images in your content. Adding media can boost your SEO.', 'siteseo') . '

    '; return $result; } if($images_count !== $alt_text_count){ $result['status'] = ($alt_text_count > 0) ? 'Warning' : 'Error'; $result['status_class'] = strtolower($result['status']); $result['details'] = '

    ' . esc_html__('No alternative text has been found for these images. Alt tags are essential for SEO and accessibility. Please edit your images in the media library or using your preferred page builder, and provide alternative text.', 'siteseo') . '

    '; if(!empty($image_matches[1])){ $result['details'] .= '
      '; foreach($image_matches[1] as $index => $img){ if(empty($alt_matches[1][$index])){ $result['details'] .= '
    • ' . '' . '
      ' . esc_html($img) . '
      ' . '
    • '; } } $result['details'] .= '
    '; } $result['details'] .= '

    '. __('Please note that we scan all your source code, which means some missing alternative text for images may be found in your header, sidebar, or footer.', 'siteseo') .'

    '; } else{ $result['details'] = '

    ' . __('All alternative tags have been completed. Great job!', 'siteseo') .'

    '; } return $result; } static function analyze_outbound_links($content){ preg_match_all('/]+href=([\'"])(?!#)([^\'"]+)[\'"][^>]*>/i', $content, $links); $outbound_links = array_filter($links[2], function($link){ return strpos($link, get_site_url()) === false; }); $total_outbound = count($outbound_links); $nofollow_count = preg_match_all('/rel=[\'"]nofollow[\'"]/', implode(' ', $links[0])); $status = $total_outbound > 0 ? 'Good' : 'Warning'; $details = ''; $details .= '

    '. __('The internet is based on the concept of hyperlinks, so linking to different websites is completely natural. However, avoid linking to low-quality or spammy sites. If youre uncertain about a site quality, add the "nofollow" attribute to your link.', 'siteseo') .'

    '; if($total_outbound > 0){ /* translators: %s represents the detected outbound links on page */ $details .= '

    '.sprintf(__('We detected %s outbound links on your page. Below is the list.', 'siteseo'), $total_outbound) .'

    '; $details .= '
      '; foreach($outbound_links as $link){ $details .= '
    • '; $details .= ''.esc_url($link).'
    • '; } $details .= '
    '; } else{ $details .= '

    ' . __('This page does not contain any outbound links.', 'siteseo') . '

    '; } return [ 'label' => __('Outbound Links', 'siteseo'), 'status' => $status, 'status_class' => strtolower($status), 'details' => $details ]; } static function analyze_internal_links($content){ preg_match_all('/]+href=([\'"])(?!#)([^\'"]+)[\'"][^>]*>(.*?)<\/a>/i', $content, $links, PREG_SET_ORDER); $internal_links = array_filter($links, function($link) { return strpos($link[2], get_site_url()) !== false; }); $total_internal = count($internal_links); $status = $total_internal > 0 ? 'Good' : 'Warning'; $details = ''; $details .= '

    '. __('Internal links are crucial for both SEO and user experience. Always aim to interconnect your content using meaningful and relevant anchor text.', 'siteseo') .'

    '; if($total_internal > 0){ /* translators: %s represents the internal links pointing to page */ $details .= '

    '. sprintf(__('We identified %s internal links pointing to this page.', 'siteseo'), $total_internal) .'

    '; $details .= '
      '; foreach($internal_links as $link){ $url = $link[2]; $post_id = url_to_postid($url); $details .= '
    • '; $details .= ''. esc_html($url) . ''; if($post_id){ $details .= '' . ''; } $details .= '
    • '; } $details .= '
    '; } else{ $details .= '

    ' . __('This page has no internal links from other content. Links from archive pages are not counted as internal links because they lack contextual relevance.', 'siteseo') . '

    '; } return [ 'label' => __('Internal Links', 'siteseo'), 'status' => $status, 'status_class' => strtolower($status), 'details' => $details ]; } static function analyze_nofollow_links($content){ preg_match_all('/]+href=([\'"])([^\'"]+)[\'"][^>]*>(.*?)<\/a>/i', $content, $all_links, PREG_SET_ORDER); $nofollow_links = array_filter($all_links, function($link){ return preg_match('/rel=[\'"][^\'"]*nofollow[^\'"]*[\'"]/', $link[0]); }); $total_nofollow = count($nofollow_links); $status = $total_nofollow > 0 ? 'Warning' : 'Good'; $details = ''; if($total_nofollow > 0){ $details .= '

    ' . /* translators: %d represents the number nofollow attribute */ sprintf( esc_html__('We found %d links with the nofollow attribute on your page. Avoid overusing the nofollow attribute in links. Below is the list:', 'siteseo'), $total_nofollow ) . '

    '; $details .= '
      '; foreach($nofollow_links as $link){ $href = $link[2]; $link_text = $link[3]; $details .= '
    • '. ''. ''.esc_html($link_text).''. ''. '
    • '; } $details .= '
    '; } else{ $details .= '

    ' . __('This page does not contain any nofollow links.', 'siteseo') . '

    '; } return [ 'label' => __('Nofollow Links', 'siteseo'), 'status' => $status, 'status_class' => strtolower($status), 'details' => $details ]; } static function check_headings($content, $keywords = []) { $details = ''; $status = 'Good'; $status_class = 'good'; if(empty(trim($content))){ return [ 'label' => 'Headings', 'status' => 'Warning', 'status_class' => 'warning', 'details' => '

    ' . __('No content available to check headings.', 'siteseo') . '

    ' ]; } preg_match_all('/]*)>(.*?)<\/h\1>/is', $content, $heading_matches); if(empty($heading_matches[0])){ return [ 'label' => 'Headings', 'status' => 'Error', 'status_class' => 'error', 'details' => '

    ' . __('No headings found in the content. Using headings is essential for both SEO and accessibility!', 'siteseo') . '

    ' ]; } $heading_counts = array_count_values($heading_matches[1]); $total_headings = count($heading_matches[0]); $h1_count = $heading_counts[1] ?? 0; if($h1_count > 0){ $details .= '

    ' . /* translators: %d represents the number of h1 tags */ sprintf(esc_html__('We found %d Heading 1 (H1) tags in your content.', 'siteseo'), $h1_count+1) . '

    '; $details .= '

    ' . __('You should avoid using more than one H1 heading in your post content. The rule is simple: each web page should have only one H1, which benefits both SEO and accessibility. Below is the list:', 'siteseo') . '

    '; $details .= '
      '; foreach(array_keys($heading_matches[1], '1') as $index){ $details .= '
    • ' . wp_strip_all_tags($heading_matches[0][$index]) . '
    • '; } $details .= '
    '; $status = 'Warning'; $status_class = 'warning'; } foreach([2, 3] as $level){ $level_count = $heading_counts[$level] ?? 0; $details .= '

    ' . /* translators: %d represents the heading */ sprintf(__('Found %1$d H%2$d heading(s)', 'siteseo'), $level_count, $level) . '

    '; if($level_count > 0){ $details .= '
      '; foreach(array_keys($heading_matches[1], (string)$level) as $index){ $details .= '
    • '. wp_strip_all_tags($heading_matches[0][$index]) .'
    • '; } $details .= '
    '; } if(!empty($keywords) && $level_count > 0){ $keyword_found = false; $keyword_details = '
      '; foreach($keywords as $kw_name){ $kw_count = 0; foreach(array_keys($heading_matches[1], (string)$level) as $index){ $kw_count += substr_count( strtolower(wp_strip_all_tags($heading_matches[0][$index])), strtolower($kw_name) ); } if($kw_count > 0){ $keyword_found = true; $keyword_details .= '
    • ' . /* translators: %s represents the degree of severity */ sprintf(esc_html__('%1$s was found %2$d times.', 'siteseo'), $kw_name, $kw_count) . '
    • '; } } $keyword_details .= '
    '; if($keyword_found){ $details .= '

    ' . /* translators: %s represents the target keywords */ sprintf(__('Target keywords were found in Heading %1$d (H%2$d).', 'siteseo'), $level, $level) . '

    ' . $keyword_details; } else{ $details .= '

    ' . /* translators: %d represents the target keywords */ sprintf(__('None of your target keywords were found in Heading %1$d (H%2$d).', 'siteseo'), $level, $level) . '

    '; if($status === 'Good'){ $status = 'Warning'; $status_class = 'warning'; } } } } return [ 'label' => 'Headings', 'status' => $status, 'status_class' => $status_class, 'details' => $details ]; } static function check_social_meta_tags($post = null){ if(!$post){ $post = get_queried_object(); } $details = ''; $status = 'Good'; $status_class = 'good'; $og_titles = get_post_meta($post->ID, '_siteseo_social_fb_title', true); $og_title = $og_titles ? $og_titles : ''; if(empty($og_title)){ $details .= '

    '. __('Your Open Graph Title tag has not been set!', 'siteseo') .'

    '; $details .= '

    '. __('Your Open Graph Title is missing!', 'siteseo') .'

    '; $status = 'Error'; $status_class = 'error'; } else{ $details .= '

    '. __('Open Graph Title', 'siteseo') . '

    '; $details .= '

    '. __('An Open Graph Title tag was found in your source code.', 'siteseo') . '

    '; $details .= '
    • ' . esc_html($og_title) . '
    '; } $og_descriptions = get_post_meta($post->ID, '_siteseo_social_fb_desc', true); $og_description = $og_descriptions ? $og_descriptions : ''; if(empty($og_description)){ $details .= '

    '. __('Open Graph Description', 'siteseo') .'

    '; $details .= '

    '. __('Your Open Graph Description has not been set!','siteseo').'

    '; $status = $status === 'Good' ? 'Warning' : $status; $status_class = $status_class === 'good' ? 'warning' : $status_class; } else { $details .= '

    ' . /* translators: %s represents the og description */ sprintf(esc_html__('We found %s og:description in your content.', 'siteseo'), $og_descriptions) . '

    '; } // OG Check $og_images = get_post_meta($post->ID, '_siteseo_social_fb_img', true); $og_image = $og_images ? $og_images : ''; if(empty($og_image)){ $details .= '

    '. __('Open Graph Image', 'siteseo') .'

    '; $details .= '

    '. __('Your Open Graph Image has not been set!' ,'siteseo') .'

    '; $status = $status === 'Good' ? 'Warning' : $status; $status_class = $status_class === 'good' ? 'warning' : $status_class; } else{ /* translators: %s represents the og images */ $details .= '

    '. sprintf(esc_html__('We found %s og:image in your content.', 'siteseo'), $og_images) . '

    '; } // Open Graph $og_site_name = get_bloginfo('name'); if(empty($og_site_name)){ $details .= '

    '. __('Open Graph Site Name', 'siteseo') .'

    '; $details .= '

    '. __('Your Open Graph Site Name has not been set!' ,'siteseo') .'

    '; $status = $status === 'Good' ? 'Warning' : $status; $status_class = ($status_class === 'good') ? 'warning' : $status_class; } // Twitter $twitter_title = get_post_meta($post->ID, '_siteseo_social_twitter_title', true); if(empty($twitter_title)){ $details .= '

    '. __('X Title', 'siteseo').'

    '; $details .= '

    '. __('Your X Title has not been set!', 'siteseo').'

    '; $status = $status === 'Good' ? 'Warning' : $status; $status_class = $status_class === 'good' ? 'warning' : $status_class; } $twitter_description = get_post_meta($post->ID, '_siteseo_social_twitter_desc', true); if(empty($twitter_description)){ $details .= '

    '. __('X Description','siteseo').'

    '; $details .= '

    '. __('Your X Description has not been set!','siteseo') .'

    '; $status = $status === 'Good' ? 'Warning' : $status; $status_class = $status_class === 'good' ? 'warning' : $status_class; } $twitter_image = get_post_meta($post->ID, '_siteseo_social_twitter_img', true); if(empty($twitter_image)){ $details .= '

    '. __('X Image', 'siteseo') .'

    '; $details .= '

    '. __('Your X Image has not been set!', 'siteseo').'

    '; $status = $status === 'Good' ? 'Warning' : $status; $status_class = ($status_class === 'good') ? 'warning' : $status_class; } return [ 'label' => __('Social Meta Tags', 'siteseo'), 'status' => $status, 'status_class' => $status_class, 'details' => $details ]; } static function check_structured_data($post){ $schema_type = get_post_meta($post->ID, '_schema_type', true); $status = !empty($schema_type) ? 'Good' : 'Warning'; return [ 'label' => 'Structured Data', 'status' => $status, 'status_class' => strtolower($status), 'details' => !empty($schema_type) ? 'Schema Type: '.$schema_type : 'No schema defined' ]; } static function check_keywords_in_permalink($permalink, $keywords){ $keywords = array_filter(explode(',', trim($keywords))); $permalink = str_replace('-', ' ', strtolower(basename($permalink))); $content_words = str_word_count($permalink, 1); $count_words = count($content_words); $kw_density = []; $matching_keywords = []; foreach($keywords as $keyword){ $keyword_occurrence = 0; $keyword = strtolower(trim($keyword)); // If keyword has multiple words if(str_word_count($keyword) > 1){ $pattern = '/\b' . preg_quote($keyword, '/') . '\b/i'; preg_match_all($pattern, $permalink, $matches); $keyword_occurrence = count($matches[0]); } else { $keyword_occurrence = array_count_values($content_words)[$keyword] ?? 0; } // Calculate density as percentage $kw_density[] = ($keyword_occurrence * str_word_count($keyword) * 100)/$count_words; if(!empty($kw_density)){ $matching_keywords[] = $keyword; break; } } $status = !empty($kw_density) ? 'Good' : 'Error'; $details = ''; if($status === 'Good'){ $details .= '

    '. __('Great! One of your target keywords is included in your permalink.', 'siteseo') .'

    '; $details .= '
    • ' . implode(', ', $matching_keywords) . '
    '; } elseif($permalink === get_home_url() || $permalink === home_url()){ $details .= '

    '. __('This is your homepage, so this check doesn\'t apply as there is no slug.', 'siteseo') . '

    '; } else{ $details .= '

    '. __('You should include one of your target keywords in your permalink.', 'siteseo') . '

    '; } return [ 'label' => __('Keywords in Permalink', 'siteseo'), 'status' => $status, 'status_class' => strtolower($status), 'details' => $details ]; } static function check_meta_robots($post){ $noindex = get_post_meta($post->ID, '_siteseo_robots_index', true); $nofollow = get_post_meta($post->ID, '_siteseo_robots_follow', true); $noimageindex = get_post_meta($post->ID, '_siteseo_robots_imageindex', true); $noarchive = get_post_meta($post->ID, '_siteseo_robots_archive', true); $nosnippet = get_post_meta($post->ID, '_siteseo_robots_snippet', true); $count_meta_robots = 0; $details = ''; $meta_robots_checks = [ 'noindex' => $noindex, 'nofollow' => $nofollow, 'noimageindex' => $noimageindex, 'noarchive' => $noarchive, 'nosnippet' => $nosnippet, ]; foreach($meta_robots_checks as $robot => $value){ if($value !== 'yes'){ continue; } $count_meta_robots++; switch($robot){ case 'noindex': $details .= '

    ' . __('noindex is enabled! Search engines cannot index this page', 'siteseo') . '

    '; break; case 'nofollow': $details .= '

    ' . __('nofollow is on! Search engines can\'t follow your links on this page.', 'siteseo') . '

    '; break; case 'noimageindex': $details .= '

    ' . __('nofollow is enabled! Search engines cannot follow the links on this page.', 'siteseo').'

    '; break; case 'noarchive': $details .= '

    ' . __('noarchive is enabled! Search engines will not cache your page.', 'siteseo') .'

    '; break; case 'nosnippet': $details .= '

    ' . __('nosnippet is enabled! Search engines will not display a snippet of this page in the search results.', 'siteseo') .'

    '; break; } } if($count_meta_robots > 0){ /* translators: %s represents the robots tags */ $details .= '

    '. sprintf(esc_html__('We found %s meta robots tags on your page. There may be an issue with your theme!', 'siteseo'), $count_meta_robots) .'

    '; } if($noindex !== 'yes'){ $details .= '

    '. __('noindex is disabled. Search engines will index this page.', 'siteseo') .'

    '; } if($nofollow !== 'yes'){ $details .= '

    '. __('nofollow is disabled. Search engines will follow links on this page.', 'siteseo') .'

    '; } if($noimageindex !== 'yes'){ $details .= '

    '. __('noimageindex is disabled. Google will index the images on this page.', 'siteseo') .'

    '; } if($noarchive !== 'yes'){ $details .= '

    '. __('noarchive is disabled. Search engines will probably cache your page.', 'siteseo') .'

    '; } if($nosnippet !== 'yes'){ $details .= '

    '. __('nosnippet is disabled. Search engines will display a snippet of this page in search results.', 'siteseo') .'

    '; } if($count_meta_robots === 0){ $details .= '

    ' . __('We found no meta robots on this page. It means, your page is index,follow. Search engines will index it, and follow links. ', 'siteseo') . '

    '; } $status = ($count_meta_robots === 0) ? 'Good' : (($count_meta_robots <= 2) ? 'Warning' : 'Error'); return [ 'label' => 'Meta Robots', 'status' => $status, 'status_class' => strtolower($status), 'details' => $details ]; } static function check_last_modified_date($post){ $last_modified = get_the_modified_date('Y-m-d', $post); $days_since_modified = round((time() - strtotime($last_modified)) / (60 * 60 * 24)); $status = $days_since_modified < 30 ? 'Good' : ($days_since_modified < 90 ? 'Warning' : 'Error'); $details = ''; if($status == 'Error'){ $details .= '

    '. __('This post is a little old!', 'siteseo') .'

    '; } if($days_since_modified < 365){ $details .='

    '.__('The last modified date of this article is less than 1 year. Cool', 'siteseo') .'

    '; } $details .= '

    '.__('Search engines love fresh content. Update regularly your articles without entirely rewriting your content and give them a boost in search rankings. SiteSEO takes care of the technical part', 'siteseo').'

    '; return [ 'label' => 'Last Modified Date', 'status' => $status, 'status_class' => strtolower($status), 'details' => $details ]; } static function analyze_readability($post, $title){ $data = []; // These are power words specifically for headlines. // These are not hard rules, but they are perceived to have a higher CTR if used in the heading. $power_words = ['exclusive', 'revealed', 'secrets', 'ultimate', 'proven', 'unleashed', 'discover', 'breakthrough', 'shocking', 'insider', 'elite', 'uncovered', 'powerful', 'guaranteed', 'transformative', 'instant', 'revolutionary', 'unbelievable', 'top', 'best', 'must-have', 'limited', 'rare', 'unique', 'unprecedented', 'premium', 'urgent', 'today', 'now', 'latest', 'new', 'free', 'bonus', 'offer', 'sensational', 'astonishing', 'incredible', 'jaw-dropping', 'unmissable', 'essential', 'critical', 'vital', 'pivotal', 'game-changer', 'spotlight', 'trending', 'hot', 'popular', 'featured', 'special', 'limited-time', 'hurry', 'last chance', 'countdown']; if(!empty($title)){ // Checking power words. $title_words = explode(' ', strtolower($title)); $present_power_words = array_intersect($title_words, $power_words); if(!empty($present_power_words)){ $data['power_words'] = $present_power_words; } // Checking number in the Title if(preg_match('/\s?\d+\s/', preg_quote($title), $number)){ $data['number_found'] = $number[0]; } } // We are checking paragarph length too. if(!isset($data['paragraph_length'])){ $data['paragraph_length'] = 0; } if(!empty($post)){ preg_match_all('/

    .*<\/p>/U', $post, $paragraphs); foreach($paragraphs[0] as $paragraph){ $paragraph = normalize_whitespace(wp_strip_all_tags($paragraph)); $data['paragraph_length'] += substr_count($paragraph, ' ') + 1; // updating paragraph length self::analyse_passive_voice($paragraph, $data); } } return $data; } static function analyse_passive_voice($paragraph, &$data){ if(empty($paragraph)){ return; } $sentences = explode('.', $paragraph); $passive_count = 0; if(!isset($data['passive_voice']['passive_sentences'])){ $data['passive_voice']['passive_sentences'] = 0; } if(!isset($data['passive_voice']['total_sentences'])){ $data['passive_voice']['total_sentences'] = 0; } if(empty($sentences)){ return; } foreach($sentences as $sentence){ if(empty($sentence)){ continue; } $sentence = normalize_whitespace($sentence); $is_passive = self::sentence_is_passive($sentence); if($is_passive == true){ $passive_count++; } } $data['passive_voice']['passive_sentences'] += $passive_count; $data['passive_voice']['total_sentences'] += count($sentences); } static function sentence_is_passive($sentence){ $be_words = ['am', 'is', 'are', 'was', 'were', 'be', 'being', 'been']; // TODO: We can check if "en" ending words are a comman pattern too, then we will remove the en ending words too from here. $past_particles = ['gone' ,'done' ,'seen' ,'taken' ,'eaten' ,'written' ,'driven' ,'spoken' ,'broken' ,'chosen' ,'fallen' ,'forgotten' ,'forgiven' ,'hidden' ,'known' ,'grown' ,'drawn' ,'flown' ,'thrown' ,'blown' ,'shown' ,'worn' ,'sworn' ,'torn' ,'woken' ,'begun' ,'sung' ,'run' ,'swum' ,'shaken' ,'given' ,'proven' ,'ridden' ,'risen' ,'shone' ,'shot' ,'fought' ,'thought' ,'bought' ,'brought' ,'caught' ,'taught' ,'built' ,'felt' ,'kept' ,'slept' ,'left' ,'lost' ,'meant' ,'met' ,'read' ,'sold' ,'sent' ,'spent' ,'stood' ,'understood' ,'won' ,'held' ,'told' ,'heard' ,'paid' ,'laid' ,'said' ,'found' ,'made' ,'learned' ,'put']; if(empty($sentence)){ return false; } $words = explode(' ', $sentence); for($i = 0; $i < count($words); $i++){ // Checking if we have a be word if(!in_array($words[$i], $be_words)){ continue; } // If be word is there then need to check if next one is past particle with mostly ends with ed. if(strpos($words[$i+1], 'ed') != strlen($words[$i+1]) - 2){ if(!in_array($words[$i+1], $past_particles)){ continue; } } return true; } return false; } }