What is the right way to handle custom post type


this is my first site with sage and cpt and i really appreciate your help.

  1. i created new cpt “product”. (will have posts about our products)
  2. i created a new page “product”.
  3. the index.php recognize the new page as product type by the page name.
  4. it try to load the template “content-product” if exist, else fallback to template “content”

the first question: is this the right and best way to handle cpt by sage logic?

because usually before sage i used to create new custom page like “template-product” and assign it to the page “product”.
then: $loop = new WP_Query(‘post_type’ => ‘product’ );
and while ( $loop->have_posts() ) : $loop->the_post();

second question:
now, by the logic above, the title of the page is: “Archive: Products”. instead i would like my client to control the title from the CMS and call it for example: “Our Products”. how can it be done?

thanks fro your help

1 Like

Hey @dandin84 ,

What you did sounds fine. Some added tips: place your cpt code into an mu plugin instead of the theme’s functions.php, and to just follow the wordpress template heirarchy and you’ll be fine https://developer.wordpress.org/themes/basics/template-hierarchy/ .
archive-cpt.php for instance to create an archive page.

You could alter the archive title in a number of ways.
You could replace <?php get_template_part('templates/page', 'header'); ?> in your custom page template, that might be the best way for your scenario so it doesn’t alter other pages, but if you want them to be able to alter other archive titles:

The title function is in lib/titles.php. You could replace the call to get_the_archive_title() with something else there.

Or, you could also add a filter to that WordPress function to customize its output like so (this example just removes the "Archive: " prefix, you would customize it to fit your needs):

add_filter( 'get_the_archive_title', function ( $title ) {
    if( is_post_type_archive() ) {
        $title = sprintf( __( '%s' ), post_type_archive_title( '', false ) );
    return $title;

thanks for your reply,

it was really helpful and help me to understand the archive logic. nonetheless, i see now that i need to display more content in the archive page like products introduction and etc…

i can’t see how i am doing it in the “archive” way so i go back to “custom template page” way…

unless you can also teach me here a trick or two about adding “custom field/introduction content” to specific archive page? although it not a sage issue…

Assuming you want to alter the loop for the products archive here.

I’m sure there’s a better way (anyone?), but here’s how you could do it:

Create a new loop using WP_Query in your archive-cpt.php page template, and remove the include of default content on that page.

thanks for your replay,

sounds the same like creating a “custom page template” logic.

$args = array(‘post_type’ => ‘product’,‘posts_per_page’=>‘10’);
$loop = new WP_Query( $args );
while ( $loop->have_posts() ) : $loop->the_post();

the only different is if to implement it in archive-product.php or template-product.php(custom page template)

as i see it, the result will be the same… cool, thanks :smile:

First, +1 for creating CPTs in a plugin instead of your theme. Basic thinking behind it: if someone changes the theme, the content types (and content) shouldn’t disappear!

I’m working on a site at the moment with a bunch of CPTs that have ‘overview’ pages. There are a few ways to handle this but here’s what I’m doing this time. Would be interested to hear how other people handle this fairly common requirement.

  1. I created WP pages for each of the CPTs. Each page holds the overview content.
  2. I added a folder to /templates/ called /cpt-listings/ because, clean.
  3. I filled that folder with template parts, one per CPT, to display listings of each (they all have different html/css requirements). The template part has a custom loop in it to get the particular post type.
  4. In Sage’s page.php at the very top I get the pageID.
    $currentPage = get_the_ID();
  5. Everything else in page.php is the same as the default. This pulls in the page header and content . After the close of <div class="hentry">...</div> I added a switch that looks at the page ID and then calls the correct template_part for that ID.
    // foo cpt overview
     if (  $currentPage == '99' ) :
         get_template_part('templates/cpt-listing/cpt', 'foo');
    // bar cpt overview
    elseif ( $currentPage == '101' ) :
        get_template_part('templates/cpt-listing/cpt', 'bar');

   // one CPT is hierarchical and has several sub-pages with overviews
   // $bazSubPages is an array of those IDs
   // you could also use something like this to switch to a custom header for these pages
    elseif ( in_array($currentPage, $bazSubPages) ) :
        get_template_part('templates/cpt-listing/cpt', 'baz');

    // If you want to have an additional part for all other site pages, add this else.
    // if you want this on all pages, remove it from this switch
    else :
        get_template_part('templates/content', 'page');


I had initially created this with a custom field on pages that had the slug for the related CPT. That was much cleaner code in page.php (check if the meta_value exists and if it does get_template_part('templates/cpt-listing/cpt', $field);) but that seemed too breakable from the admin. I know the IDs wont change and to only have the field available on the specific overview pages and not every page in the admin, I had to list the IDs in CMB2 anyway! Might as well do it here.

Like I said, there are lots of ways to do this but three things I’m liking about this method:

  1. Template parts mean you use the same template_part for actual WP archive pages if you need to.*
  2. It’s very straight forward for the client. The page overviews are directly inline with all of the other page content on the site (which matches the menu…). They don’t need to go to some other part of the admin to add the archive overviews.
  3. Using page.php as the springboard means that any WP developer trying to come to grips with the theme will be able to load their first instinct (page.php) and see quickly what’s going on.

* One change I should make is removing the custom loops from the cpt_listing template_parts so that I might use the parts in search results or other areas around the site. I don’t actually need that for this project but a slightly more robust related-cpt check in page.php would mean creating a single loop (with the cpt being switched out) and even DRYer code. Next time. :-}

1 Like

In index.php it says something like:

<?php while (have_posts()) : the_post(); ?>
  <?php get_template_part('templates/content', get_post_type() != 'post' ? get_post_type() : get_post_format()); ?>
<?php endwhile; ?>

I used that to duplicate the templates/content.php into a templates/content-product.php (or any other cpt-slug). Then flush the cache by going to /wp-admin and then Settings > Permalinks and click “Save Changes” even if you haven’t changed anything on that page.

If it doesn’t work directly check if your cpt uses 'has_archive' => false, or not. It impacts on the wp hierarchy.

That should use your custom post type inside the loop. If you want to customize the loop too i suggest to follow @willthemoor’s answer.

1 Like