| @@ -74,9 +74,12 @@ | ||
| 74 | 74 | 'image_min_width' => 600, |
| 75 | 75 | 'image_min_height' => 900, |
| 76 | 76 | 'image_recommended_ratio' => 0.67, // 2:3 ratio |
| 77 | 77 | 'title_max_length' => 100, |
| 78 | - 'description_max_length' => 500 | |
| 78 | + // Pinterest has no tag of its own: it reads og:description, which | |
| 79 | + // the frontend caps at max_description_length. Previewing 500 | |
| 80 | + // promised up to 340 characters that are never emitted. | |
| 81 | + 'description_max_length' => 160 | |
| 79 | 82 | ], |
| 80 | 83 | 'whatsapp' => [ |
| 81 | 84 | 'og_required' => ['og:title', 'og:type', 'og:image', 'og:url'], |
| 82 | 85 | 'og_recommended' => ['og:description'], |
| @@ -145,14 +148,23 @@ | ||
| 145 | 148 | // og:title must render the full resolved Open Graph title. The 60-char |
| 146 | 149 | // cap in optimize_title_for_platform() is an SEO-title recommendation for |
| 147 | 150 | // search results and does not apply to the og:title social tag, so use the |
| 148 | 151 | // resolved title verbatim (falling back to the site name when empty). |
| 149 | - $og_tags['og:title'] = ($data['title'] ?? '') !== '' ? $data['title'] : get_bloginfo('name'); | |
| 152 | + // Stripped: a title carrying markup is attribute-escaped into this tag, | |
| 153 | + // so the reader sees a literal `<em>` rather than emphasis (#640). | |
| 154 | + $og_tags['og:title'] = \ThinkRank\Frontend\SEO_Manager::strip_title_tags( | |
| 155 | + ($data['title'] ?? '') !== '' ? (string) $data['title'] : (string) get_bloginfo('name') | |
| 156 | + ); | |
| 150 | 157 | // `?:` rather than `??`: the key is always present, seeded as '', so the |
| 151 | 158 | // null-coalesce could never reach the fallback. og:url was emitted empty |
| 152 | 159 | // and then dropped by the !empty() guard in output_social_og_tags(), |
| 153 | 160 | // which is why archives carried no og:url at all (#388). |
| 154 | - $og_tags['og:url'] = ($data['url'] ?? '') !== '' ? $data['url'] : $this->get_current_url(); | |
| 161 | + // Normalized for the site's scheme preference, so og:url and the | |
| 162 | + // canonical can never disagree about http vs https (#638); the same | |
| 163 | + // consistency #182 was about. | |
| 164 | + $og_tags['og:url'] = \ThinkRank\SEO\Url_Scheme::apply( | |
| 165 | + ($data['url'] ?? '') !== '' ? $data['url'] : $this->get_current_url() | |
| 166 | + ); | |
| 155 | 167 | |
| 156 | 168 | // Image handling with optimization |
| 157 | 169 | if (!empty($data['image'])) { |
| 158 | 170 | $optimized_image = $this->optimize_image_for_platform($data['image'], $platform); |
| @@ -219,9 +231,11 @@ | ||
| 219 | 231 | // to the resolved og:title/SEO title. Render it in full — the length cap |
| 220 | 232 | // in optimize_title_for_platform() is an SEO-title recommendation for |
| 221 | 233 | // search results, not a rule for the twitter:title social tag. |
| 222 | 234 | $twitter_title = ($data['twitter_title'] ?? '') !== '' ? $data['twitter_title'] : ($data['title'] ?? ''); |
| 223 | - $twitter_tags['twitter:title'] = $twitter_title !== '' ? $twitter_title : get_bloginfo('name'); | |
| 235 | + $twitter_tags['twitter:title'] = \ThinkRank\Frontend\SEO_Manager::strip_title_tags( | |
| 236 | + $twitter_title !== '' ? (string) $twitter_title : (string) get_bloginfo('name') | |
| 237 | + ); | |
| 224 | 238 | |
| 225 | 239 | // Recommended tags. Prefer a per-object Twitter-specific description, |
| 226 | 240 | // falling back to the resolved OG/meta description — mirroring the |
| 227 | 241 | // twitter:title cascade above. This lookup did not exist, so a Twitter |
| @@ -954,8 +968,12 @@ | ||
| 954 | 968 | |
| 955 | 969 | $settings = $this->get_settings($context_type, $context_id); |
| 956 | 970 | $output = [ |
| 957 | 971 | 'og_tags' => [], |
| 972 | + // Secondary og:image entries. A separate list because $og_tags is | |
| 973 | + // keyed by property name and so can hold exactly one 'og:image' | |
| 974 | + // (#636); the emitter writes these straight after the primary. | |
| 975 | + 'og_extra_images' => [], | |
| 958 | 976 | 'twitter_tags' => [], |
| 959 | 977 | 'meta_tags' => [], |
| 960 | 978 | 'platform_tags' => [], |
| 961 | 979 | // Per-feature flags so each emitter can honor its own toggle. The |
| @@ -989,8 +1007,18 @@ | ||
| 989 | 1007 | // Add custom OG tags |
| 990 | 1008 | if (!empty($settings['custom_og_tags'])) { |
| 991 | 1009 | $output['og_tags'] = array_merge($output['og_tags'], $settings['custom_og_tags']); |
| 992 | 1010 | } |
| 1011 | + | |
| 1012 | + // Alternatives to offer after the primary image. Collected from the | |
| 1013 | + // resolved primary rather than re-deriving it, so the two can never | |
| 1014 | + // disagree about which image is the main one. | |
| 1015 | + if (!empty($settings['og_multiple_images'])) { | |
| 1016 | + $output['og_extra_images'] = Social_Images::additional( | |
| 1017 | + (int) $context_id, | |
| 1018 | + (string) ($output['og_tags']['og:image'] ?? '') | |
| 1019 | + ); | |
| 1020 | + } | |
| 993 | 1021 | } |
| 994 | 1022 | |
| 995 | 1023 | // Generate Twitter Card tags if enabled |
| 996 | 1024 | if ($twitter_enabled) { |
| @@ -1026,8 +1054,33 @@ | ||
| 1026 | 1054 | 'default_image', |
| 1027 | 1055 | ]; |
| 1028 | 1056 | |
| 1029 | 1057 | /** |
| 1058 | + * Site-wide switches with no per-context equivalent. | |
| 1059 | + * | |
| 1060 | + * Unlike INHERITED_SITE_KEYS above, these must inherit their site value | |
| 1061 | + * even when it is FALSE. The empty()-guarded loop used for images can only | |
| 1062 | + * ever propagate "on", which is right for an image URL (empty means "not | |
| 1063 | + * configured") and wrong for a boolean, where false is a deliberate choice. | |
| 1064 | + * | |
| 1065 | + * Without this the master Open Graph / Twitter Cards switches were honoured | |
| 1066 | + * on the homepage (mapped to the `site` context) and ignored on every post | |
| 1067 | + * and page, which read as though the toggle had worked (#557). | |
| 1068 | + * | |
| 1069 | + * @since 2.2.0 | |
| 1070 | + * @var string[] | |
| 1071 | + */ | |
| 1072 | + private const SITE_ONLY_KEYS = [ | |
| 1073 | + 'enable_open_graph', | |
| 1074 | + 'enable_twitter_cards', | |
| 1075 | + // Same shape as the two above and the same trap: a site-wide switch | |
| 1076 | + // with no per-context equivalent. Left out, it read as on while the | |
| 1077 | + // homepage rendered and as off on every post and page — which is where | |
| 1078 | + // the alternatives it controls actually come from (#636). | |
| 1079 | + 'og_multiple_images', | |
| 1080 | + ]; | |
| 1081 | + | |
| 1082 | + /** | |
| 1030 | 1083 | * Get settings for a context, inheriting the site-wide default images |
| 1031 | 1084 | * |
| 1032 | 1085 | * Two corrections over the generic lookup: |
| 1033 | 1086 | * |
| @@ -1060,8 +1113,18 @@ | ||
| 1060 | 1113 | return $settings; |
| 1061 | 1114 | } |
| 1062 | 1115 | |
| 1063 | 1116 | $site_settings = parent::get_settings('site', null); |
| 1117 | + | |
| 1118 | + // Site-wide switches: the site value is authoritative, false included. | |
| 1119 | + // array_key_exists, not empty() — '' is how a disabled toggle is stored. | |
| 1120 | + foreach (self::SITE_ONLY_KEYS as $key) { | |
| 1121 | + if (array_key_exists($key, $site_settings)) { | |
| 1122 | + $settings[$key] = $site_settings[$key]; | |
| 1123 | + } | |
| 1124 | + } | |
| 1125 | + | |
| 1126 | + // Default images: inherit only when this context has none of its own. | |
| 1064 | 1127 | foreach (self::INHERITED_SITE_KEYS as $key) { |
| 1065 | 1128 | if (empty($settings[$key]) && !empty($site_settings[$key])) { |
| 1066 | 1129 | $settings[$key] = $site_settings[$key]; |
| 1067 | 1130 | } |
| @@ -1097,8 +1160,12 @@ | ||
| 1097 | 1160 | 'og_locale' => '', |
| 1098 | 1161 | 'default_og_image' => '', |
| 1099 | 1162 | 'og_image_width' => 1200, |
| 1100 | 1163 | 'og_image_height' => 630, |
| 1164 | + // Offer alternative og:image tags after the primary one (#636). | |
| 1165 | + // Off by default: a page that shares with one image today must | |
| 1166 | + // keep sharing with that image after an update. | |
| 1167 | + 'og_multiple_images' => false, | |
| 1101 | 1168 | |
| 1102 | 1169 | // Twitter Cards settings |
| 1103 | 1170 | 'enable_twitter_cards' => true, |
| 1104 | 1171 | 'twitter_username' => '', |
| @@ -1138,8 +1205,15 @@ | ||
| 1138 | 1205 | 'fallback_to_excerpt' => true, |
| 1139 | 1206 | 'strip_html_tags' => true, |
| 1140 | 1207 | 'max_description_length' => 160, |
| 1141 | 1208 | |
| 1209 | + // oEmbed card (#637). All three default off: this rewrites what | |
| 1210 | + // other people's sites display, so an upgrade must not silently | |
| 1211 | + // change a card an existing embed has been showing for months. | |
| 1212 | + 'oembed_use_seo_title' => false, | |
| 1213 | + 'oembed_use_social_image' => false, | |
| 1214 | + 'oembed_remove_author' => false, | |
| 1215 | + | |
| 1142 | 1216 | // Legacy format for backward compatibility (only when needed) |
| 1143 | 1217 | 'custom_og_tags' => [], |
| 1144 | 1218 | 'image_optimization' => true, |
| 1145 | 1219 | 'auto_generate' => true |
| @@ -1768,8 +1842,12 @@ | ||
| 1768 | 1842 | $image_id = get_post_thumbnail_id($post); |
| 1769 | 1843 | if ($image_id) { |
| 1770 | 1844 | $image_data = wp_get_attachment_image_src($image_id, 'large'); |
| 1771 | 1845 | if (!empty($image_data[0])) { |
| 1846 | + // The ID is in hand here and gone once this returns a | |
| 1847 | + // bare URL; say so rather than have the tag builders look | |
| 1848 | + // it up again, twice, with a URL core cannot match (#847). | |
| 1849 | + Attachment_Lookup::remember((string) $image_data[0], (int) $image_id); | |
| 1772 | 1850 | return $image_data[0]; |
| 1773 | 1851 | } |
| 1774 | 1852 | } |
| 1775 | 1853 | } |
| @@ -1816,8 +1894,12 @@ | ||
| 1816 | 1894 | $image_id = get_post_thumbnail_id($post); |
| 1817 | 1895 | if ($image_id) { |
| 1818 | 1896 | $image_data = wp_get_attachment_image_src($image_id, 'large'); |
| 1819 | 1897 | if (!empty($image_data[0])) { |
| 1898 | + // The ID is in hand here and gone once this returns a | |
| 1899 | + // bare URL; say so rather than have the tag builders look | |
| 1900 | + // it up again, twice, with a URL core cannot match (#847). | |
| 1901 | + Attachment_Lookup::remember((string) $image_data[0], (int) $image_id); | |
| 1820 | 1902 | return $image_data[0]; |
| 1821 | 1903 | } |
| 1822 | 1904 | } |
| 1823 | 1905 | } |
| @@ -1860,16 +1942,21 @@ | ||
| 1860 | 1942 | if (function_exists('post_password_required') && post_password_required($post)) { |
| 1861 | 1943 | return '' !== $post->post_excerpt ? $post->post_excerpt : get_bloginfo('description'); |
| 1862 | 1944 | } |
| 1863 | 1945 | |
| 1864 | - // Try excerpt first | |
| 1865 | - $description = get_the_excerpt($post); | |
| 1946 | + // Try excerpt first. On a Bricks page core would derive that excerpt | |
| 1947 | + // from the `post_content` Bricks throws away, so the visible body is | |
| 1948 | + // used instead — a hand-written excerpt still wins (#651). | |
| 1949 | + $superseding = Builder_Content::superseding_excerpt_source($post); | |
| 1950 | + $description = '' !== $superseding | |
| 1951 | + ? Pattern_Resolver::derive_excerpt($superseding, 30) | |
| 1952 | + : get_the_excerpt($post); | |
| 1866 | 1953 | |
| 1867 | 1954 | // If no excerpt, generate from content. Shortcodes and block delimiters |
| 1868 | 1955 | // are removed the way core's wp_trim_excerpt() does, so a shortcode-built |
| 1869 | 1956 | // page does not publish its source as og:description (#387). |
| 1870 | 1957 | if (empty($description)) { |
| 1871 | - $description = Pattern_Resolver::derive_excerpt((string) $post->post_content, 30); | |
| 1958 | + $description = Pattern_Resolver::derive_excerpt(Builder_Content::visible_content($post), 30); | |
| 1872 | 1959 | } |
| 1873 | 1960 | |
| 1874 | 1961 | // If still empty, use site description |
| 1875 | 1962 | if (empty($description)) { |
| @@ -2044,16 +2131,13 @@ | ||
| 2044 | 2131 | if (mb_strlen($title) <= $max_length) { |
| 2045 | 2132 | return $title; |
| 2046 | 2133 | } |
| 2047 | 2134 | |
| 2048 | - // Truncate at word boundary | |
| 2049 | - $truncated = wp_trim_words($title, 10, ''); | |
| 2050 | - if (mb_strlen($truncated) <= $max_length) { | |
| 2051 | - return $truncated; | |
| 2052 | - } | |
| 2053 | - | |
| 2054 | - // Hard truncate if necessary | |
| 2055 | - return mb_substr($title, 0, $max_length - 3) . '...'; | |
| 2135 | + // Truncate at a word boundary where there is one, and hard-cut where | |
| 2136 | + // there is not. This used to try wp_trim_words() first, which counts | |
| 2137 | + // CHARACTERS on th/ja/zh_* — so it returned ~10 characters, passed the | |
| 2138 | + // $max_length check below, and that was accepted as the title (#687). | |
| 2139 | + return \ThinkRank\Core\Seo_Text::trim_to_length($title, $max_length); | |
| 2056 | 2140 | } |
| 2057 | 2141 | |
| 2058 | 2142 | /** |
| 2059 | 2143 | * Optimize description for platform requirements |
| @@ -2076,16 +2160,14 @@ | ||
| 2076 | 2160 | if (mb_strlen($description) <= $max_length) { |
| 2077 | 2161 | return $description; |
| 2078 | 2162 | } |
| 2079 | 2163 | |
| 2080 | - // Truncate at word boundary | |
| 2081 | - $truncated = wp_trim_words($description, 25, ''); | |
| 2082 | - if (mb_strlen($truncated) <= $max_length) { | |
| 2083 | - return $truncated; | |
| 2084 | - } | |
| 2085 | - | |
| 2086 | - // Hard truncate if necessary | |
| 2087 | - return mb_substr($description, 0, $max_length - 3) . '...'; | |
| 2164 | + // Truncate at a word boundary where there is one, and hard-cut where | |
| 2165 | + // there is not. This used to try wp_trim_words() first, which counts | |
| 2166 | + // words in English but CHARACTERS in th/ja/zh_* — so on those locales | |
| 2167 | + // it returned ~25 characters, comfortably under $max_length, and that | |
| 2168 | + // was accepted as the answer (#687). | |
| 2169 | + return \ThinkRank\Core\Seo_Text::trim_to_length($description, $max_length); | |
| 2088 | 2170 | } |
| 2089 | 2171 | |
| 2090 | 2172 | /** |
| 2091 | 2173 | * Optimize image for platform requirements |
| @@ -2110,21 +2192,23 @@ | ||
| 2110 | 2192 | if (empty($image_url)) { |
| 2111 | 2193 | return $image_data; |
| 2112 | 2194 | } |
| 2113 | 2195 | |
| 2114 | - // Get image metadata | |
| 2115 | - $attachment_id = attachment_url_to_postid($image_url); | |
| 2196 | + // Get image metadata. The dimensions are those of the file this URL | |
| 2197 | + // names — usually a generated size — not of the original upload, so | |
| 2198 | + // the width and height published beside it describe the same image. | |
| 2199 | + $attachment_id = Attachment_Lookup::id_from_url($image_url); | |
| 2116 | 2200 | if ($attachment_id) { |
| 2117 | 2201 | $image_meta = wp_get_attachment_metadata($attachment_id); |
| 2118 | 2202 | $image_alt = get_post_meta($attachment_id, '_wp_attachment_image_alt', true); |
| 2119 | - $mime_type = get_post_mime_type($attachment_id); | |
| 2203 | + $image_file = Attachment_Lookup::describe($attachment_id, $image_url); | |
| 2120 | 2204 | |
| 2121 | 2205 | if ($image_meta && isset($image_meta['width'], $image_meta['height'])) { |
| 2122 | - $width = (int) $image_meta['width']; | |
| 2123 | - $height = (int) $image_meta['height']; | |
| 2206 | + $width = $image_file['width']; | |
| 2207 | + $height = $image_file['height']; | |
| 2124 | 2208 | |
| 2125 | 2209 | $image_data['alt'] = $image_alt ?: ''; |
| 2126 | - $image_data['type'] = $mime_type ?: ''; | |
| 2210 | + $image_data['type'] = $image_file['type']; | |
| 2127 | 2211 | |
| 2128 | 2212 | // SVGs and other vector uploads store 0x0 metadata. Dimension |
| 2129 | 2213 | // checks are meaningless there and dividing by 0 is fatal. |
| 2130 | 2214 | if ($width > 0 && $height > 0) { |
| @@ -2176,8 +2260,9 @@ | ||
| 2176 | 2260 | $custom_logo_id = get_theme_mod('custom_logo'); |
| 2177 | 2261 | if ($custom_logo_id) { |
| 2178 | 2262 | $logo_data = wp_get_attachment_image_src($custom_logo_id, 'large'); |
| 2179 | 2263 | if ($logo_data) { |
| 2264 | + Attachment_Lookup::remember((string) $logo_data[0], (int) $custom_logo_id); | |
| 2180 | 2265 | return $logo_data[0]; |
| 2181 | 2266 | } |
| 2182 | 2267 | } |
| 2183 | 2268 | |
| @@ -2185,8 +2270,9 @@ | ||
| 2185 | 2270 | $site_icon_id = get_option('site_icon'); |
| 2186 | 2271 | if ($site_icon_id) { |
| 2187 | 2272 | $icon_data = wp_get_attachment_image_src($site_icon_id, 'large'); |
| 2188 | 2273 | if ($icon_data) { |
| 2274 | + Attachment_Lookup::remember((string) $icon_data[0], (int) $site_icon_id); | |
| 2189 | 2275 | return $icon_data[0]; |
| 2190 | 2276 | } |
| 2191 | 2277 | } |
| 2192 | 2278 | |
| @@ -2613,12 +2699,13 @@ | ||
| 2613 | 2699 | private function make_description_catchy(string $description): string { |
| 2614 | 2700 | // TikTok prefers short, catchy descriptions |
| 2615 | 2701 | $catchy_words = ['viral', 'trending', 'must-see', 'epic', 'mind-blowing']; |
| 2616 | 2702 | |
| 2617 | - // Limit to 100 characters for TikTok | |
| 2618 | - if (strlen($description) > 100) { | |
| 2619 | - $description = substr($description, 0, 97) . '...'; | |
| 2620 | - } | |
| 2703 | + // Limit to 100 characters for TikTok. strlen()/substr() count BYTES, | |
| 2704 | + // so this both fired three times too early on Thai/CJK text and cut | |
| 2705 | + // mid-character, emitting a broken UTF-8 sequence rather than a short | |
| 2706 | + // description (#687). | |
| 2707 | + $description = \ThinkRank\Core\Seo_Text::trim_to_length($description, 100); | |
| 2621 | 2708 | |
| 2622 | 2709 | if (!preg_match('/\b(' . implode('|', $catchy_words) . ')\b/i', $description)) { |
| 2623 | 2710 | $description = '🔥 ' . $description; |
| 2624 | 2711 | } |
| @@ -2811,8 +2898,16 @@ | ||
| 2811 | 2898 | private function generate_platform_meta_tags(array $settings): array { |
| 2812 | 2899 | $platform_tags = []; |
| 2813 | 2900 | |
| 2814 | 2901 | $core = \ThinkRank\Core\Settings::instance(); |
| 2902 | + | |
| 2903 | + // One query for all seven instead of one query each. They are | |
| 2904 | + // autoload=off like every thinkrank_* option, so WordPress cannot | |
| 2905 | + // batch them out of `alloptions`, and this runs on every anonymous | |
| 2906 | + // front-end request — on most sites to discover that all seven are | |
| 2907 | + // empty and no tag is emitted at all (#393). | |
| 2908 | + $core->prime(array_keys(self::PLATFORM_META_KEYS)); | |
| 2909 | + | |
| 2815 | 2910 | // Core Settings (decrypted for sensitive keys) wins; the table value is a |
| 2816 | 2911 | // backward-compat fallback for installs that saved these before the UI |
| 2817 | 2912 | // moved to the Social Platforms tab. |
| 2818 | 2913 | $resolve = static function (string $key) use ($core, $settings): string { |
| @@ -2822,53 +2917,38 @@ | ||
| 2822 | 2917 | } |
| 2823 | 2918 | return $value; |
| 2824 | 2919 | }; |
| 2825 | 2920 | |
| 2826 | - // Facebook meta tags | |
| 2827 | - $facebook_app_id = $resolve('facebook_app_id'); | |
| 2828 | - if ('' !== $facebook_app_id) { | |
| 2829 | - $platform_tags['fb:app_id'] = $facebook_app_id; | |
| 2830 | - } | |
| 2921 | + foreach (self::PLATFORM_META_KEYS as $key => $meta_name) { | |
| 2922 | + $value = $resolve($key); | |
| 2831 | 2923 | |
| 2832 | - $facebook_admins = $resolve('facebook_admins'); | |
| 2833 | - if ('' !== $facebook_admins) { | |
| 2834 | - $platform_tags['fb:admins'] = $facebook_admins; | |
| 2924 | + if ('' !== $value) { | |
| 2925 | + $platform_tags[$meta_name] = $value; | |
| 2926 | + } | |
| 2835 | 2927 | } |
| 2836 | 2928 | |
| 2837 | - // Pinterest site verification | |
| 2838 | - $pinterest = $resolve('pinterest_site_verification'); | |
| 2839 | - if ('' !== $pinterest) { | |
| 2840 | - $platform_tags['pinterest-site-verification'] = $pinterest; | |
| 2841 | - } | |
| 2842 | - | |
| 2843 | - // Instagram verification | |
| 2844 | - $instagram = $resolve('instagram_verification'); | |
| 2845 | - if ('' !== $instagram) { | |
| 2846 | - $platform_tags['instagram-site-verification'] = $instagram; | |
| 2847 | - } | |
| 2848 | - | |
| 2849 | - // TikTok verification | |
| 2850 | - $tiktok = $resolve('tiktok_verification'); | |
| 2851 | - if ('' !== $tiktok) { | |
| 2852 | - $platform_tags['tiktok-site-verification'] = $tiktok; | |
| 2853 | - } | |
| 2854 | - | |
| 2855 | - // YouTube channel verification | |
| 2856 | - $youtube = $resolve('youtube_channel_id'); | |
| 2857 | - if ('' !== $youtube) { | |
| 2858 | - $platform_tags['youtube-channel-id'] = $youtube; | |
| 2859 | - } | |
| 2860 | - | |
| 2861 | - // WhatsApp Business verification | |
| 2862 | - $whatsapp = $resolve('whatsapp_business_id'); | |
| 2863 | - if ('' !== $whatsapp) { | |
| 2864 | - $platform_tags['whatsapp-business-id'] = $whatsapp; | |
| 2865 | - } | |
| 2866 | - | |
| 2867 | 2929 | return $platform_tags; |
| 2868 | 2930 | } |
| 2869 | 2931 | |
| 2870 | 2932 | /** |
| 2933 | + * Platform verification settings, mapped to the meta name each is emitted | |
| 2934 | + * under. One list so the batch primed in generate_platform_meta_tags() and | |
| 2935 | + * the keys it then reads cannot drift apart. | |
| 2936 | + * | |
| 2937 | + * @since 2.1.0 | |
| 2938 | + * @var array<string,string> | |
| 2939 | + */ | |
| 2940 | + private const PLATFORM_META_KEYS = [ | |
| 2941 | + 'facebook_app_id' => 'fb:app_id', | |
| 2942 | + 'facebook_admins' => 'fb:admins', | |
| 2943 | + 'pinterest_site_verification' => 'pinterest-site-verification', | |
| 2944 | + 'instagram_verification' => 'instagram-site-verification', | |
| 2945 | + 'tiktok_verification' => 'tiktok-site-verification', | |
| 2946 | + 'youtube_channel_id' => 'youtube-channel-id', | |
| 2947 | + 'whatsapp_business_id' => 'whatsapp-business-id', | |
| 2948 | + ]; | |
| 2949 | + | |
| 2950 | + /** | |
| 2871 | 2951 | * Convert OG, Twitter, and Platform tags to HTML meta tags |
| 2872 | 2952 | * |
| 2873 | 2953 | * @since 1.0.0 |
| 2874 | 2954 | * |
| @@ -2949,13 +3029,14 @@ | ||
| 2949 | 3029 | return $validation; |
| 2950 | 3030 | } |
| 2951 | 3031 | |
| 2952 | 3032 | // Get image metadata if it's a local attachment |
| 2953 | - $attachment_id = attachment_url_to_postid($image_url); | |
| 3033 | + $attachment_id = Attachment_Lookup::id_from_url($image_url); | |
| 2954 | 3034 | if ($attachment_id) { |
| 2955 | 3035 | $image_meta = wp_get_attachment_metadata($attachment_id); |
| 2956 | - $meta_width = isset($image_meta['width']) ? (int) $image_meta['width'] : 0; | |
| 2957 | - $meta_height = isset($image_meta['height']) ? (int) $image_meta['height'] : 0; | |
| 3036 | + $image_file = Attachment_Lookup::describe($attachment_id, $image_url); | |
| 3037 | + $meta_width = $image_file['width']; | |
| 3038 | + $meta_height = $image_file['height']; | |
| 2958 | 3039 | |
| 2959 | 3040 | // SVGs and other vector uploads store 0x0 metadata — skip the |
| 2960 | 3041 | // dimension/ratio checks instead of dividing by 0. |
| 2961 | 3042 | if ($image_meta && $meta_width > 0 && $meta_height > 0) { |