*/ private const EXACT = [ // Yoast SEO: 1 = noindex, 2 = index, absent = follow the default. '_yoast_wpseo_meta-robots-noindex' => '1', // The SEO Framework: 1 = noindex, -1 = index, 0 = default. '_genesis_noindex' => '1', // SEOPress: the field asks "do not display in search results", so yes means noindex. '_seopress_robots_index' => 'yes', ]; /** * Meta keys holding a list of robots directives. * * @var array */ private const LISTS = [ 'rank_math_robots', ]; /** * Meta keys holding a settings array with a `noindex` member. * * @var array */ private const MAPS = [ 'slim_seo', ]; /** * Post IDs already answered for, by ID. * * @var array */ private static array $cache = []; /** * Forgets the cached answers. * * @return void */ public static function reset(): void { self::$cache = []; } /** * Whether this post is marked `noindex` by something else on the site. * * @param int $post_id Post ID. * @return bool True when the post should not be advertised. */ public static function is( int $post_id ): bool { if ( $post_id <= 0 ) { return false; } if ( ! isset( self::$cache[ $post_id ] ) ) { self::$cache[ $post_id ] = self::detect( $post_id ); } /** * Filters whether a post counts as `noindex`. * * A site with an SEO plugin this does not know about, or with its own * idea of what belongs in the generated files, answers here. * * @param bool $noindex Whether the post is marked `noindex`. * @param int $post_id Post ID. */ return (bool) apply_filters( 'acid_aeo_is_noindex', self::$cache[ $post_id ], $post_id ); } /** * The uncached answer. * * @param int $post_id Post ID. * @return bool True when something on the site marked the post `noindex`. */ private static function detect( int $post_id ): bool { foreach ( self::EXACT as $key => $value ) { if ( self::meta( $post_id, $key ) === $value ) { return true; } } foreach ( self::LISTS as $key ) { $directives = get_post_meta( $post_id, $key, true ); if ( is_array( $directives ) && in_array( 'noindex', array_map( 'strval', $directives ), true ) ) { return true; } } foreach ( self::MAPS as $key ) { $settings = get_post_meta( $post_id, $key, true ); if ( is_array( $settings ) && ! empty( $settings['noindex'] ) ) { return true; } } return self::aioseo( $post_id ) || self::woocommerce_page( $post_id ); } /** * One meta value as a trimmed string. * * @param int $post_id Post ID. * @param string $key Meta key. * @return string The value, empty when it is not a scalar. */ private static function meta( int $post_id, string $key ): string { $value = get_post_meta( $post_id, $key, true ); return is_scalar( $value ) ? trim( (string) $value ) : ''; } /** * All in One SEO's answer, which lives in its own table. * * Only asked when the plugin is actually active, and only for one post, so * the query is cheap and the table is known to exist. `robots_default` means * "whatever the site wide setting says", and only an explicit override * counts as an answer about this page. * * @param int $post_id Post ID. * @return bool True when All in One SEO marks the post `noindex`. */ private static function aioseo( int $post_id ): bool { if ( ! defined( 'AIOSEO_VERSION' ) ) { return false; } global $wpdb; if ( ! isset( $wpdb ) || ! is_object( $wpdb ) ) { return false; } $table = $wpdb->prefix . 'aioseo_posts'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reading another plugin's per-post row; it has no API that answers without booting its own stack, and the answer is cached in this class for the request. $row = $wpdb->get_row( $wpdb->prepare( 'SELECT robots_default, robots_noindex FROM %i WHERE post_id = %d', $table, $post_id ), ARRAY_A ); if ( ! is_array( $row ) ) { return false; } return empty( $row['robots_default'] ) && ! empty( $row['robots_noindex'] ); } /** * Whether this is one of WooCommerce's functional pages. * * The cart, the checkout and the account page are machinery, not content: * WooCommerce itself sends them `noindex`, they read as an empty cart or a * login form to anyone who fetches them, and an assistant citing them as a * source for the business is citing a form. The shop page is left in — it is * the catalogue, and it says something about what is sold. * * @param int $post_id Post ID. * @return bool True for the cart, checkout or account page. */ private static function woocommerce_page( int $post_id ): bool { if ( ! function_exists( 'wc_get_page_id' ) ) { return false; } foreach ( [ 'cart', 'checkout', 'myaccount' ] as $page ) { if ( (int) wc_get_page_id( $page ) === $post_id ) { return true; } } return false; } }