Sessions 3-4 deliverables. Establishes the Asterion content pipeline :
EN pages generated programmatically from content-source/*-data.php, FR
translations rebuilt natively by Bricks's WPML integration from EN structure
+ WPML strings (no direct FR _bricks_page_content_2 writes).
## tools/_av-bricks-shared.php (the library)
Shared helpers + element atoms (eyebrow/heading/text/button/text-link) +
section builders (hero/prose/list/cards/cta/upsell/closer) + page write
pipeline that handles : write EN content via $wpdb (bypass Bricks's filter),
force Bricks::Elements::load_elements() so controls are populated, fire
wpml_page_builder_register_strings, push dict FR into icl_string_translations
(skip user CTE edits, auto-prefix /fr on internal URLs), trigger Bricks
native FR rebuild via wpml_page_builder_string_translated, and ensure a CTE
job exists (icl_translate_job.editor='wpml' + revision=NULL + populate
icl_translate rows with base64+gzip-encoded EN/FR strings).
## tools/av-generate-{industries,solutions}.php
Thin wrappers : URL trigger (template_redirect, not init — Bricks elements
load on 'wp' hook), add_filter('av_fr_translation_dict', ...) to register
FR strings, content-type-specific hub builder. ~150 FR strings per type.
## tools/av-translate-{industries,solutions}-fr.php
One-shot seeds for the initial FR linked posts via WPML trid mapping.
## tools/av-wpml-* operational mu-plugins
- set-cte : switch doc_translation_method to ICL_TM_TMETHOD_EDITOR (=1)
- create-jobs : reset CTE jobs (status=10 + needs_update=1 + editor=wpml +
revision=NULL). Integrated into pipeline via av_ensure_wpml_cte_job().
- fix-status / repair : align FR posts to correct trid + cleanup orphans.
- push-translations : push dict into icl_string_translations.
- plugins-check / debug / dump-strings / icl-dump / rescan : diagnostics.
## tools/av-fix-link-types.php
One-shot DB migration : convert legacy link.type='internal'+url to 'external'
across all _bricks_page_*_2 meta values (JSON and PHP-serialized). Bricks's
set_link_attributes() only renders href= for type='external' — older session-3
templates lost their href silently.
## Other tools/ mu-plugins
- av-bricks-rest-bridge : pivot — exposes Bricks CPTs over REST, dual-format
read filter (JSON for MCP / array for renderer), auto-mirror content→header/
footer for templates.
- av-apply-bricks-styles, av-cleanup-orphans, av-create-wp-menu, av-debug-meta,
av-dump-bricks, av-fix-template-meta : misc operational scripts.
## Bricks Config/
JSON snapshots of bricks_theme_styles, bricks_color_palette,
bricks_global_settings for diffable inspection.
## .gitignore
Add .mcp.json — holds per-developer WP App Password for the Bricks MCP server.
## wp-content/themes/asterion-bricks/assets/css/utilities.css
Note that all responsive overrides are now emitted natively by the page
generator via Bricks per-breakpoint settings, no custom CSS needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
305 lines
14 KiB
PHP
305 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* Plugin Name: Asterion — Bricks REST Bridge
|
|
* Description: Exposes Bricks Builder data (templates CPT + page/post/CPT meta keys) over the WP REST API so the bricks-mcp-server (sabiertas) can read/write Bricks content. Idempotent — safe to keep installed.
|
|
*
|
|
* Why this is needed :
|
|
* - Bricks registers the bricks_template CPT WITHOUT show_in_rest, so /wp/v2/bricks_template returns 404.
|
|
* - The Bricks element JSON lives in post_meta keys (_bricks_page_content_2 etc.) that aren't exposed by default.
|
|
* - register_post_meta() with show_in_rest=true is the canonical way to expose them; meta is then read/written via /wp/v2/<post_type>/<id> 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/<type> 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_<ts>)
|
|
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('<pre style="font:12px/1.5 monospace;padding:2rem;background:#0B1F3A;color:#E0C892;">'
|
|
. 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)'))
|
|
. '</pre>', 'Mirror done');
|
|
});
|