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.