Plugin Directory

Changeset 3481458


Ignore:
Timestamp:
03/12/2026 07:52:40 PM (33 hours ago)
Author:
vectoron
Message:

v2.11.7: Fix multiple JSON-LD schema rendering, fix Elementor hide_title suppressing post titles

Location:
vectoron/trunk
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • vectoron/trunk/includes/settings-page.php

    r3451062 r3481458  
    595595                        vectoron_debug( 'update_post_meta(_elementor_edit_mode): ' . vectoron_debug_export( $result_mode ) );
    596596                        vectoron_debug( 'update_post_meta(_elementor_version): ' . vectoron_debug_export( $result_version ) );
     597
     598                        // Remove hide_title from page settings — preserve other user-configured settings.
     599                        $page_settings = get_post_meta( $test_post_id, '_elementor_page_settings', true );
     600                        if ( is_array( $page_settings ) && isset( $page_settings['hide_title'] ) ) {
     601                            unset( $page_settings['hide_title'] );
     602                            if ( empty( $page_settings ) ) {
     603                                delete_post_meta( $test_post_id, '_elementor_page_settings' );
     604                            } else {
     605                                update_post_meta( $test_post_id, '_elementor_page_settings', $page_settings );
     606                            }
     607                        }
    597608
    598609                        // Clear Elementor CSS cache.
  • vectoron/trunk/integrations/elementor.php

    r3462938 r3481458  
    293293        }
    294294
    295         // Set page settings to hide title (matches working posts 687, 904).
    296         // Without this, pages show with default WordPress title styling ("unstyled").
    297         // Value is serialized PHP array: array('hide_title' => 'yes')
    298         $page_settings = array( 'hide_title' => 'yes' );
    299         update_post_meta( $post_id, '_elementor_page_settings', $page_settings );
     295        // Remove hide_title from page settings — let the theme handle title rendering.
     296        // Setting hide_title=yes suppresses the title entirely on themes that don't
     297        // render it outside of Elementor. Use selective unset to preserve any other
     298        // user-configured page settings (custom_css, padding, background_overlay, etc.).
     299        $page_settings = get_post_meta( $post_id, '_elementor_page_settings', true );
     300        if ( is_array( $page_settings ) && isset( $page_settings['hide_title'] ) ) {
     301            unset( $page_settings['hide_title'] );
     302            if ( empty( $page_settings ) ) {
     303                delete_post_meta( $post_id, '_elementor_page_settings' );
     304            } else {
     305                update_post_meta( $post_id, '_elementor_page_settings', $page_settings );
     306            }
     307        }
    300308
    301309        // Clear Elementor CSS cache for this post.
  • vectoron/trunk/integrations/schema-sync.php

    r3459361 r3481458  
    5858    // Try JSON first (Tier 1: Vectoron plugin API, Tier 2: Native WP REST API).
    5959    $data = json_decode( $request_body, true );
     60    if ( json_last_error() !== JSON_ERROR_NONE ) {
     61        $data = null; // Will fall through to Tier 3 form-encoded fallback below.
     62    }
    6063
    6164    // Fall back to form-encoded (Tier 3: AJAX proxy sends rest_body as form field).
     
    7073        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    7174        $data = json_decode( wp_unslash( $_POST['rest_body'] ), true );
     75        if ( json_last_error() !== JSON_ERROR_NONE ) {
     76            $data = null;
     77        }
    7278    }
    7379
     
    8995    // even if re-decode fails downstream.
    9096    $clean_json = wp_json_encode( $decoded, JSON_UNESCAPED_UNICODE );
     97    if ( false === $clean_json ) {
     98        vectoron_debug( "[Schema Sync] wp_json_encode failed at storage for post {$post_id}" );
     99        return;
     100    }
    91101    update_post_meta( $post_id, '_vectoron_schema_markup', $clean_json );
    92102    vectoron_debug( "[Schema Sync] Stored schema_markup from API for post {$post_id} (" . strlen( $clean_json ) . ' chars)' );
     
    131141    }
    132142
    133     // Flexible regex: handles extra attributes in any order.
    134     if ( preg_match( '/<script\b[^>]*\btype=["\']application\/ld\+json["\'][^>]*>(.*?)<\/script>/s', $content, $matches ) ) {
    135         $schema_json = trim( $matches[1] );
    136         $post_id     = get_the_ID();
    137 
    138         // Validate JSON before storing.
    139         $decoded = json_decode( $schema_json );
    140         if ( json_last_error() === JSON_ERROR_NONE && ! empty( $schema_json ) ) {
     143    // Flexible regex: handles extra attributes in any order. Uses find_all equivalent (preg_match_all)
     144    // to capture ALL JSON-LD tags — matches Python extract_json_ld_schema() behavior for multi-schema articles.
     145    if ( preg_match_all( '/<script\b[^>]*\btype=["\']application\/ld\+json["\'][^>]*>(.*?)<\/script>/s', $content, $matches ) ) {
     146        $post_id = get_the_ID();
     147        $schemas = array();
     148
     149        foreach ( $matches[1] as $raw_json ) {
     150            $schema_json = trim( $raw_json );
     151            if ( empty( $schema_json ) ) {
     152                continue;
     153            }
     154            $decoded_item = json_decode( $schema_json );
     155            if ( json_last_error() === JSON_ERROR_NONE ) {
     156                $schemas[] = $decoded_item;
     157            }
     158        }
     159
     160        if ( ! empty( $schemas ) ) {
     161            // Store as JSON array if multiple schemas, single object if one — matches Python behavior.
    141162            // SECURITY: Do NOT use JSON_UNESCAPED_SLASHES — matches output policy.
    142             $clean_json = wp_json_encode( $decoded, JSON_UNESCAPED_UNICODE );
    143             update_post_meta( $post_id, '_vectoron_schema_markup', $clean_json );
    144             vectoron_debug( "[Schema Sync] Migrated inline schema for post {$post_id} (" . strlen( $clean_json ) . ' chars)' );
    145         }
    146 
    147         // Strip ALL JSON-LD script tags from content (even if JSON invalid).
    148         $content = preg_replace( '/<script\b[^>]*\btype=["\']application\/ld\+json["\'][^>]*>.*?<\/script>/s', '', $content );
     163            $to_store   = count( $schemas ) === 1 ? $schemas[0] : $schemas;
     164            $clean_json = wp_json_encode( $to_store, JSON_UNESCAPED_UNICODE );
     165            if ( false === $clean_json ) {
     166                // Do NOT strip content — preserve inline schemas since storage failed.
     167                vectoron_debug( '[Schema Sync] wp_json_encode failed at storage for post ' . $post_id );
     168            } else {
     169                update_post_meta( $post_id, '_vectoron_schema_markup', $clean_json );
     170                vectoron_debug( '[Schema Sync] Migrated ' . count( $schemas ) . " inline schema(s) for post {$post_id} (" . strlen( $clean_json ) . ' chars)' );
     171                // Strip only after successful storage — prevents data loss if storage fails.
     172                $content = preg_replace( '/<script\b[^>]*\btype=["\']application\/ld\+json["\'][^>]*>.*?<\/script>/s', '', $content );
     173            }
     174        } else {
     175            // All found schema tags failed JSON decode — preserve content unchanged to avoid data loss.
     176            vectoron_debug( '[Schema Sync] WARNING: ' . count( $matches[1] ) . ' inline schema tag(s) found but none decoded successfully for post ' . get_the_ID() . '. Content preserved.' );
     177        }
    149178    }
    150179
     
    175204
    176205    // Re-decode and re-encode to prevent XSS (sanitize stored JSON).
    177     $decoded = json_decode( $schema );
    178     if ( json_last_error() === JSON_ERROR_NONE ) {
     206    $decoded = json_decode( $schema, true );
     207    if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $decoded ) || empty( $decoded ) ) {
     208        return;
     209    }
     210
     211    // Emit one <script> per schema object.
     212    // Distinguish a JSON array of schema objects from a single schema object by checking
     213    // whether the decoded PHP array is sequential (integer-keyed = JSON array of schemas)
     214    // or associative (string-keyed = single schema object like {"@type":"FAQPage",...}).
     215    // JSON-LD schema objects always use string keys (@type, @context, etc.) — never sequential
     216    // integer keys — so this check is reliable and handles backward compat with legacy stored data.
     217    $is_schema_list = ( array_keys( $decoded ) === range( 0, count( $decoded ) - 1 ) );
     218    $items          = $is_schema_list ? $decoded : array( $decoded );
     219    foreach ( $items as $schema_item ) {
    179220        // SECURITY: Do NOT use JSON_UNESCAPED_SLASHES in HTML output context.
    180221        // json_encode() escapes "/" as "\/" by default, which prevents </script>
    181222        // tag injection (XSS) when JSON values contain attacker-controlled strings.
    182223        // The "\/" is valid JSON and parsed correctly by all JSON-LD consumers.
    183         $safe_json = wp_json_encode( $decoded, JSON_UNESCAPED_UNICODE );
    184         // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON is re-encoded via wp_json_encode() with slash escaping for XSS prevention.
     224        $safe_json = wp_json_encode( $schema_item, JSON_UNESCAPED_UNICODE );
     225        if ( false === $safe_json ) {
     226            vectoron_debug( '[Schema Sync] wp_json_encode failed for schema item on post ' . get_the_ID() );
     227            continue;
     228        }
     229        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_json_encode() re-encodes JSON with slash escaping (XSS prevention). false return guarded above.
    185230        echo '<script type="application/ld+json">' . $safe_json . '</script>' . "\n";
    186231    }
  • vectoron/trunk/readme.txt

    r3469529 r3481458  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 2.11.6
     7Stable tag: 2.11.7
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    135135
    136136== Changelog ==
     137
     138= 2.11.7 =
     139* Fixed: Multiple JSON-LD schema tags (e.g. FAQPage + ItemList for listicle articles) now all render in `<head>`
     140* Previously only the first schema tag was extracted; the second was left in the content body as visible text
     141* The `wp_head` output now emits one `<script type="application/ld+json">` per schema object
     142* Fixed: Elementor sync no longer sets `hide_title=yes` in page settings, which caused post titles to be hidden on themes that don't render titles outside Elementor
     143* On re-sync, existing `hide_title` is surgically removed while preserving other Elementor page settings
    137144
    138145= 2.11.6 =
  • vectoron/trunk/vectoron-api.php

    r3469529 r3481458  
    12721272        $response = array(
    12731273            'plugin' => 'Vectoron API',
    1274             'version' => '2.11.6',
     1274            'version' => '2.11.7',
    12751275            'status' => 'active',
    12761276            'endpoints' => array(
  • vectoron/trunk/vectoron.php

    r3469529 r3481458  
    33 * Plugin Name:       Vectoron
    44 * Description:       Provides the [vectoron_article] shortcode to disable wpautop and load assets for custom content like the FAQ accordion and GA4 tracking. Includes REST API endpoints for external content management and ACF integration.
    5  * Version:           2.11.6
     5 * Version:           2.11.7
    66 * Author:            Vectoron
    77 * Author URI:        https://vectoron.ai
Note: See TracChangeset for help on using the changeset viewer.