> */ public static function preview_options(): array { return array( 'max_snippet' => array( '', '-1', '0', '50', '100', '160', '200' ), 'max_image_preview' => array( '', 'none', 'standard', 'large' ), 'max_video_preview' => array( '', '-1', '0', '15', '30' ), ); } /** * Canonical URL for a post: override if valid, permalink otherwise. */ public static function canonical( WP_Post $post ): string { $custom = trim( (string) get_post_meta( $post->ID, self::CANONICAL, true ) ); if ( '' !== $custom ) { $url = esc_url_raw( $custom ); // Refuse anything that is not http(s) — a javascript: canonical would // otherwise land in the head of every visitor's page. if ( '' !== $url && in_array( (string) wp_parse_url( $url, PHP_URL_SCHEME ), array( 'http', 'https' ), true ) ) { return $url; } } return (string) get_permalink( $post ); } /** * Breadcrumb label: override, then the SEO title, then the post title. */ public static function breadcrumb_title( WP_Post $post ): string { $custom = trim( (string) get_post_meta( $post->ID, self::CRUMB_TITLE, true ) ); if ( '' !== $custom ) { return $custom; } return (string) get_the_title( $post ); } /** * Per-post preview directive overrides. * * @return array Only keys with a non-empty override. */ public static function previews( int $post_id ): array { $stored = get_post_meta( $post_id, self::PREVIEWS, true ); $out = array(); if ( ! is_array( $stored ) ) { return $out; } foreach ( self::preview_options() as $key => $allowed ) { $value = isset( $stored[ $key ] ) ? (string) $stored[ $key ] : ''; if ( '' !== $value && in_array( $value, $allowed, true ) ) { $out[ $key ] = $value; } } return $out; } /** * Sanitize and store preview overrides. * * @param array $input Raw input. */ public static function save_previews( int $post_id, array $input ): void { $clean = array(); foreach ( self::preview_options() as $key => $allowed ) { $value = isset( $input[ $key ] ) ? (string) $input[ $key ] : ''; if ( in_array( $value, $allowed, true ) && '' !== $value ) { $clean[ $key ] = $value; } } if ( empty( $clean ) ) { delete_post_meta( $post_id, self::PREVIEWS ); return; } update_post_meta( $post_id, self::PREVIEWS, $clean ); } /** * Social override fields. * * @return array */ public static function social_keys(): array { return array( 'og_title', 'og_desc', 'og_image', 'tw_title', 'tw_desc', 'tw_image', 'tw_card' ); } /** * Raw stored social overrides, always shaped with every key. * * @return array */ public static function social( int $post_id ): array { $stored = get_post_meta( $post_id, self::SOCIAL, true ); $out = array(); foreach ( self::social_keys() as $key ) { $out[ $key ] = is_array( $stored ) && isset( $stored[ $key ] ) ? (string) $stored[ $key ] : ''; } return $out; } /** * Sanitize and store social overrides. * * @param array $input Raw input. */ public static function save_social( int $post_id, array $input ): void { $clean = array(); foreach ( self::social_keys() as $key ) { $value = isset( $input[ $key ] ) ? (string) $input[ $key ] : ''; if ( '' === trim( $value ) ) { continue; } if ( 'og_image' === $key || 'tw_image' === $key ) { $clean[ $key ] = esc_url_raw( $value ); continue; } if ( 'tw_card' === $key ) { $clean[ $key ] = in_array( $value, array( 'summary', 'summary_large_image' ), true ) ? $value : ''; continue; } $clean[ $key ] = sanitize_text_field( $value ); } $clean = array_filter( $clean, static fn( $v ) => '' !== $v ); if ( empty( $clean ) ) { delete_post_meta( $post_id, self::SOCIAL ); return; } update_post_meta( $post_id, self::SOCIAL, $clean ); } /** * The social card exactly as Facebook, LinkedIn and X will render it — * used both for the head output and for the editor preview, so what the * customer sees in the preview is byte-for-byte what ships. * * @param string $fallback_title Resolved document title. * @param string $fallback_desc Resolved meta description. * @return array */ public static function social_card( WP_Post $post, string $fallback_title, string $fallback_desc ): array { $stored = self::social( $post->ID ); // Fallback chain: per-post override → featured image (full size, so // crawlers get the real resolution) → site-wide default share image. $thumb = (string) get_the_post_thumbnail_url( $post, 'full' ); if ( '' === $thumb && class_exists( \AiRankSuite\Modules\Social\SocialModule::class ) ) { $thumb = \AiRankSuite\Modules\Social\SocialModule::default_image(); } $og_image = '' !== $stored['og_image'] ? $stored['og_image'] : $thumb; $tw_image = '' !== $stored['tw_image'] ? $stored['tw_image'] : $og_image; $card = $stored['tw_card']; if ( '' === $card ) { $card = '' !== $tw_image ? 'summary_large_image' : 'summary'; } $meta = class_exists( \AiRankSuite\Modules\Social\SocialModule::class ) ? \AiRankSuite\Modules\Social\SocialModule::image_meta( $og_image ) : null; $warning = ''; if ( '' === $og_image ) { $warning = 'none'; } elseif ( null !== $meta && ( $meta['width'] < 600 || $meta['height'] < 315 ) ) { $warning = 'too_small'; // Below Facebook's minimum — link previews degrade. } elseif ( null !== $meta && ( $meta['width'] < 1200 || $meta['height'] < 630 ) ) { $warning = 'small'; // Renders, but blurry on high-DPI feeds. } return array( 'og_title' => '' !== $stored['og_title'] ? $stored['og_title'] : $fallback_title, 'og_desc' => '' !== $stored['og_desc'] ? $stored['og_desc'] : $fallback_desc, 'og_image' => $og_image, 'tw_title' => '' !== $stored['tw_title'] ? $stored['tw_title'] : ( '' !== $stored['og_title'] ? $stored['og_title'] : $fallback_title ), 'tw_desc' => '' !== $stored['tw_desc'] ? $stored['tw_desc'] : ( '' !== $stored['og_desc'] ? $stored['og_desc'] : $fallback_desc ), 'tw_image' => $tw_image, 'tw_card' => $card, 'image_meta' => $meta, 'image_warn' => $warning, ); } }