/ with the `meta` field. * * Auth : * - Read : public (REST default for show_in_rest) * - Write : edit_posts capability (=> the WP Application Password user must have at least Editor role) */ defined('ABSPATH') || exit; /* ---------------------------------------------------------------- 1) Force show_in_rest=true on bricks_template + our CPTs. ---------------------------------------------------------------- */ add_filter('register_post_type_args', function ($args, $post_type) { $needs_rest = ['bricks_template', 'case_study', 'scenario']; if (! in_array($post_type, $needs_rest, true)) { return $args; } $args['show_in_rest'] = true; $args['rest_base'] = $post_type === 'bricks_template' ? 'bricks_template' : ($post_type . 's'); $args['rest_controller_class'] = 'WP_REST_Posts_Controller'; $args['supports'] = array_unique(array_merge( $args['supports'] ?? [], ['title', 'editor', 'author', 'custom-fields'] // custom-fields needed so /wp/v2/ exposes the `meta` field )); return $args; }, 10, 2); /* ---------------------------------------------------------------- 2) Register Bricks meta keys for REST on every relevant post type. ---------------------------------------------------------------- */ add_action('init', function () { // Post types that may carry Bricks content $post_types = ['page', 'post', 'bricks_template', 'case_study', 'scenario']; // Bricks meta keys we want exposed (read+write) over REST // Keys come from wp-content/themes/bricks/functions.php $bricks_meta = [ '_bricks_page_content_2' => 'string', // The element JSON (the main payload) '_bricks_page_css_2' => 'string', '_bricks_page_header_2' => 'string', '_bricks_page_footer_2' => 'string', '_bricks_page_settings' => 'string', '_bricks_template_type' => 'string', '_bricks_template_settings' => 'string', ]; $auth_cb = function () { return current_user_can('edit_posts'); }; // Pass-through sanitize : keep the JSON string verbatim. Default sanitize for // type=string strips slashes / escapes which corrupts our JSON payload. $sanitize_passthrough = function ($value) { return $value; }; foreach ($post_types as $type) { foreach ($bricks_meta as $meta_key => $meta_type) { register_post_meta($type, $meta_key, [ 'type' => $meta_type, 'single' => true, 'show_in_rest' => true, 'auth_callback' => $auth_cb, 'sanitize_callback' => $sanitize_passthrough, ]); } } }, 30); // priority 30 — after both Bricks (10-25) and our CPTs registration (10) /* ---------------------------------------------------------------- 3) Permit also unknown _bricks_* meta keys (e.g. _bricks_snapshot_) so MCP snapshots work without us pre-registering each key. ---------------------------------------------------------------- */ add_filter('is_protected_meta', function ($protected, $meta_key, $meta_type) { if ($meta_type !== 'post') return $protected; if (strpos($meta_key, '_bricks_snapshot_') === 0) { return false; // expose snapshot meta in REST despite the leading underscore } return $protected; }, 10, 3); /* ---------------------------------------------------------------- 4) AUTO-MIRROR : the bricks-mcp-server writes element JSON to _bricks_page_content_2, but Bricks reads HEADER templates from _bricks_page_header_2 and FOOTER templates from _bricks_page_footer_2. Mirror the content into the correct key whenever a bricks_template with type=header/footer is saved. ---------------------------------------------------------------- */ /* DUAL-FORMAT META READ : the MCP server writes Bricks element data as a JSON string into post_meta. Bricks's renderer expects a PHP array. We keep the JSON string format in DB (so REST reads it back fine for the MCP's Json.parse), and decode on the fly when Bricks reads via get_post_meta (non-REST context). Result : same value, two views — JSON for the MCP, array for the renderer. */ add_filter('get_post_metadata', 'av_decode_bricks_meta_for_renderer', 10, 5); function av_decode_bricks_meta_for_renderer($value, $post_id, $meta_key, $single, $meta_type) { // Only post-type meta concerns us if ($meta_type !== 'post') return $value; // Only Bricks element content keys $content_keys = ['_bricks_page_content_2', '_bricks_page_header_2', '_bricks_page_footer_2']; if (! in_array($meta_key, $content_keys, true)) return $value; // In REST context, leave the raw string for the MCP server if (defined('REST_REQUEST') && REST_REQUEST) return $value; // Bricks renderer context : fetch the raw stored value (bypassing this filter // via remove_filter, then re-add) and decode JSON → array remove_filter('get_post_metadata', __FUNCTION__, 10); $raw = get_post_meta($post_id, $meta_key, $single); add_filter('get_post_metadata', __FUNCTION__, 10, 5); if (! is_string($raw)) return $value; $trim = trim($raw); if ($trim === '' || $trim === '[]') return $value; if ($trim[0] !== '[' && $trim[0] !== '{') return $value; $decoded = json_decode($raw, true); if (! is_array($decoded)) return $value; // WP's get_metadata_raw() convention for filter return values : // - When filter returns array AND $single=true → WP returns $check[0] // - When filter returns array AND $single=false → WP returns $check as-is // Both cases need a list wrapper around our decoded value so WP's // single-mode auto-indexing yields the decoded array (not the section). return [$decoded]; } add_action('updated_post_meta', 'av_mirror_bricks_template_content', 10, 4); add_action('added_post_meta', 'av_mirror_bricks_template_content', 10, 4); function av_mirror_bricks_template_content($meta_id, $post_id, $meta_key, $meta_value) { // Trigger 1 : content updated → mirror to header/footer if type already known // Trigger 2 : type updated → re-mirror existing content (fixes the order-of-writes // on REST create_template which writes content before type) if (! in_array($meta_key, ['_bricks_page_content_2', '_bricks_template_type'], true)) return; if (get_post_type($post_id) !== 'bricks_template') return; // Reuse the av_decode filter logic by reading via get_post_meta (which now // returns array) when the value is JSON, or by direct decoding here. if ($meta_key === '_bricks_page_content_2') { $type = get_post_meta($post_id, '_bricks_template_type', true); $content = is_string($meta_value) ? $meta_value : ''; } else { // type just updated → fetch raw content (bypass our filter to get string) $type = is_string($meta_value) ? $meta_value : ''; remove_filter('get_post_metadata', 'av_decode_bricks_meta_for_renderer', 10); $content = get_post_meta($post_id, '_bricks_page_content_2', true); add_filter('get_post_metadata', 'av_decode_bricks_meta_for_renderer', 10, 5); } $mirror_key = match ($type) { 'header' => '_bricks_page_header_2', 'footer' => '_bricks_page_footer_2', default => null, }; if (! $mirror_key) return; // Bricks stores element JSON as a PHP-serialized array. Decode JSON string to array first. $value = is_string($content) ? json_decode($content, true) : $content; if (! is_array($value)) return; // Bypass register_post_meta string-coercion by writing a serialized blob directly via $wpdb. // This is what Bricks's own builder save does — the value lands as a serialized PHP array, // which `get_post_meta($id, $key, true)` auto-unserializes back to array on read. global $wpdb; $serialized = maybe_serialize($value); $existing = $wpdb->get_var($wpdb->prepare( "SELECT meta_id FROM {$wpdb->postmeta} WHERE post_id=%d AND meta_key=%s LIMIT 1", $post_id, $mirror_key )); if ($existing) { $wpdb->update( $wpdb->postmeta, ['meta_value' => $serialized], ['meta_id' => $existing], ['%s'], ['%d'] ); } else { $wpdb->insert( $wpdb->postmeta, ['post_id' => $post_id, 'meta_key' => $mirror_key, 'meta_value' => $serialized], ['%d', '%s', '%s'] ); } wp_cache_delete($post_id, 'post_meta'); } /* ---------------------------------------------------------------- 5) BACKFILL : trigger a manual mirror via URL. Useful one-shot for templates created before this plugin was installed. Visit /?av_mirror_template=ID ---------------------------------------------------------------- */ /* One-shot : ensure all Bricks content meta is stored as JSON STRING in DB. - PHP-serialized arrays (legacy from earlier hook) → re-encode to JSON - JSON strings → leave alone This is the canonical storage format. The av_decode filter handles the array-view for Bricks's renderer at read time. /?av_fix_all_bricks_content=1 */ add_action('init', function () { if (empty($_GET['av_fix_all_bricks_content'])) return; if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only'); global $wpdb; $rows = $wpdb->get_results( "SELECT meta_id, post_id, meta_key, meta_value FROM {$wpdb->postmeta} WHERE meta_key IN ('_bricks_page_content_2','_bricks_page_header_2','_bricks_page_footer_2') AND LENGTH(meta_value) > 4" ); $report = ['Normalizing Bricks content meta to JSON strings...']; foreach ($rows as $r) { $val = $r->meta_value; $first = $val[0] ?? ''; if ($first === '[' || $first === '{') { // Already JSON string : skip $report[] = " OK meta_id={$r->meta_id} post={$r->post_id} key={$r->meta_key} (JSON)"; continue; } // Serialized PHP array → unserialize, JSON-encode if (is_serialized($val)) { $array = unserialize($val); if (is_array($array)) { $json = wp_json_encode($array); $wpdb->update( $wpdb->postmeta, ['meta_value' => $json], ['meta_id' => $r->meta_id], ['%s'], ['%d'] ); wp_cache_delete($r->post_id, 'post_meta'); $report[] = " CONV meta_id={$r->meta_id} post={$r->post_id} key={$r->meta_key} (" . count($array) . " elements, " . strlen($json) . " bytes)"; continue; } } $report[] = " SKIP meta_id={$r->meta_id} post={$r->post_id} key={$r->meta_key} (unknown format)"; } header('Content-Type: text/plain; charset=utf-8'); echo implode("\n", $report); exit; }); add_action('init', function () { if (empty($_GET['av_mirror_template'])) return; // localhost-only debug — no auth required (we're on http://localhost:10004) if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only'); $id = (int) $_GET['av_mirror_template']; $content = get_post_meta($id, '_bricks_page_content_2', true); $type = get_post_meta($id, '_bricks_template_type', true); if (! is_string($content) && ! is_array($content)) wp_die('no content meta'); $value = is_string($content) ? json_decode($content, true) : $content; if (! is_array($value)) wp_die('content is not an array'); $mirror_key = match ($type) { 'header' => '_bricks_page_header_2', 'footer' => '_bricks_page_footer_2', default => null, }; if (! $mirror_key) wp_die("no mirror needed for type=$type"); // Write directly to DB as a serialized PHP array (the format Bricks's renderer expects) global $wpdb; $serialized = maybe_serialize($value); $existing = $wpdb->get_var($wpdb->prepare( "SELECT meta_id FROM {$wpdb->postmeta} WHERE post_id=%d AND meta_key=%s LIMIT 1", $id, $mirror_key )); if ($existing) { $wpdb->update($wpdb->postmeta, ['meta_value' => $serialized], ['meta_id' => $existing], ['%s'], ['%d']); } else { $wpdb->insert($wpdb->postmeta, ['post_id' => $id, 'meta_key' => $mirror_key, 'meta_value' => $serialized], ['%d', '%s', '%s']); } wp_cache_delete($id, 'post_meta'); // Verify $after = get_post_meta($id, $mirror_key, true); $is_array = is_array($after); wp_die('
'
        . esc_html("Mirror done : template #$id ($type)\n  _bricks_page_content_2 -> $mirror_key\n  " . count($value) . " elements\n  stored as " . ($is_array ? 'array (correct)' : 'string (WRONG)'))
        . '
', 'Mirror done'); });