How to Create and Display a Custom Portfolio Post Type in WordPress Without a Plugin

WordPress is incredibly versatile, often used far beyond its original blogging roots to power all sorts of websites. One of its most powerful features for building custom sites is the Custom Post Type (CPT). While plugins like Custom Post Type UI (CPT UI) or Advanced Custom Fields (ACF) can simplify the process, understanding how to…

WordPress is incredibly versatile, often used far beyond its original blogging roots to power all sorts of websites. One of its most powerful features for building custom sites is the Custom Post Type (CPT). While plugins like Custom Post Type UI (CPT UI) or Advanced Custom Fields (ACF) can simplify the process, understanding how to create and manage CPTs directly through code gives you ultimate control, avoids plugin dependencies, and can slightly improve performance.

This tutorial will guide you step-by-step through creating a “Portfolio Item” custom post type, adding custom fields to it, and displaying it on your WordPress site—all without relying on a single plugin. This is perfect for designers, developers, photographers, or any professional looking to showcase their work in a structured way that integrates seamlessly with WordPress.

Introduction to Custom Post Types

At its core, WordPress treats all content as “posts.” However, it organizes different types of content into categories like ‘posts’ (for blog entries), ‘pages’ (for static content), ‘attachments’ (for media), and so on. A Custom Post Type allows you to create your own content type, tailored to specific needs.

Imagine you’re building a portfolio website. You wouldn’t want to list your individual projects as regular “posts” because they have different characteristics (e.g., a “Project URL” instead of an “Author,” or a “Client Name” instead of a “Category”). A CPT allows you to define a new content type—let’s call it “Portfolio Item”—that behaves like posts or pages but with its own set of rules, archive, and associated custom fields.

Why go without a plugin?

  • Full Control: You dictate every aspect of the CPT.
  • Performance: Fewer plugins can sometimes mean a lighter, faster site.
  • Learning Opportunity: Deepen your understanding of WordPress’s core architecture.
  • Client Handover: A leaner codebase is often easier for clients to maintain (though this depends on their technical comfort).

Before we begin, remember that directly editing theme files, especially ZEALTERCODE0, requires caution. Always use a child theme to prevent your changes from being overwritten during theme updates. If you don’t have one, create one or use a plugin like “Child Theme Configurator” before proceeding. You will also need FTP/SFTP access or a file manager through your hosting control panel to edit files.


Step 1: Register Your Custom Post Type (CPT)

The first step is to tell WordPress about your new content type. This is done by adding code to your child theme’s ZEALTERCODE0 file.

  1. Access Your ZEALTERCODE0 File:
  • Connect to your website via FTP/SFTP or use your hosting’s file manager.
  • Navigate to ZEALTERCODE0.
  • Locate and open the ZEALTERCODE0 file for editing.
  1. Add the CPT Registration Code:

We’ll use the ZEALTERCODE0 function within a hook called ZEALTERCODE1. The ZEALTERCODE2 hook is perfect because it fires after WordPress has finished loading, but before headers are sent.

    <?php
    /**
     * Register a 'Portfolio Item' custom post type.
     */
    function create_portfolio_cpt() {
        $labels = array(
            'name'                  => _x( 'Portfolio Items', 'Post type general name', 'your-text-domain' ),
            'singular_name'         => _x( 'Portfolio Item', 'Post type singular name', 'your-text-domain' ),
            'menu_name'             => _x( 'Portfolio', 'Admin Menu text', 'your-text-domain' ),
            'name_admin_bar'        => _x( 'Portfolio Item', 'Add New on Toolbar', 'your-text-domain' ),
            'add_new'               => __( 'Add New', 'your-text-domain' ),
            'add_new_item'          => __( 'Add New Portfolio Item', 'your-text-domain' ),
            'new_item'              => __( 'New Portfolio Item', 'your-text-domain' ),
            'edit_item'             => __( 'Edit Portfolio Item', 'your-text-domain' ),
            'view_item'             => __( 'View Portfolio Item', 'your-text-domain' ),
            'all_items'             => __( 'All Portfolio Items', 'your-text-domain' ),
            'search_items'          => __( 'Search Portfolio Items', 'your-text-domain' ),
            'parent_item_colon'     => __( 'Parent Portfolio Items:', 'your-text-domain' ),
            'not_found'             => __( 'No portfolio items found.', 'your-text-domain' ),
            'not_found_in_trash'    => __( 'No portfolio items found in Trash.', 'your-text-domain' ),
            'featured_image'        => _x( 'Portfolio Cover Image', 'Overrides the “Featured Image” phrase for this post type. Added in 4.3', 'your-text-domain' ),
            'set_featured_image'    => _x( 'Set cover image', 'Overrides the “Set featured image” phrase for this post type. Added in 4.3', 'your-text-domain' ),
            'remove_featured_image' => _x( 'Remove cover image', 'Overrides the “Remove featured image” phrase for this post type. Added in 4.3', 'your-text-domain' ),
            'use_featured_image'    => _x( 'Use as cover image', 'Overrides the “Use as featured image” phrase for this post type. Added in 4.3', 'your-text-domain' ),
            'archives'              => _x( 'Portfolio Item archives', 'The post type archive label used in nav menus. Default “Post Archives”. Added in 4.4', 'your-text-domain' ),
            'insert_into_item'      => _x( 'Insert into portfolio item', 'Overrides the “Insert into post”/”Insert into page” phrase (used when inserting media into a post). Added in 4.4', 'your-text-domain' ),
            'uploaded_to_this_item' => _x( 'Uploaded to this portfolio item', 'Overrides the “Uploaded to this post”/”Uploaded to this page” phrase (used when viewing media attached to a post). Added in 4.4', 'your-text-domain' ),
            'filter_items_list'     => _x( 'Filter portfolio items list', 'Screen reader text for the filter links heading on the post type listing screen. Default “Filter posts list”/”Filter pages list”. Added in 4.4', 'your-text-domain' ),
            'items_list_navigation' => _x( 'Portfolio items list navigation', 'Screen reader text for the pagination heading on the post type listing screen. Default “Posts list navigation”/”Pages list navigation”. Added in 4.4', 'your-text-domain' ),
            'items_list'            => _x( 'Portfolio items list', 'Screen reader text for the items list heading on the post type listing screen. Default “Posts list”/”Pages list”. Added in 4.4', 'your-text-domain' ),
        );

        $args = array(
            'labels'             => $labels,
            'public'             => true,
            'publicly_queryable' => true,
            'show_ui'            => true,
            'show_in_menu'       => true,
            'query_var'          => true,
            'rewrite'            => array( 'slug' => 'portfolio' ), // The URL slug for your portfolio items
            'capability_type'    => 'post',
            'has_archive'        => true, // Enable an archive page for this CPT (e.g., yoursite.com/portfolio/)
            'hierarchical'       => false,
            'menu_position'      => 5, // Position in the admin menu (5 is below Posts)
            'supports'           => array( 'title', 'editor', 'thumbnail', 'excerpt', 'custom-fields', 'revisions' ), // Features this CPT supports
            'taxonomies'         => array( 'category', 'post_tag' ), // Use default categories and tags if desired
            'show_in_rest'       => true // Important for Gutenberg editor
        );

        register_post_type( 'portfolio_item', $args );
    }
    add_action( 'init', 'create_portfolio_cpt' );

Code Explanation:

  • ZEALTERCODE0: An array of text strings used throughout the WordPress admin interface to refer to your CPT (e.g., “Add New Portfolio Item”). It’s crucial for user experience. Replace ZEALTERCODE1 with your actual theme’s text domain or a unique identifier (e.g., ZEALTERCODE2).
  • ZEALTERCODE0: An array defining the behavior and appearance of your CPT:
  • ZEALTERCODE0: Controls visibility and queryability. Set to ZEALTERCODE1 for most public CPTs.
  • ZEALTERCODE0: Whether to generate a default UI for managing this CPT in the admin.
  • ZEALTERCODE0: Where to show the CPT in the admin menu.
  • ZEALTERCODE0: Defines the URL structure (e.g., ZEALTERCODE1).
  • ZEALTERCODE0: If ZEALTERCODE1, WordPress automatically creates an archive page (e.g., ZEALTERCODE2) listing all items.
  • ZEALTERCODE0: Where the CPT appears in the admin sidebar.
  • ZEALTERCODE0: An array specifying what features your CPT will have, like ZEALTERCODE1, ZEALTERCODE2 (for content), ZEALTERCODE3 (for featured images), ZEALTERCODE4, ZEALTERCODE5 (essential for our next step!), and ZEALTERCODE6.
  • ZEALTERCODE0: Allows you to associate existing taxonomies (like categories and tags) with your CPT.
  • ZEALTERCODE0: Essential if you want to use the Gutenberg block editor for your custom post type.
  1. Save the File.
  1. Flush Permalinks: This is a crucial step whenever you register a new CPT or change its ZEALTERCODE0 rules.
  • Go to your WordPress Admin Dashboard.
  • Navigate to Settings > Permalinks.
  • Simply click the Save Changes button. You don’t need to change anything, just saving flushes the rewrite rules.

You should now see a new “Portfolio” menu item in your WordPress admin sidebar! Try adding a new portfolio item with a title, content, and featured image.


Step 2: Add Custom Fields (Manually)

While WordPress has a basic “Custom Fields” meta box (which we enabled with ZEALTERCODE0 in the ZEALTERCODE1 array), it’s not very user-friendly for specific fields. We’ll create our own custom meta box to add fields like “Project URL” and “Client Name.”

  1. Continue Editing ZEALTERCODE0: Add the following code below your ZEALTERCODE1 function.
    /**
     * Add a custom meta box for Portfolio Item details.
     */
    function portfolio_add_meta_box() {
        add_meta_box(
            'portfolio_details_box',      // Unique ID for the meta box
            __( 'Portfolio Details', 'your-text-domain' ), // Title of the meta box
            'portfolio_details_meta_box_callback', // Callback function to display the fields
            'portfolio_item',             // The post type to which the meta box applies
            'normal',                     // Where to show the meta box ('normal', 'side', 'advanced')
            'high'                        // Priority ('high', 'core', 'default', 'low')
        );
    }
    add_action( 'add_meta_boxes', 'portfolio_add_meta_box' );

    /**
     * Callback function to display the custom fields HTML.
     */
    function portfolio_details_meta_box_callback( $post ) {
        // Add a nonce field so we can check it later for security
        wp_nonce_field( basename( __FILE__ ), 'portfolio_meta_box_nonce' );

        // Retrieve existing meta values for these fields
        $project_url = get_post_meta( $post->ID, '_portfolio_project_url', true );
        $client_name = get_post_meta( $post->ID, '_portfolio_client_name', true );
        ?>

        <p>
            <label for="portfolio_project_url"><?php _e( 'Project URL:', 'your-text-domain' ); ?></label><br>
            <input type="url" id="portfolio_project_url" name="portfolio_project_url"
                   value="<?php echo esc_attr( $project_url ); ?>" size="70" placeholder="https://www.example.com/project-link/">
        </p>
        <p>
            <label for="portfolio_client_name"><?php _e( 'Client Name:', 'your-text-domain' ); ?></label><br>
            <input type="text" id="portfolio_client_name" name="portfolio_client_name"
                   value="<?php echo esc_attr( $client_name ); ?>" size="70" placeholder="Client Name">
        </p>
        <?php
    }

    /**
     * Save the custom meta box data when the post is saved.
     */
    function portfolio_save_meta_box_data( $post_id ) {
        // Verify nonce for security
        if ( !isset( $_POST['portfolio_meta_box_nonce'] ) || !wp_verify_nonce( $_POST['portfolio_meta_box_nonce'], basename( __FILE__ ) ) ) {
            return $post_id;
        }

        // Check user permission
        if ( !current_user_can( 'edit_post', $post_id ) ) {
            return $post_id;
        }

        // Check if it's an autosave
        if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
            return $post_id;
        }

        // Check if it's our CPT
        if ( 'portfolio_item' !== get_post_type( $post_id ) ) {
            return $post_id;
        }

        // Save Project URL
        if ( isset( $_POST['portfolio_project_url'] ) ) {
            $new_project_url = esc_url_raw( $_POST['portfolio_project_url'] ); // Sanitize URL
            update_post_meta( $post_id, '_portfolio_project_url', $new_project_url );
        } else {
            delete_post_meta( $post_id, '_portfolio_project_url' ); // Delete if empty
        }

        // Save Client Name
        if ( isset( $_POST['portfolio_client_name'] ) ) {
            $new_client_name = sanitize_text_field( $_POST['portfolio_client_name'] ); // Sanitize text
            update_post_meta( $post_id, '_portfolio_client_name', $new_client_name );
        } else {
            delete_post_meta( $post_id, '_portfolio_client_name' ); // Delete if empty
        }
    }
    add_action( 'save_post', 'portfolio_save_meta_box_data' );

Code Explanation:

  • ZEALTERCODE0: This function uses ZEALTERCODE1 to register a new meta box.
  • ZEALTERCODE0 is its unique ID.
  • ZEALTERCODE0 is the title shown to the user.
  • ZEALTERCODE0 is the name of the function that will generate the HTML content of the meta box.
  • ZEALTERCODE0 ensures it only appears on our custom post type.
  • ZEALTERCODE0: This is where we create the actual input fields.
  • ZEALTERCODE0: Adds a security nonce to prevent cross-site request forgery.
  • ZEALTERCODE0: Retrieves any existing values for the fields. We prefix our meta keys with ZEALTERCODE1 (e.g., ZEALTERCODE2) which is a standard WordPress convention to mark them as “hidden” from the default custom fields list, making the interface cleaner.
  • ZEALTERCODE0 tags: These are standard HTML form fields.
  • ZEALTERCODE0: This function is hooked to ZEALTERCODE1 and runs whenever a post is saved.
  • It performs crucial security checks: nonce verification, user permissions, and checking for autosaves.
  • It verifies that the current post being saved is our ZEALTERCODE0 CPT.
  • ZEALTERCODE0 and ZEALTERCODE1: Absolutely vital for sanitizing user input before saving it to the database, protecting against security vulnerabilities.
  • ZEALTERCODE0: Saves the data to the database. If the meta key doesn’t exist, it adds it; otherwise, it updates it.
  • ZEALTERCODE0: Clears the data if the field is submitted empty.
  1. Save the File.
  2. Check Your Admin: Now, when you edit a “Portfolio Item,” you should see a “Portfolio Details” meta box with your new input fields.

Step 3: Create a Custom Archive Template for Your CPT

To display a list of all your portfolio items, WordPress looks for specific template files based on its template hierarchy. For a CPT named ZEALTERCODE0, it will first look for ZEALTERCODE1. If it doesn’t find it, it falls back to ZEALTERCODE2, and then ZEALTERCODE3.

  1. Create ZEALTERCODE0:
  • In your child theme’s root directory (ZEALTERCODE0), create a new file named ZEALTERCODE1.
  • You can copy the contents of your parent theme’s ZEALTERCODE0 or ZEALTERCODE1 as a starting point, then modify it. Here’s a basic example:
    <?php
    /**
     * The template for displaying an archive of Portfolio Items.
     */

    get_header(); ?>

    <div id="primary" class="content-area">
        <main id="main" class="site-main" role="main">

            <header class="page-header">
                <h1 class="page-title"><?php post_type_archive_title(); ?></h1>
            </header><!-- .page-header -->

            <?php if ( have_posts() ) : ?>

                <div class="portfolio-grid">
                    <?php while ( have_posts() ) : the_post(); ?>

                        <article id="post-<?php the_ID(); ?>" <?php post_class( 'portfolio-item-archive' ); ?>>
                            <?php if ( has_post_thumbnail() ) : ?>
                                <div class="portfolio-thumbnail">
                                    <a href="<?php the_permalink(); ?>">
                                        <?php the_post_thumbnail( 'medium' ); // Adjust size as needed ?>
                                    </a>
                                </div>
                            <?php endif; ?>

                            <header class="entry-header">
                                <h2 class="entry-title"><a href="<?php the_permalink(); ?>" rel="bookmark"><?php the_title(); ?></a></h2>
                            </header><!-- .entry-header -->

                            <div class="entry-summary">
                                <?php the_excerpt(); ?>
                            </div><!-- .entry-summary -->

                            <div class="entry-meta">
                                <?php // Display custom fields here if desired in the archive view ?>
                                <?php $client_name = get_post_meta( get_the_ID(), '_portfolio_client_name', true ); ?>
                                <?php if ( $client_name ) : ?>
                                    <span class="portfolio-client"><?php _e( 'Client:', 'your-text-domain' ); ?> <?php echo esc_html( $client_name ); ?></span>
                                <?php endif; ?>
                            </div>
                        </article><!-- #post-## -->

                    <?php endwhile; ?>
                </div><!-- .portfolio-grid -->

                <?php the_posts_navigation(); ?>

            <?php else : ?>

                <?php get_template_part( 'template-parts/content', 'none' ); ?>

            <?php endif; ?>

        </main><!-- #main -->
    </div><!-- #primary -->

    <?php get_sidebar(); ?>
    <?php get_footer(); ?>

Code Explanation:

  • ZEALTERCODE0 and ZEALTERCODE1: Include your theme’s header and footer.
  • ZEALTERCODE0: Displays the general name of your post type (e.g., “Portfolio Items”).
  • ZEALTERCODE0 and ZEALTERCODE1: The standard WordPress loop to iterate through all found posts.
  • ZEALTERCODE0, ZEALTERCODE1, ZEALTERCODE2, ZEALTERCODE3: Standard template tags to display post information.
  • ZEALTERCODE0: This is how you retrieve the custom field values you saved earlier. ZEALTERCODE1 gets the current post’s ID within the loop.
  1. Save the File.
  2. View Your Archive: Visit ZEALTERCODE0 (or whatever slug you set in the CPT registration) to see your archive page.

Step 4: Create a Custom Single Template for Your CPT

To display individual portfolio items, WordPress will look for ZEALTERCODE0.

  1. Create ZEALTERCODE0:
  • In your child theme’s root directory, create a new file named ZEALTERCODE0.
  • Again, you can start by copying your parent theme’s ZEALTERCODE0 or ZEALTERCODE1. Here’s a basic example:
    <?php
    /**
     * The template for displaying all single Portfolio Items.
     */

    get_header(); ?>

    <div id="primary" class="content-area">
        <main id="main" class="site-main" role="main">

            <?php while ( have_posts() ) : the_post(); ?>

                <article id="post-<?php the_ID(); ?>" <?php post_class( 'portfolio-item-single' ); ?>>
                    <header class="entry-header">
                        <?php the_title( '<h1 class="entry-title">', '</h1>' ); ?>

                        <div class="entry-meta">
                            <?php // Display your custom fields here ?>
                            <?php $project_url = get_post_meta( get_the_ID(), '_portfolio_project_url', true ); ?>
                            <?php $client_name = get_post_meta( get_the_ID(), '_portfolio_client_name', true ); ?>

                            <?php if ( $client_name ) : ?>
                                <p class="portfolio-client"><?php _e( 'Client:', 'your-text-domain' ); ?> <?php echo esc_html( $client_name ); ?></p>
                            <?php endif; ?>

                            <?php if ( $project_url ) : ?>
                                <p class="portfolio-url">
                                    <a href="<?php echo esc_url( $project_url ); ?>" target="_blank" rel="noopener noreferrer">
                                        <?php _e( 'View Project', 'your-text-domain' ); ?>
                                    </a>
                                </p>
                            <?php endif; ?>
                        </div><!-- .entry-meta -->
                    </header><!-- .entry-header -->

                    <?php if ( has_post_thumbnail() ) : ?>
                        <div class="post-thumbnail">
                            <?php the_post_thumbnail( 'large' ); // Adjust size as needed ?>
                        </div>
                    <?php endif; ?>

                    <div class="entry-content">
                        <?php
                            the_content( sprintf(
                                wp_kses(
                                    /* translators: %s: Name of current post. Only visible to screen readers */
                                    __( 'Continue reading<span class="screen-reader-text"> "%s"</span>', 'your-text-domain' ),
                                    array(
                                        'span' => array(
                                            'class' => array(),
                                        ),
                                    )
                                ),
                                get_the_title()
                            ) );

                            wp_link_pages( array(
                                'before' => '<div class="page-links">' . esc_html__( 'Pages:', 'your-text-domain' ),
                                'after'  => '</div>',
                            ) );
                        ?>
                    </div><!-- .entry-content -->

                    <footer class="entry-footer">
                        <?php // You might add tags/categories for portfolio items here ?>
                    </footer><!-- .entry-footer -->
                </article><!-- #post-## -->

                <?php
                    // If comments are open or we have at least one comment, load up the comment template.
                    if ( comments_open() || get_comments_number() ) :
                        comments_template();
                    endif;
                ?>

            <?php endwhile; // End of the loop. ?>

        </main><!-- #main -->
    </div><!-- #primary -->

    <?php get_sidebar(); ?>
    <?php get_footer(); ?>

Code Explanation:

  • This template is similar to a regular ZEALTERCODE0 but specifically tailored for ZEALTERCODE1.
  • We again use ZEALTERCODE0 to retrieve and display our custom fields (Project URL, Client Name) within the ZEALTERCODE1 section.
  • ZEALTERCODE0 is used to sanitize the URL when outputting it in an ZEALTERCODE1 tag.
  • ZEALTERCODE0 are good practices for external links.
  1. Save the File.
  2. View a Single Item: Click on any of your portfolio items from the archive page or navigate directly to its URL (e.g., ZEALTERCODE0) to see your custom single page.

Step 5: Display Your CPT on a Page or in Your Menu

Now that you have your CPT and its templates set up, you’ll want to make it accessible to your visitors.

  1. Add to Your Navigation Menu:
  • Go to Appearance > Menus in your WordPress admin.
  • On the left side, you should see a new section labeled “Portfolio Items.” Expand it.
  • Under “View All,” you’ll find the option for “Portfolio Items Archive.” Check this box and click “Add to Menu.”
  • Drag and drop the new menu item to your desired location in your menu structure.
  • Click Save Menu.
  1. Display Recent Items on a Page (e.g., Homepage):

If you want to show a selection of your latest portfolio items on your homepage or another static page, you can use a custom ZEALTERCODE0.

  • Option A: Dedicated Page Template: The cleanest way is to create a custom page template (e.g., ZEALTERCODE0 or a generic ZEALTERCODE1) in your child theme.
        <?php
        /**
         * Template Name: Portfolio Display Page
         *
         * This template displays recent portfolio items.
         */

        get_header(); ?>

        <div id="primary" class="content-area">
            <main id="main" class="site-main" role="main">

                <?php while ( have_posts() ) : the_post(); // Standard page content ?>
                    <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
                        <?php the_title( '<h1 class="entry-title">', '</h1>' ); ?>
                        <div class="entry-content">
                            <?php the_content(); ?>
                        </div>
                    </article>
                <?php endwhile; ?>

                <hr>

                <h2><?php _e( 'Latest Projects', 'your-text-domain' ); ?></h2>
                <div class="recent-portfolio-items">
                    <?php
                    // Custom query to fetch recent portfolio items
                    $args = array(
                        'post_type'      => 'portfolio_item',
                        'posts_per_page' => 4, // Display 4 latest portfolio items
                        'orderby'        => 'date',
                        'order'          => 'DESC',
                    );
                    $portfolio_query = new WP_Query( $args );

                    if ( $portfolio_query->have_posts() ) :
                        while ( $portfolio_query->have_posts() ) : $portfolio_query->the_post();
                            ?>
                            <div class="portfolio-item-teaser">
                                <?php if ( has_post_thumbnail() ) : ?>
                                    <a href="<?php the_permalink(); ?>">
                                        <?php the_post_thumbnail( 'thumbnail' ); ?>
                                    </a>
                                <?php endif; ?>
                                <h3><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
                                <?php the_excerpt(); ?>
                            </div>
                            <?php
                        endwhile;
                        wp_reset_postdata(); // Restore original post data
                    else :
                        echo '<p>' . __( 'No portfolio items found.', 'your-text-domain' ) . '</p>';
                    endif;
                    ?>
                </div><!-- .recent-portfolio-items -->

                <p><a href="<?php echo get_post_type_archive_link( 'portfolio_item' ); ?>" class="button">View All Portfolio</a></p>

            </main><!-- #main -->
        </div><!-- #primary -->

        <?php get_sidebar(); ?>
        <?php get_footer(); ?>

Then, create a new Page in WordPress and select “Portfolio Display Page” from the “Template” dropdown in the Page Attributes section.

  • Option B: Shortcode (for simpler displays): If you prefer to insert portfolio items directly into the visual editor without a full page template, you could create a shortcode in your ZEALTERCODE0:
        function portfolio_items_shortcode( $atts ) {
            $atts = shortcode_atts( array(
                'count' => 3, // Default to 3 items
            ), $atts, 'portfolio_items' );

            $args = array(
                'post_type'      => 'portfolio_item',
                'posts_per_page' => absint( $atts['count'] ),
                'orderby'        => 'date',
                'order'          => 'DESC',
            );
            $portfolio_query = new WP_Query( $args );

            ob_start(); // Start output buffering

            if ( $portfolio_query->have_posts() ) :
                echo '<div class="portfolio-shortcode-grid">';
                while ( $portfolio_query->have_posts() ) : $portfolio_query->the_post();
                    ?>
                    <div class="portfolio-shortcode-item">
                        <?php if ( has_post_thumbnail() ) : ?>
                            <a href="<?php the_permalink(); ?>">
                                <?php the_post_thumbnail( 'thumbnail' ); ?>
                            </a>
                        <?php endif; ?>
                        <h4><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h4>
                    </div>
                    <?php
                endwhile;
                echo '</div>';
                wp_reset_postdata();
            else :
                echo '<p>' . __( 'No portfolio items found.', 'your-text-domain' ) . '</p>';
            endif;

            return ob_get_clean(); // Return buffered content
        }
        add_shortcode( 'portfolio_items', 'portfolio_items_shortcode' );

Then, you can simply add ZEALTERCODE0 to any page or post.


After creating new pages, especially those that might rely on custom templates or if you later change any CPT slug, it’s always a good idea to flush permalinks one more time.

  • Go to Settings > Permalinks in your WordPress admin.
  • Click Save Changes.

Conclusion

You’ve now successfully created a custom “Portfolio Item” post type, added custom fields to it, and created custom templates to display both the archive and single views—all without a single plugin! This demonstrates the immense power and flexibility of WordPress development.

From here, you can further customize your templates with CSS for better styling, add more custom fields (e.g., for specific project technologies, images galleries, or video embeds), or even create custom taxonomies (like “Project Types”) to further organize your portfolio items beyond standard categories and tags. The possibilities are endless when you master CPTs.

TAGS: WordPress Development, Custom Post Type, WordPress Tutorial, Web Development, PHP, Theme Customization, Child Theme, Portfolio Website CATEGORIES: WordPress Guides, Developer Resources

Was this helpful?

Previous Article

How to Safely Update WordPress Plugins and Themes (Using a Staging Environment)

Next Article

How to Fix the "Error Establishing a Database Connection" in WordPress**

Write a Comment

Leave a Comment