| @@ -1273,9 +1273,8 @@ | ||
| 1273 | 1273 | |
| 1274 | 1274 | # DEDUPLICATION: Remove duplicate <title>, meta description, OG, Twitter tags, canonical, and JSON-LD schema |
| 1275 | 1275 | $result_html = $this->otto_guard_html($this->deduplicate_title_tags($result_html), $result_html, 'deduplicate_title_tags'); |
| 1276 | 1276 | $result_html = $this->otto_guard_html($this->deduplicate_description_tags($result_html), $result_html, 'deduplicate_description_tags'); |
| 1277 | - $result_html = $this->otto_guard_html($this->deduplicate_keywords_tags($result_html), $result_html, 'deduplicate_keywords_tags'); | |
| 1278 | 1277 | $result_html = $this->otto_guard_html($this->deduplicate_og_twitter_tags($result_html), $result_html, 'deduplicate_og_twitter_tags'); |
| 1279 | 1278 | $result_html = $this->otto_guard_html($this->apply_metabox_og_precedence($result_html), $result_html, 'apply_metabox_og_precedence'); |
| 1280 | 1279 | $result_html = $this->otto_guard_html($this->apply_custom_seo_precedence($result_html), $result_html, 'apply_custom_seo_precedence'); |
| 1281 | 1280 | $result_html = $this->otto_guard_html($this->deduplicate_schema_tags($result_html), $result_html, 'deduplicate_schema_tags'); |
| @@ -2240,18 +2239,9 @@ | ||
| 2240 | 2239 | $marker_attribute = $marker === 'otto' |
| 2241 | 2240 | ? ' data-metasync-otto="true"' |
| 2242 | 2241 | : ' data-metasync-seo="custom"'; |
| 2243 | 2242 | $tag = '<meta ' . $attr . '="' . $val . '" content="' . $content_escaped . '"' . $marker_attribute . ' />'; |
| 2244 | - # The optional prefix group must end on whitespace or a quote so the | |
| 2245 | - # attribute matched is $attr itself, not something merely ending in it: | |
| 2246 | - # a bare [^>]*name= also matches data-name="keywords", so a third party's | |
| 2247 | - # unrelated tag was consumed and rewritten while the real one survived — | |
| 2248 | - # destroying their value AND leaving the duplicate the dedup had just | |
| 2249 | - # collapsed. Making the group optional keeps the common | |
| 2250 | - # <meta name="..." ...> form matching, where <meta\s has already consumed | |
| 2251 | - # the only separator. Same rule as deduplicate_keywords_tags(); this | |
| 2252 | - # helper serves title, description and keywords, so all three are covered. | |
| 2253 | - $pattern = '/<meta\s(?:[^>]*[\s"\'])?' . preg_quote($attr, '/') . '\s*=\s*["\']' . preg_quote($val, '/') . '["\'][^>]*\/?>/i'; | |
| 2243 | + $pattern = '/<meta\s[^>]*' . preg_quote($attr, '/') . '\s*=\s*["\']' . preg_quote($val, '/') . '["\'][^>]*\/?>/i'; | |
| 2254 | 2244 | $count = 0; |
| 2255 | 2245 | $new = preg_replace_callback($pattern, function ($m) use ($tag) { |
| 2256 | 2246 | return $tag; |
| 2257 | 2247 | }, $html, 1, $count); |
| @@ -2282,10 +2272,9 @@ | ||
| 2282 | 2272 | return class_exists('Metasync_Seo_Precedence'); |
| 2283 | 2273 | } |
| 2284 | 2274 | |
| 2285 | 2275 | /** |
| 2286 | - * Enforce the resolved SEO title, description and keywords as the FINAL | |
| 2287 | - * word over OTTO. | |
| 2276 | + * Enforce the resolved SEO title/description as the FINAL word over OTTO. | |
| 2288 | 2277 | * |
| 2289 | 2278 | * The resolver owns the global custom-versus-OTTO order. This final buffer |
| 2290 | 2279 | * pass must use it too: otherwise the setting works through the sidebar |
| 2291 | 2280 | * and third-party filters but OTTO's later string pass puts the custom |
| @@ -2355,40 +2344,8 @@ | ||
| 2355 | 2344 | : 'custom'; |
| 2356 | 2345 | $html = $this->force_custom_meta($html, 'name', 'description', $esc, $marker); |
| 2357 | 2346 | } |
| 2358 | 2347 | |
| 2359 | - # Keywords — the same final pass as the description above, on the chain | |
| 2360 | - # the resolver owns, so the global priority setting governs this tag | |
| 2361 | - # too. OTTO's keywords tag arrives via header_html_insertion carrying | |
| 2362 | - # a data-otto marker and is spliced in after wp_head, so it is always | |
| 2363 | - # the later tag and always wins deduplicate_keywords_tags(); forcing | |
| 2364 | - # the resolved winner here is what keeps the losing tier reachable at | |
| 2365 | - # all — the single-printing-owner rule the description branch follows. | |
| 2366 | - # | |
| 2367 | - # The tier this forces is NOT guaranteed customer-typed, unlike the | |
| 2368 | - # title and description keys above: the meta_keywords persistence flag | |
| 2369 | - # writes OTTO's value straight to _metasync_focus_keyword, and neither | |
| 2370 | - # that conditional write nor the clone cleaner (which strips only the | |
| 2371 | - # _metasync_otto_* keys) ever clears it, so a stale flag copy can | |
| 2372 | - # outrank a newer suggestion under custom-first. That key sharing is | |
| 2373 | - # tracked separately — the fix is a key of its own for the persisted | |
| 2374 | - # keyword, not a special case here. | |
| 2375 | - $resolved_keyword = Metasync_Seo_Precedence::resolve( | |
| 2376 | - $post_id, | |
| 2377 | - Metasync_Seo_Precedence::FIELD_FOCUS_KEYWORD | |
| 2378 | - ); | |
| 2379 | - if (!empty($resolved_keyword['value'])) { | |
| 2380 | - $esc = htmlspecialchars($resolved_keyword['value'], ENT_QUOTES, 'UTF-8'); | |
| 2381 | - $marker = Metasync_Seo_Precedence::is_otto_value( | |
| 2382 | - $post_id, | |
| 2383 | - Metasync_Seo_Precedence::FIELD_FOCUS_KEYWORD, | |
| 2384 | - $resolved_keyword | |
| 2385 | - ) | |
| 2386 | - ? 'otto' | |
| 2387 | - : 'custom'; | |
| 2388 | - $html = $this->force_custom_meta($html, 'name', 'keywords', $esc, $marker); | |
| 2389 | - } | |
| 2390 | - | |
| 2391 | 2348 | return $html; |
| 2392 | 2349 | } |
| 2393 | 2350 | |
| 2394 | 2351 | # this function does the header replacements |
| @@ -3133,82 +3090,8 @@ | ||
| 3133 | 3090 | return $html; |
| 3134 | 3091 | } |
| 3135 | 3092 | |
| 3136 | 3093 | /** |
| 3137 | - * Remove duplicate <meta name="keywords"> tags from HTML. | |
| 3138 | - * | |
| 3139 | - * OTTO ships its keywords tag inside header_html_insertion, and MetaSync's | |
| 3140 | - * own SEO output now emits one too whenever the precedence chain resolves a | |
| 3141 | - * keyword — so a server-side-rendered page can carry both. | |
| 3142 | - * | |
| 3143 | - * The keeper is OTTO's tag, or the first occurrence when no OTTO tag is | |
| 3144 | - * present. Unlike deduplicate_description_tags() there is no | |
| 3145 | - * customer-authored branch here: print_metatag() emits no data attribute, so | |
| 3146 | - * a keywords tag never carries data-metasync-seo="custom" at this point in | |
| 3147 | - * the pipeline. The customer tier is re-applied afterwards by | |
| 3148 | - * apply_custom_seo_precedence(), which runs later and stamps the marker via | |
| 3149 | - * force_custom_meta(). Collapsing to OTTO's tag here is therefore safe: it is | |
| 3150 | - * not the final word on what the page renders. | |
| 3151 | - * | |
| 3152 | - * @param string $html Full HTML document. | |
| 3153 | - * @return string HTML with at most one <meta name="keywords">. | |
| 3154 | - */ | |
| 3155 | - private function deduplicate_keywords_tags($html) { | |
| 3156 | - # No is_string() guard, unlike deduplicate_description_tags() beside it. | |
| 3157 | - # That asymmetry is forced, not an oversight: @param string makes the | |
| 3158 | - # check provably always-true at PHPStan level 5, and the description | |
| 3159 | - # sibling's copy is one of a fixed number of occurrences the baseline | |
| 3160 | - # pins by count, so adding another fails the build. Both methods are | |
| 3161 | - # only ever called with the buffer string. | |
| 3162 | - if ($html === '') { | |
| 3163 | - return $html; | |
| 3164 | - } | |
| 3165 | - | |
| 3166 | - # Match <meta ... name="keywords" ...> in either attribute order. | |
| 3167 | - # | |
| 3168 | - # The optional prefix group must end on whitespace or a quote so the | |
| 3169 | - # attribute is `name`, not something merely ending in it: a bare | |
| 3170 | - # [^>]*name= also matches data-name="keywords". Making the group | |
| 3171 | - # optional keeps the common `<meta name="keywords" ...>` form matching, | |
| 3172 | - # where <meta\s has already consumed the only separator. | |
| 3173 | - # | |
| 3174 | - # The opening quote must be immediately followed by "keywords", so | |
| 3175 | - # name="news_keywords" is not matched. | |
| 3176 | - $pattern = '/<meta\s(?:[^>]*[\s"\'])?name\s*=\s*["\']keywords["\'][^>]*\/?>/i'; | |
| 3177 | - | |
| 3178 | - if (preg_match_all($pattern, $html, $matches) <= 1) { | |
| 3179 | - return $html; # 0 or 1 — nothing to deduplicate | |
| 3180 | - } | |
| 3181 | - | |
| 3182 | - $all_tags = $matches[0]; | |
| 3183 | - | |
| 3184 | - $otto_tag = null; | |
| 3185 | - foreach ($all_tags as $tag) { | |
| 3186 | - if (stripos($tag, 'data-otto-pixel') !== false || | |
| 3187 | - stripos($tag, 'data-metasync-otto') !== false || | |
| 3188 | - stripos($tag, 'data-otto') !== false | |
| 3189 | - ) { | |
| 3190 | - $otto_tag = $tag; | |
| 3191 | - break; | |
| 3192 | - } | |
| 3193 | - } | |
| 3194 | - $keeper = $otto_tag ?: $all_tags[0]; | |
| 3195 | - | |
| 3196 | - # Callback form avoids backreference injection when the keyword list | |
| 3197 | - # contains $ followed by digits. | |
| 3198 | - $first_replaced = false; | |
| 3199 | - $html = preg_replace_callback($pattern, function ($m) use ($keeper, &$first_replaced) { | |
| 3200 | - if (!$first_replaced) { | |
| 3201 | - $first_replaced = true; | |
| 3202 | - return $keeper; | |
| 3203 | - } | |
| 3204 | - return ''; # Remove subsequent duplicates | |
| 3205 | - }, $html); | |
| 3206 | - | |
| 3207 | - return $html; | |
| 3208 | - } | |
| 3209 | - | |
| 3210 | - /** | |
| 3211 | 3094 | * Remove duplicate <link rel="canonical"> tags from HTML. |
| 3212 | 3095 | * |
| 3213 | 3096 | * When OTTO injects a canonical via header_html_insertion and MetaSync's |
| 3214 | 3097 | * SEO output (or WordPress core) has already emitted one, keep only the |
| @@ -4154,9 +4037,8 @@ | ||
| 4154 | 4037 | |
| 4155 | 4038 | # DEDUPLICATION: Remove duplicate <title>, meta description, OG, Twitter tags, canonical, and JSON-LD schema |
| 4156 | 4039 | $result_html = $this->otto_guard_html($this->deduplicate_title_tags($result_html), $result_html, 'deduplicate_title_tags'); |
| 4157 | 4040 | $result_html = $this->otto_guard_html($this->deduplicate_description_tags($result_html), $result_html, 'deduplicate_description_tags'); |
| 4158 | - $result_html = $this->otto_guard_html($this->deduplicate_keywords_tags($result_html), $result_html, 'deduplicate_keywords_tags'); | |
| 4159 | 4041 | $result_html = $this->otto_guard_html($this->deduplicate_og_twitter_tags($result_html), $result_html, 'deduplicate_og_twitter_tags'); |
| 4160 | 4042 | $result_html = $this->otto_guard_html($this->apply_metabox_og_precedence($result_html), $result_html, 'apply_metabox_og_precedence'); |
| 4161 | 4043 | $result_html = $this->otto_guard_html($this->apply_custom_seo_precedence($result_html), $result_html, 'apply_custom_seo_precedence'); |
| 4162 | 4044 | $result_html = $this->otto_guard_html($this->deduplicate_schema_tags($result_html), $result_html, 'deduplicate_schema_tags'); |