6

My goal is to have the slug www.domain.com/resources used by a Page Template rather than an Archive Page, and have single CPT posts as children of that slug, such as www.domain.com/resources/post.

Solution I thought would work:

I created a new Page in Wordpress and gave it a custom page template. I'm also registering my custom post type as follows. Pay special attention to has_archive and rewrite:

function bb_resources() {
    register_post_type( 'resources', array(
            'labels' => array(
                    'name' => 'Resources',
                    'singular_name' => 'Resource',
            ),
            'taxonomies' => array('resources_cat'),
            'public' => true,
            'has_archive' => false,
            'show_ui' => true,
            'supports' => array( 'title', 'editor' ),
            'rewrite' => array('slug'=>'resources', 'with_front'=>false),
    ) );
}

This, however, creates a 404 on the single post pages.

I'm stumped, please advise!

  • 1
    did you flush rewrite rules after adding this? – Milo Mar 23 '15 at 15:50
  • Yes I did, still no luck :( here's the link to the site if that helps: griffinpest.com/resources – wackerfuss Mar 23 '15 at 16:18
  • it appears you've also registered a taxonomy with the same slug? that would be your problem. – Milo Mar 23 '15 at 17:32
  • Did you try setting 'has_archive' => true? And just out of curiosity, why not create an archive page ("archive_resources.php") instead of a page template? – caroldeck Mar 24 '15 at 23:34
10

Since WordPress version 4.4 the hook 'theme_page_templates' allows to set arbitrary page templates.

It means that it's possible to show arbitrary values in the "Page Template" menu of the page edit screen, and when selected, the value will be stored in the page template meta for the page.

This means that you can "automagically" create a page template for any of the CPT's registered.

Setup page templates

The code would look something like this:

add_action( 'current_screen', function( \WP_Screen $screen ) {
    if( 'page' !== $screen->id || 'post' !== $screen->base ) {
        return;
    }

    // retrieve CPT objects with archive
    $types = get_post_types( ['has_archive' => true], 'objects' );

    // store CPT slug and labels in an array
    $menu = array();
    foreach( $types as $cpt ) {
        $menu[ $cpt->name ] = 'Archive: ' . $cpt->label;
    }

    // merge with page templates
    $menu and add_filter(
        'theme_page_templates',
        function( array $templates ) use ( $menu ) {
            $templates = array_merge( $templates, $menu );
            return $templates;
        }
    );
} );

With this code in place, assuming you have registered a post type resources with has_archive set to true, when you create or edit a page, you'll see in the "Page Template" dropdown a page template named Archive: Resources.

You can choose that template when creating a page, you want to use to display your resources posts.

Redirect direct page access

Since this page is an archive more than a page, you could use some code to redirect users to the actual CPT's archive when the page is accessed:

add_action( 'template_redirect', function() {
    if( is_page_template() ) {

        // current page templates
        $template = get_page_template_slug( get_queried_object_id() );

        // array of CPT names, filtering the ones with archives
        $types = get_post_types( array( 'has_archive' => true ), 'names' );

        // if the current template is one of the CPT, redirect
        if( in_array( $template, $types, true ) ) {
            wp_safe_redirect( get_post_type_archive_link( $template ) );
            exit();
        }
    }
} );

Getting page data

You probably want to use content and title from that page when viewing the article.

So, let's write a function that let's you get the page for a specific post type.

function get_archive_page( $post_type = '' ) {

    // if no CPT given, let's use the current main query to get it
    if( ! $post_type ) {
        global $wp_query;
        $query_var = ( array ) $wp_query->get( 'post_type' );
        $post_type = reset($query_var);
    }

    // if we have a valid post type
    if( post_type_exists( $post_type ) ) {

        // let's query the first page that has a page template
        // named after the the post type
        $posts = get_posts( array(
            'post_type'      => 'page',
            'post_status'    => 'publish',
            'posts_per_page' => 1,
            'orderby'        => 'menu_order',
            'order'          => 'ASC',
            'meta_query' => array( array( 
                'key'   => '_wp_page_template',
                'value' => $post_type
            ) )
        ) );

        // if we have results, return first (and only) post object
        if( $posts ) {
            return reset($posts);
        }
    }
}

You could use the function above to write two additional functions to get the title and the content of the page:

function get_archive_page_title( $post_type = '' ) {
    $page = get_archive_page( $post_type );

    return ( $page ) ? get_the_title( $page ) : '';
}    

function get_archive_page_content( $post_type = '' ) {
    $page       = get_archive_page( $post_type );
    $content    = '';

    if( $page ) {
        setup_postdata( $page );
        ob_start();
            the_content();
        $content = ob_get_clean();
        wp_reset_postdata();
    }

    return $content;
}

function the_archive_page_title() {
    echo get_archive_page_title();
}

function the_archive_page_content() {
    echo get_archive_page_content();
}

Function names should make clear what they do.

You could also write functions like get_archive_page_meta() if you need custom fields.

In the archive template

Now, in the archive.php (or in the archive-resources.php) template, you can make use of the above functions to show title and content of the archive page.

It will look something like:

<h1><?php the_archive_page_title(); ?></h1>

<div><?php the_archive_page_content(); ?></div>

<?php
    while( have_posts() ) : the_post();
        // loop stuff here
    endwhile;
?>

This way your CPT archive url will be example.com/resources/ and your single CPT url will be example.com/resources/post-name/ just like you want.

At the same time you'll be able to write content and title (and custom fields, if you want) specific for the post type.

Also, consider this method is reusable for any post type, you have or might use in the future.

  • 1
    I've used this recently, super useful. It's probably easiest to replace the current_screen hook and use theme_page_templates directly so that the options appear in both Quick Edit and Edit screens. – Howdy_McGee Apr 23 at 15:28
2

gmazzap's answer is great! I'd just like to add that since 4.1 there are two new functions that perfectly fit that use case: the_archive_title and the_archive_description. For themes that already use those (like Twenty Fifteen) you can skip the last part where you edit the theme template and do this instead:

Add data through filters

add_filter( 'get_the_archive_title', function( $title ) {
    if( is_post_type_archive() ) {   
        $page  = get_archive_page( $post_type );    // Function Found in @gmazzap's answer
        $title = $page ? get_the_title( $page ) : $title;
    }

    return $title;
} );

add_filter( 'get_the_archive_description', function( $content ) {
    if( is_post_type_archive() ) {
        $page    = get_archive_page( $post_type );  // Function Found in @gmazzap's answer
        $content = '';

        if( $page ) {
            setup_postdata( $page );
            ob_start();
                the_content();
            $content = ob_get_clean();
            wp_reset_postdata();
        }

        $content = ( $content !== '' ) ? get_the_title( $page ) : $content;
    }

    return $content;
} );
  • is there a reason you don't use $content = apply_filters('the_content', get_the_content()); ? – iantsch Mar 18 '16 at 20:21
  • 1. I C&P from gmazzap :) 2. We need to setup_postdata in any case so it wouldn't really make things shorter. 3. It isn't exactly the same if you look at the source: developer.wordpress.org/reference/functions/the_content/… – kraftner Mar 18 '16 at 20:42
  • i was just curious, if there was a reason. thx for the explanations – iantsch Mar 18 '16 at 20:48
  • @iantsch get_the_content() already applies the_content filter, why run it double? – lkraav Mar 18 '16 at 23:58
0

My own method for this feels decidedly caveman after reading the previous two answers but I'll share it anyway because while it's not nearly as robust, it's incredibly easy to set up. It also makes it dead simple for content authors to know where to edit the 'overview' content for a given archive in a largely page based/CMS style site structure.

For a post_type called region I'd register it along with an actual WP page called regions.

Create a template called page-regions.php (or whatever strategy you want to use to connect it that page) and do a custom loop to pull all of the region post_types. If you use template parts for your templates, it's easy enough to include the page header and main body as well so your code remains DRY. It's also really easy to customize because there's always that one post_type. :)

With this, the URLs will be slightly different than what you asked for:

http://example.com/regions for the page with the archive posts and
http://example.com/region/africa or
http://example.com/region/asia for a single CPT view.

But to me, /region/africa actually makes more sense than /regions/africa though that might be driven by working on rails/cakephp sites where it's the norm. /region alone winds up being a post_type=region archive (depending on how you set up the post_type) without the overview content from the page but I simply don't link to it. If URL bar surfers get there, no big deal.

Slight, secondary bonus: you can make the URL for the overview/archive page whatever you want, say /regions-of-the-world.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.