How I Manually Added a Custom Post Type in WordPress with PHP and ACF

Screenshot. Lara Lee Design WordPress. View of several Case Study pages using the new Custom Post Type.
Share on facebook
Share on twitter
Share on pinterest
Share on linkedin

Custom Post Types (CPTs) are a handy tool for creating WordPress posts that adhere to a new, standard format of your choosing. For instance, I wanted to streamline some of the tedious tasks I did at avian.design for my website here at laralee.design. Previously, I resorted to hand-coding and/or hard-coding page sections in several areas. Nowhere was this more evident than my Case Studies pages which didn’t adhere to a traditional blog format. Yet, my Case Studies here at laralee.design now eliminate the need to inline HTML and CSS hand-coding because they fit into the WordPress CMS system—as a new, Custom Post Type (CPT). This was my first foray into WordPress development. While there are certainly other, perhaps even better, ways to accomplish the same CPT, I implemented this CPT manually. Further, I tried to also implement “WordPress best practices.” Continue reading to learn how to create your own WordPress Custom Post Type (CPT) using just PHP and the Advanced Custom Fields (ACF) plugin.


See the February 2019 update: “How to Make Custom Post Types with Elementor Pro”


To add your own WordPress Custom Post Type (CPT), follow these simple steps:

  1. Code a plug-in to store custom functions.php changes.
  2. Create the Custom Post Type (CPT).
  3. Define custom fields for the CPT.
  4. Write a custom template to display the CPT and its content from the custom fields.

Step 1: Choose Where to Add the PHP

Best practice in web development dictates to save custom modifications as a duplicate copy rather than overriding the Master document. This preserves file integrity, separates mods from the originals, and assists with maintenance and troubleshooting.

Similarly, WordPress is the same way. WordPress child themes are created by copying the parent directory of the chosen theme, renaming it often with “-child” appended, and storing it in the same place on the web server as the parent theme for WordPress to find.

Child Theme Functions PHP Document

Custom modifications to the functions.php file can be made here, in the child theme’s functions.php. This method works great, because you can edit this code right from the WordPress dashboard in the Theme Editor. Additionally, no other plugins are necessary.

Custom WordPress Plugin

However, the custom functions can be independent from the theme by storing them as their own plug-in. As a result, this allows webmasters to change themes on whim without losing the mods. Furthermore, it also allows webmasters to maintain all mods as a single plugin, allowing more control over plugin activations and updates.

Creating a custom plugin is simple.

First create a new PHP document in a text editor or other program like Adobe Dreamweaver. I simply called it, “lara-custom-functions.php”. Obviously, leave no spaces.

Then, just below the opening PHP tag, insert metadata that tells WordPress about the file.

<?php
/**
* Plugin Name: Custom Functions Plugin
* Description: This plugin contains all of my own original custom functions.
* Author: Lara Lee
* Version: 0.4
*/
 
/*
* Custom mods go here
*/
 
?>

WPMU DEV provides an excellent explanation for saving functions in a plugin, plus a template of demo code.

Login to the web server with a Secure File Transfer Program (SFTP) like FileZilla. Next, navigate to public_html > wp-content > plugins, create a new directory, and enter it. For example, I called my custom plugin directory “lara-custom-functions” with my name prepended to avoid naming conflicts. After that, upload the .php document here.

Screenshot. FileZilla. A custom functions plug-in appears in the web server.

Login into the WordPress dashboard and navigate to Plugins. A successful “installation” of this custom plugin will display in the WordPress dashboard in the Plugins.

Now you can edit this PHP document just like you would the child theme’s functions.php document.

Step 2: Create the Custom Post Type (CPT)

Make the Custom Post Type (CPT) in the new custom plugin. Open the PHP document from step one. For instance, either the child theme’s functions.php document or the custom plugin.

Grab some PHP code to create the Custom Post Type. While you could code from scratch, you could also use a trusted authority’s code as a template and tweak to taste. I did the latter.

WPBeginner has a excellent Custom Post Type tutorial I referenced heavily and from which I borrowed template code. Go there and copy the code into the PHP document. Change the names and settings as desired.

My Case Study Custom Post Type name has multiple words. The formal name in code just needed hyphens to make this compatible. I’ve personally found hyphens are better than underscores for pretty URLs. For instance:

function new_post_type_() {
$labels = array(
'name'                => _x( 'Case Studies', 'Post Type General Name', 'OceanWP' ),
'singular_name'       => _x( 'Case Study', 'Post Type Singular Name', 'OceanWP' ),
 //rest of code
  
$args = array(
'label'               => __( 'case-studies', 'OceanWP' ),
'description'         => __( 'Human-centered creative solutions.', 'OceanWP' ),
'labels'              => $labels,
'supports'            => array( 'title', /*'editor', 'excerpt',*/ 'author', 'thumbnail', 'revisions', /*'custom-fields',*/ 'categories', ),
 //rest of code

Additionally, enable or disable how the WordPress editor appears for the Custom Post Type (CPT). Shown above, I disabled the default WordPress editor as well as WordPress’ fields for adding excerpts and custom-fields.

Although I do want to add custom fields, I will have more control doing so later with a third-party plugin instead.

Update: My ACF custom fields encountered issues with the big Gutenberg update. I switched to using the Elementor plugin, but ACF has since updated for Gutenberg support. Either way, custom fields are optional and won’t stop a CPT.

Next, I wanted to add custom sections with unique CSS styling for my Case Studies. For example, one opening section uses a two-column layout. I also applied custom bullets and borders to my lists for Usability Errors and Results. Another example, I automatically applied an image border so the edges of any images with white backgrounds are clear. Generally, you can add these CSS styles multiple ways. At avian.design, I used the HTML view of the editor to add CSS IDs and classes to my content for my stylesheet to read, like so:

Screenshot. Avian.Design WordPress. I added CSS names using hand-coding within the WYSIWYG editor.

However, this quickly gets tedious. It meant duplicating posts to carry over my code, or else remember exactly what I wrote each time. Consequently, WordPress applied nothing automatically. Yet, the combination of a Custom Post Type (CPT) with custom fields resolves this issue!

Once edits are complete, simply re-upload the document to the web server and overwrite the old.

Some troubleshooting tips:

  • Switch out “OceanWP” with whatever your theme is.
  • If the editor looks “different”, double-check the PHP code line that says,
    'supports' => array( 'title', 'editor', 'excerpt', 'author', 'thumbnail', 'revisions', 'custom-fields', ),
    and add or remove pieces as desired.
  • Ensure the label, slug, and registered name all match. You need exact spelling to name the custom post template so WordPress can find and apply it correctly.

The full code for creating the Custom Post Type (CPT) in functions.php:

<?php

/**
* Plugin Name: Custom Functions Plugin
* Description: This plugin contains all of my own original custom functions.
* Author: Lara Lee
* Version: 0.4
*/

// Creating a function to create my Case Study Custom Post Type

function new_post_type_() {
  
  // Set UI labels for Case Study CPT
  
  $labels = array(
    'name' => _x( 'Case Studies', 'Post Type General Name', 'OceanWP' ),
    'singular_name' => _x( 'Case Study', 'Post Type Singular Name', 'OceanWP' ),
    'menu_name' => __( 'Case Studies', 'OceanWP' ),
    'parent_item_colon' => __( 'Parent Case Study', 'OceanWP' ),
    'all_items' => __( 'All Case Studies', 'OceanWP' ),
    'view_item' => __( 'View Case Study', 'OceanWP' ),
    'add_new_item' => __( 'Add New Case Study', 'OceanWP' ),
    'add_new' => __( 'Add New', 'OceanWP' ), 'edit_item' => __( 'Edit Case Study', 'OceanWP' ),
    'update_item' => __( 'Update Case Study', 'OceanWP' ),
    'search_items' => __( 'Search Case Study', 'OceanWP' ),
    'not_found' => __( 'Not Found', 'OceanWP' ),
    'not_found_in_trash' => __( 'Not found in Trash', 'OceanWP' ),
  );
  
  // Set other options for Case Study CPT
  
  $args = array(
    'label' => __( 'case-studies', 'OceanWP' ),
    'description' => __( 'Human-centered creative solutions.', 'OceanWP' ),
    'labels' => $labels,
    'supports' => array( 'title', /*'editor', 'excerpt',*/ 'author', 'thumbnail', 'revisions', /*'custom-fields',*/ 'categories', ),
    'taxonomies' => array( 'category' ),
    'hierarchical' => false, 'public' => true,
    'show_ui' => true, 'show_in_menu' => true, 'show_in_nav_menus' => true,
    'show_in_admin_bar' => true, 'menu_position' => 6, 'menu_icon' => 'dashicons-analytics',
    'can_export' => true,
    'has_archive' => true,
    'exclude_from_search' => false,
    'publicly_queryable' => true,
    'capability_type' => 'post',
    'rewrite' => array('slug' => 'case-studies'),
  );
  
  function namespace_add_custom_types( $query ) { 
    if( (is_category() || is_tag()) && $query->is_archive() && empty( $query->query_vars['suppress_filters'] ) ) { 
      $query->set( 'post_type', array( 'post', 'case-studies' ))
    ; } return $query;
  }
  
  add_filter( 'pre_get_posts', 'namespace_add_custom_types' );
  
  // Registering my Case Study CPT
  
  register_post_type( 'case-studies', $args ); }

  /*
  * Hook into the 'init' action so that the function
  * Containing our post type registration is not
  * unnecessarily executed.
  */

  add_action( 'init', 'new_post_type_', 0 );
?>

Step 3: Create custom fields for the Custom Post Type (CPT)

Now that I created the Custom Post Type (CPT) itself, it needs custom fields that will apply any CSS IDs and classes automatically. One of the most popular, trusted, and reliable third-party WordPress plugins for this is Advanced Custom Fields (ACF). Install this plugin and go to the new Custom Fields tab in the WordPress dash.

Add a new field group, then label appropriately. Next, I created a new field group to save all my Case Study fields, simply called “Case Study Settings.” Additionally, add any fields for each desired custom section. ACF automatically generates a unique CSS ID (but it can changed later). Add new fields and set their names and types as necessary. Choose field types that make sense.

Screenshot. Lara Lee Design WordPress. The Case Study Settings field group has several custom fields with varying field types.

My field, “Photo of Existing Design,” is obviously an Image field type. However, I also use images for my work-in-progress shots, which display captions underneath each photo. I also wanted to add an animation effect so each image “fades” into view as the user scrolls near to each.

To customize the captions and this animation effect, it was better to select the WYSIWYG Editor field type for this section. From the editor, I am able to insert media (the photos), add captions, and paste in shortcode for the animation effect.

The other reason is the WYSIWYG Editor is better is because there’s no fixed number of images for each Case Study. It varies, and I often don’t know beforehand. The Image field type could work. I could add multiple new fields with the Image field type and ensure that each is not a required field—allowing users the option of leaving it blank. However, that’s tedious and redundant! Furthermore, since I want to add borders to each image, an empty image field would display as a horizontal line across the page—my empty image borders. So, I chose the WYSIWYG Editor field type instead.

Beneath Field Order in the Location menu, set to show this field group if Post Type is equal to [the new custom post type made above]. Then, set Options as desired.

Finally click Update in the right-hand sidebar to populate the Custom Post Type (CPT) with the new, custom fields. New Custom Post Type (CPT) pages from here on will allow users to enter information using the custom fields.

Step 4: Create a custom template to display the Custom Post Type (CPT) and its content from the custom fields

Now you can make a new page using the Custom Post Type (CPT) and populated with the custom fields. Unfortunately, nothing displays on “Publish” yet. They need to go through the WordPress engine via a new PHP template.

It’s best not to write the Custom Post Type (CPT) template page from scratch to both save time and adhere to whatever patterns your theme established. If you chose an “easy” theme, navigate to the parent theme folder, locate a .php document named something like “single.php” or “content.php.” Opening the files in Dreamweaver sometimes reveals another template is better to use. For example, <?php get_template_part( 'content', 'single' ); ?> in the single.php file suggests I should take a look at content.php too.

In avian.design’s Sydney theme, this would be: public_html > wp-content > themes > syndey > content.php.  Simply copy this document into the child-theme directory and edit away.

However, the theme I chose for here has been…more difficult. OceanWP is highly customizable, but it also scatters partial templates across several files. OceanWP doesn’t have a clear, single single.php or page.php file. I confess–I never found which one to use. I had make a new template for my CPT from scratch…or almost.

To make a new Custom Post Type (CPT) template (from nearly scratch), create a new .php file saved as “single-[custom-post-type-name].php”. For my Case Study Custom Post Type (CPT), this file was named simply, “single-case-studies.php”.

Find a suitable template online to start. The Advanced Custom Fields (ACF) documentation was a life-saving resource for me. While describing how to insert its custom fields within the WordPress loop of template files, it provided a very basic, rudimentary template for me.

Go to the Advanced Custom Fields (ACF) documentation page and copy that code into the new custom .php template file.

Replace the demo IDs with the custom field IDs from Step 3, and modify HTML tags as necessary. I simply placed the custom field IDs like ACF’s documentation has suggested, with inline PHP tags, plus whatever custom CSS styles I myself had made in anticipation of my Custom Post Type (CPT) template.

Here is an example snippet of what this looked like:

<h2>About the Client</h2>
<p>
  <?php the_field('cs_about_client'); ?>
</p>
<div class="cs__spacer--vertical"></div>
<h2>Analysis of Existing Design</h2> <img id="cs-before-photo" src="<?php the_field('cs_existing_design_photo'); ?>" />
<div class="cs-existing-design-analysis__row clearfix">
  <div class="cs-existing-design-analysis__column">
    <h3>Goals</h3>
    <p>
      <?php the_field('cs_goals'); ?>
    </p>
  </div>
  <!-- .cs-existing-design-analysis__column -->
  <div class="cs-existing-design-analysis__column">
    <h3>Usability Issues</h3>
    <p>
      <?php the_field('cs_usability_issues'); ?>
    </p>
  </div>
  <!-- .cs-existing-design-analysis__column -->
</div>
<!-- .cs-existing-design-analysis__row -->
<div class="cs__spacer--vertical"></div>

This file represents the barebones Custom Post Type (CPT) template. Upload it to the theme’s root folder on the web server to publish the page. WordPress should now use the single-[custom-post-type-name].php file as a template. Look for signs that it’s working. You’ll probably also notice several things not working. This bare-bones file doesn’t have any of the styles and settings from the WordPress theme (in my case, OceanWP).

Next, apply the theme’s CSS IDs and classes. Without a theme template to reference, these CSS names may not be immediately clear. Yet, there’s a solution for this too. Return to the WordPress dashboard and create a new page—any page—that will have some of the CSS names. Use the web browser’s Inspect tool (right-click > Inspect) to highlight HTML elements and find CSS names associated with them. Copy-paste these back into the Custom Post Type (CPT) template. For my Case Study Custom Post Type (CPT) template, I mostly needed to grab CSS names for wrappers and containers.

Publish the page periodically to test which names are working and needed. Update the .php file on the remote web server to see the theme layout take effect; continue to re-work as needed until satisfied!

Final PHP Documents

Create New Portfolio Custom Post Type (CPT):

<?php

/**
* Plugin Name: Custom Functions Plugin
* Description: This plugin contains all of my own original custom functions.
* Author: Lara Lee
* Version: 0.9
*/

*
* Creating a function to create Portfolio Custom Post Type
*/
 
function new_post_type() {
    $labels = array(
        'name'                => _x( 'Portfolio', 'Post Type General Name', 'OceanWP' ),
        'singular_name'       => _x( 'Portfolio', 'Post Type Singular Name', 'OceanWP' ),
        'menu_name'           => __( 'Portfolio', 'OceanWP' ),
        'parent_item_colon'   => __( 'Parent Portfolio', 'OceanWP' ),
        'all_items'           => __( 'All Portfolio', 'OceanWP' ),
        'view_item'           => __( 'View Portfolio', 'OceanWP' ),
        'add_new_item'        => __( 'Add New Portfolio', 'OceanWP' ),
        'add_new'             => __( 'Add New', 'OceanWP' ),
        'edit_item'           => __( 'Edit Portfolio', 'OceanWP' ),
        'update_item'         => __( 'Update Portfolio', 'OceanWP' ),
        'search_items'        => __( 'Search Portfolio', 'OceanWP' ),
        'not_found'           => __( 'Not Found', 'OceanWP' ),
        'not_found_in_trash'  => __( 'Not found in Trash', 'OceanWP' ),
    );
     
    $args = array(
        'label'               => __( 'portfolio', 'OceanWP' ),
        'description'         => __( 'Featured case studies.', 'OceanWP' ),
        'labels'              => $labels,
        'supports'            => array( 'title', /*'editor', 'excerpt',*/ 'author', 'thumbnail', 'revisions', /*'custom-fields',*/ 'categories', ),
        'taxonomies'          => array( 'category' ),
        'hierarchical'        => false,
        'public'              => true,
        'show_ui'             => true,
        'show_in_menu'        => true,
        'show_in_nav_menus'   => true,
        'show_in_admin_bar'   => true,
        'menu_position'       => 6,
        'menu_icon'           => 'dashicons-analytics',
        'can_export'          => true,
        'has_archive'         => true,
        'exclude_from_search' => false,
        'publicly_queryable'  => true,
        'capability_type'     => 'post',
        'rewrite'             => array('slug' => 'portfolio'),
    );
     
     
function namespace_add_custom_types( $query ) {
  if( (is_category() || is_tag()) && $query->is_archive() && empty( $query->query_vars['suppress_filters'] ) ) {
    $query->set( 'post_type', array(
     'post', 'portfolio'
        ));
    }
    return $query;
}

add_filter( 'pre_get_posts', 'namespace_add_custom_types' );
    register_post_type( 'portfolio', $args );
}

add_action( 'init', 'new_post_type', 0 );

?>

Display Portfolio Custom Post Type (CPT) Template:

I saved this as my single-portfolio.php template saved within my child theme’s directory. The CSS class names come from my WordPress theme, OceanWP, and an animation plug-in, Animate It!.

<?php

/*
 * Template Name: Portfolio
 * Template Post Type: portfolio
*/

get_header(); 

?>

<div id="content-wrap" class="container clr">
<div id="primary" class="content-area clr">
<div id="content" class="site-content clr">
<article id="post-<?php the_ID(); ?>" class="single-page-article clr single-portfolio">	
<div class="entry clr" itemprop="text">

		<?php while ( have_posts() ) : the_post(); ?>
  <div class="portfolio--narrow-width">
   <h1><?php the_field('custom_title'); ?></h1>
   
   <div id="portfolio__featured-image" class="thumbnail eds-animate edsanimate-sis-hidden" data-eds-entry-animation="fadeIn" data-eds-entry-delay="0" data-eds-entry-duration="0.5" data-eds-entry-timing="linear" data-eds-exit-animation="" data-eds-exit-delay="" data-eds-exit-duration="" data-eds-exit-timing="" data-eds-repeat-count="1" data-eds-keep="yes" data-eds-animate-on="scroll" data-eds-scroll-offset="10"><?php the_post_thumbnail(); ?></div>
  
   <h2>About the Client</h2>
   
   <p><?php the_field('portfolio_about_client'); ?></p>
   
   <div class="portfolio__spacer--vertical"></div>
   
   <h2>Analysis of Existing Design</h2>

			<img id="portfolio-before-photo" src="<?php the_field('portfolio_existing_design_photo'); ?>" />
   
   <div class="portfolio-existing-design-analysis__row clearfix">
    <div class="portfolio-existing-design-analysis__column">
     <h3>Goals</h3>
     
     <p><?php the_field('portfolio_goals'); ?></p>
    </div><!-- .portfolio-existing-design-analysis__column -->
    <div class="portfolio-existing-design-analysis__column">
     <h3>Usability Issues</h3>
     
     <p><?php the_field('portfolio_usability_issues'); ?></p>
    </div><!-- .portfolio-existing-design-analysis__column -->
   </div><!-- .portfolio-existing-design-analysis__row -->
   
   <div class="portfolio__spacer--vertical"></div>
   
   <h2>Creative Strategy</h2>
   
   <p><?php the_field('portfolio_creative_strategy'); ?></p>
   
   </div><!-- .portfolio--narrow-width -->
   
   <div class="portfolio__spacer--vertical"></div>
   
   <div id="portfolio-wip-photos"><?php the_field('cs_wip'); ?></div>
   
   <div class="portfolio__spacer--vertical"></div>
   
 <div class="portfolio--narrow-width">
   <h2>Results</h2>
   
   <p><?php the_field('portfolio_results'); ?></p>
   
   <div class="portfolio__spacer--vertical"></div>
   
   <div id="portfolio-cta-next-case-study"><?php the_field('cta_next_previous'); ?></div>
 </div><!-- .portfolio--narrow-width -->
		<?php endwhile; // end of the loop. ?>

</div> 
</article>
</div><!-- #content -->
</div><!-- #primary -->
</div><!-- #content-wrap -->

<?php get_footer(); ?>

Display Portfolio Custom Post Type (CPT) Archive:

Like single-portfolio.php, I saved this code as archive-portfolio.php within the same child theme directory.

<?php
/*
Template Name: Portfolio Archive
*/
get_header(); ?>

<div id="content-wrap" class="container clr">
<div id="primary" class="content-area clr">
<div id="content" class="site-content clr">
<article id="post-999999999" class="single-page-article clr archive-portfolio">
<div class="entry clr" itemprop="text">
         <?php while ( have_posts() ) : the_post(); ?>
 
             <!-- Page Content goes here -->             
             <?php the_content(); ?>
 
         <?php endwhile; // End of the loop. ?>
         
         <?php
             // Custom Post Query 
            $args = array(
                'post_type' => 'portfolio',
                'posts_per_page' => -1,
                'paged='.$paged,
            );
        
            $the_query = new WP_Query($args);
        
            while( $the_query->have_posts() ) : $the_query->the_post();
        ?>
            
            <!-- Post Content goes here -->
            <div class="portfolio-entries">            
             <section id="post-<?php the_ID(); ?>" class="blog-entry clr large-entry col-1 post-<?php the_ID(); ?> post type-post status-publish format-standard has-post-thumbnail hentry category-uncategorized entry has-media">             
             <div class="blog-entry-inner clr">
             <div class="thumbnail eds-animate edsanimate-sis-hidden" data-eds-entry-animation="fadeIn" data-eds-entry-delay="0" data-eds-entry-duration="0.5" data-eds-entry-timing="linear" data-eds-exit-animation="" data-eds-exit-delay="" data-eds-exit-duration="" data-eds-exit-timing="" data-eds-repeat-count="1" data-eds-keep="yes" data-eds-animate-on="scroll" data-eds-scroll-offset="10">
             <a href="<?php the_permalink(); ?>" class="thumbnail-link">
             <figure class="attachment-full size-full wp-post-image" itemprop="image" sizes="(max-width: 600px) 100vw, 600px"><?php the_post_thumbnail(); ?><span class="overlay"></span></figure>
             </a>
             </div><!-- .thumbnail -->
             
             <header class="blog-entry-header clr">
             <h2 class="blog-entry-title entry-title">
             <a href="<?php the_permalink(); ?>" title="<?php the_title(); ?>" rel="bookmark"><?php the_title(); ?></a>
             </h2><!-- .blog-entry-title -->
             </header><!-- .blog-entry-header -->
             
             <ul class="meta clr">
                 <li class="meta-author" itemprop="name"><i class="icon-user"></i><a href="https://www.laralee.design/author/<?php the_author($nickname); ?>/" title="Posts by <?php the_author(); ?>" rel="author"  itemprop="author" itemscope="itemscope" itemtype="http://schema.org/Person"><?php the_author(); ?></a></li>
                 <li class="meta-date" itemprop="datePublished" pubdate><i class="icon-clock"></i><?php the_date(); ?></li>
                 <li class="meta-cat"><i class="icon-folder"></i><?php the_category(', '); ?></li>
                </ul>
                
                
                <!-- .blog-entry-readmore --><a href="<?php the_permalink(); ?>" title="Continue Reading" class="blog-entry-readmore clr">Continue Reading <i class="fa fa-angle-right"></i></a>
               
            </div><!-- .blog-entry-inner.clr -->
            </section>
            </div><!-- .case-study-entries -->
        
        <?php endwhile; wp_reset_postdata(); ?>

</div><!-- .entry.clr -->
</article>
</div><!-- #content -->
</div><!-- #primary -->
</div><!-- #content-wrap -->
</main>

<?php get_footer(); ?>

February 2019 Update: If this whole process looks intimidating, consider creating WordPress Custom Post Types (CPTs) with a plugin. Check out my tutorial, “How to Make Custom Post Types with Elementor Pro.”

September 2019 Update: Wow, this post is popular! I’ve edited my copy to clarify a few points. Thanks for reading!