PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
← All changes | otto/Otto_html_class.php +2 -120 2.7.2 → 2.7.1 View file →
@@ -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');