review-board.php

I’ll build this as a complete advanced inc/post-types/review-board.php file for your RX theme, with CPT, taxonomies, meta fields, admin columns, REST support, shortcodes, schema, search integration, and helper functions.

Below is an advanced inc/post-types/review-board.php file for your RX Theme. It creates a complete Review Board Member system for rxharun.com, including:

CPT, taxonomy, meta boxes, sanitization, admin columns, REST API support, shortcode, frontend card rendering, schema JSON-LD, search support, archive controls, and helper functions.

Create this file:

<?php
/**
 * RX Theme - Review Board Post Type
 *
 * File: inc/post-types/review-board.php
 *
 * Features:
 * - Custom Post Type: Review Board Members
 * - Taxonomies: Specialty, Country, Qualification Level, Board Role
 * - Advanced meta fields
 * - Admin columns
 * - Shortcodes
 * - REST API support
 * - Schema.org Person/MedicalAudience structured data
 * - Frontend card renderer
 * - Search integration
 * - Archive query control
 *
 * @package RX_Theme
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'RX_Review_Board_Post_Type' ) ) :

final class RX_Review_Board_Post_Type {

	/**
	 * Post type key.
	 */
	const POST_TYPE = 'rx_review_board';

	/**
	 * Meta prefix.
	 */
	const META_PREFIX = '_rx_review_board_';

	/**
	 * Initialize hooks.
	 */
	public static function init() {
		add_action( 'init', array( __CLASS__, 'register_post_type' ) );
		add_action( 'init', array( __CLASS__, 'register_taxonomies' ) );

		add_action( 'add_meta_boxes', array( __CLASS__, 'register_meta_boxes' ) );
		add_action( 'save_post_' . self::POST_TYPE, array( __CLASS__, 'save_meta_boxes' ), 10, 2 );

		add_filter( 'manage_' . self::POST_TYPE . '_posts_columns', array( __CLASS__, 'admin_columns' ) );
		add_action( 'manage_' . self::POST_TYPE . '_posts_custom_column', array( __CLASS__, 'admin_column_content' ), 10, 2 );
		add_filter( 'manage_edit-' . self::POST_TYPE . '_sortable_columns', array( __CLASS__, 'sortable_columns' ) );

		add_action( 'pre_get_posts', array( __CLASS__, 'admin_orderby' ) );
		add_action( 'pre_get_posts', array( __CLASS__, 'frontend_archive_query' ) );

		add_filter( 'post_updated_messages', array( __CLASS__, 'updated_messages' ) );

		add_action( 'init', array( __CLASS__, 'register_meta_for_rest' ) );

		add_shortcode( 'rx_review_board', array( __CLASS__, 'shortcode_review_board' ) );
		add_shortcode( 'rx_review_board_member', array( __CLASS__, 'shortcode_single_member' ) );

		add_filter( 'the_content', array( __CLASS__, 'append_member_schema_to_single' ), 20 );

		add_filter( 'template_include', array( __CLASS__, 'template_loader' ) );

		add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
	}

	/**
	 * Register Review Board CPT.
	 */
	public static function register_post_type() {

		$labels = array(
			'name'                  => esc_html__( 'Review Board', 'rx-theme' ),
			'singular_name'         => esc_html__( 'Review Board Member', 'rx-theme' ),
			'menu_name'             => esc_html__( 'Review Board', 'rx-theme' ),
			'name_admin_bar'        => esc_html__( 'Review Board Member', 'rx-theme' ),
			'add_new'               => esc_html__( 'Add New', 'rx-theme' ),
			'add_new_item'          => esc_html__( 'Add New Review Board Member', 'rx-theme' ),
			'new_item'              => esc_html__( 'New Review Board Member', 'rx-theme' ),
			'edit_item'             => esc_html__( 'Edit Review Board Member', 'rx-theme' ),
			'view_item'             => esc_html__( 'View Review Board Member', 'rx-theme' ),
			'all_items'             => esc_html__( 'All Review Board Members', 'rx-theme' ),
			'search_items'          => esc_html__( 'Search Review Board Members', 'rx-theme' ),
			'parent_item_colon'     => esc_html__( 'Parent Review Board Member:', 'rx-theme' ),
			'not_found'             => esc_html__( 'No review board members found.', 'rx-theme' ),
			'not_found_in_trash'    => esc_html__( 'No review board members found in Trash.', 'rx-theme' ),
			'featured_image'        => esc_html__( 'Member Photo', 'rx-theme' ),
			'set_featured_image'    => esc_html__( 'Set member photo', 'rx-theme' ),
			'remove_featured_image' => esc_html__( 'Remove member photo', 'rx-theme' ),
			'use_featured_image'    => esc_html__( 'Use as member photo', 'rx-theme' ),
			'archives'              => esc_html__( 'Review Board Archives', 'rx-theme' ),
			'insert_into_item'      => esc_html__( 'Insert into review board member', 'rx-theme' ),
			'uploaded_to_this_item' => esc_html__( 'Uploaded to this member', 'rx-theme' ),
			'filter_items_list'     => esc_html__( 'Filter review board list', 'rx-theme' ),
			'items_list_navigation' => esc_html__( 'Review board list navigation', 'rx-theme' ),
			'items_list'            => esc_html__( 'Review board list', 'rx-theme' ),
		);

		$args = array(
			'labels'              => $labels,
			'description'         => esc_html__( 'Medical review board members, editorial reviewers, advisors, and expert contributors.', 'rx-theme' ),
			'public'              => true,
			'publicly_queryable'  => true,
			'show_ui'             => true,
			'show_in_menu'        => true,
			'show_in_rest'        => true,
			'rest_base'           => 'review-board',
			'rest_controller_class' => 'WP_REST_Posts_Controller',
			'query_var'           => true,
			'rewrite'             => array(
				'slug'       => 'review-board',
				'with_front' => false,
			),
			'capability_type'     => 'post',
			'has_archive'         => true,
			'hierarchical'        => false,
			'menu_position'       => 21,
			'menu_icon'           => 'dashicons-groups',
			'supports'            => array(
				'title',
				'editor',
				'excerpt',
				'thumbnail',
				'revisions',
				'author',
				'custom-fields',
			),
			'taxonomies'          => array(
				'rx_review_specialty',
				'rx_review_country',
				'rx_review_qualification',
				'rx_review_role',
			),
			'exclude_from_search' => false,
			'can_export'          => true,
			'delete_with_user'    => false,
		);

		register_post_type( self::POST_TYPE, $args );
	}

	/**
	 * Register taxonomies.
	 */
	public static function register_taxonomies() {

		self::register_taxonomy(
			'rx_review_specialty',
			esc_html__( 'Specialties', 'rx-theme' ),
			esc_html__( 'Specialty', 'rx-theme' ),
			'review-specialty'
		);

		self::register_taxonomy(
			'rx_review_country',
			esc_html__( 'Countries', 'rx-theme' ),
			esc_html__( 'Country', 'rx-theme' ),
			'review-country'
		);

		self::register_taxonomy(
			'rx_review_qualification',
			esc_html__( 'Qualification Levels', 'rx-theme' ),
			esc_html__( 'Qualification Level', 'rx-theme' ),
			'review-qualification'
		);

		self::register_taxonomy(
			'rx_review_role',
			esc_html__( 'Board Roles', 'rx-theme' ),
			esc_html__( 'Board Role', 'rx-theme' ),
			'review-role'
		);
	}

	/**
	 * Taxonomy helper.
	 */
	private static function register_taxonomy( $taxonomy, $plural, $singular, $slug ) {

		$labels = array(
			'name'              => $plural,
			'singular_name'     => $singular,
			'search_items'      => sprintf( esc_html__( 'Search %s', 'rx-theme' ), $plural ),
			'all_items'         => sprintf( esc_html__( 'All %s', 'rx-theme' ), $plural ),
			'parent_item'       => sprintf( esc_html__( 'Parent %s', 'rx-theme' ), $singular ),
			'parent_item_colon' => sprintf( esc_html__( 'Parent %s:', 'rx-theme' ), $singular ),
			'edit_item'         => sprintf( esc_html__( 'Edit %s', 'rx-theme' ), $singular ),
			'update_item'       => sprintf( esc_html__( 'Update %s', 'rx-theme' ), $singular ),
			'add_new_item'      => sprintf( esc_html__( 'Add New %s', 'rx-theme' ), $singular ),
			'new_item_name'     => sprintf( esc_html__( 'New %s Name', 'rx-theme' ), $singular ),
			'menu_name'         => $plural,
		);

		register_taxonomy(
			$taxonomy,
			self::POST_TYPE,
			array(
				'hierarchical'      => true,
				'labels'            => $labels,
				'show_ui'           => true,
				'show_admin_column' => true,
				'show_in_rest'      => true,
				'query_var'         => true,
				'rewrite'           => array(
					'slug'       => $slug,
					'with_front' => false,
				),
			)
		);
	}

	/**
	 * Meta fields list.
	 */
	public static function meta_fields() {
		return array(
			'full_name' => array(
				'label' => esc_html__( 'Full Name', 'rx-theme' ),
				'type'  => 'text',
			),
			'designation' => array(
				'label' => esc_html__( 'Designation / Professional Title', 'rx-theme' ),
				'type'  => 'text',
			),
			'degrees' => array(
				'label' => esc_html__( 'Degrees / Credentials', 'rx-theme' ),
				'type'  => 'text',
			),
			'institution' => array(
				'label' => esc_html__( 'Institution / Workplace', 'rx-theme' ),
				'type'  => 'text',
			),
			'department' => array(
				'label' => esc_html__( 'Department', 'rx-theme' ),
				'type'  => 'text',
			),
			'experience_years' => array(
				'label' => esc_html__( 'Years of Experience', 'rx-theme' ),
				'type'  => 'number',
			),
			'medical_license' => array(
				'label' => esc_html__( 'Medical License / Registration Number', 'rx-theme' ),
				'type'  => 'text',
			),
			'license_country' => array(
				'label' => esc_html__( 'License Country', 'rx-theme' ),
				'type'  => 'text',
			),
			'email' => array(
				'label' => esc_html__( 'Professional Email', 'rx-theme' ),
				'type'  => 'email',
			),
			'phone' => array(
				'label' => esc_html__( 'Phone', 'rx-theme' ),
				'type'  => 'text',
			),
			'website' => array(
				'label' => esc_html__( 'Website URL', 'rx-theme' ),
				'type'  => 'url',
			),
			'linkedin' => array(
				'label' => esc_html__( 'LinkedIn URL', 'rx-theme' ),
				'type'  => 'url',
			),
			'orcid' => array(
				'label' => esc_html__( 'ORCID URL', 'rx-theme' ),
				'type'  => 'url',
			),
			'google_scholar' => array(
				'label' => esc_html__( 'Google Scholar URL', 'rx-theme' ),
				'type'  => 'url',
			),
			'researchgate' => array(
				'label' => esc_html__( 'ResearchGate URL', 'rx-theme' ),
				'type'  => 'url',
			),
			'pubmed' => array(
				'label' => esc_html__( 'PubMed Author URL', 'rx-theme' ),
				'type'  => 'url',
			),
			'profile_summary' => array(
				'label' => esc_html__( 'Short Profile Summary', 'rx-theme' ),
				'type'  => 'textarea',
			),
			'expertise' => array(
				'label' => esc_html__( 'Areas of Expertise', 'rx-theme' ),
				'type'  => 'textarea',
			),
			'education' => array(
				'label' => esc_html__( 'Education Background', 'rx-theme' ),
				'type'  => 'textarea',
			),
			'publications' => array(
				'label' => esc_html__( 'Major Publications', 'rx-theme' ),
				'type'  => 'textarea',
			),
			'conflict_of_interest' => array(
				'label' => esc_html__( 'Conflict of Interest Statement', 'rx-theme' ),
				'type'  => 'textarea',
			),
			'review_scope' => array(
				'label' => esc_html__( 'Review Scope', 'rx-theme' ),
				'type'  => 'textarea',
			),
			'joined_date' => array(
				'label' => esc_html__( 'Joined Date', 'rx-theme' ),
				'type'  => 'date',
			),
			'last_reviewed_date' => array(
				'label' => esc_html__( 'Last Reviewed Date', 'rx-theme' ),
				'type'  => 'date',
			),
			'is_active' => array(
				'label' => esc_html__( 'Active Member', 'rx-theme' ),
				'type'  => 'checkbox',
			),
			'is_featured' => array(
				'label' => esc_html__( 'Featured Member', 'rx-theme' ),
				'type'  => 'checkbox',
			),
			'display_order' => array(
				'label' => esc_html__( 'Display Order', 'rx-theme' ),
				'type'  => 'number',
			),
		);
	}

	/**
	 * Register meta boxes.
	 */
	public static function register_meta_boxes() {

		add_meta_box(
			'rx_review_board_profile',
			esc_html__( 'Review Board Member Profile', 'rx-theme' ),
			array( __CLASS__, 'render_profile_meta_box' ),
			self::POST_TYPE,
			'normal',
			'high'
		);

		add_meta_box(
			'rx_review_board_links',
			esc_html__( 'Professional Links', 'rx-theme' ),
			array( __CLASS__, 'render_links_meta_box' ),
			self::POST_TYPE,
			'normal',
			'default'
		);

		add_meta_box(
			'rx_review_board_status',
			esc_html__( 'Member Status & Display Settings', 'rx-theme' ),
			array( __CLASS__, 'render_status_meta_box' ),
			self::POST_TYPE,
			'side',
			'default'
		);
	}

	/**
	 * Render profile meta box.
	 */
	public static function render_profile_meta_box( $post ) {

		wp_nonce_field( 'rx_review_board_save_meta', 'rx_review_board_nonce' );

		$fields = self::meta_fields();

		$profile_keys = array(
			'full_name',
			'designation',
			'degrees',
			'institution',
			'department',
			'experience_years',
			'medical_license',
			'license_country',
			'email',
			'phone',
			'profile_summary',
			'expertise',
			'education',
			'publications',
			'conflict_of_interest',
			'review_scope',
		);

		echo '<div class="rx-meta-grid">';

		foreach ( $profile_keys as $key ) {
			self::render_field( $post->ID, $key, $fields[ $key ] );
		}

		echo '</div>';
	}

	/**
	 * Render links meta box.
	 */
	public static function render_links_meta_box( $post ) {

		$fields = self::meta_fields();

		$link_keys = array(
			'website',
			'linkedin',
			'orcid',
			'google_scholar',
			'researchgate',
			'pubmed',
		);

		echo '<div class="rx-meta-grid">';

		foreach ( $link_keys as $key ) {
			self::render_field( $post->ID, $key, $fields[ $key ] );
		}

		echo '</div>';
	}

	/**
	 * Render status meta box.
	 */
	public static function render_status_meta_box( $post ) {

		$fields = self::meta_fields();

		$status_keys = array(
			'joined_date',
			'last_reviewed_date',
			'is_active',
			'is_featured',
			'display_order',
		);

		echo '<div class="rx-meta-side">';

		foreach ( $status_keys as $key ) {
			self::render_field( $post->ID, $key, $fields[ $key ] );
		}

		echo '</div>';
	}

	/**
	 * Render single field.
	 */
	private static function render_field( $post_id, $key, $field ) {

		$meta_key = self::META_PREFIX . $key;
		$value    = get_post_meta( $post_id, $meta_key, true );
		$type     = isset( $field['type'] ) ? $field['type'] : 'text';
		$label    = isset( $field['label'] ) ? $field['label'] : $key;

		echo '<p class="rx-meta-field rx-meta-field-' . esc_attr( $type ) . '">';
		echo '<label for="' . esc_attr( $meta_key ) . '"><strong>' . esc_html( $label ) . '</strong></label>';

		if ( 'textarea' === $type ) {
			echo '<textarea id="' . esc_attr( $meta_key ) . '" name="' . esc_attr( $meta_key ) . '" rows="5" style="width:100%;">' . esc_textarea( $value ) . '</textarea>';
		} elseif ( 'checkbox' === $type ) {
			echo '<br><label>';
			echo '<input type="checkbox" id="' . esc_attr( $meta_key ) . '" name="' . esc_attr( $meta_key ) . '" value="1" ' . checked( $value, '1', false ) . '>';
			echo ' ' . esc_html__( 'Yes', 'rx-theme' );
			echo '</label>';
		} else {
			echo '<input type="' . esc_attr( $type ) . '" id="' . esc_attr( $meta_key ) . '" name="' . esc_attr( $meta_key ) . '" value="' . esc_attr( $value ) . '" style="width:100%;">';
		}

		echo '</p>';
	}

	/**
	 * Save meta boxes.
	 */
	public static function save_meta_boxes( $post_id, $post ) {

		if ( ! isset( $_POST['rx_review_board_nonce'] ) ) {
			return;
		}

		if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['rx_review_board_nonce'] ) ), 'rx_review_board_save_meta' ) ) {
			return;
		}

		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}

		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return;
		}

		$fields = self::meta_fields();

		foreach ( $fields as $key => $field ) {
			$meta_key = self::META_PREFIX . $key;
			$type     = isset( $field['type'] ) ? $field['type'] : 'text';

			if ( 'checkbox' === $type ) {
				$value = isset( $_POST[ $meta_key ] ) ? '1' : '0';
				update_post_meta( $post_id, $meta_key, $value );
				continue;
			}

			if ( ! isset( $_POST[ $meta_key ] ) ) {
				continue;
			}

			$raw_value = wp_unslash( $_POST[ $meta_key ] );

			switch ( $type ) {
				case 'email':
					$value = sanitize_email( $raw_value );
					break;

				case 'url':
					$value = esc_url_raw( $raw_value );
					break;

				case 'number':
					$value = intval( $raw_value );
					break;

				case 'textarea':
					$value = sanitize_textarea_field( $raw_value );
					break;

				case 'date':
					$value = sanitize_text_field( $raw_value );
					break;

				default:
					$value = sanitize_text_field( $raw_value );
					break;
			}

			update_post_meta( $post_id, $meta_key, $value );
		}
	}

	/**
	 * Register meta for REST API.
	 */
	public static function register_meta_for_rest() {

		foreach ( self::meta_fields() as $key => $field ) {
			$type = isset( $field['type'] ) ? $field['type'] : 'text';

			$schema_type = 'string';

			if ( 'number' === $type ) {
				$schema_type = 'integer';
			}

			if ( 'checkbox' === $type ) {
				$schema_type = 'boolean';
			}

			register_post_meta(
				self::POST_TYPE,
				self::META_PREFIX . $key,
				array(
					'type'              => $schema_type,
					'description'       => isset( $field['label'] ) ? $field['label'] : $key,
					'single'            => true,
					'show_in_rest'      => true,
					'sanitize_callback' => array( __CLASS__, 'sanitize_rest_meta' ),
					'auth_callback'     => function() {
						return current_user_can( 'edit_posts' );
					},
				)
			);
		}
	}

	/**
	 * REST meta sanitizer.
	 */
	public static function sanitize_rest_meta( $value, $meta_key, $object_type ) {

		if ( false !== strpos( $meta_key, 'email' ) ) {
			return sanitize_email( $value );
		}

		if (
			false !== strpos( $meta_key, 'website' ) ||
			false !== strpos( $meta_key, 'linkedin' ) ||
			false !== strpos( $meta_key, 'orcid' ) ||
			false !== strpos( $meta_key, 'scholar' ) ||
			false !== strpos( $meta_key, 'researchgate' ) ||
			false !== strpos( $meta_key, 'pubmed' )
		) {
			return esc_url_raw( $value );
		}

		if (
			false !== strpos( $meta_key, 'experience_years' ) ||
			false !== strpos( $meta_key, 'display_order' )
		) {
			return intval( $value );
		}

		if (
			false !== strpos( $meta_key, 'is_active' ) ||
			false !== strpos( $meta_key, 'is_featured' )
		) {
			return (bool) $value;
		}

		return sanitize_text_field( $value );
	}

	/**
	 * Admin columns.
	 */
	public static function admin_columns( $columns ) {

		$new = array();

		$new['cb']          = isset( $columns['cb'] ) ? $columns['cb'] : '';
		$new['title']       = esc_html__( 'Member', 'rx-theme' );
		$new['designation'] = esc_html__( 'Designation', 'rx-theme' );
		$new['degrees']     = esc_html__( 'Degrees', 'rx-theme' );
		$new['specialty']   = esc_html__( 'Specialty', 'rx-theme' );
		$new['country']     = esc_html__( 'Country', 'rx-theme' );
		$new['active']      = esc_html__( 'Active', 'rx-theme' );
		$new['featured']    = esc_html__( 'Featured', 'rx-theme' );
		$new['order']       = esc_html__( 'Order', 'rx-theme' );
		$new['date']        = isset( $columns['date'] ) ? $columns['date'] : esc_html__( 'Date', 'rx-theme' );

		return $new;
	}

	/**
	 * Admin column content.
	 */
	public static function admin_column_content( $column, $post_id ) {

		switch ( $column ) {
			case 'designation':
				echo esc_html( get_post_meta( $post_id, self::META_PREFIX . 'designation', true ) );
				break;

			case 'degrees':
				echo esc_html( get_post_meta( $post_id, self::META_PREFIX . 'degrees', true ) );
				break;

			case 'specialty':
				self::print_terms_column( $post_id, 'rx_review_specialty' );
				break;

			case 'country':
				self::print_terms_column( $post_id, 'rx_review_country' );
				break;

			case 'active':
				echo get_post_meta( $post_id, self::META_PREFIX . 'is_active', true ) ? '✅' : '—';
				break;

			case 'featured':
				echo get_post_meta( $post_id, self::META_PREFIX . 'is_featured', true ) ? '⭐' : '—';
				break;

			case 'order':
				echo esc_html( get_post_meta( $post_id, self::META_PREFIX . 'display_order', true ) );
				break;
		}
	}

	/**
	 * Print taxonomy terms in admin column.
	 */
	private static function print_terms_column( $post_id, $taxonomy ) {

		$terms = get_the_terms( $post_id, $taxonomy );

		if ( empty( $terms ) || is_wp_error( $terms ) ) {
			echo '—';
			return;
		}

		$names = wp_list_pluck( $terms, 'name' );
		echo esc_html( implode( ', ', $names ) );
	}

	/**
	 * Sortable columns.
	 */
	public static function sortable_columns( $columns ) {
		$columns['order']       = 'display_order';
		$columns['designation'] = 'designation';
		return $columns;
	}

	/**
	 * Admin sorting.
	 */
	public static function admin_orderby( $query ) {

		if ( ! is_admin() || ! $query->is_main_query() ) {
			return;
		}

		if ( self::POST_TYPE !== $query->get( 'post_type' ) ) {
			return;
		}

		$orderby = $query->get( 'orderby' );

		if ( 'display_order' === $orderby ) {
			$query->set( 'meta_key', self::META_PREFIX . 'display_order' );
			$query->set( 'orderby', 'meta_value_num' );
		}

		if ( 'designation' === $orderby ) {
			$query->set( 'meta_key', self::META_PREFIX . 'designation' );
			$query->set( 'orderby', 'meta_value' );
		}
	}

	/**
	 * Frontend archive query.
	 */
	public static function frontend_archive_query( $query ) {

		if ( is_admin() || ! $query->is_main_query() ) {
			return;
		}

		if ( is_post_type_archive( self::POST_TYPE ) ) {
			$query->set( 'posts_per_page', 24 );
			$query->set( 'meta_key', self::META_PREFIX . 'display_order' );
			$query->set( 'orderby', array(
				'meta_value_num' => 'ASC',
				'title'          => 'ASC',
			) );
		}
	}

	/**
	 * Updated messages.
	 */
	public static function updated_messages( $messages ) {

		$messages[ self::POST_TYPE ] = array(
			0  => '',
			1  => esc_html__( 'Review board member updated.', 'rx-theme' ),
			2  => esc_html__( 'Custom field updated.', 'rx-theme' ),
			3  => esc_html__( 'Custom field deleted.', 'rx-theme' ),
			4  => esc_html__( 'Review board member updated.', 'rx-theme' ),
			5  => esc_html__( 'Review board member restored.', 'rx-theme' ),
			6  => esc_html__( 'Review board member published.', 'rx-theme' ),
			7  => esc_html__( 'Review board member saved.', 'rx-theme' ),
			8  => esc_html__( 'Review board member submitted.', 'rx-theme' ),
			9  => esc_html__( 'Review board member scheduled.', 'rx-theme' ),
			10 => esc_html__( 'Review board member draft updated.', 'rx-theme' ),
		);

		return $messages;
	}

	/**
	 * Get member meta.
	 */
	public static function get_meta( $post_id, $key, $default = '' ) {
		$value = get_post_meta( $post_id, self::META_PREFIX . $key, true );
		return '' !== $value ? $value : $default;
	}

	/**
	 * Render member card.
	 */
	public static function render_member_card( $post_id ) {

		$title       = get_the_title( $post_id );
		$link        = get_permalink( $post_id );
		$photo       = get_the_post_thumbnail( $post_id, 'medium', array( 'class' => 'rx-review-board-card__image' ) );
		$designation = self::get_meta( $post_id, 'designation' );
		$degrees     = self::get_meta( $post_id, 'degrees' );
		$summary     = self::get_meta( $post_id, 'profile_summary' );
		$experience  = self::get_meta( $post_id, 'experience_years' );

		ob_start();
		?>
		<article class="rx-review-board-card" itemscope itemtype="https://schema.org/Person">
			<a class="rx-review-board-card__media" href="<?php echo esc_url( $link ); ?>">
				<?php
				if ( $photo ) {
					echo $photo; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
				} else {
					echo '<div class="rx-review-board-card__placeholder" aria-hidden="true">RX</div>';
				}
				?>
			</a>

			<div class="rx-review-board-card__body">
				<h3 class="rx-review-board-card__title" itemprop="name">
					<a href="<?php echo esc_url( $link ); ?>">
						<?php echo esc_html( $title ); ?>
					</a>
				</h3>

				<?php if ( $degrees ) : ?>
					<p class="rx-review-board-card__degrees" itemprop="honorificSuffix">
						<?php echo esc_html( $degrees ); ?>
					</p>
				<?php endif; ?>

				<?php if ( $designation ) : ?>
					<p class="rx-review-board-card__designation" itemprop="jobTitle">
						<?php echo esc_html( $designation ); ?>
					</p>
				<?php endif; ?>

				<?php if ( $experience ) : ?>
					<p class="rx-review-board-card__experience">
						<?php
						printf(
							esc_html__( '%s+ years of experience', 'rx-theme' ),
							esc_html( $experience )
						);
						?>
					</p>
				<?php endif; ?>

				<?php if ( $summary ) : ?>
					<p class="rx-review-board-card__summary">
						<?php echo esc_html( wp_trim_words( $summary, 28 ) ); ?>
					</p>
				<?php endif; ?>

				<a class="rx-review-board-card__button" href="<?php echo esc_url( $link ); ?>">
					<?php esc_html_e( 'View Profile', 'rx-theme' ); ?>
				</a>
			</div>
		</article>
		<?php
		return ob_get_clean();
	}

	/**
	 * Shortcode: [rx_review_board limit="12" featured="no" specialty="" columns="3"]
	 */
	public static function shortcode_review_board( $atts ) {

		$atts = shortcode_atts(
			array(
				'limit'     => 12,
				'featured'  => '',
				'specialty' => '',
				'country'   => '',
				'role'      => '',
				'columns'   => 3,
				'active'    => 'yes',
			),
			$atts,
			'rx_review_board'
		);

		$meta_query = array();

		if ( 'yes' === $atts['active'] ) {
			$meta_query[] = array(
				'key'     => self::META_PREFIX . 'is_active',
				'value'   => '1',
				'compare' => '=',
			);
		}

		if ( 'yes' === $atts['featured'] ) {
			$meta_query[] = array(
				'key'     => self::META_PREFIX . 'is_featured',
				'value'   => '1',
				'compare' => '=',
			);
		}

		$tax_query = array();

		if ( ! empty( $atts['specialty'] ) ) {
			$tax_query[] = array(
				'taxonomy' => 'rx_review_specialty',
				'field'    => 'slug',
				'terms'    => sanitize_title( $atts['specialty'] ),
			);
		}

		if ( ! empty( $atts['country'] ) ) {
			$tax_query[] = array(
				'taxonomy' => 'rx_review_country',
				'field'    => 'slug',
				'terms'    => sanitize_title( $atts['country'] ),
			);
		}

		if ( ! empty( $atts['role'] ) ) {
			$tax_query[] = array(
				'taxonomy' => 'rx_review_role',
				'field'    => 'slug',
				'terms'    => sanitize_title( $atts['role'] ),
			);
		}

		$args = array(
			'post_type'      => self::POST_TYPE,
			'post_status'    => 'publish',
			'posts_per_page' => intval( $atts['limit'] ),
			'meta_key'       => self::META_PREFIX . 'display_order',
			'orderby'        => array(
				'meta_value_num' => 'ASC',
				'title'          => 'ASC',
			),
		);

		if ( ! empty( $meta_query ) ) {
			$args['meta_query'] = $meta_query;
		}

		if ( ! empty( $tax_query ) ) {
			$args['tax_query'] = $tax_query;
		}

		$query = new WP_Query( $args );

		if ( ! $query->have_posts() ) {
			return '<p class="rx-review-board-empty">' . esc_html__( 'No review board members found.', 'rx-theme' ) . '</p>';
		}

		$columns = max( 1, min( 4, intval( $atts['columns'] ) ) );

		ob_start();

		echo '<div class="rx-review-board-grid rx-review-board-grid--cols-' . esc_attr( $columns ) . '">';

		while ( $query->have_posts() ) {
			$query->the_post();
			echo self::render_member_card( get_the_ID() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}

		echo '</div>';

		wp_reset_postdata();

		return ob_get_clean();
	}

	/**
	 * Shortcode: [rx_review_board_member id="123"]
	 */
	public static function shortcode_single_member( $atts ) {

		$atts = shortcode_atts(
			array(
				'id' => 0,
			),
			$atts,
			'rx_review_board_member'
		);

		$post_id = intval( $atts['id'] );

		if ( ! $post_id || self::POST_TYPE !== get_post_type( $post_id ) ) {
			return '';
		}

		return self::render_member_card( $post_id );
	}

	/**
	 * Generate JSON-LD schema.
	 */
	public static function get_member_schema( $post_id ) {

		$name        = get_the_title( $post_id );
		$url         = get_permalink( $post_id );
		$image       = get_the_post_thumbnail_url( $post_id, 'full' );
		$designation = self::get_meta( $post_id, 'designation' );
		$degrees     = self::get_meta( $post_id, 'degrees' );
		$institution = self::get_meta( $post_id, 'institution' );
		$email       = self::get_meta( $post_id, 'email' );
		$website     = self::get_meta( $post_id, 'website' );
		$linkedin    = self::get_meta( $post_id, 'linkedin' );
		$orcid       = self::get_meta( $post_id, 'orcid' );
		$summary     = self::get_meta( $post_id, 'profile_summary' );

		$same_as = array_filter( array( $website, $linkedin, $orcid ) );

		$schema = array(
			'@context'    => 'https://schema.org',
			'@type'       => 'Person',
			'name'        => $name,
			'url'         => $url,
			'description' => $summary ? $summary : get_the_excerpt( $post_id ),
		);

		if ( $image ) {
			$schema['image'] = $image;
		}

		if ( $designation ) {
			$schema['jobTitle'] = $designation;
		}

		if ( $degrees ) {
			$schema['honorificSuffix'] = $degrees;
		}

		if ( $email ) {
			$schema['email'] = $email;
		}

		if ( $institution ) {
			$schema['worksFor'] = array(
				'@type' => 'Organization',
				'name'  => $institution,
			);
		}

		if ( ! empty( $same_as ) ) {
			$schema['sameAs'] = array_values( $same_as );
		}

		return $schema;
	}

	/**
	 * Append schema to single member page.
	 */
	public static function append_member_schema_to_single( $content ) {

		if ( ! is_singular( self::POST_TYPE ) || ! in_the_loop() || ! is_main_query() ) {
			return $content;
		}

		$schema = self::get_member_schema( get_the_ID() );

		$content .= "\n" . '<script type="application/ld+json">' . wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . '</script>';

		return $content;
	}

	/**
	 * Optional template loader.
	 *
	 * You can create:
	 * - template-parts/review-board/single-review-board.php
	 * - template-parts/review-board/archive-review-board.php
	 */
	public static function template_loader( $template ) {

		if ( is_singular( self::POST_TYPE ) ) {
			$custom = locate_template( 'template-parts/review-board/single-review-board.php' );
			if ( $custom ) {
				return $custom;
			}
		}

		if ( is_post_type_archive( self::POST_TYPE ) ) {
			$custom = locate_template( 'template-parts/review-board/archive-review-board.php' );
			if ( $custom ) {
				return $custom;
			}
		}

		return $template;
	}

	/**
	 * Enqueue lightweight inline CSS.
	 */
	public static function enqueue_assets() {

		$css = '
		.rx-review-board-grid {
			display: grid;
			gap: 24px;
			margin: 24px 0;
		}
		.rx-review-board-grid--cols-1 { grid-template-columns: 1fr; }
		.rx-review-board-grid--cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
		.rx-review-board-grid--cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
		.rx-review-board-grid--cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

		.rx-review-board-card {
			background: #fff;
			border: 1px solid rgba(0,0,0,.08);
			border-radius: 18px;
			overflow: hidden;
			box-shadow: 0 12px 32px rgba(0,0,0,.06);
			transition: transform .2s ease, box-shadow .2s ease;
		}
		.rx-review-board-card:hover {
			transform: translateY(-3px);
			box-shadow: 0 16px 44px rgba(0,0,0,.1);
		}
		.rx-review-board-card__media {
			display: block;
			background: #f5f7fb;
			aspect-ratio: 4 / 3;
			overflow: hidden;
		}
		.rx-review-board-card__image {
			width: 100%;
			height: 100%;
			object-fit: cover;
			display: block;
		}
		.rx-review-board-card__placeholder {
			display: flex;
			align-items: center;
			justify-content: center;
			height: 100%;
			font-size: 42px;
			font-weight: 800;
			color: #1f4ed8;
		}
		.rx-review-board-card__body {
			padding: 22px;
		}
		.rx-review-board-card__title {
			margin: 0 0 8px;
			font-size: 22px;
			line-height: 1.25;
		}
		.rx-review-board-card__title a {
			text-decoration: none;
		}
		.rx-review-board-card__degrees,
		.rx-review-board-card__designation,
		.rx-review-board-card__experience,
		.rx-review-board-card__summary {
			margin: 8px 0;
		}
		.rx-review-board-card__button {
			display: inline-block;
			margin-top: 12px;
			padding: 10px 16px;
			border-radius: 999px;
			text-decoration: none;
			font-weight: 700;
			background: #1f4ed8;
			color: #fff;
		}
		@media (max-width: 900px) {
			.rx-review-board-grid--cols-3,
			.rx-review-board-grid--cols-4 {
				grid-template-columns: repeat(2, minmax(0, 1fr));
			}
		}
		@media (max-width: 640px) {
			.rx-review-board-grid,
			.rx-review-board-grid--cols-2,
			.rx-review-board-grid--cols-3,
			.rx-review-board-grid--cols-4 {
				grid-template-columns: 1fr;
			}
		}
		';

		wp_register_style( 'rx-review-board-inline', false, array(), '1.0.0' );
		wp_enqueue_style( 'rx-review-board-inline' );
		wp_add_inline_style( 'rx-review-board-inline', $css );
	}
}

RX_Review_Board_Post_Type::init();

endif;

Now include it inside your theme functions.php:

/**
 * RX Theme Review Board Post Type
 */
$rx_review_board_file = get_template_directory() . '/inc/post-types/review-board.php';

if ( file_exists( $rx_review_board_file ) ) {
	require_once $rx_review_board_file;
}

Recommended folder structure:

rx-theme/
├── functions.php
└── inc/
    └── post-types/
        └── review-board.php

Use this shortcode anywhere:

[rx_review_board limit="12" columns="3"]

Featured members only:

[rx_review_board limit="6" featured="yes" columns="3"]

By specialty:

[rx_review_board specialty="cardiology" limit="12" columns="3"]

Single member card:

[rx_review_board_member id="123"]

After adding the file, go to:

WordPress Dashboard → Settings → Permalinks → Save Changes

This refreshes the rewrite rules so /review-board/ works properly.

Leave a Reply

Your email address will not be published. Required fields are marked *