I’ll build this as a complete advanced inc/post-types/portfolio.php file for your RX theme, including custom post type, taxonomies, meta boxes, admin columns, REST support, shortcodes, templates/hooks, rewrite rules, and helper functions.
Below is an advanced inc/post-types/portfolio.php file for your RX theme. It registers a powerful Portfolio custom post type with REST/Gutenberg support, taxonomies, meta fields, admin columns, filters, shortcodes, template helpers, image sizes, schema data, and archive query controls.
WordPress recommends registering custom post types on the init hook, and REST support is enabled with show_in_rest => true. Taxonomies should also be registered properly and attached to the post type for query consistency.
Create this file:
<?php
/**
* RX Theme Portfolio Post Type
*
* File: inc/post-types/portfolio.php
*
* @package RX_Theme
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'RX_Portfolio_Post_Type' ) ) :
final class RX_Portfolio_Post_Type {
/**
* Post type slug.
*/
const POST_TYPE = 'rx_portfolio';
/**
* Taxonomy slugs.
*/
const TAX_CATEGORY = 'rx_portfolio_category';
const TAX_TAG = 'rx_portfolio_tag';
const TAX_SKILL = 'rx_portfolio_skill';
const TAX_CLIENT = 'rx_portfolio_client';
/**
* Meta nonce.
*/
const NONCE_ACTION = 'rx_portfolio_meta_save';
const NONCE_NAME = 'rx_portfolio_meta_nonce';
/**
* Boot hooks.
*/
public static function init() {
add_action( 'init', array( __CLASS__, 'register_post_type' ), 5 );
add_action( 'init', array( __CLASS__, 'register_taxonomies' ), 6 );
add_action( 'init', array( __CLASS__, 'register_meta_fields' ), 7 );
add_action( 'after_setup_theme', array( __CLASS__, 'add_image_sizes' ) );
add_action( 'add_meta_boxes', array( __CLASS__, 'add_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( 'restrict_manage_posts', array( __CLASS__, 'admin_taxonomy_filters' ) );
add_filter( 'parse_query', array( __CLASS__, 'admin_filter_query' ) );
add_action( 'pre_get_posts', array( __CLASS__, 'portfolio_archive_query' ) );
add_shortcode( 'rx_portfolio', array( __CLASS__, 'portfolio_shortcode' ) );
add_shortcode( 'rx_featured_portfolio', array( __CLASS__, 'featured_portfolio_shortcode' ) );
add_filter( 'post_updated_messages', array( __CLASS__, 'updated_messages' ) );
add_filter( 'bulk_post_updated_messages', array( __CLASS__, 'bulk_updated_messages' ), 10, 2 );
add_filter( 'single_template', array( __CLASS__, 'single_template' ) );
add_filter( 'archive_template', array( __CLASS__, 'archive_template' ) );
add_action( 'wp_head', array( __CLASS__, 'schema_json_ld' ), 20 );
}
/**
* Register Portfolio CPT.
*/
public static function register_post_type() {
$labels = array(
'name' => _x( 'Portfolios', 'Post type general name', 'rx-theme' ),
'singular_name' => _x( 'Portfolio', 'Post type singular name', 'rx-theme' ),
'menu_name' => __( 'Portfolio', 'rx-theme' ),
'name_admin_bar' => __( 'Portfolio', 'rx-theme' ),
'add_new' => __( 'Add New', 'rx-theme' ),
'add_new_item' => __( 'Add New Portfolio', 'rx-theme' ),
'new_item' => __( 'New Portfolio', 'rx-theme' ),
'edit_item' => __( 'Edit Portfolio', 'rx-theme' ),
'view_item' => __( 'View Portfolio', 'rx-theme' ),
'all_items' => __( 'All Portfolios', 'rx-theme' ),
'search_items' => __( 'Search Portfolios', 'rx-theme' ),
'parent_item_colon' => __( 'Parent Portfolios:', 'rx-theme' ),
'not_found' => __( 'No portfolios found.', 'rx-theme' ),
'not_found_in_trash' => __( 'No portfolios found in Trash.', 'rx-theme' ),
'featured_image' => __( 'Portfolio Featured Image', 'rx-theme' ),
'set_featured_image' => __( 'Set portfolio image', 'rx-theme' ),
'remove_featured_image' => __( 'Remove portfolio image', 'rx-theme' ),
'use_featured_image' => __( 'Use as portfolio image', 'rx-theme' ),
'archives' => __( 'Portfolio Archives', 'rx-theme' ),
'insert_into_item' => __( 'Insert into portfolio', 'rx-theme' ),
'uploaded_to_this_item' => __( 'Uploaded to this portfolio', 'rx-theme' ),
'filter_items_list' => __( 'Filter portfolios list', 'rx-theme' ),
'items_list_navigation' => __( 'Portfolios list navigation', 'rx-theme' ),
'items_list' => __( 'Portfolios list', 'rx-theme' ),
'item_published' => __( 'Portfolio published.', 'rx-theme' ),
'item_published_privately' => __( 'Portfolio published privately.', 'rx-theme' ),
'item_reverted_to_draft' => __( 'Portfolio reverted to draft.', 'rx-theme' ),
'item_scheduled' => __( 'Portfolio scheduled.', 'rx-theme' ),
'item_updated' => __( 'Portfolio updated.', 'rx-theme' ),
);
$args = array(
'labels' => $labels,
'description' => __( 'Portfolio projects, case studies, works, products, designs, apps, websites, and creative showcases.', 'rx-theme' ),
'public' => true,
'publicly_queryable' => true,
'exclude_from_search' => false,
'show_ui' => true,
'show_in_menu' => true,
'show_in_nav_menus' => true,
'show_in_admin_bar' => true,
'show_in_rest' => true,
'rest_base' => 'rx-portfolio',
'rest_controller_class'=> 'WP_REST_Posts_Controller',
'menu_position' => 21,
'menu_icon' => 'dashicons-portfolio',
'capability_type' => 'post',
'hierarchical' => false,
'has_archive' => 'portfolio',
'rewrite' => array(
'slug' => 'portfolio',
'with_front' => false,
'feeds' => true,
'pages' => true,
),
'query_var' => true,
'can_export' => true,
'delete_with_user' => false,
'taxonomies' => array(
self::TAX_CATEGORY,
self::TAX_TAG,
self::TAX_SKILL,
self::TAX_CLIENT,
),
'supports' => array(
'title',
'editor',
'excerpt',
'author',
'thumbnail',
'comments',
'trackbacks',
'revisions',
'custom-fields',
'page-attributes',
'post-formats',
),
'template' => array(
array(
'core/paragraph',
array(
'placeholder' => __( 'Write a short project overview...', 'rx-theme' ),
),
),
array(
'core/heading',
array(
'level' => 2,
'content' => __( 'Project Challenge', 'rx-theme' ),
),
),
array(
'core/paragraph',
array(
'placeholder' => __( 'Explain the problem or challenge...', 'rx-theme' ),
),
),
array(
'core/heading',
array(
'level' => 2,
'content' => __( 'Solution', 'rx-theme' ),
),
),
array(
'core/paragraph',
array(
'placeholder' => __( 'Explain your solution...', 'rx-theme' ),
),
),
array(
'core/heading',
array(
'level' => 2,
'content' => __( 'Result', 'rx-theme' ),
),
),
),
);
register_post_type( self::POST_TYPE, apply_filters( 'rx_portfolio_post_type_args', $args ) );
}
/**
* Register taxonomies.
*/
public static function register_taxonomies() {
register_taxonomy(
self::TAX_CATEGORY,
array( self::POST_TYPE ),
array(
'labels' => array(
'name' => __( 'Portfolio Categories', 'rx-theme' ),
'singular_name' => __( 'Portfolio Category', 'rx-theme' ),
'search_items' => __( 'Search Portfolio Categories', 'rx-theme' ),
'all_items' => __( 'All Portfolio Categories', 'rx-theme' ),
'parent_item' => __( 'Parent Portfolio Category', 'rx-theme' ),
'parent_item_colon' => __( 'Parent Portfolio Category:', 'rx-theme' ),
'edit_item' => __( 'Edit Portfolio Category', 'rx-theme' ),
'update_item' => __( 'Update Portfolio Category', 'rx-theme' ),
'add_new_item' => __( 'Add New Portfolio Category', 'rx-theme' ),
'new_item_name' => __( 'New Portfolio Category Name', 'rx-theme' ),
'menu_name' => __( 'Categories', 'rx-theme' ),
),
'public' => true,
'hierarchical' => true,
'show_ui' => true,
'show_admin_column' => true,
'show_in_nav_menus' => true,
'show_tagcloud' => true,
'show_in_rest' => true,
'rest_base' => 'rx-portfolio-categories',
'query_var' => true,
'rewrite' => array(
'slug' => 'portfolio-category',
'with_front' => false,
'hierarchical' => true,
),
)
);
register_taxonomy(
self::TAX_TAG,
array( self::POST_TYPE ),
array(
'labels' => array(
'name' => __( 'Portfolio Tags', 'rx-theme' ),
'singular_name' => __( 'Portfolio Tag', 'rx-theme' ),
'search_items' => __( 'Search Portfolio Tags', 'rx-theme' ),
'popular_items' => __( 'Popular Portfolio Tags', 'rx-theme' ),
'all_items' => __( 'All Portfolio Tags', 'rx-theme' ),
'edit_item' => __( 'Edit Portfolio Tag', 'rx-theme' ),
'update_item' => __( 'Update Portfolio Tag', 'rx-theme' ),
'add_new_item' => __( 'Add New Portfolio Tag', 'rx-theme' ),
'new_item_name' => __( 'New Portfolio Tag Name', 'rx-theme' ),
'separate_items_with_commas' => __( 'Separate tags with commas', 'rx-theme' ),
'add_or_remove_items' => __( 'Add or remove tags', 'rx-theme' ),
'choose_from_most_used' => __( 'Choose from the most used tags', 'rx-theme' ),
'menu_name' => __( 'Tags', 'rx-theme' ),
),
'public' => true,
'hierarchical' => false,
'show_ui' => true,
'show_admin_column' => true,
'show_in_nav_menus' => true,
'show_tagcloud' => true,
'show_in_rest' => true,
'rest_base' => 'rx-portfolio-tags',
'query_var' => true,
'rewrite' => array(
'slug' => 'portfolio-tag',
'with_front' => false,
),
)
);
register_taxonomy(
self::TAX_SKILL,
array( self::POST_TYPE ),
array(
'labels' => array(
'name' => __( 'Skills / Technologies', 'rx-theme' ),
'singular_name' => __( 'Skill / Technology', 'rx-theme' ),
'search_items' => __( 'Search Skills', 'rx-theme' ),
'all_items' => __( 'All Skills', 'rx-theme' ),
'edit_item' => __( 'Edit Skill', 'rx-theme' ),
'update_item' => __( 'Update Skill', 'rx-theme' ),
'add_new_item' => __( 'Add New Skill', 'rx-theme' ),
'new_item_name' => __( 'New Skill Name', 'rx-theme' ),
'menu_name' => __( 'Skills', 'rx-theme' ),
),
'public' => true,
'hierarchical' => false,
'show_ui' => true,
'show_admin_column' => true,
'show_in_rest' => true,
'rest_base' => 'rx-portfolio-skills',
'query_var' => true,
'rewrite' => array(
'slug' => 'portfolio-skill',
'with_front' => false,
),
)
);
register_taxonomy(
self::TAX_CLIENT,
array( self::POST_TYPE ),
array(
'labels' => array(
'name' => __( 'Clients', 'rx-theme' ),
'singular_name' => __( 'Client', 'rx-theme' ),
'search_items' => __( 'Search Clients', 'rx-theme' ),
'all_items' => __( 'All Clients', 'rx-theme' ),
'edit_item' => __( 'Edit Client', 'rx-theme' ),
'update_item' => __( 'Update Client', 'rx-theme' ),
'add_new_item' => __( 'Add New Client', 'rx-theme' ),
'new_item_name' => __( 'New Client Name', 'rx-theme' ),
'menu_name' => __( 'Clients', 'rx-theme' ),
),
'public' => true,
'hierarchical' => false,
'show_ui' => true,
'show_admin_column' => true,
'show_in_rest' => true,
'rest_base' => 'rx-portfolio-clients',
'query_var' => true,
'rewrite' => array(
'slug' => 'portfolio-client',
'with_front' => false,
),
)
);
}
/**
* Register portfolio meta for REST API and Gutenberg.
*/
public static function register_meta_fields() {
$fields = self::meta_fields();
foreach ( $fields as $key => $field ) {
register_post_meta(
self::POST_TYPE,
$key,
array(
'type' => $field['type'],
'description' => $field['label'],
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => $field['sanitize'],
'auth_callback' => function() {
return current_user_can( 'edit_posts' );
},
)
);
}
}
/**
* Meta field map.
*/
public static function meta_fields() {
return array(
'_rx_portfolio_subtitle' => array(
'label' => __( 'Subtitle', 'rx-theme' ),
'type' => 'string',
'input' => 'text',
'sanitize' => 'sanitize_text_field',
),
'_rx_portfolio_client_name' => array(
'label' => __( 'Client Name', 'rx-theme' ),
'type' => 'string',
'input' => 'text',
'sanitize' => 'sanitize_text_field',
),
'_rx_portfolio_project_url' => array(
'label' => __( 'Project URL', 'rx-theme' ),
'type' => 'string',
'input' => 'url',
'sanitize' => 'esc_url_raw',
),
'_rx_portfolio_demo_url' => array(
'label' => __( 'Live Demo URL', 'rx-theme' ),
'type' => 'string',
'input' => 'url',
'sanitize' => 'esc_url_raw',
),
'_rx_portfolio_github_url' => array(
'label' => __( 'GitHub URL', 'rx-theme' ),
'type' => 'string',
'input' => 'url',
'sanitize' => 'esc_url_raw',
),
'_rx_portfolio_start_date' => array(
'label' => __( 'Start Date', 'rx-theme' ),
'type' => 'string',
'input' => 'date',
'sanitize' => 'sanitize_text_field',
),
'_rx_portfolio_end_date' => array(
'label' => __( 'End Date', 'rx-theme' ),
'type' => 'string',
'input' => 'date',
'sanitize' => 'sanitize_text_field',
),
'_rx_portfolio_duration' => array(
'label' => __( 'Duration', 'rx-theme' ),
'type' => 'string',
'input' => 'text',
'sanitize' => 'sanitize_text_field',
),
'_rx_portfolio_budget' => array(
'label' => __( 'Budget', 'rx-theme' ),
'type' => 'string',
'input' => 'text',
'sanitize' => 'sanitize_text_field',
),
'_rx_portfolio_location' => array(
'label' => __( 'Location', 'rx-theme' ),
'type' => 'string',
'input' => 'text',
'sanitize' => 'sanitize_text_field',
),
'_rx_portfolio_role' => array(
'label' => __( 'Your Role', 'rx-theme' ),
'type' => 'string',
'input' => 'text',
'sanitize' => 'sanitize_text_field',
),
'_rx_portfolio_team_size' => array(
'label' => __( 'Team Size', 'rx-theme' ),
'type' => 'string',
'input' => 'number',
'sanitize' => 'sanitize_text_field',
),
'_rx_portfolio_rating' => array(
'label' => __( 'Rating', 'rx-theme' ),
'type' => 'number',
'input' => 'number',
'sanitize' => array( __CLASS__, 'sanitize_float' ),
),
'_rx_portfolio_featured' => array(
'label' => __( 'Featured Portfolio', 'rx-theme' ),
'type' => 'boolean',
'input' => 'checkbox',
'sanitize' => array( __CLASS__, 'sanitize_checkbox' ),
),
'_rx_portfolio_case_study' => array(
'label' => __( 'Case Study Text', 'rx-theme' ),
'type' => 'string',
'input' => 'textarea',
'sanitize' => 'sanitize_textarea_field',
),
);
}
/**
* Add image sizes.
*/
public static function add_image_sizes() {
add_image_size( 'rx-portfolio-card', 600, 420, true );
add_image_size( 'rx-portfolio-grid', 900, 650, true );
add_image_size( 'rx-portfolio-hero', 1600, 900, true );
}
/**
* Add meta boxes.
*
* WordPress meta boxes are added with add_meta_box() on the meta box hook. Nonces should be used during save for security. :contentReference[oaicite:1]{index=1}
*/
public static function add_meta_boxes() {
add_meta_box(
'rx_portfolio_details',
__( 'Portfolio Details', 'rx-theme' ),
array( __CLASS__, 'render_details_meta_box' ),
self::POST_TYPE,
'normal',
'high'
);
add_meta_box(
'rx_portfolio_links',
__( 'Portfolio Links', 'rx-theme' ),
array( __CLASS__, 'render_links_meta_box' ),
self::POST_TYPE,
'side',
'default'
);
add_meta_box(
'rx_portfolio_options',
__( 'Portfolio Options', 'rx-theme' ),
array( __CLASS__, 'render_options_meta_box' ),
self::POST_TYPE,
'side',
'default'
);
}
/**
* Details meta box.
*/
public static function render_details_meta_box( $post ) {
wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME );
$fields = array(
'_rx_portfolio_subtitle',
'_rx_portfolio_client_name',
'_rx_portfolio_start_date',
'_rx_portfolio_end_date',
'_rx_portfolio_duration',
'_rx_portfolio_budget',
'_rx_portfolio_location',
'_rx_portfolio_role',
'_rx_portfolio_team_size',
'_rx_portfolio_rating',
'_rx_portfolio_case_study',
);
echo '<div class="rx-portfolio-metabox rx-portfolio-details-metabox">';
foreach ( $fields as $key ) {
self::render_field( $post->ID, $key );
}
echo '</div>';
}
/**
* Links meta box.
*/
public static function render_links_meta_box( $post ) {
$fields = array(
'_rx_portfolio_project_url',
'_rx_portfolio_demo_url',
'_rx_portfolio_github_url',
);
echo '<div class="rx-portfolio-metabox rx-portfolio-links-metabox">';
foreach ( $fields as $key ) {
self::render_field( $post->ID, $key );
}
echo '</div>';
}
/**
* Options meta box.
*/
public static function render_options_meta_box( $post ) {
self::render_field( $post->ID, '_rx_portfolio_featured' );
}
/**
* Render individual field.
*/
private static function render_field( $post_id, $key ) {
$fields = self::meta_fields();
if ( empty( $fields[ $key ] ) ) {
return;
}
$field = $fields[ $key ];
$value = get_post_meta( $post_id, $key, true );
$id = str_replace( '_', '-', ltrim( $key, '_' ) );
echo '<p class="rx-field rx-field-' . esc_attr( $field['input'] ) . '">';
echo '<label for="' . esc_attr( $id ) . '"><strong>' . esc_html( $field['label'] ) . '</strong></label><br>';
if ( 'textarea' === $field['input'] ) {
echo '<textarea id="' . esc_attr( $id ) . '" name="' . esc_attr( $key ) . '" rows="5" style="width:100%;">' . esc_textarea( $value ) . '</textarea>';
} elseif ( 'checkbox' === $field['input'] ) {
echo '<label>';
echo '<input type="checkbox" id="' . esc_attr( $id ) . '" name="' . esc_attr( $key ) . '" value="1" ' . checked( $value, '1', false ) . '>';
echo ' ' . esc_html__( 'Mark this portfolio as featured', 'rx-theme' );
echo '</label>';
} else {
echo '<input type="' . esc_attr( $field['input'] ) . '" id="' . esc_attr( $id ) . '" name="' . esc_attr( $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[ self::NONCE_NAME ] ) ) {
return;
}
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ self::NONCE_NAME ] ) ), self::NONCE_ACTION ) ) {
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( wp_is_post_revision( $post_id ) ) {
return;
}
if ( self::POST_TYPE !== $post->post_type ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
$fields = self::meta_fields();
foreach ( $fields as $key => $field ) {
if ( 'checkbox' === $field['input'] ) {
$value = isset( $_POST[ $key ] ) ? '1' : '0';
} else {
$value = isset( $_POST[ $key ] ) ? wp_unslash( $_POST[ $key ] ) : '';
}
if ( is_callable( $field['sanitize'] ) ) {
$value = call_user_func( $field['sanitize'], $value );
} else {
$value = sanitize_text_field( $value );
}
if ( '' === $value || null === $value ) {
delete_post_meta( $post_id, $key );
} else {
update_post_meta( $post_id, $key, $value );
}
}
}
/**
* Sanitize checkbox.
*/
public static function sanitize_checkbox( $value ) {
return ! empty( $value ) ? '1' : '0';
}
/**
* Sanitize float.
*/
public static function sanitize_float( $value ) {
$value = is_numeric( $value ) ? (float) $value : 0;
return max( 0, min( 5, $value ) );
}
/**
* Admin columns.
*/
public static function admin_columns( $columns ) {
$new = array();
$new['cb'] = $columns['cb'];
$new['thumbnail'] = __( 'Image', 'rx-theme' );
$new['title'] = __( 'Portfolio Title', 'rx-theme' );
$new['rx_portfolio_category'] = __( 'Categories', 'rx-theme' );
$new['rx_portfolio_client_name'] = __( 'Client', 'rx-theme' );
$new['rx_portfolio_featured'] = __( 'Featured', 'rx-theme' );
$new['rx_portfolio_rating'] = __( 'Rating', 'rx-theme' );
$new['rx_portfolio_project_url'] = __( 'URL', 'rx-theme' );
$new['date'] = $columns['date'];
return $new;
}
/**
* Admin column output.
*/
public static function admin_column_content( $column, $post_id ) {
switch ( $column ) {
case 'thumbnail':
if ( has_post_thumbnail( $post_id ) ) {
echo get_the_post_thumbnail( $post_id, array( 70, 70 ) );
} else {
echo '<span aria-hidden="true">—</span>';
}
break;
case 'rx_portfolio_category':
echo wp_kses_post( get_the_term_list( $post_id, self::TAX_CATEGORY, '', ', ', '' ) );
break;
case 'rx_portfolio_client_name':
echo esc_html( get_post_meta( $post_id, '_rx_portfolio_client_name', true ) );
break;
case 'rx_portfolio_featured':
$featured = get_post_meta( $post_id, '_rx_portfolio_featured', true );
echo $featured ? '<span style="color:green;">★</span>' : '<span aria-hidden="true">—</span>';
break;
case 'rx_portfolio_rating':
$rating = get_post_meta( $post_id, '_rx_portfolio_rating', true );
echo $rating ? esc_html( $rating ) . '/5' : '<span aria-hidden="true">—</span>';
break;
case 'rx_portfolio_project_url':
$url = get_post_meta( $post_id, '_rx_portfolio_project_url', true );
if ( $url ) {
echo '<a href="' . esc_url( $url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open', 'rx-theme' ) . '</a>';
} else {
echo '<span aria-hidden="true">—</span>';
}
break;
}
}
/**
* Sortable columns.
*/
public static function sortable_columns( $columns ) {
$columns['rx_portfolio_client_name'] = '_rx_portfolio_client_name';
$columns['rx_portfolio_rating'] = '_rx_portfolio_rating';
$columns['rx_portfolio_featured'] = '_rx_portfolio_featured';
return $columns;
}
/**
* Admin dropdown filters.
*/
public static function admin_taxonomy_filters() {
global $typenow;
if ( self::POST_TYPE !== $typenow ) {
return;
}
$taxonomies = array(
self::TAX_CATEGORY => __( 'All Portfolio Categories', 'rx-theme' ),
self::TAX_SKILL => __( 'All Skills', 'rx-theme' ),
self::TAX_CLIENT => __( 'All Clients', 'rx-theme' ),
);
foreach ( $taxonomies as $taxonomy => $label ) {
$selected = isset( $_GET[ $taxonomy ] ) ? sanitize_text_field( wp_unslash( $_GET[ $taxonomy ] ) ) : '';
wp_dropdown_categories(
array(
'show_option_all' => $label,
'taxonomy' => $taxonomy,
'name' => $taxonomy,
'orderby' => 'name',
'selected' => $selected,
'hierarchical' => true,
'depth' => 3,
'show_count' => true,
'hide_empty' => false,
'value_field' => 'slug',
)
);
}
}
/**
* Convert admin filter dropdowns to taxonomy queries.
*/
public static function admin_filter_query( $query ) {
global $pagenow;
if ( ! is_admin() || 'edit.php' !== $pagenow ) {
return;
}
$post_type = isset( $_GET['post_type'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type'] ) ) : '';
if ( self::POST_TYPE !== $post_type ) {
return;
}
foreach ( array( self::TAX_CATEGORY, self::TAX_SKILL, self::TAX_CLIENT ) as $taxonomy ) {
if ( ! empty( $_GET[ $taxonomy ] ) ) {
$query->query_vars[ $taxonomy ] = sanitize_text_field( wp_unslash( $_GET[ $taxonomy ] ) );
}
}
}
/**
* Portfolio archive control.
*/
public static function portfolio_archive_query( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( is_post_type_archive( self::POST_TYPE ) || is_tax( array( self::TAX_CATEGORY, self::TAX_TAG, self::TAX_SKILL, self::TAX_CLIENT ) ) ) {
$query->set( 'posts_per_page', apply_filters( 'rx_portfolio_archive_posts_per_page', 12 ) );
$query->set( 'orderby', apply_filters( 'rx_portfolio_archive_orderby', 'menu_order date' ) );
$query->set( 'order', apply_filters( 'rx_portfolio_archive_order', 'DESC' ) );
}
}
/**
* Portfolio shortcode.
*
* Usage:
* [rx_portfolio posts="6" columns="3" category="web-design" featured="1"]
*/
public static function portfolio_shortcode( $atts ) {
$atts = shortcode_atts(
array(
'posts' => 6,
'columns' => 3,
'category' => '',
'skill' => '',
'client' => '',
'featured' => '',
'orderby' => 'date',
'order' => 'DESC',
),
$atts,
'rx_portfolio'
);
$args = array(
'post_type' => self::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => absint( $atts['posts'] ),
'orderby' => sanitize_key( $atts['orderby'] ),
'order' => 'ASC' === strtoupper( $atts['order'] ) ? 'ASC' : 'DESC',
'ignore_sticky_posts' => true,
);
$tax_query = array();
if ( ! empty( $atts['category'] ) ) {
$tax_query[] = array(
'taxonomy' => self::TAX_CATEGORY,
'field' => 'slug',
'terms' => sanitize_title( $atts['category'] ),
);
}
if ( ! empty( $atts['skill'] ) ) {
$tax_query[] = array(
'taxonomy' => self::TAX_SKILL,
'field' => 'slug',
'terms' => sanitize_title( $atts['skill'] ),
);
}
if ( ! empty( $atts['client'] ) ) {
$tax_query[] = array(
'taxonomy' => self::TAX_CLIENT,
'field' => 'slug',
'terms' => sanitize_title( $atts['client'] ),
);
}
if ( ! empty( $tax_query ) ) {
$args['tax_query'] = $tax_query;
}
if ( '' !== $atts['featured'] ) {
$args['meta_query'] = array(
array(
'key' => '_rx_portfolio_featured',
'value' => '1',
'compare' => '=',
),
);
}
$query = new WP_Query( apply_filters( 'rx_portfolio_shortcode_query_args', $args, $atts ) );
ob_start();
if ( $query->have_posts() ) {
$columns = max( 1, min( 6, absint( $atts['columns'] ) ) );
echo '<div class="rx-portfolio-grid rx-portfolio-columns-' . esc_attr( $columns ) . '">';
while ( $query->have_posts() ) {
$query->the_post();
self::render_portfolio_card( get_the_ID() );
}
echo '</div>';
} else {
echo '<p class="rx-no-portfolio">' . esc_html__( 'No portfolio items found.', 'rx-theme' ) . '</p>';
}
wp_reset_postdata();
return ob_get_clean();
}
/**
* Featured shortcode.
*
* Usage:
* [rx_featured_portfolio posts="3"]
*/
public static function featured_portfolio_shortcode( $atts ) {
$atts = shortcode_atts(
array(
'posts' => 3,
'columns' => 3,
),
$atts,
'rx_featured_portfolio'
);
return self::portfolio_shortcode(
array(
'posts' => $atts['posts'],
'columns' => $atts['columns'],
'featured' => '1',
)
);
}
/**
* Render portfolio card.
*/
public static function render_portfolio_card( $post_id ) {
$client = get_post_meta( $post_id, '_rx_portfolio_client_name', true );
$demo = get_post_meta( $post_id, '_rx_portfolio_demo_url', true );
$rating = get_post_meta( $post_id, '_rx_portfolio_rating', true );
$terms = get_the_term_list( $post_id, self::TAX_CATEGORY, '', ', ', '' );
echo '<article class="rx-portfolio-card">';
if ( has_post_thumbnail( $post_id ) ) {
echo '<a class="rx-portfolio-card__image" href="' . esc_url( get_permalink( $post_id ) ) . '">';
echo get_the_post_thumbnail( $post_id, 'rx-portfolio-card' );
echo '</a>';
}
echo '<div class="rx-portfolio-card__content">';
if ( $terms ) {
echo '<div class="rx-portfolio-card__terms">' . wp_kses_post( $terms ) . '</div>';
}
echo '<h3 class="rx-portfolio-card__title">';
echo '<a href="' . esc_url( get_permalink( $post_id ) ) . '">' . esc_html( get_the_title( $post_id ) ) . '</a>';
echo '</h3>';
if ( has_excerpt( $post_id ) ) {
echo '<div class="rx-portfolio-card__excerpt">' . esc_html( get_the_excerpt( $post_id ) ) . '</div>';
}
if ( $client ) {
echo '<p class="rx-portfolio-card__client"><strong>' . esc_html__( 'Client:', 'rx-theme' ) . '</strong> ' . esc_html( $client ) . '</p>';
}
if ( $rating ) {
echo '<p class="rx-portfolio-card__rating"><strong>' . esc_html__( 'Rating:', 'rx-theme' ) . '</strong> ' . esc_html( $rating ) . '/5</p>';
}
echo '<div class="rx-portfolio-card__actions">';
echo '<a class="rx-btn rx-btn-primary" href="' . esc_url( get_permalink( $post_id ) ) . '">' . esc_html__( 'View Case Study', 'rx-theme' ) . '</a>';
if ( $demo ) {
echo '<a class="rx-btn rx-btn-secondary" href="' . esc_url( $demo ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Live Demo', 'rx-theme' ) . '</a>';
}
echo '</div>';
echo '</div>';
echo '</article>';
}
/**
* Updated messages.
*/
public static function updated_messages( $messages ) {
$messages[ self::POST_TYPE ] = array(
0 => '',
1 => __( 'Portfolio updated.', 'rx-theme' ),
2 => __( 'Custom field updated.', 'rx-theme' ),
3 => __( 'Custom field deleted.', 'rx-theme' ),
4 => __( 'Portfolio updated.', 'rx-theme' ),
5 => isset( $_GET['revision'] )
? sprintf(
/* translators: %s revision date */
__( 'Portfolio restored to revision from %s.', 'rx-theme' ),
wp_post_revision_title( absint( $_GET['revision'] ), false )
)
: false,
6 => __( 'Portfolio published.', 'rx-theme' ),
7 => __( 'Portfolio saved.', 'rx-theme' ),
8 => __( 'Portfolio submitted.', 'rx-theme' ),
9 => __( 'Portfolio scheduled.', 'rx-theme' ),
10 => __( 'Portfolio draft updated.', 'rx-theme' ),
);
return $messages;
}
/**
* Bulk messages.
*/
public static function bulk_updated_messages( $bulk_messages, $bulk_counts ) {
$bulk_messages[ self::POST_TYPE ] = array(
'updated' => _n( '%s portfolio updated.', '%s portfolios updated.', $bulk_counts['updated'], 'rx-theme' ),
'locked' => _n( '%s portfolio not updated, somebody is editing it.', '%s portfolios not updated, somebody is editing them.', $bulk_counts['locked'], 'rx-theme' ),
'deleted' => _n( '%s portfolio permanently deleted.', '%s portfolios permanently deleted.', $bulk_counts['deleted'], 'rx-theme' ),
'trashed' => _n( '%s portfolio moved to the Trash.', '%s portfolios moved to the Trash.', $bulk_counts['trashed'], 'rx-theme' ),
'untrashed' => _n( '%s portfolio restored from the Trash.', '%s portfolios restored from the Trash.', $bulk_counts['untrashed'], 'rx-theme' ),
);
return $bulk_messages;
}
/**
* Load theme single template if exists.
*/
public static function single_template( $template ) {
if ( is_singular( self::POST_TYPE ) ) {
$theme_template = locate_template( array( 'single-rx_portfolio.php', 'templates/single-rx_portfolio.php' ) );
if ( $theme_template ) {
return $theme_template;
}
}
return $template;
}
/**
* Load theme archive template if exists.
*/
public static function archive_template( $template ) {
if ( is_post_type_archive( self::POST_TYPE ) ) {
$theme_template = locate_template( array( 'archive-rx_portfolio.php', 'templates/archive-rx_portfolio.php' ) );
if ( $theme_template ) {
return $theme_template;
}
}
return $template;
}
/**
* Add JSON-LD schema for single portfolio.
*/
public static function schema_json_ld() {
if ( ! is_singular( self::POST_TYPE ) ) {
return;
}
global $post;
if ( ! $post instanceof WP_Post ) {
return;
}
$image = has_post_thumbnail( $post->ID )
? get_the_post_thumbnail_url( $post->ID, 'full' )
: '';
$client = get_post_meta( $post->ID, '_rx_portfolio_client_name', true );
$url = get_post_meta( $post->ID, '_rx_portfolio_project_url', true );
$rating = get_post_meta( $post->ID, '_rx_portfolio_rating', true );
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'CreativeWork',
'name' => get_the_title( $post ),
'description' => wp_strip_all_tags( get_the_excerpt( $post ) ),
'url' => get_permalink( $post ),
'datePublished'=> get_the_date( DATE_W3C, $post ),
'dateModified' => get_the_modified_date( DATE_W3C, $post ),
'author' => array(
'@type' => 'Person',
'name' => get_the_author_meta( 'display_name', $post->post_author ),
),
);
if ( $image ) {
$schema['image'] = esc_url_raw( $image );
}
if ( $client ) {
$schema['client'] = array(
'@type' => 'Organization',
'name' => $client,
);
}
if ( $url ) {
$schema['sameAs'] = esc_url_raw( $url );
}
if ( $rating ) {
$schema['aggregateRating'] = array(
'@type' => 'AggregateRating',
'ratingValue' => (float) $rating,
'bestRating' => 5,
'worstRating' => 1,
'ratingCount' => 1,
);
}
echo '<script type="application/ld+json">' . wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . '</script>' . "\n";
}
/**
* Helper: get portfolio meta.
*/
public static function get_meta( $post_id, $key, $default = '' ) {
$value = get_post_meta( $post_id, $key, true );
return '' !== $value ? $value : $default;
}
/**
* Helper: get featured portfolios.
*/
public static function get_featured( $limit = 6 ) {
return new WP_Query(
array(
'post_type' => self::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => absint( $limit ),
'ignore_sticky_posts' => true,
'meta_query' => array(
array(
'key' => '_rx_portfolio_featured',
'value' => '1',
'compare' => '=',
),
),
)
);
}
}
RX_Portfolio_Post_Type::init();
endif;
/**
* Global helper function.
*/
if ( ! function_exists( 'rx_get_portfolio_meta' ) ) {
function rx_get_portfolio_meta( $post_id, $key, $default = '' ) {
if ( class_exists( 'RX_Portfolio_Post_Type' ) ) {
return RX_Portfolio_Post_Type::get_meta( $post_id, $key, $default );
}
return $default;
}
}
/**
* Global helper function.
*/
if ( ! function_exists( 'rx_render_portfolio_card' ) ) {
function rx_render_portfolio_card( $post_id = 0 ) {
$post_id = $post_id ? absint( $post_id ) : get_the_ID();
if ( class_exists( 'RX_Portfolio_Post_Type' ) ) {
RX_Portfolio_Post_Type::render_portfolio_card( $post_id );
}
}
}
Then include it from your functions.php:
require_once get_template_directory() . '/inc/post-types/portfolio.php';
After adding this file, go to:
WordPress Dashboard → Settings → Permalinks → Save Changes
This refreshes the rewrite rules so /portfolio/, /portfolio-category/, and single portfolio URLs work correctly.