add_action( 'wp_head', [ $this, 'output_meta_tags' ], 1 );
// Remove default WP title if we provide one
add_filter( 'pre_get_document_title', [ $this, 'filter_title' ], 99 );
// XML Sitemap
add_action( 'init', [ $this, 'register_sitemap_route' ] );
add_action( 'save_post', [ $this, 'invalidate_sitemap' ] );
add_action( 'delete_post', [ $this, 'invalidate_sitemap' ] );
}
// ─── Meta Box ───────────────────────────────────────────
public function add_meta_box(): void {
$post_types = get_post_types( [ 'public' => true ] );
foreach ( $post_types as $pt ) {
add_meta_box(
'airewp_seo',
__( 'SEO Settings', 'ai-ready-wp' ),
[ $this, 'render_meta_box' ],
$pt,
'normal',
'high'
);
}
}
public function render_meta_box( \WP_Post $post ): void {
wp_nonce_field( 'airewp_seo_save', 'airewp_seo_nonce' );
$title = get_post_meta( $post->ID, self::META_KEY_TITLE, true );
$desc = get_post_meta( $post->ID, self::META_KEY_DESC, true );
$keyword = get_post_meta( $post->ID, self::META_KEY_KEYWORD, true );
$noindex = get_post_meta( $post->ID, self::META_KEY_NOINDEX, true );
$nofollow = get_post_meta( $post->ID, self::META_KEY_NOFOLLOW, true );
$canonical = get_post_meta( $post->ID, self::META_KEY_CANONICAL, true );
?>
'airewp_seo_title',
self::META_KEY_DESC => 'airewp_seo_desc',
self::META_KEY_KEYWORD => 'airewp_seo_keyword',
self::META_KEY_CANONICAL => 'airewp_seo_canonical',
];
foreach ( $fields as $meta_key => $input_key ) {
$value = isset( $_POST[ $input_key ] )
? sanitize_text_field( wp_unslash( $_POST[ $input_key ] ) )
: '';
if ( $meta_key === self::META_KEY_CANONICAL && $value ) {
$value = esc_url_raw( $value );
}
update_post_meta( $post_id, $meta_key, $value );
}
// Checkboxes
update_post_meta( $post_id, self::META_KEY_NOINDEX, ! empty( $_POST['airewp_seo_noindex'] ) ? '1' : '' );
update_post_meta( $post_id, self::META_KEY_NOFOLLOW, ! empty( $_POST['airewp_seo_nofollow'] ) ? '1' : '' );
}
// ─── Meta Tags Output ───────────────────────────────────
public function output_meta_tags(): void {
if ( is_admin() ) {
return;
}
$post = get_queried_object();
if ( ! $post instanceof \WP_Post ) {
$this->output_home_meta();
return;
}
$title = get_post_meta( $post->ID, self::META_KEY_TITLE, true ) ?: $post->post_title;
$desc = get_post_meta( $post->ID, self::META_KEY_DESC, true )
?: wp_trim_words( wp_strip_all_tags( $post->post_content ), 25, '...' );
$canonical = get_post_meta( $post->ID, self::META_KEY_CANONICAL, true ) ?: get_permalink( $post->ID );
$noindex = get_post_meta( $post->ID, self::META_KEY_NOINDEX, true );
$nofollow = get_post_meta( $post->ID, self::META_KEY_NOFOLLOW, true );
// Get OG image
$image = get_the_post_thumbnail_url( $post->ID, 'large' );
if ( ! $image ) {
$settings = AI_Ready_WP::get_settings();
$image = $settings['company_logo'] ?? '';
}
// Meta description
if ( $desc ) {
printf( '' . "\n", esc_attr( $desc ) );
}
// Canonical
printf( '' . "\n", esc_url( $canonical ) );
// Robots
$robots = [];
if ( $noindex ) {
$robots[] = 'noindex';
}
if ( $nofollow ) {
$robots[] = 'nofollow';
}
if ( $robots ) {
printf( '' . "\n", esc_attr( implode( ', ', $robots ) ) );
}
// OpenGraph
echo '' . "\n";
printf( '' . "\n", esc_attr( $title ) );
if ( $desc ) {
printf( '' . "\n", esc_attr( $desc ) );
}
printf( '' . "\n", esc_url( $canonical ) );
if ( $image ) {
printf( '' . "\n", esc_url( $image ) );
}
printf( '' . "\n", esc_attr( get_bloginfo( 'name' ) ) );
// Twitter Card
echo '' . "\n";
printf( '' . "\n", esc_attr( $title ) );
if ( $desc ) {
printf( '' . "\n", esc_attr( $desc ) );
}
if ( $image ) {
printf( '' . "\n", esc_url( $image ) );
}
}
private function output_home_meta(): void {
if ( ! is_front_page() && ! is_home() ) {
return;
}
$settings = AI_Ready_WP::get_settings();
$title = $settings['company_name'] ?? get_bloginfo( 'name' );
$desc = $settings['company_desc'] ?? get_bloginfo( 'description' );
$url = home_url( '/' );
$image = $settings['company_logo'] ?? '';
if ( $desc ) {
printf( '' . "\n", esc_attr( $desc ) );
}
printf( '' . "\n", esc_url( $url ) );
echo '' . "\n";
printf( '' . "\n", esc_attr( $title ) );
if ( $desc ) {
printf( '' . "\n", esc_attr( $desc ) );
}
printf( '' . "\n", esc_url( $url ) );
if ( $image ) {
printf( '' . "\n", esc_url( $image ) );
}
}
public function filter_title( string $title ): string {
if ( is_admin() || ! is_singular() ) {
return $title;
}
$post = get_queried_object();
if ( ! $post instanceof \WP_Post ) {
return $title;
}
$custom = get_post_meta( $post->ID, self::META_KEY_TITLE, true );
if ( $custom ) {
return $custom . ' — ' . get_bloginfo( 'name' );
}
return $title;
}
// ─── XML Sitemap ──────────────────────────────────
public function register_sitemap_route(): void {
add_rewrite_rule( '^sitemap\.xml$', 'index.php?airewp_sitemap=1', 'top' );
add_filter( 'query_vars', function ( array $vars ): array {
$vars[] = 'airewp_sitemap';
return $vars;
} );
add_action( 'template_redirect', [ $this, 'serve_sitemap' ] );
}
public function serve_sitemap(): void {
if ( ! get_query_var( 'airewp_sitemap' ) ) {
return;
}
header( 'Content-Type: application/xml; charset=utf-8' );
header( 'X-Robots-Tag: noindex' );
echo '' . "\n";
echo '' . "\n";
// Homepage
printf(
" \n %s\n daily\n 1.0\n \n",
esc_url( home_url( '/' ) )
);
// Pages
$pages = get_posts( [
'post_type' => 'page',
'post_status' => 'publish',
'posts_per_page' => 500,
'meta_query' => [
'relation' => 'OR',
[
'key' => self::META_KEY_NOINDEX,
'compare' => 'NOT EXISTS',
],
[
'key' => self::META_KEY_NOINDEX,
'value' => '1',
'compare' => '!=',
],
],
] );
foreach ( $pages as $page ) {
printf(
" \n %s\n %s\n weekly\n 0.8\n \n",
esc_url( get_permalink( $page->ID ) ),
esc_html( get_the_modified_date( 'c', $page ) )
);
}
// Posts
$posts = get_posts( [
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 1000,
'meta_query' => [
'relation' => 'OR',
[
'key' => self::META_KEY_NOINDEX,
'compare' => 'NOT EXISTS',
],
[
'key' => self::META_KEY_NOINDEX,
'value' => '1',
'compare' => '!=',
],
],
] );
foreach ( $posts as $p ) {
printf(
" \n %s\n %s\n monthly\n 0.6\n \n",
esc_url( get_permalink( $p->ID ) ),
esc_html( get_the_modified_date( 'c', $p ) )
);
}
echo '' . "\n";
exit;
}
public function invalidate_sitemap(): void {
delete_option( self::SITEMAP_OPTION );
}
// ─── Focus Keyword Analysis ───────────────────────
/**
* Analyze focus keyword placement.
*
* @return array
*/
public static function analyze_keyword( string $keyword, \WP_Post $post ): array {
$kw_lower = mb_strtolower( $keyword );
$title = mb_strtolower( $post->post_title );
$content = mb_strtolower( wp_strip_all_tags( $post->post_content ) );
$slug = $post->post_name;
$meta_desc = mb_strtolower( get_post_meta( $post->ID, self::META_KEY_DESC, true ) ?: '' );
$word_count = str_word_count( $content );
$kw_count = mb_substr_count( $content, $kw_lower );
$density = $word_count > 0 ? ( $kw_count / $word_count ) * 100 : 0;
return [
[
'label' => __( 'Keyword in title', 'ai-ready-wp' ),
'pass' => str_contains( $title, $kw_lower ),
],
[
'label' => __( 'Keyword in URL slug', 'ai-ready-wp' ),
'pass' => str_contains( $slug, sanitize_title( $keyword ) ),
],
[
'label' => __( 'Keyword in meta description', 'ai-ready-wp' ),
'pass' => $meta_desc && str_contains( $meta_desc, $kw_lower ),
],
[
'label' => sprintf(
/* translators: %s: density percentage */
__( 'Keyword density: %s%% (aim for 1-3%%)', 'ai-ready-wp' ),
number_format( $density, 1 )
),
'pass' => $density >= 0.5 && $density <= 4.0,
],
[
'label' => sprintf(
/* translators: %d: word count */
__( 'Content length: %d words (min 300)', 'ai-ready-wp' ),
$word_count
),
'pass' => $word_count >= 300,
],
[
'label' => __( 'Keyword in first paragraph', 'ai-ready-wp' ),
'pass' => self::keyword_in_first_paragraph( $kw_lower, $content ),
],
];
}
private static function keyword_in_first_paragraph( string $kw, string $content ): bool {
$first = mb_substr( $content, 0, 500 );
return str_contains( $first, $kw );
}
}