PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
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_pixel_class.php +311 -115 2.6.7 → 2.7.2 View file →
@@ -7,27 +7,24 @@
7 7 /**
8 8 * This class handles the Otto Pixel Functions
9 9 */
10 10 Class Metasync_otto_pixel{
11 -
11 +
12 12 #otto html class
13 13 public $o_html;
14 14
15 - # crawl data option
16 - public $option_name = 'metasync_otto_crawldata';
17 -
18 - # no cache wp pages
15 + # no cache wp pages
19 16 public $no_cache_pages = ['wp-login.php'];
20 17
21 18 # OTTO UUID
22 19 private $otto_uuid;
23 20
24 - #
21 + #
25 22 function __construct($otto_uuid){
26 -
23 +
27 24 # store the UUID
28 25 $this->otto_uuid = $otto_uuid;
29 -
26 +
30 27 # load the html class
31 28 $this->o_html = new Metasync_otto_html($otto_uuid);
32 29
33 30 }
@@ -39,102 +36,8 @@
39 36 # All pages are processed in real-time from OTTO API
40 37 return true;
41 38 }
42 39
43 -
44 - # method to save crawl data into
45 - function save_crawl_data($data){
46 -
47 - # get the option name
48 - $option_name = $this->option_name;
49 -
50 - # handle data
51 - $saved = get_option($option_name);
52 -
53 - # log saved
54 -
55 - # if saved false save
56 - if(empty($saved['urls'])){
57 -
58 - # save the option
59 - update_option($option_name, $data);
60 -
61 - return;
62 - }
63 -
64 - # get new unique list of urls
65 - $new_list = array_unique(array_merge($data['urls'], $saved['urls']));
66 -
67 - # set data
68 - $data['urls'] = $new_list;
69 -
70 - # log saved
71 -
72 - # save the option
73 - update_option($option_name, $data);
74 - }
75 -
76 - /**
77 - * Check if a URL has been crawled by Otto
78 - * - Validates domain against saved domain
79 - * - Ignores query strings
80 - * - Does exact path matching as Otto stores paths exactly as they appear
81 - * @param string $url - Full URL to check
82 - * @return bool - True if URL was crawled, false otherwise
83 - */
84 - function is_url_crawled($url) {
85 - # Ensure valid input
86 - if (empty($url) || !is_string($url)) {
87 - return false;
88 - }
89 -
90 - # Get saved crawl data
91 - $saved = get_option($this->option_name);
92 -
93 - if (
94 - empty($saved) ||
95 - !is_array($saved) ||
96 - empty($saved['domain']) ||
97 - empty($saved['urls']) ||
98 - !is_array($saved['urls'])
99 - ) {
100 - return false;
101 - }
102 -
103 - # Parse saved domain + incoming URL
104 - $saved_domain = parse_url($saved['domain'], PHP_URL_HOST);
105 - $incoming_domain = parse_url($url, PHP_URL_HOST);
106 -
107 - # Domain mismatch - not crawled
108 - if (empty($saved_domain) || empty($incoming_domain) || strcasecmp($saved_domain, $incoming_domain) !== 0) {
109 - return false;
110 - }
111 -
112 - # Parse incoming path (ignore query string)
113 - $parsed_url = parse_url($url);
114 - $url_path = $parsed_url['path'] ?? '/';
115 -
116 - # Ensure path starts with / but don't modify trailing slashes
117 - # Otto stores paths exactly as they appear in URLs
118 - if (substr($url_path, 0, 1) !== '/') {
119 - $url_path = '/' . $url_path;
120 - }
121 -
122 - # Compare against crawled URLs - exact match
123 - foreach ($saved['urls'] as $crawled_url) {
124 - if (!is_string($crawled_url)) {
125 - continue;
126 - }
127 -
128 - # Direct comparison - Otto stores paths exactly as they are
129 - if (strcasecmp($crawled_url, $url_path) === 0) {
130 - return true;
131 - }
132 - }
133 -
134 - return false;
135 - }
136 -
137 40 # get the current route
138 41 function get_route(){
139 42
140 43 # check if we're in an HTTP context
@@ -196,8 +99,36 @@
196 99
197 100 # render route html
198 101 function render_route_html(){
199 102
103 + # Headless frontends do not consume WordPress-rendered HTML, so avoid
104 + # suggestion lookups, buffering and internal fetches altogether.
105 + # Falls back to a guarded direct read rather than skipping the check:
106 + # silently skipping would render OTTO markup on a headless site.
107 + # @phpstan-ignore-next-line function.alreadyNarrowedType
108 + $headless = function_exists('metasync_headless_is_active')
109 + ? metasync_headless_is_active()
110 + : (class_exists('Metasync_Headless_Config') && Metasync_Headless_Config::is_active());
111 +
112 + if ($headless) {
113 + return;
114 + }
115 +
116 + # Clear per-request statics before any path runs. Harmless under PHP-FPM
117 + # (the process ends with the request) but required under persistent-worker
118 + # SAPIs, where a latched flag would silently disable cache capping and
119 + # failure reporting for every later request in that worker.
120 + #
121 + # method_exists() is redundant to static analysis — this MR adds the method,
122 + # so PHPStan proves the call always true. It is kept for the upgrade window:
123 + # during a plugin update an opcache can still hold the previous
124 + # Metasync_Otto_Render_Strategy, where class_exists() passes but the method
125 + # is absent, and an unguarded static call would fatal the page.
126 + # @phpstan-ignore-next-line function.alreadyNarrowedType
127 + if (class_exists('Metasync_Otto_Render_Strategy') && method_exists('Metasync_Otto_Render_Strategy', 'reset_request_state')) {
128 + Metasync_Otto_Render_Strategy::reset_request_state();
129 + }
130 +
200 131 # Disable SG Cache for Brizy pages FIRST - before any other processing
201 132 # Using global function defined in otto_pixel.php
202 133 if (function_exists('metasync_otto_disable_sg_cache_for_brizy')) {
203 134 metasync_otto_disable_sg_cache_for_brizy();
@@ -247,13 +178,36 @@
247 178 # Use get_suggestions directly to track cache status
248 179 $suggestions = $transient_cache->get_suggestions($route, $cache_track_key);
249 180
250 181 if (!$suggestions || !$transient_cache->has_payload($suggestions)) {
251 - # No suggestions available for this URL
252 - # Set header to indicate cache status
253 - if (!headers_sent()) {
254 - $cache_status = Metasync_Otto_Transient_Cache::get_cache_status($cache_track_key);
255 - header('X-MetaSync-OTTO-Cache: ' . ($cache_status ?: 'NO_SUGGESTIONS'));
182 + # No suggestions available for this URL.
183 + $cache_status = Metasync_Otto_Transient_Cache::get_cache_status($cache_track_key);
184 +
185 + # Two very different situations reach this point, and they must be
186 + # treated differently.
187 + #
188 + # A transient failure means the suggestions exist upstream but could
189 + # not be retrieved on this request — the API errored or timed out, the
190 + # per-minute budget was spent, or another worker held the lock. The
191 + # next request will probably succeed. Serving the un-optimized page
192 + # with normal cache headers lets a caching layer store it and hand it
193 + # to everyone until its TTL expires, so cap how long it may be kept.
194 + #
195 + # Everything else here is a legitimate answer: OTTO simply has nothing
196 + # for this URL (NO_SUGGESTIONS, including the 404 case). Most URLs on
197 + # most sites are in that state permanently, so capping those would
198 + # strip caching from the bulk of the site and move that load onto the
199 + # origin. Those stay fully cacheable.
200 + $transient_failures = ['RATE_LIMITED', 'LOCKED', 'API_ERROR'];
201 +
202 + if (in_array($cache_status, $transient_failures, true)
203 + && function_exists('metasync_otto_limit_response_cache')) {
204 + metasync_otto_limit_response_cache($cache_status);
205 + }
206 +
207 + # Set diagnostic headers only while Debug Mode is enabled.
208 + if (!headers_sent() && Metasync_Otto_Render_Strategy::diagnostics_enabled()) {
209 + header('X-MetaSync-OTTO-Cache: ' . Metasync_Otto_Render_Strategy::display_cache_status($cache_status ?: 'NO_SUGGESTIONS'));
256 210 header('X-MetaSync-OTTO-Method: NONE');
257 211 }
258 212 return;
259 213 }
@@ -259,9 +213,15 @@
259 213 }
260 214
261 215 # comment out for fixing pagination issues
262 216 # $route = rtrim($route, '/');
263 -
217 +
218 + # SiteGround's page cache is no longer disabled up-front for every OTTO page.
219 + # The cacheability decision now happens on the HTTP path in render_via_http(),
220 + # AFTER the internal fetch — only session/form pages and incomplete renders are
221 + # blocked, so plain content pages are finally cacheable. (SiteGround always uses
222 + # the HTTP path; the disable is a no-op off SiteGround, so nothing else loses it.)
223 +
264 224 # Analyze what OTTO is providing for SEO plugin blocking
265 225 $blocking_flags = $this->analyze_otto_blocking($suggestions);
266 226
267 227 # Get cache status for headers
@@ -266,8 +226,30 @@
266 226
267 227 # Get cache status for headers
268 228 $cache_status = Metasync_Otto_Transient_Cache::get_cache_status($cache_track_key);
269 229
230 + # POSTMETA BACKSTOP: a MISS means this request already paid for a live
231 + # OTTO fetch, which makes it the cheapest possible place to repair the
232 + # stored SEO meta when the crawl-notify webhook never ran. The sync is
233 + # deferred to a shutdown callback that runs after the response HTML is
234 + # built, so the rendered page and its timing are untouched. A HIT never
235 + # reaches this branch — steady-state requests keep doing zero extra
236 + # work. The class_exists()/method_exists() pair mirrors the guard
237 + # above: during a plugin update an opcache can hold this file without
238 + # the new class, and an unguarded static call would fatal the page.
239 + # The whole hook sits in try/catch: in the mixed-opcache window a missing
240 + # class constant is an Error, and the backstop must never fatal the page.
241 + try {
242 + $is_cache_miss = ($cache_status === Metasync_Otto_Transient_Cache::STATUS_MISS);
243 + # @phpstan-ignore-next-line function.alreadyNarrowedType
244 + if ($is_cache_miss && class_exists('Metasync_Otto_Backstop_Sync') && method_exists('Metasync_Otto_Backstop_Sync', 'maybe_defer_sync')) {
245 + Metasync_Otto_Backstop_Sync::maybe_defer_sync($route, $suggestions, $cache_status);
246 + }
247 + } catch (Throwable $backstop_error) {
248 + // Best-effort repair only; rendering continues untouched.
249 + unset($backstop_error);
250 + }
251 +
270 252 # RENDER PRIORITY:
271 253 # 1. WP Rocket rocket_buffer filter — WP Rocket caches OTTO-modified HTML,
272 254 # so Kinsta also caches the correct version. No exit(), no cache bypass.
273 255 # 2. Output Buffer (fast) — for environments without WP Rocket.
@@ -283,9 +265,9 @@
283 265 # WP ROCKET PATH: highest priority when WP Rocket is active.
284 266 # rocket_buffer fires inside WP Rocket's own cache pipeline, so the
285 267 # cache file WP Rocket writes (and that Kinsta stores) is already
286 268 # OTTO-modified. Eliminates the race where WP Rocket saved pre-OTTO HTML.
287 - if (class_exists('WP_Rocket')) {
269 + if (defined('WP_ROCKET_VERSION') || class_exists('WP_Rocket')) {
288 270 $wp_rocket_compat_mode = Metasync_Otto_Config::get_wp_rocket_compat_mode();
289 271 if ($wp_rocket_compat_mode !== 'http') {
290 272 # disable_otto exits in metasync_start_otto() before we get here, so
291 273 # only 'http' compat mode needs to skip the rocket_buffer path.
@@ -323,8 +305,32 @@
323 305 }
324 306 }
325 307
326 308 /**
309 + * Stop WP Rocket writing the current response to its disk cache.
310 + *
311 + * DONOTCACHEPAGE is not sufficient from inside the rocket_buffer filter: WP
312 + * Rocket reads that constant when it decides whether to buffer the request,
313 + * which has already happened by the time the filter runs. It evaluates
314 + * do_rocket_generate_caching_files immediately before writing the file, so
315 + * that is the only lever still effective at this point.
316 + *
317 + * This matters more than the response headers do. A page served from WP
318 + * Rocket's disk cache is delivered by rewrite rules without invoking PHP, so
319 + * once a bad file is on disk no header can override it — only deleting the
320 + * file or waiting out Rocket's own expiry will clear it.
321 + *
322 + * PHP_INT_MAX priority so a site's own filters cannot re-enable the write.
323 + */
324 + private static function block_rocket_cache_write() {
325 + if (!defined('DONOTCACHEPAGE')) {
326 + define('DONOTCACHEPAGE', true);
327 + }
328 +
329 + add_filter('do_rocket_generate_caching_files', '__return_false', PHP_INT_MAX);
330 + }
331 +
332 + /**
327 333 * Render page using WP Rocket's rocket_buffer filter (PREFERRED for WP Rocket sites)
328 334 *
329 335 * When WP Rocket is active, hooking into rocket_buffer ensures WP Rocket
330 336 * caches the OTTO-modified HTML. This means:
@@ -345,12 +351,12 @@
345 351 # Hook into WP Rocket's HTML buffer at priority 1 so other rocket_buffer
346 352 # filters (minifiers, CDN rewriters, etc.) run on already-OTTO-modified HTML.
347 353 add_filter('rocket_buffer', function($html) use ($o_html, $suggestions, $blocking_flags) {
348 354 if (empty($html) || strlen($html) < 100) {
349 - // Tell WP Rocket not to cache this empty/broken response.
350 - if (!defined('DONOTCACHEPAGE')) {
351 - define('DONOTCACHEPAGE', true);
352 - }
355 + // Tell WP Rocket not to cache this empty/broken response. The
356 + // constant alone is too late here (see block_rocket_cache_write),
357 + // so the write-time filter is applied as well.
358 + self::block_rocket_cache_write();
353 359 return $html;
354 360 }
355 361
356 362 # Only process full HTML documents — skip partials, JSON, error responses
@@ -364,8 +370,23 @@
364 370 if (!empty($blocking_flags)) {
365 371 $data['_otto_blocking'] = $blocking_flags;
366 372 }
367 373
374 + # From here on we hold usable suggestions, so any exit that returns
375 + # $html unmodified is a genuine failure: WP Rocket would write the
376 + # un-optimized markup to its cache file, and whatever sits in front of
377 + # it would then serve that copy. This is worse than the plain buffer
378 + # path, because the bad page is persisted on disk as well as at the
379 + # edge — and this route exists specifically to make the cached copy the
380 + # optimized one.
381 + #
382 + # Note that DONOTCACHEPAGE alone does NOT prevent that write. WP Rocket
383 + # evaluates the constant when it decides whether to buffer the page,
384 + # which has already happened by the time this filter runs. The
385 + # do_rocket_generate_caching_files filter is evaluated immediately
386 + # before the file is written, so it is the only lever still available
387 + # from here. A disk-cached page is served by rewrite rules without
388 + # invoking PHP, so no response header can undo it afterwards.
368 389 try {
369 390 $modified = $o_html->process_html_directly($html, $data);
370 391 # Sanity check: modified HTML must be at least 50% the size of original
371 392 if ($modified && strlen($modified) > strlen($html) * 0.5) {
@@ -370,12 +391,64 @@
370 391 # Sanity check: modified HTML must be at least 50% the size of original
371 392 if ($modified && strlen($modified) > strlen($html) * 0.5) {
372 393 return $modified;
373 394 }
395 +
396 + # An unprocessable document (oversized / over the memory budget) is
397 + # a permanent property of the page, not a failure to retry. Leave it
398 + # fully cacheable — capping it would make the most expensive page on
399 + # the site uncacheable forever for no gain.
400 + if (class_exists('Metasync_Otto_Render_Strategy')
401 + && Metasync_Otto_Render_Strategy::document_was_unprocessable()) {
402 + return $html;
403 + }
404 +
405 + # Either the rewrite returned nothing usable, or the result was so
406 + # much smaller than the input that it is assumed to be mangled.
407 + self::block_rocket_cache_write();
408 + if (function_exists('metasync_otto_limit_response_cache')) {
409 + metasync_otto_limit_response_cache('RENDER_DISCARDED_ROCKET');
410 + }
411 + if (function_exists('metasync_otto_report_render_failure')) {
412 + metasync_otto_report_render_failure(
413 + 'RENDER_DISCARDED_ROCKET',
414 + 'OTTO rewrite produced no usable output on the WP Rocket path',
415 + [
416 + 'original_bytes' => strlen($html),
417 + 'result_bytes' => is_string($modified) ? strlen($modified) : 0,
418 + 'render_method' => 'wp_rocket',
419 + ],
420 + 'warning'
421 + );
422 + }
374 423 } catch (Exception $e) {
375 424 // Fall through — return original HTML on any failure
425 + self::block_rocket_cache_write();
426 + if (function_exists('metasync_otto_limit_response_cache')) {
427 + metasync_otto_limit_response_cache('RENDER_EXCEPTION_ROCKET');
428 + }
429 + if (function_exists('metasync_otto_report_render_failure')) {
430 + metasync_otto_report_render_failure(
431 + 'RENDER_EXCEPTION_ROCKET',
432 + 'OTTO rewrite threw on the WP Rocket path: ' . $e->getMessage(),
433 + ['render_method' => 'wp_rocket', 'exception' => get_class($e)],
434 + 'error'
435 + );
436 + }
376 437 } catch (Error $e) {
377 438 // Fall through — return original HTML on any failure
439 + self::block_rocket_cache_write();
440 + if (function_exists('metasync_otto_limit_response_cache')) {
441 + metasync_otto_limit_response_cache('RENDER_EXCEPTION_ROCKET');
442 + }
443 + if (function_exists('metasync_otto_report_render_failure')) {
444 + metasync_otto_report_render_failure(
445 + 'RENDER_EXCEPTION_ROCKET',
446 + 'OTTO rewrite errored on the WP Rocket path: ' . $e->getMessage(),
447 + ['render_method' => 'wp_rocket', 'error' => get_class($e)],
448 + 'error'
449 + );
450 + }
378 451 }
379 452
380 453 return $html;
381 454 }, 1);
@@ -515,18 +588,96 @@
515 588 private function render_via_http($route, $suggestions, $cache_track_key, $cache_status) {
516 589 # Set method indicator
517 590 Metasync_Otto_Render_Strategy::set_current_method(Metasync_Otto_Render_Strategy::METHOD_HTTP);
518 591
592 + # On Divi sites, skip OTTO for the first HTTP render per page
593 + # per 24h. OTTO's internal wp_remote_get creates corrupted CSS cache
594 + # files in et-cache/{post_id}/ (wrong server context). By returning
595 + # false once, WordPress renders the page normally — building correct
596 + # Divi CSS caches. Subsequent OTTO renders within 24h use those caches.
597 + # The transient stores the activation timestamp so it auto-invalidates
598 + # when the plugin is deactivated/reactivated.
599 + if (defined('ET_CORE_VERSION')) {
600 + $cache_key = 'otto_divi_css_fix_' . md5($route);
601 + $activated = get_option('metasync_activated_at', '');
602 + $cached = get_transient($cache_key);
603 + if ($cached === false || $cached !== $activated) {
604 + set_transient($cache_key, $activated, DAY_IN_SECONDS);
605 + # Deliberate one-off skip, and the next request renders correctly —
606 + # so cap how long this un-optimized copy may be cached rather than
607 + # letting a caching layer pin it for its full TTL.
608 + if (function_exists('metasync_otto_limit_response_cache')) {
609 + metasync_otto_limit_response_cache('DIVI_FIRST_RENDER_SKIP');
610 + }
611 + return false;
612 + }
613 + }
614 +
519 615 # check if we have the route html (pass suggestions to avoid duplicate API call)
520 616 $route_html = $this->get_route_html($route, $cache_track_key, $suggestions);
521 617
522 618 # check that route html is valid
523 619 if(empty($route_html)){
620 + # Fetch failed / empty (e.g. SG Optimizer truncated the internal response) —
621 + # WordPress will serve the un-optimized page. Cap how long that copy may be
622 + # cached, using the same mechanism as the deliberate skips above rather than a
623 + # second, competing one: it reconciles existing directives, keeps logged-in
624 + # responses fully no-store, and never grants caching nothing had granted.
625 + if (function_exists('metasync_otto_limit_response_cache')) {
626 + metasync_otto_limit_response_cache('HTTP_FETCH_FAILED');
627 + }
524 628 return false;
525 629 }
526 630
527 631 $route_html_string = is_string($route_html) ? $route_html : $route_html->__toString();
528 -
632 +
633 + # Detect incomplete Divi rendering in OTTO's HTTP render output.
634 + # OTTO's internal wp_remote_get runs in a different server context than the
635 + # normal page load. This can cause Divi to produce incomplete output:
636 + #
637 + # 1. Cold CSS cache: Divi outputs inline <style> blocks instead of external
638 + # <link> tags (TB CSS files not yet generated)
639 + # 2. Missing Google Fonts: Divi's font enqueue depends on request context
640 + # (cookies, headers) that differ in the internal fetch
641 + #
642 + # If OTTO serves this incomplete HTML via exit(), SG Optimizer caches it —
643 + # breaking CSS for all visitors until manual cache purge.
644 + #
645 + # Fix: skip OTTO for this request (return false), letting WordPress render
646 + # normally. This builds Divi's caches so the next OTTO request works correctly.
647 + #
648 + # Both checks below are self-resolving — the next render finds warm caches —
649 + # so the un-optimized copy served now gets a capped cache lifetime rather
650 + # than being pinned for a caching layer's full TTL.
651 + #
652 + # The remaining fail-open exits on this path are deliberately NOT capped.
653 + # They mean the site could not fetch its own page (blocked loopback request,
654 + # HTTP Basic Auth, a firewall rejecting the internal user-agent) or that the
655 + # document cannot be processed at all. Those are whole-site, permanent
656 + # conditions: OTTO cannot work on any page, so capping cache lifetime would
657 + # rescue nothing while moving the entire site's traffic to the origin.
658 + if (defined('ET_CORE_VERSION')) {
659 + # Check 1: Cold TB CSS cache (inline styles instead of external link)
660 + if (preg_match('/<style\s[^>]*id=["\']et-core-unified-tb-[^"\']*-cached-inline-styles["\']/', $route_html_string)) {
661 + if (function_exists('metasync_otto_limit_response_cache')) {
662 + metasync_otto_limit_response_cache('DIVI_COLD_CSS_CACHE');
663 + }
664 + return false;
665 + }
666 + # Check 2: Missing Google Fonts CSS (Divi enqueues this on every page)
667 + # Normal render has: <link ... id='et-builder-googlefonts-cached-css' ...>
668 + # or <link ... id='et-builder-googlefonts-css' ...>
669 + # If neither is present, the internal fetch didn't load fonts properly.
670 + if (strpos($route_html_string, 'et-builder-googlefonts') === false
671 + && strpos($route_html_string, 'fonts.googleapis.com') === false
672 + ) {
673 + if (function_exists('metasync_otto_limit_response_cache')) {
674 + metasync_otto_limit_response_cache('DIVI_MISSING_FONTS');
675 + }
676 + return false;
677 + }
678 + }
679 +
529 680 if (strpos($route_html_string, 'pix-sliding-headline-2') !== false || strpos($route_html_string, 'pix-intro-sliding-text') !== false) {
530 681 # Only apply fix within sliding text contexts to avoid breaking other layouts
531 682 $route_html_string = preg_replace('#(</span></span>)(<span\s+class=["\'][^"\']*slide-in-container[^"\']*["\'][^>]*>)#i', '$1 $2', $route_html_string);
532 683 }
@@ -566,12 +717,57 @@
566 717 $discarded_output = ob_get_clean();
567 718
568 719 }
569 720
721 + # If Divi's module-design CSS is missing from OTTO output,
722 + # fetch it directly and inject it. This handles the case where the
723 + # internal wp_remote_get response was truncated by SG Optimizer's parser.
724 + if (strpos($route_html_string, 'et-builder-module-design') === false
725 + && function_exists('et_theme_builder_decorate_page_resource_slug')
726 + && function_exists('et_core_page_resource_get')
727 + ) {
728 + # Try to get Divi's inline CSS from its page resource manager
729 + $post_id = get_the_ID();
730 + if ($post_id) {
731 + $resource_slug = et_theme_builder_decorate_page_resource_slug($post_id, 'module-design');
732 + $manager = et_core_page_resource_get('builder', $resource_slug, $post_id, 40);
733 + if ($manager && method_exists($manager, 'get_data')) {
734 + $css_data = $manager->get_data('inline');
735 + if (!empty($css_data)) {
736 + $style_tag = '<style id="et-builder-' . esc_attr($resource_slug) . '-cached-inline-styles">'
737 + . wp_strip_all_tags($css_data) . '</style>';
738 + $route_html_string = str_replace('</body>', $style_tag . "\n" . '</body>', $route_html_string);
739 + }
740 + }
741 + }
742 + }
743 +
744 + # Decide cacheability from the internal fetch + the final output, then serve.
745 + # 1) Hand the visitor the session their page was built around (fixes forms that
746 + # were silently failing on the HTTP path because the fetch cookie was dropped).
747 + # 2) Block shared caching when the fetched page set a cookie, when its own render
748 + # declared itself uncacheable (PHP's session cache-limiter — true for resumed
749 + # sessions too), when the visitor carries cart/auth cookies that change the
750 + # rendered content, or when the render came back incomplete.
751 + # 3) A plain, complete page gets no cache header at all → the host finally caches
752 + # it (the SiteGround/Oxygen relief this ticket is about).
753 + Metasync_Otto_Render_Strategy::passthrough_http_fetch_cookies();
754 +
755 + $must_not_cache =
756 + Metasync_Otto_Render_Strategy::http_fetch_had_cookie()
757 + || Metasync_Otto_Render_Strategy::http_fetch_no_cache()
758 + || Metasync_Otto_Render_Strategy::request_has_visitor_specific_cookie()
759 + || !Metasync_Otto_Render_Strategy::http_output_is_complete($route_html_string);
760 +
570 761 # Send response headers
571 762 Metasync_Otto_Render_Strategy::send_headers($cache_status);
572 763
764 + if ($must_not_cache) {
765 + Metasync_Otto_Render_Strategy::block_http_cache();
766 + }
767 +
573 768 # continue to render the html
769 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- server-rendered route HTML from the OTTO render strategy; escaping would corrupt the document
574 770 echo $route_html_string;
575 771
576 772 # prevent further wp execution
577 773 exit();
@@ -576,5 +772,5 @@
576 772 # prevent further wp execution
577 773 exit();
578 774 }
579 775
580 -}
776 +}