Ask your WordPress questions! Pay money and get answers fast! Comodo Trusted Site Seal
Official PayPal Seal

Rewrite Date Archives for Custom Post Type WordPress


I have a Custom Post Type called 'project'.
The post type is written as:

'rewrite' => array(
'slug' => 'project',
'with_front' => false,
'has_archive' => 'projects',

Custom Post Type all works fine.

My permalink structure for my blog is currently set:

Date Archive
Here is my date archive query:
$args = array(
'post_type' => 'project',
'type' => 'yearly',
'echo' => 0
echo '<h3>Archives</h3>';
echo '<ul>'.wp_get_archives($args).'</ul>';

Currently my date archive returns the following URLs:


I need them rewritten to return:


Note that '/newsletter/' also needs to be removed.

Answers (1)


Alex Miller answers:

Unfortunately, pretty permalinks for date archives only work for the built-in posts. We'll need to add our own rewrite rules and archives function to really make this work. First thing's first, lets add our rewrite rules:

* Add date archive rewrite rules for Custom Post Type
* @return void
function prefix_cpt_rewrites() {

add_rewrite_rule( '^projects/([0-9]{4})/([0-9]{2})/?', 'index.php?post_type=project&year=$matches[1]&monthnum=$matches[2]', 'top' );
add_rewrite_rule( '^projects/([0-9]{4})/?', 'index.php?post_type=project&year=$matches[1]', 'top' );

add_action( 'init', 'prefix_cpt_rewrites' );

We need to resave the permalink structure for this code to actually work. That is, log into WordPress, head to Settings -> Permalinks, and simply click the "Update" button at the bottom. This will flush the previous rewrite rules and add the above into place.

Next we need to write an archive function to output the above permalinks:

* WP Date Archives that includes custom post types
* @param Array $args
* @return void|String
function wp_get_archives_improved( $args = array() ) {

global $prefix_cpt_date_archives;

$prefix_cpt_date_archives = false;
$echo = ( ! empty( $args['echo'] ) ) ? $args['echo'] : false;
$args['echo'] = false;

if( empty( $args['post_type'] ) || 'post' == $args['post_type'] ) {

$new_output = wp_get_archives( $args );

} else {

$prefix_cpt_date_archives = $args['post_type'];
$post_archives = get_permalink( get_option( 'page_for_posts' ) );
$cpt_archives = get_post_type_archive_link( $args['post_type'] );

unset( $args['post_type'] );
add_filter( 'getarchives_where', 'prefix_getarchives_where' );
$archive_output = wp_get_archives( $args );
remove_filter( 'getarchives_where', 'prefix_getarchives_where' );

$new_output = str_replace( $post_archives, $cpt_archives, $archive_output );


if( ! $echo ) {
return $new_output;

echo $new_output;


* Modify the get archives WHERE to include our custom post type
* @param String $where
* @return String
function prefix_getarchives_where( $where ) {

global $prefix_cpt_date_archives;

if( empty( $prefix_cpt_date_archives ) ) {
return $where;

return str_replace( "WHERE post_type = 'post'", sprintf( "WHERE post_type = '%s'", esc_sql( $prefix_cpt_date_archives ) ), $where );

The above uses some gross globals but that's the only way I could think of getting the custom post type slug into the WHERE hook while also tricking the `wp_get_archives()` function into thinking we're still working with posts. Otherwise it would tack on the `post_type=project` query var which we don't want.

Finally, if you want to display a list of date archives you can call the function as you normally would with the previous args:

$args = array(
'post_type' => 'project',
'type' => 'yearly',
'echo' => 0
echo '<h3>Archives</h3>';
echo '<ul>'.wp_get_archives_improved($args).'</ul>';

I used multiple resources to build this out. I've reference them below. Let me know if you have any questions!

Answer on WordPress StackExchange by Stephen Harris ( )

Custom Post Type Date Archive Tutorial ( )

WordPress Developer Resources - wp_get_archives() ( )

WordPress Developer Resources - wp_get_archives_link() ( )

parksey18 comments:

Thanks heaps Alex,
That worked a treat.