Create Custom Post Types & Taxonomies

A Step-by-Step Guide: 
Create Custom Post Types & Taxonomies In WordPress

Are you ready to take your WordPress site beyond basic posts and pages? Creating custom post types and taxonomies offers a powerful way to organize and present your content in a way that suits your unique needs. In this guide, we’ll walk you through the essentials of registering a custom post type and establishing relevant taxonomies (categories), so you can enhance your site’s structure, boost discoverability, and provide visitors with a more intuitive user experiences.

What is a custom post type in WordPress?

A Custom Post Type (CPT) in WordPress is a way to add your own content types beyond the default post types like Posts, Pages, and Media. It allows you to structure and manage specific kinds of content in your WordPress site, tailored to your needs.

Portfolio:

Perfect for showcasing your creative or professional work, such as graphic designs, photography, or freelance projects. You can categorize and tag portfolio items by client, industry, or skill, making it easy for visitors to filter through your work and find what interests them.

Recipes:

Food bloggers benefit from structuring recipes separately. Add custom fields for ingredients, preparation steps, and nutritional facts. You can also enable ratings to let readers vote on their favorite dishes.

Movies:

For a movie review website, create a Movie Reviews custom post type. Incorporate fields for cast, director, release date, and a rating system. Readers can browse reviews by genre, era, or language.

Charity Donations:

Keep track of and display organizations or individuals who’ve contributed to a charitable cause. You could store donor details, donation amounts, and dates, and even generate reports or gratitude lists.

Events:

Create an events calendar complete with dates, times, and locations. You can add a metabox for event registration information, attach Google Maps for venue locations, and even set up recurring events.

Testimonials:

Collect and display customer reviews or feedback in a neat layout. You can showcase these testimonials throughout your site—on landing pages, sales pages, or sidebars—to build social proof and trust.

Book Summaries:

If you run a blog or resource site focused on reading and literature, creating a Book Summaries post type allows you to categorize titles by author, genre, or topic. Add fields like “Key Takeaways” and “Favorite Quotes” to give readers valuable insights at a glance.

Locations:

Showcase multiple business locations, complete with addresses, phone numbers, and working hours. Use custom taxonomies like “City” or “Region” for easy sorting, plus a map integration for user-friendly navigation.

What are the features of a custom post type?

A CPT behaves similarly to posts and pages but can have unique features such as:

  • Custom Labels: Titles, menu names, etc., can reflect your content type (e.g., "Books" instead of "Posts").
  • Custom Fields: Additional metadata for storing extra details like price, rating, or location.
  • Custom Templates: Custom archive and single-view templates (e.g., archive-portfolio.php or single-event.php).
  • Taxonomies: You can create custom categories or tags specific to your post type. For example locations, genre for movies, type of work for portfolio etc

Real World Examples on How to Use Custom Post Types:

CHARITY DONATIONS

Custom Fields:
- Donation Amount
- Photo Description
- Date
- Link

Archive Template:
List of all organizations that have donated sorted from newest to oldest. Displayed in a timeline like design.

Shortcode:
Preview most rencent donors.

Things Todo App

Category: Location
- Bali
- Osaka
- Vancouver

Category: Activity Type
- Beach
- Outdoor
- Indoor
- Tourist Attractions

Custom Fields:
- Map Embed
- Reviews
- Map Link
- Social Media Links
- Open times
- Address

Archive Template:
Display a list of all things todo in a particular location.

Single Template:
Displays all the detailed information about the specific business or thing todo in that city.

RECYCLING FACILITY

Category: Items Accepted
- Bottles & Cans
- Electronics
- Paint
- Cardboard
- Scrap Metal

Custom Fields:
- Location
- Reviews
- Embed Map Link
- Map Link
- Phone
- Email

Archive Template:
Displays all recycling depot locations.

Single Template:
Displays all the details about a specific recycling depot.

How To Create a Custom Post Type in WordPress

Step 1: Setup a custom wordpress plugin.

Make sure to follow the previous steps on how to setup wordpress development environment and how to setup a custom wordpress plugin. Then you will be ready to add these code snippets to your plugin. You can also add these code snippets to your functions.php file in your theme.

How To Register a Simple Custom Post Type

Below is one of the simplest examples of registering a custom post type in WordPress. It uses the bare minimum parameters to create a public post type.

Simple Custom Post Type

<?php
/**
 * Very simple example of a custom post type called "Books".
 */
function create_books_cpt() {
    register_post_type( 'book', array(
        // The name displayed in the Admin menu and post type listings
        'label'  => 'Books',
        // Makes it visible on the front end and in the admin
        'public' => true,    
    ) );
}
add_action( 'init', 'create_books_cpt' );

How It Works:

register_post_type()
This function registers a custom post type in WordPress. This tells WordPress how to handle, display, and manage the new content type. They can also be made public or private and tailored to work with custom taxonomies, metaboxes, and plugins. We will covered a more advanced example next.

add_action( 'init', 'create_books_cpt' );
The init hook, which fires after WordPress has finished loading but before any headers are sent. This is where many core functions are set up, making it a common and safe place to register custom post types. We’re instructing WordPress when you reach the init phase of your loading process, please run the create_books_cpt function

Custom Post Type - All The Options & Settings Explained In Detail

Complete Custom Post Type Settings

function register_movie_reviews_cpt() {

    /*-------------------------------------------------------------------

    LABELS ARRAY:

    Describes the various text strings used throughout the admin interface.
    You can pick and choose what labels you want to change from default.

    LOCALIZATION:

    __( 'Add New', 'textdomain' ) - Functions used for translations. (Considered best practice)
    textdomain - Change to slug of your plugin so wordpress knows where to find translation files.
   
    -------------------------------------------------------------------*/


    $labels = array(

    //----- RRIMARY LABELS ----//
    // Plural name E.g., “Movie Reviews”
    'name'                     => _x( 'Movie Reviews', 'Post type general name', 'textdomain' ),
    // More than 1 name E.g., “Movie Review”
    'singular_name'            => _x( 'Movie Review', 'Post type singular name', 'textdomain' ),

    //----- EDIT POST MENU LABELS  ----//
    'add_new'                  => __( 'Add New', 'textdomain' ),
    'add_new_item'             => __( 'Add New Movie Review', 'textdomain' ),
    'edit_item'                => __( 'Edit Movie Review', 'textdomain' ),
    'new_item'                 => __( 'New Movie Review', 'textdomain' ),
    'view_item'                => __( 'View Movie Review', 'textdomain' ),
    'update_item'              => __( 'Update Movie Review', 'textdomain' ),
    'search_items'             => __( 'Search Movie Reviews', 'textdomain' ),
    // This label is shown in WP 5.0+ in some places.
    'view_items'               => __( 'View Movie Reviews', 'textdomain' ),  
    'not_found'                => __( 'No Movie Reviews found.', 'textdomain' ),
    'not_found_in_trash'       => __( 'No Movie Reviews found in Trash.', 'textdomain' ),
    // Parent/Child Items (mostly relevant when hierarchical = true)
    'parent_item_colon'        => __( 'Parent Movie Review:', 'textdomain' ),
    // Bulk & Listing Labels
    'all_items'                => __( 'All Movie Reviews', 'textdomain' ),
    'archives'                 => __( 'Movie Review Archives', 'textdomain' ),

    // Metabox title on the single post screen.
    'attributes'               => __( 'Movie Review Attributes', 'textdomain' ),
    'insert_into_item'         => __( 'Insert into movie review', 'textdomain' ),
    'uploaded_to_this_item'    => __( 'Uploaded to this movie review', 'textdomain' ),

    //----- MENU & ADMIN BAR  ----//
    'menu_name'                => __( 'Movie Reviews', 'textdomain' ),
    'name_admin_bar'           => _x( 'Movie Review', 'Add New on Toolbar', 'textdomain' ),
    'item_published'           => __( 'Movie Review published.', 'textdomain' ),
    'item_published_privately' => __( 'Movie Review published privately.', 'textdomain' ),
    'item_reverted_to_draft'   => __( 'Movie Review reverted to draft.', 'textdomain' ),
    'item_scheduled'           => __( 'Movie Review scheduled.', 'textdomain' ),
    'item_updated'             => __( 'Movie Review updated.', 'textdomain' ),

    // These two are rarely used, but exist for list screens in the admin
    'items_list'               => __( 'Movie Reviews list', 'textdomain' ),
    'items_list_navigation'    => __( 'Movie Reviews list navigation', 'textdomain' ),
    'filter_items_list'        => __( 'Filter Movie Reviews list', 'textdomain' ),
    );


   
    /* -------------------------------------------------------------------
    2. Arguments Array:
    Control how the CPT behaves in both the admin area and the public website.
    ------------------------------------------------------------------- */
   
    $args = array(
        //----- BASIC -----
        // Connects to labels array for list of label changes
        'labels'                 => $labels,
        // A short descriptive summary of the CPT’s purpose. Isn’t displayed anywhere in the WordPress admin.
        'description'            => __( 'A custom post type for adding Movie Reviews.', 'textdomain' ),
        // Whether it should be accessible publicly (front-end).
        'public'                 => true,
        // Whether the post type can have parent/child.
        'hierarchical'           => false,  

        //-----  Visibility -----
        // Whether to exclude from search results.
        'exclude_from_search'    => false,  
        // Can be accessed with a URL
        'publicly_queryable'     => true,  
        // The custom post type appears in the WordPress dashboard
        'show_ui'                => true,
        // Whether to display in Appearance → Menus.  
        'show_in_nav_menus'      => true,
        // Your post type may appear in the “+ New” drop-down in the WordPress admin bar (the black bar at the top).
        'show_in_admin_bar'      => true,   // Whether to show in the top admin bar.

        // Whether to show in the left admin menu (true or specify a custom parent slug).
        // 'show_in_menu' => 'tools.php', // Nest under "Tools"
        // 'show_in_menu' => 'edit.php?post_type=page', // Nest under "Pages"
        // 'show_in_menu' => 'my_plugin_menu_slug', // Nest under "My Plugin" menu
        'show_in_menu'           => true,

        // Position in the Admin menu (5 = below Posts).
        'menu_position'          => 5,      

        // Choose a Dashicon: https://developer.wordpress.org/resource/dashicons/
        'menu_icon'              => 'dashicons-format-video',

        // Use custom image as icon
        // 'menu_icon' => 'https://example.com/images/movie-icon.png',

        //-----  CAPABILITIES ----- //
        // ‘post’, ‘page’, or custom capabilities array.
        'capability_type' => 'post',        // Treat like standard Posts
        // 'capability_type' => 'page',     // Takes on Page capabilities

        //----- FEATURES -----//  
        'supports'               => array( 'title', 'editor', 'thumbnail', 'excerpt', 'comments' ),

        //----- TAXONOMIES -----//
        // Register built-in taxonomies like category, post_tag
        // 'taxonomies'           => array( 'genre', 'post_tag' ),

        // For custom taxonomies to work you need all 3
        // 1. Gutenberg editor relies on the REST API
        'show_in_rest'          => true,
        // 2. Sets whether the CPT has an archive page (e.g., /movie-reviews/).
        'has_archive'           => true,
        // 3. Links to a registered taxonomy
        'taxonomies'=>array('genre'),

        //-----  Permalinks & Queries -----//
        'rewrite'                => array(
            'slug'       => 'movie-reviews',  // The URL slug to use.
            'with_front' => true,             // Whether to prepend the front base to the slug.
            'feeds'      => true,             // Whether to generate feed links.
            'pages'      => true,             // Whether to use page number in permalinks (e.g., /movie-reviews/page/2).
        ),

        // 'permalink_epmask'     => EP_PERMALINK, // Advanced. Endpoint mask to specify how WP recognizes the rewrite.

        // Set to a custom string to specify query_var key (e.g., 'movie_review').
        // https://example.com/?movie_review=the-godfather
        'query_var'              => true,    

        //-----  Admin-Side Behavior -----//  
        'can_export'             => true,     // Whether to allow this post type to be exported via Tools → Export.
        'delete_with_user'       => false,    // Whether to delete posts when the user who created them is deleted.
   
    );
        register_post_type( 'movie_review', $args );
}
add_action( 'init', 'register_movie_reviews_cpt' );



/* -------------------------------------------------------------------
3. REGISTER CUSTOM TAXONOMY:
------------------------------------------------------------------- */
function register_genre_taxonomy() {
    $labels = array(
        'name'              => __( 'Genres', 'textdomain' ),
        'singular_name'     => __( 'Genre', 'textdomain' ),
        // … other labels
    );

    $args = array(
        'labels'            => $labels,
        // true = behaves like categories; false = behaves like tags
        'hierarchical'      => true, 
        'public'            => true,
        // Change slug
        //'rewrite'           => array( 'slug' => 'genre' ),

        // -- REST API / Gutenberg Support --
        // Gutenberg editor relies on the REST API
        'show_in_rest' => true,
        'rest_controller_class' => 'WP_REST_Terms_Controller',
    );

    register_taxonomy( 'genre', array( 'movie_review' ), $args );
}
add_action( 'init', 'register_genre_taxonomy' );

How It Works:

1. LABELS ARRAY: Describes the various text strings used throughout the admin interface. You can pick and choose what labels you want to change from the default.

__(...) - These functions are just used for translations. If you don't use them it wont break anything. Just considered best practice for the WordPress interface.

_x(...) Returns a translated string with a context parameter to handle words or phrases that need different translations depending on usage.

textdomain - Change to slug of your plugin so WordPress knows where to find translation files.

2. Arguments Array: Control how the CPT behaves in both the admin area and the public website. You can pick and choose what arguments you want to change from the default.

3. Register Custom Taxonomy: Works similar to registering a custom post type. Has an array of labels and arguments that plug into a function to register it. These settings can be customized to your liking.

Movie Reviews Custom Post Type
Movie Reviews Genre Taxonomy

Common Issues When Registering a Custom Post Type:

Custom taxonomy not showing up when adding a new custom post type?

The Gutenberg update relies on the REST API so you must include 'show_in_rest' settings set to True for both Taxonomy and Custom post type. Otherwise it wont work.

404 page not found:

Flushing Rewrite Rules:

After adding or updating a custom post type slug, or taxonomy slug you need to flush the rewrite rules so WordPress recognizes the new URLs: 1. Go to Settings → Permalinks in the admin. 2. Click Save Changes (no need to edit anything). 3. This triggers WordPress to refresh its internal rewrite rules.

Flushing Rewrite Pragmatically:

The most common (and best practice) approach is to flush your rewrite rules only once when your plugin or theme activates or deactivates, rather than flushing them on every page load (which is resource-intensive).

Flushing Rewrite Rules Pragmatically On Activation & Deactivation hooks

// Flush rewrite rules upon plugin activation
function WCFP_activate() {
    // Ensure the custom post type is registered before flush
    // myplugin_register_post_type();
    register_movie_reviews_cpt();

    // Flush the rules
    flush_rewrite_rules();
}
// Register activation hook
register_activation_hook( __FILE__, 'WCFP_activate' );

// (Optional) Flush rewrite rules on plugin deactivation
function WCFP_deactivate() {
    flush_rewrite_rules();
}

register_deactivation_hook( __FILE__, 'WCFP_deactivate' );

Adding Metaboxs & custom fields to custom post types:

Add Metabox To Movie Reviews Custom Post Type

/**
 * Add "Custom Fields" Metabox to Movie Reviews
 */
function mr_add_custom_fields_metabox() {
    add_meta_box(
        'mr_custom_fields_metabox',               // Unique ID for the metabox
        __( 'Movie Review Custom Fields', 'textdomain' ), // Box title
        'mr_render_custom_fields_metabox',        // Callback to render the metabox content
        'movie_review',                           // Post type where to display
        'normal',                                 // Context: normal, side, or advanced
        'default'                                 // Priority
    );
}
add_action( 'add_meta_boxes', 'mr_add_custom_fields_metabox' );

How add_meta_box() Function Works:

Unique ID: 'mr_custom_fields_metabox'register_post_type()
This is a string that uniquely identifies your metabox. You reference this ID later if you need to remove or modify the same metabox. Prefixing with “mr_” helps identify that this metabox is specific to your “Movie Reviews” functionality.

Title: __( 'Movie Review Custom Fields', 'textdomain' )
The second parameter is the title that appears at the top of the metabox in the WordPress admin.
It’s wrapped in __() which is a WordPress internationalization function for translating strings.
textdomain should match the domain of your plugin if you plan to localize/transliterate this string into other languages.

Callback: 'mr_render_custom_fields_metabox'
This tells WordPress which function to call to display the contents (HTML) of the metabox.
When a user loads or edits a “Movie Review” in the admin, WordPress will execute this callback, and any HTML output from it will appear inside the metabox. You’ll typically retrieve existing values with get_post_meta(), output fields (<input>, <textarea>, etc.), and add a nonce for security.

Screen/Post Type: 'movie_review'
Here you specify which post type gets this metabox. Since you created a custom post type named movie_review, you’re telling WordPress: “This metabox should appear on the Add/Edit screens for posts belonging to the movie_review post type.” You can also pass arrays of post types if you want a single metabox to appear for multiple post types.

Context: 'normal'
Indicates where the metabox should be displayed on the Edit screen.
normal: Displays under the content editor (the main area). “normal” is typically used for custom fields that require more space or are central to your content.
side: Displays in the sidebar (where Publish box, Featured Image box, etc., usually appear).

Priority:'default'
Common Values: high, core, default, low
Determines the priority of the metabox within the chosen context. This can affect the order in which multiple metaboxes appear in the same area. “default” is a general setting; if you find your metabox is buried or overshadowed, you could try 'high'.

Hooking Into add_meta_boxes
This tells WordPress when it’s time to add metaboxes (on the Edit Post screen load), run the function mr_add_custom_fields_metabox. The add_meta_boxes hook is part of WordPress’s core plugin API. It fires on the edit screen initialization process, ensuring your custom metabox is registered at the correct time.

How to render custom fields inside your metabox:

Below is a detailed explanation of how this function renders custom fields inside a metabox on the “Movie Review” edit screen. Each part of the code plays a key role in retrieving, displaying, and securing data for your custom post type.

Movie Review Custom Fields Metabox

Add the custom fields to the metabox

/**
 * Render the "Custom Fields" Metabox Fields
 */
function mr_render_custom_fields_metabox( $post ) {
    // Retrieve any existing values from post meta
    $director          = get_post_meta( $post->ID, '_mr_director', true );
    $release_year      = get_post_meta( $post->ID, '_mr_release_year', true );
    $rating            = get_post_meta( $post->ID, '_mr_rating', true );
    $cast              = get_post_meta( $post->ID, '_mr_cast', true );
    $production_budget = get_post_meta( $post->ID, '_mr_production_budget', true );
    $box_office        = get_post_meta( $post->ID, '_mr_box_office', true );

    // Add a nonce field for security
    wp_nonce_field( 'mr_custom_fields_nonce_action', 'mr_custom_fields_nonce' );

    ?>
    <table class="form-table">
        <tr>
            <th><label for="mr_director"><?php esc_html_e( 'Director', 'textdomain' ); ?></label></th>
            <td>
                <input type="text" name="mr_director" id="mr_director" class="regular-text"
                       value="<?php echo esc_attr( $director ); ?>" />
            </td>
        </tr>
        <tr>
            <th><label for="mr_release_year"><?php esc_html_e( 'Release Year', 'textdomain' ); ?></label></th>
            <td>
                <input type="number" name="mr_release_year" id="mr_release_year" class="small-text"
                       value="<?php echo esc_attr( $release_year ); ?>" />
            </td>
        </tr>
        <tr>
            <th><label for="mr_rating"><?php esc_html_e( 'Rating', 'textdomain' ); ?></label></th>
            <td>
                <input type="text" name="mr_rating" id="mr_rating" class="regular-text"
                       value="<?php echo esc_attr( $rating ); ?>" />
                <p class="description"><?php esc_html_e( 'Example: PG, PG-13, R, etc.', 'textdomain' ); ?></p>
            </td>
        </tr>
        <tr>
            <th><label for="mr_cast"><?php esc_html_e( 'Cast', 'textdomain' ); ?></label></th>
            <td>
                <textarea name="mr_cast" id="mr_cast" rows="3" class="large-text"><?php echo esc_textarea( $cast ); ?></textarea>
                <p class="description">
                    <?php esc_html_e( 'Separate cast members with commas, e.g., "Actor 1, Actor 2, Actor 3"', 'textdomain' ); ?>
                </p>
            </td>
        </tr>
        <tr>
            <th><label for="mr_production_budget"><?php esc_html_e( 'Production Budget', 'textdomain' ); ?></label></th>
            <td>
                <input type="text" name="mr_production_budget" id="mr_production_budget" class="regular-text"
                       value="<?php echo esc_attr( $production_budget ); ?>" />
            </td>
        </tr>
        <tr>
            <th><label for="mr_box_office"><?php esc_html_e( 'Box Office Earnings', 'textdomain' ); ?></label></th>
            <td>
                <input type="text" name="mr_box_office" id="mr_box_office" class="regular-text"
                       value="<?php echo esc_attr( $box_office ); ?>" />
            </td>
        </tr>
    </table>
    <?php
}

Retrieving Existing Values: get_post_meta()
Retrieves the value from the post meta field for the current post. The third parameter true means it returns a single value rather than an array. Each custom field (Director, Release Year, Rating, etc.) is stored under its own unique meta key. The result is stored in a variable ($director, $release_year, etc.)

Security: wp_nonce_field()
Nonce field adds a hidden HTML input to your form with a nonce (a one-time use token). Protects against Cross-Site Request Forgery (CSRF) by verifying that legitimate users (with correct permissions) are the ones submitting updates.

'mr_custom_fields_nonce_action' A unique identifier (nonce “action”) used during verification in your save_post function.

'mr_custom_fields_nonce' The name of the hidden input field. When you save data, WordPress checks that this nonce is valid before processing.

esc_html_e()
Echoes a translated (internationalized) string, escaping it for HTML to prevent XSS attacks or markup issues.

value="<?php echo esc_attr( $director ); ?>"
Dynamically populates the input with the previously saved meta value (if any), escaping it with esc_attr().

esc_textarea( $cast )
Escapes special characters for safe insertion inside a textarea.

How to save custom fields inside custom post type:

Below is a detailed explanation of each step in the function that saves the custom fields data for your “Movie Reviews” post type. This function is hooked to the save_post_movie_review action, which fires whenever a “Movie Review” post is saved or updated.

Save the custom fields when saved post

/**
 * Save the "Custom Fields" Metabox Data
 */
function mr_save_custom_fields_metabox( $post_id ) {
    // 1. Check if our nonce is set.
    if ( ! isset( $_POST['mr_custom_fields_nonce'] ) ) {
        return;
    }

    // 2. Verify the nonce is valid.
    if ( ! wp_verify_nonce( $_POST['mr_custom_fields_nonce'], 'mr_custom_fields_nonce_action' ) ) {
        return;
    }

    // 3. Check for autosave or user permissions.
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }
    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }

    // 4. Sanitize and update each field
    if ( isset( $_POST['mr_director'] ) ) {
        update_post_meta( $post_id, '_mr_director', sanitize_text_field( $_POST['mr_director'] ) );
    }

    if ( isset( $_POST['mr_release_year'] ) ) {
        update_post_meta( $post_id, '_mr_release_year', sanitize_text_field( $_POST['mr_release_year'] ) );
    }

    if ( isset( $_POST['mr_rating'] ) ) {
        update_post_meta( $post_id, '_mr_rating', sanitize_text_field( $_POST['mr_rating'] ) );
    }

    if ( isset( $_POST['mr_cast'] ) ) {
        update_post_meta( $post_id, '_mr_cast', sanitize_textarea_field( $_POST['mr_cast'] ) );
    }

    if ( isset( $_POST['mr_production_budget'] ) ) {
        update_post_meta( $post_id, '_mr_production_budget', sanitize_text_field( $_POST['mr_production_budget'] ) );
    }

    if ( isset( $_POST['mr_box_office'] ) ) {
        update_post_meta( $post_id, '_mr_box_office', sanitize_text_field( $_POST['mr_box_office'] ) );
    }
}
add_action( 'save_post_movie_review', 'mr_save_custom_fields_metabox' );
Check if the Nonce Is Set: We look for a specific form field named mr_custom_fields_nonce that was created by wp_nonce_field() in the metabox. If it’s missing, it might indicate that the form wasn’t submitted through our expected interface, or there was a malicious attempt to send data without the proper security token. In this case we exit the function early and no data is saved.
Verify the Nonce Is Valid: wp_verify_nonce() Checks the token’s legitimacy against the specified “action” (in this case, 'mr_custom_fields_nonce_action'). If invalid the request might be forged or expired, so we return the function before saving. This protects against Cross-Site Request Forgery (CSRF), ensuring only authorized forms from your site can update meta data.
Check for Autosave: defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE WordPress autosaves posts in the background periodically or when a user is editing multiple instances. If this is an autosave, we don’t want to override data with potentially incomplete or partial input.

Check for User Permissions: current_user_can( 'edit_post', $post_id )
Checks if the current logged-in user has permission to edit the specific post. If they don’t have the required capability, we return (stop the function).

if ( isset( $_POST['field_name'] ) ) { ... }

Verify that the form field was submitted before saving it to the database. Ensures we only process the data if it’s actually included in the form submission. Prevents overwriting data in unexpected situations.

sanitize_text_field( $_POST['mr_director'] )

Strips out unwanted tags, slashes, or malicious code from user input. This is essential for security, ensuring you’re only storing safe text in the database.

update_post_meta( $post_id, '_mr_director', ... )

Stores the sanitized value in the wp_postmeta table, under the key _mr_director. If a value already existed, it updates it; if not, it creates a new entry. The underscore (_mr_) convention is used to denote a private meta key, preventing it from appearing by default in some admin screens or custom fields interfaces.

sanitize_textarea_field() Works similar to sanitize_text_field(), stripping harmful elements but it allows multi-line text.

How To Remove Unwanted Metaboxes From Post Edit Page:

The code snippet removes (or “unregisters”) certain metaboxes from a specified custom post type in the WordPress admin. Metaboxes are those boxes or panels you see when editing a post, such as Yoast SEO, Custom Fields, etc. By removing them, you’re essentially decluttering the edit screen, which can be useful if those boxes are not relevant for certain users or post types.

Remove Unwanted Metaboxes

/*------------------------------
REMOVE UNWANTED METABOXES
--------------------------------*/
function WCFP_remove_custom_post_type_metaboxes() {
    // Remove meta box by ID from posttype
    // remove_meta_box('meta_box_id', 'post_type_name', 'normal|side');

    //--- REAL EXAMPLES ---
    // Remove Yoast SEO Meta Box
    remove_meta_box('wpseo_meta', 'post_type_name', 'normal');
    // Remove Asset Cleanup Meta Box
    remove_meta_box('wpassetcleanup_asset_list', 'post_type_name', 'normal');
    // Remove Monster Insights
    remove_meta_box('monsterinsights-metabox', 'post_type_name', 'side');
    // Remove Custom Fileds
    remove_meta_box('postcustom', 'post_type_name', 'normal');   
}
add_action('add_meta_boxes', 'WCFP_remove_custom_post_type_metaboxes', 9999);

Replace 'post_type_name' with the name of the custom post type you want to remove the metabox from.

How to Find the Metabox ID?

To remove a metabox, you first need its unique ID. Here’s the quickest way to find it:

  1. Open the Edit Screen: Navigate to the WordPress editor where the metabox appears.
  2. Right-Click & Inspect: Right-click anywhere on the metabox and select “Inspect” (or “Inspect Element,” depending on your browser).
  3. Identify the ID Attribute: In the developer tools, look for the top-level HTML <div> element that wraps the metabox. The ID attribute of this element (e.g., id="wpseo_meta") is the identifier you’ll use.

Use a High Priority

Setting the priority to 9999 in the add_action ensures that your function runs last (or almost last). Some meta boxes may be added with a high priority, and you want to be sure you remove them after they are registered.

How To Change The Edit Page Table Of a Custom Post Type:

Below is an example of how to edit the “Movie Reviews” admin list page (the table at edit.php?post_type=movie_review) so that it displays three additional columns for Director, Release Year, and Rating. These columns will pull their data from the respective custom fields (_mr_director, _mr_release_year, and _mr_rating). We will also remove the comments and reorder date to always show at the end.

Custom Post Type Edit Page Display Custom Fields

Add new fields to custom post type edit table

/**
 * Customize columns for "Movie Reviews"
 * - Remove the comments column
 * - Keep the "Date" column at the end
 */
function wcfp_movie_review_columns( $columns ) {
    // Remove the comments column
    unset( $columns['comments'] ); 

    // Save the date column temporarily
    $date_column = $columns['date'];
    // Remove the date column so we can add it back later
    unset( $columns['date'] );

    // Add our custom columns
    $columns['wcfp_director']     = __( 'Director', 'textdomain' );
    $columns['wcfp_release_year'] = __( 'Release Year', 'textdomain' );
    $columns['wcfp_rating']       = __( 'Rating', 'textdomain' );

    // Put the date column back at the end
    $columns['date'] = $date_column;

    return $columns;
}
add_filter( 'manage_movie_review_posts_columns', 'wcfp_movie_review_columns' );

/**
 * Populate data for the custom columns.
 */
function wcfp_movie_review_columns_content( $column, $post_id ) {
    switch ( $column ) {
        case 'wcfp_director':
            $director = get_post_meta( $post_id, '_mr_director', true );
            echo ! empty( $director ) ? esc_html( $director ) : '<em>No director set</em>';
            break;

        case 'wcfp_release_year':
            $release_year = get_post_meta( $post_id, '_mr_release_year', true );
            echo ! empty( $release_year ) ? esc_html( $release_year ) : '<em>N/A</em>';
            break;

        case 'wcfp_rating':
            $rating = get_post_meta( $post_id, '_mr_rating', true );
            echo ! empty( $rating ) ? esc_html( $rating ) : '<em>Not rated</em>';
            break;
    }
}
add_action( 'manage_movie_review_posts_custom_column', 'wcfp_movie_review_columns_content', 10, 2 );

Add sorting features to the new fields

/**
 * Make custom columns sortable.
 */
function wcfp_movie_review_sortable_columns( $columns ) {
    // Map the column slug to a meta key or sort expression.
    // For example, we’ll map "wcfp_release_year" column to a meta_key "mr_release_year"
    $columns['wcfp_release_year'] = 'mr_release_year';
    $columns['wcfp_director']     = 'mr_director';
    $columns['wcfp_rating']       = 'mr_rating';

    return $columns;
}
add_filter( 'manage_edit-movie_review_sortable_columns', 'wcfp_movie_review_sortable_columns' );

/**
 * Modify the query so WordPress knows how to sort by these meta fields.
 */
function wcfp_movie_review_orderby( $query ) {
    // Only modify the main admin query for movie_review post type
    if ( ! is_admin() || ! $query->is_main_query() ) {
        return;
    }

    // Check if we're sorting by our custom meta keys
    $orderby = $query->get( 'orderby' );
    if ( 'mr_release_year' === $orderby || 'mr_director' === $orderby || 'mr_rating' === $orderby ) {
        // Force a meta_value sort
        $query->set( 'meta_key', '_' . $orderby );
        $query->set( 'orderby', 'meta_value' );
        // If Release Year is numeric, you can set 'meta_type' => 'numeric' to sort numerically
        if ( 'mr_release_year' === $orderby ) {
            $query->set( 'meta_type', 'NUMERIC' );
        }
    }
}
add_action( 'pre_get_posts', 'wcfp_movie_review_orderby' );

Check out our FREE ONLINE
WordPress Plugin Development Course

Looking to develop a custom plugin, or create new features for your website?

Facebook Video