with data attributes * and a PHP render_callback that uses the existing MFE field API. * * To enable for a field group, set `register_as_block => 1` in its config. * Optionally set `block_title`, `block_category`, and `block_icon`. */ if ( ! defined( 'ABSPATH' ) ) { exit; } class MetaForge_BlockBridge { public function __construct() { add_action( 'init', array( $this, 'register_blocks' ), 20 ); add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ) ); // Modern Block Editor category registration (WP 5.8+) add_filter( 'block_categories_all', array( $this, 'register_block_category' ), 10, 2 ); } /** * Register MetaForge block category in Gutenberg inserter. */ public function register_block_category( $categories, $block_editor_context = null ) { if ( ! is_array( $categories ) ) { $categories = array(); } foreach ( $categories as $cat ) { if ( isset( $cat['slug'] ) && $cat['slug'] === 'metaforge' ) { return $categories; } } array_unshift( $categories, array( 'slug' => 'metaforge', 'title' => __( 'MetaForge', 'metaforge-engine' ), 'icon' => 'admin-generic', ) ); return $categories; } /** * Enqueue block editor assets and localize blocks metadata. */ public function enqueue_block_editor_assets() { wp_enqueue_script( 'metaforge-blocks-js', METAFORGE_URL . 'assets/js/blocks.js', array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor' ), METAFORGE_VERSION, true ); wp_add_inline_style( 'wp-block-editor', ' .mfe-block-card .mfe-canvas-fields-stack { display: flex !important; flex-direction: column !important; gap: 16px !important; width: 100% !important; } .mfe-block-card .mfe-canvas-field-row { width: 100% !important; } .mfe-block-card .components-base-control, .mfe-block-card .components-base-control__field, .mfe-block-card .components-text-control__input, .mfe-block-card .components-textarea-control__input, .mfe-block-card .components-select-control__input { width: 100% !important; max-width: 100% !important; box-sizing: border-box !important; } .mfe-block-card .components-base-control__label { font-weight: 600 !important; font-size: 12px !important; text-transform: uppercase !important; letter-spacing: 0.5px !important; color: #475569 !important; margin-bottom: 6px !important; display: block !important; } .mfe-block-card input.components-text-control__input, .mfe-block-card textarea.components-textarea-control__input { border: 1px solid #cbd5e1 !important; border-radius: 6px !important; padding: 8px 12px !important; font-size: 14px !important; width: 100% !important; box-sizing: border-box !important; } ' ); $posts = get_posts( array( 'post_type' => 'metaforge_group', 'posts_per_page' => -1, 'post_status' => 'publish', ) ); $configs = array(); foreach ( $posts as $post ) { $config = get_post_meta( $post->ID, '_metaforge_group_config', true ); if ( is_array( $config ) ) { $configs[] = $config; } } global $metaforge_local_field_groups; if ( is_array( $metaforge_local_field_groups ) ) { foreach ( $metaforge_local_field_groups as $l_config ) { if ( is_array( $l_config ) ) { $configs[] = $l_config; } } } $blocks_data = array(); foreach ( $configs as $config ) { if ( empty( $config['register_as_block'] ) ) { continue; } $raw_name = ! empty( $config['group_name'] ) ? $config['group_name'] : ( ! empty( $config['block_title'] ) ? $config['block_title'] : 'Field Group' ); $block_slug = ! empty( $config['block_slug'] ) ? sanitize_title( $config['block_slug'] ) : sanitize_title( $raw_name ); if ( empty( $block_slug ) ) { $block_slug = 'block-' . ( ! empty( $config['group_key'] ) ? sanitize_title( $config['group_key'] ) : uniqid() ); } $raw_cat = ! empty( $config['block_category'] ) ? sanitize_text_field( $config['block_category'] ) : 'metaforge'; $block_cat = ( $raw_cat === 'common' ) ? 'metaforge' : $raw_cat; $blocks_data[] = array( 'slug' => $block_slug, 'title' => ! empty( $config['block_title'] ) ? sanitize_text_field( $config['block_title'] ) : sanitize_text_field( $raw_name ), 'icon' => ! empty( $config['block_icon'] ) ? sanitize_text_field( $config['block_icon'] ) : 'admin-generic', 'category' => $block_cat, 'fields' => $config['fields'] ?? array(), ); } wp_localize_script( 'metaforge-blocks-js', 'metaforgeBlocksData', $blocks_data ); } /** * Iterate all metaforge_group CPTs and register blocks for * any that have `register_as_block = 1` in their config. */ public function register_blocks() { if ( ! function_exists( 'register_block_type' ) ) { return; } wp_register_script( 'metaforge-blocks-js', METAFORGE_URL . 'assets/js/blocks.js', array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-data' ), METAFORGE_VERSION, true ); $posts = get_posts( array( 'post_type' => 'metaforge_group', 'posts_per_page' => -1, 'post_status' => 'publish', ) ); $configs = array(); $ids = array(); foreach ( $posts as $post ) { $config = get_post_meta( $post->ID, '_metaforge_group_config', true ); if ( is_array( $config ) ) { $configs[] = $config; $ids[] = $post->ID; } } global $metaforge_local_field_groups; if ( is_array( $metaforge_local_field_groups ) ) { foreach ( $metaforge_local_field_groups as $l_config ) { if ( is_array( $l_config ) ) { $configs[] = $l_config; $ids[] = 0; } } } foreach ( $configs as $i => $config ) { if ( empty( $config['register_as_block'] ) ) { continue; } $this->register_single_block( $config, $ids[ $i ] ); } } /** * Register one block type from a field group config. */ private function register_single_block( array $config, int $group_post_id ) { $raw_name = ! empty( $config['group_name'] ) ? $config['group_name'] : ( ! empty( $config['block_title'] ) ? $config['block_title'] : 'Field Group' ); $block_slug = ! empty( $config['block_slug'] ) ? sanitize_title( $config['block_slug'] ) : sanitize_title( $raw_name ); if ( empty( $block_slug ) ) { $block_slug = 'block-' . ( $group_post_id > 0 ? $group_post_id : ( ! empty( $config['group_key'] ) ? sanitize_title( $config['group_key'] ) : uniqid() ) ); } if ( empty( $block_slug ) ) { return; } $block_name = 'metaforge/' . $block_slug; // e.g. metaforge/hero-banner $title = ! empty( $config['block_title'] ) ? sanitize_text_field( $config['block_title'] ) : sanitize_text_field( $raw_name ); $icon = ! empty( $config['block_icon'] ) ? sanitize_text_field( $config['block_icon'] ) : 'admin-generic'; $raw_cat = ! empty( $config['block_category'] ) ? sanitize_text_field( $config['block_category'] ) : 'metaforge'; $category = ( $raw_cat === 'common' ) ? 'metaforge' : $raw_cat; // Build the block attributes schema from the field group's fields $attributes = $this->build_block_attributes( $config['fields'] ?? array() ); register_block_type( $block_name, array( 'api_version' => 3, 'title' => $title, 'icon' => $icon, 'category' => in_array( $category, array( 'metaforge', 'text', 'media', 'design', 'widgets', 'common' ) ) ? $category : 'metaforge', 'editor_script' => 'metaforge-blocks-js', 'attributes' => $attributes, 'render_callback' => function ( array $block_attrs, string $content ) use ( $config ) { return $this->render_block( $block_attrs, $config ); }, 'supports' => array( 'align' => array( 'wide', 'full' ), 'anchor' => true, 'customClassName' => true, 'jsx' => true, 'html' => false, ), ) ); } /** * Build a Gutenberg attributes schema from MetaForge field definitions. * Each field becomes a typed block attribute. */ private function build_block_attributes( array $fields ): array { $attrs = array(); foreach ( $fields as $field ) { $name = $field['name'] ?? ''; if ( ! $name ) { continue; } switch ( $field['type'] ?? 'text' ) { case 'true_false': $attrs[ $name ] = array( 'type' => 'boolean', 'default' => false, ); break; case 'number': case 'range': $attrs[ $name ] = array( 'type' => 'number', 'default' => 0, ); break; case 'image': case 'file': $attrs[ $name ] = array( 'type' => 'object', 'default' => null, ); break; case 'repeater': case 'group': $attrs[ $name ] = array( 'type' => 'array', 'default' => array(), ); break; case 'checkbox': $attrs[ $name ] = array( 'type' => 'array', 'default' => array(), ); break; case 'textarea': case 'wysiwyg': $attrs[ $name ] = array( 'type' => 'string', 'default' => '', ); break; default: $attrs[ $name ] = array( 'type' => 'string', 'default' => '', ); } } return $attrs; } /** * Server-side render callback for a registered block. * * Priority: block editor attributes → post meta from the current post. * This means blocks can display either inline editor values OR * the saved post meta values when rendered on the frontend. */ private function render_block( array $block_attrs, array $config ): string { $fields = $config['fields'] ?? array(); if ( empty( $fields ) ) { return ''; } // Attempt to get the post ID from the current global context $post_id = get_the_ID() ?: null; $block_slug = ! empty( $config['block_slug'] ) ? sanitize_title( $config['block_slug'] ) : ( ! empty( $config['group_name'] ) ? sanitize_title( $config['group_name'] ) : 'block' ); // Prepare context variables for the template $block = array( 'title' => $config['block_title'] ?? $config['group_name'] ?? '', 'description' => $config['block_description'] ?? '', 'category' => $config['block_category'] ?? 'common', 'icon' => $config['block_icon'] ?? 'admin-generic', 'slug' => $block_slug, 'id' => 'mfe-block-' . $block_slug . '-' . uniqid(), 'align' => $block_attrs['align'] ?? '', 'className' => $block_attrs['className'] ?? '', ); // Format and gather all values into an array context so template can use them $context_fields = array(); foreach ( $fields as $field ) { $name = $field['name'] ?? ''; if ( ! $name ) { continue; } // Prefer block editor attributes; fallback to post meta $raw = $block_attrs[ $name ] ?? ( $post_id ? get_post_meta( $post_id, $name, true ) : '' ); $context_fields[ $name ] = metaforge_format_value( $raw, $field ); } // Search for template overrides in theme / child theme $template_slugs = array( "template-parts/blocks/block-{$block_slug}.php", "template-parts/blocks/{$block_slug}.php", "blocks/block-{$block_slug}.php", "blocks/{$block_slug}.php", ); $template = locate_template( $template_slugs ); ob_start(); if ( $template ) { // Include template in a clean function scope so variables don't bleed. // Expose $block and field values as variables. ( static function ( $__template_path, $fields, $block ) { // Extract keys into variables (e.g. $hero_image, $title) if ( is_array( $fields ) ) { foreach ( $fields as $key => $val ) { if ( is_string( $key ) && preg_match( '/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $key ) && $key !== '__template_path' && $key !== 'block' && $key !== 'fields' ) { $$key = $val; } } } include $__template_path; } )( $template, $context_fields, $block ); } else { // Fallback render markup echo '
'; foreach ( $fields as $field ) { $name = $field['name'] ?? ''; if ( ! $name ) { continue; } $this->render_block_field( $field, $context_fields[ $name ] ); } echo '
'; } return ob_get_clean(); } /** * Render a single field value in a block context. * Produces clean, semantic HTML for each field type. */ private function render_block_field( array $field, $value ) { $type = $field['type'] ?? 'text'; $label = esc_html( $field['label'] ?? '' ); $name = esc_attr( $field['name'] ?? '' ); echo '
'; switch ( $type ) { case 'image': if ( is_array( $value ) && ! empty( $value['url'] ) ) { $alt = esc_attr( $value['alt'] ?? '' ); echo '' . esc_attr( $alt ) . ''; } elseif ( is_numeric( $value ) && $value > 0 ) { $src = wp_get_attachment_image_url( $value, 'full' ); if ( $src ) { echo ''; } } elseif ( is_string( $value ) && ! empty( $value ) ) { echo ''; } break; case 'wysiwyg': case 'textarea': echo '
' . wp_kses_post( $value ) . '
'; break; case 'true_false': echo '' . ( $value ? 'Yes' : 'No' ) . ''; break; case 'icon_picker': $icon = is_array( $value ) ? ( $value['icon'] ?? '' ) : (string) $value; if ( $icon ) { echo ''; } break; case 'code_editor': echo '
' . esc_html( (string) $value ) . '
'; break; case 'range': echo '' . esc_html( (string) $value ) . ''; break; case 'button': if ( is_array( $value ) && ! empty( $value['url'] ) ) { $btn_class = 'mfe-btn mfe-btn-' . esc_attr( $value['style'] ?? 'primary' ); echo '' . esc_html( $value['title'] ?: $value['url'] ) . ''; } break; case 'link': if ( ! empty( $value['url'] ) ) { echo '' . esc_html( $value['title'] ?: $value['url'] ) . ''; } break; case 'repeater': if ( is_array( $value ) && ! empty( $value ) ) { echo '
'; foreach ( $value as $row_index => $row ) { echo '
'; if ( is_array( $row ) ) { foreach ( $row as $sub_name => $sub_val ) { echo '' . esc_html( is_scalar( $sub_val ) ? $sub_val : wp_json_encode( $sub_val ) ) . ''; } } echo '
'; } echo '
'; } break; default: echo '' . esc_html( is_scalar( $value ) ? $value : '' ) . ''; } echo '
'; } }