$raw_html, 'json' => $tree, ); } /** * Recursively scans the JSON for 'data-store' blocks with a 'query', * fetches the WordPress data, and saves it for HTML injection. * * @param array $node The current node in the JSON tree being processed. Passed by reference. * @param array $queries_data Reference to the array holding fetched WordPress data. * @return void */ private static function hydrate_tree_recursive( &$node, &$queries_data ) { if ( isset( $node['mode'] ) && 'data-store' === $node['mode'] && ! empty( $node['query'] ) ) { $args = array( 'post_type' => isset( $node['query']['post_type'] ) ? sanitize_text_field( $node['query']['post_type'] ) : 'post', 'posts_per_page' => isset( $node['query']['posts_per_page'] ) ? intval( $node['query']['posts_per_page'] ) : 10, 'orderby' => isset( $node['query']['orderby'] ) ? sanitize_text_field( $node['query']['orderby'] ) : 'date', 'order' => isset( $node['query']['order'] ) ? sanitize_text_field( $node['query']['order'] ) : 'DESC', 'post_status' => 'publish', 'no_found_rows' => true, ); $posts = get_posts( $args ); $records = array(); foreach ( $posts as $index => $post ) { $categories = get_the_category( $post->ID ); $category_name = ! empty( $categories ) ? $categories[0]->name : 'Uncategorized'; // Standardize the ternary to pass PHPCS. $thumbnail_url = get_the_post_thumbnail_url( $post->ID, 'large' ); $records[] = array( '_index' => $index, 'id' => (string) $post->ID, 'title' => get_the_title( $post ), 'excerpt' => get_the_excerpt( $post ), 'permalink' => get_permalink( $post ), 'date' => get_the_date( '', $post ), 'thumbnail' => $thumbnail_url ? $thumbnail_url : '', 'category' => $category_name, ); } // Hydrate the JSON node for React. $node['records'] = $records; // Save to our array for the HTML DOMDocument parser. $queries_data[ $node['id'] ] = $records; } // Recurse down the tree. foreach ( $node as $key => &$value ) { if ( is_array( $value ) ) { self::hydrate_tree_recursive( $value, $queries_data ); } } } /** * Uses DOMDocument to find the React templates, process attribute bindings, * replace text tokens, and inject the WordPress data directly into the HTML string. * * @param string $html The raw HTML template string. * @param array $queries_data The array of fetched WordPress data mapped by query IDs. * @return string The final, hydrated HTML string. */ private static function render_html_templates( $html, $queries_data ) { libxml_use_internal_errors( true ); $dom = new DOMDocument(); $dom->loadHTML( '' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); $xpath = new DOMXPath( $dom ); // Find all elements marked with data-ssr-query. $wrappers = $xpath->query( '//*[@data-ssr-query]' ); foreach ( $wrappers as $wrapper ) { $query_id = $wrapper->getAttribute( 'data-ssr-query' ); if ( isset( $queries_data[ $query_id ] ) ) { // 1. Extract the raw React HTML template $template_html = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase foreach ( $wrapper->childNodes as $child ) { $template_html .= $dom->saveHTML( $child ); } $rendered_html = ''; // 2. Loop through the actual WP Posts foreach ( $queries_data[ $query_id ] as $item_data ) { // Create a temporary DOM for this specific iteration to safely manipulate attributes $temp_dom = new DOMDocument(); $temp_dom->loadHTML( '