$config ) { $post_type = CPT::post_type_slug( $slug ); register_post_meta( $post_type, '_mapfield_spots_website', array( 'type' => 'string', 'single' => true, 'show_in_rest' => true, 'sanitize_callback' => 'esc_url_raw', 'auth_callback' => function () { return current_user_can( 'edit_posts' ); }, ) ); register_post_meta( $post_type, '_mapfield_spots_phone', array( 'type' => 'string', 'single' => true, 'show_in_rest' => true, 'sanitize_callback' => array( self::class, 'sanitize_phone' ), 'auth_callback' => function () { return current_user_can( 'edit_posts' ); }, ) ); register_post_meta( $post_type, '_mapfield_spots_cta_label', array( 'type' => 'string', 'single' => true, 'show_in_rest' => true, 'sanitize_callback' => 'sanitize_text_field', 'auth_callback' => function () { return current_user_can( 'edit_posts' ); }, ) ); register_post_meta( $post_type, '_mapfield_spots_cta_url', array( 'type' => 'string', 'single' => true, 'show_in_rest' => true, 'sanitize_callback' => 'esc_url_raw', 'auth_callback' => function () { return current_user_can( 'edit_posts' ); }, ) ); } } // Phone formats vary too much per country to fully validate without a // library — just strips illegal characters. Matches the field's `pattern` (see render()). public static function sanitize_phone( string $value ): string { $value = preg_replace( '/[^0-9+\-\s()]/', '', sanitize_text_field( $value ) ); return substr( $value, 0, 20 ); } public function add_meta_box(): void { foreach ( Map_Types::get_all() as $slug => $config ) { if ( empty( $config['fields']['website'] ) && empty( $config['fields']['phone'] ) && empty( $config['fields']['cta'] ) && empty( $config['fields']['social'] ) ) { continue; } // 'normal' (not 'side'): a CSS grid rule in admin.css places // this box next to Description rather than the sidebar. add_meta_box( 'mfs-contact', __( 'Contact & Social', 'mapfield-spots' ), function ( \WP_Post $post ) use ( $config ) { $this->render( $post, $config ); }, CPT::post_type_slug( $slug ), 'normal', 'default' ); } } public function render( \WP_Post $post, array $config ): void { wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME ); $website = get_post_meta( $post->ID, '_mapfield_spots_website', true ); $phone = get_post_meta( $post->ID, '_mapfield_spots_phone', true ); $cta_label = get_post_meta( $post->ID, '_mapfield_spots_cta_label', true ); $cta_url = get_post_meta( $post->ID, '_mapfield_spots_cta_url', true ); ?>