@@ -9,19 +9,25 @@ class Feed_Shortcode {
99 private static $ schema_scripts = array ();
1010 private static $ schemas_processed = false ;
1111 private static $ processed_feed_ids = array (); // Track which feed IDs have been processed
12+ private static $ needs_buffer_injection = false ; // Flag to indicate schemas need buffer injection
13+ private static $ schemas_output_in_head = false ; // Track if schemas were output in wp_head
1214 private $ extraction_debug = null ; // Store debug info for schema extraction
1315
1416 public function __construct (Feed_Deserializer $ feed_deserializer ) {
1517 $ this ->feed_deserializer = $ feed_deserializer ;
1618 // Hook early to pre-process shortcodes and extract schemas before wp_head runs
1719 add_action ('template_redirect ' , array ($ this , 'pre_process_schemas ' ), 1 );
20+ // Start output buffering to capture page output and inject schemas
21+ add_action ('template_redirect ' , array ($ this , 'start_output_buffering ' ), 999 );
1822 // Hook into wp_head to output schemas - use late priority to avoid conflicts with other plugins
1923 // Many SEO plugins output at priority 10, so we use 99 to go after them
2024 add_action ('wp_head ' , array ($ this , 'output_schemas_to_head ' ), 99 );
25+ // Inject schemas into output buffer after wp_head (if schemas were found later)
26+ add_action ('wp_footer ' , array ($ this , 'inject_schemas_into_buffer ' ), 1 );
2127 // Also try wp_footer as ultimate fallback in case wp_head is blocked
2228 add_action ('wp_footer ' , array ($ this , 'output_schemas_to_footer_fallback ' ), 999 );
23- // Track if we've successfully output schemas
24- add_action ('shutdown ' , array ($ this , 'check_schema_output ' ), 999 );
29+ // Track if we've successfully output schemas and process output buffer
30+ add_action ('shutdown ' , array ($ this , 'process_output_buffer ' ), 999 );
2531 }
2632
2733 function custom_esc ($ str ) {
@@ -506,6 +512,7 @@ public function output_schemas_to_head() {
506512
507513 if (!empty ($ unique_schemas )) {
508514 $ already_output = true ; // Mark as output
515+ self ::$ schemas_output_in_head = true ; // Mark that schemas were output in head
509516 echo "<!-- OPIO DEBUG: START outputting schemas to head --> \n" ;
510517 foreach ($ unique_schemas as $ index => $ schema ) {
511518 // Validate and sanitize schema before outputting
@@ -558,6 +565,94 @@ public function output_schemas_to_footer_fallback() {
558565 }
559566 }
560567
568+ /**
569+ * Start output buffering to capture page output
570+ */
571+ public function start_output_buffering () {
572+ if (!is_admin () && !wp_doing_ajax ()) {
573+ ob_start (array ($ this , 'buffer_callback ' ));
574+ }
575+ }
576+
577+ /**
578+ * Inject schemas into buffer if they weren't in head
579+ */
580+ public function inject_schemas_into_buffer () {
581+ // Check if schemas exist but weren't output in head
582+ if (!empty (self ::$ schema_scripts )) {
583+ // Mark that we need to inject into buffer
584+ self ::$ needs_buffer_injection = true ;
585+ }
586+ }
587+
588+ /**
589+ * Process output buffer and inject schemas into head if needed
590+ */
591+ public function process_output_buffer () {
592+ if (!is_admin () && !wp_doing_ajax () && ob_get_level () > 0 ) {
593+ ob_end_flush ();
594+ }
595+ }
596+
597+ /**
598+ * Buffer callback - inject schemas into head section
599+ */
600+ public function buffer_callback ($ buffer ) {
601+ // Only process if we have schemas
602+ if (empty (self ::$ schema_scripts )) {
603+ return $ buffer ;
604+ }
605+
606+ // If schemas were already output in wp_head, don't inject again
607+ if (self ::$ schemas_output_in_head ) {
608+ return $ buffer ;
609+ }
610+
611+ // Check if schemas are already in the head section by looking for our debug comments or schema structure
612+ if (stripos ($ buffer , 'OPIO DEBUG: START outputting schemas to head ' ) !== false ||
613+ stripos ($ buffer , 'OPIO: Injected schemas via output buffering ' ) !== false ) {
614+ // Schemas already in head, don't inject again
615+ return $ buffer ;
616+ }
617+
618+ // Check if any schema-like content is already in head
619+ $ head_section = '' ;
620+ if (preg_match ('/<head[^>]*>(.*?)<\/head>/is ' , $ buffer , $ head_match )) {
621+ $ head_section = $ head_match [1 ];
622+ // Check if head already contains JSON-LD schemas
623+ if (stripos ($ head_section , 'id="jsonldSchema" ' ) !== false ||
624+ (stripos ($ head_section , 'type="application/ld+json" ' ) !== false && stripos ($ head_section , '@context ' ) !== false )) {
625+ // Schemas already in head, don't inject
626+ return $ buffer ;
627+ }
628+ }
629+
630+ // Find the closing </head> tag
631+ $ head_position = stripos ($ buffer , '</head> ' );
632+ if ($ head_position === false ) {
633+ // No head tag found, return as-is
634+ return $ buffer ;
635+ }
636+
637+ // Get unique schemas
638+ $ unique_schemas = array_unique (self ::$ schema_scripts , SORT_STRING );
639+
640+ // Build schema HTML
641+ $ schema_html = "\n<!-- OPIO: Injected schemas via output buffering (wp_head already fired) --> \n" ;
642+ foreach ($ unique_schemas as $ schema ) {
643+ $ safe_schema = $ this ->sanitize_schema_output ($ schema );
644+ if ($ safe_schema ) {
645+ $ schema_html .= $ safe_schema . "\n" ;
646+ }
647+ }
648+ $ schema_html .= "<!-- OPIO: End injected schemas --> \n" ;
649+
650+ // Inject schemas before closing </head> tag
651+ $ buffer = substr_replace ($ buffer , $ schema_html . '</head> ' , $ head_position , 7 );
652+
653+ return $ buffer ;
654+ }
655+
561656 /**
562657 * Check if schemas were successfully output (called on shutdown)
563658 */
0 commit comments