Create a custom “Download” post type using WordPress

Create a custom "Download" post type using WordPress
Please note: This is an old post. The information is probably not accurate and up-to-date anymore.

I’ve been asked several times about the download functionality on my blog so I’ve decided to create this 2-part tutorial on how you can create your own download section on your site using custom post types.

In this part I’ll teach you

  1. How the register the post type
  2. A custom “Download Categories” taxonomy
  3. A custom meta box with information about the download
  4. Create the archive
  5. Create the download counter
  6. Conclusion

The next part will be about creating a download shortcode, a widget and the archive for the custom taxonomy. Read part two here.

If you want a more advanced/flexible control over your downloads (and might want to sell them), I recommend the Easy Digital Downloads plugin.

Download the full code (including part 2)

[This download is no longer available]

The code in this tutorial should be in your (child) theme functions.php or your own functionality plugin.

Create the Post Type

 * Add the download post type
add_action( 'init', 'jayj_create_download_post_type' );

function jayj_create_download_post_type() {

   register_post_type( 'jayj_download', array(
      'description'         => 'The latest downloads',
      'has_archive'         => 'downloads', // The archive slug
      'rewrite'             => array( 'slug' => 'download' ), // The individual download slug
      'supports'            => array( 'title', 'excerpt', 'thumbnail', 'custom-fields' ),
      'public'              => true,
      'show_ui'             => true,
      'exclude_from_search' => true,
      'labels' => array(
         'name'               => 'Downloads',
         'add_new'            => 'Add New',
         'add_new_item'       => 'Add New Download',
         'edit'               => 'Edit',
         'edit_item'          => 'Edit Download',
         'new_item'           => 'New Download',
         'view'               => 'View Download',
         'view_item'          => 'View Download',
         'search_items'       => 'Search Downloads',
         'not_found'          => 'No downloads found',
         'not_found_in_trash' => 'No downloads found in Trash',


This will register the custom post type and give you a ‘Downloads’ menu in your dashboard, as seen on the image below.

I won’t go over all the values – if you want a better understanding on what’s going on, I recommend reading “Custom post types in WordPress” by Justin Tadlock.

But I’d like to explain this part:

'has_archive' => 'downloads', // The archive slug
'rewrite' => array( 'slug' => 'download' )

It means that your download archive can be found at and the individual downloads can be found at

A custom “Download Categories” taxonomy

Now that we have the post type, let’s create a custom taxonomy called “Download Categories”. That way you can categorize your downloads just like you can with posts. Examples of download categories can be PDFs, PSD files, WordPress themes and so on.

After the register_post_type(); function, add the following code:

 * Custom "Download Categories" taxonomy
register_taxonomy( 'download-categories', array( 'jayj_download' ),
	'public' => true,
	'labels' => array( 'name' => 'Download Categories', 'singular_name' => 'Download Category' ),
	'hierarchical' => true,
	'rewrite' => array( 'slug' => 'download-types' )

'hierarchical' => true makes the taxonomy behave like categories. If you want it to behave like tags, set it to false.

When you create or edit a download, you’ll now have have the option to choose a category

Create a custom meta box

The third step is to add custom data fields to the add/edit download page. To give you an idea of what I mean, take a look at the following image:

I’ve decided to keep it simple in this tutorial. You should add and edit them so they fit to your needs.

Let’s move on!

 * Adds the download meta box for the download post type
function jayj_meta_box_download() {
  add_meta_box( 'jayj-meta-box-download', 'Download Settings', 'jayj_meta_box_download_display', 'jayj_download', 'normal', 'high' );

add_action( 'add_meta_boxes', 'jayj_meta_box_download' );

This will add the meta box to the jayj_download post type.
If you try it out and gets an error, don’t worry. That’s because we haven’t defined the jayj_meta_box_download_display() function yet. Let’s do that know.

The code has been breaked up into pieces. First part:

 * Displays the download meta box
function jayj_meta_box_download_display( $object, $box ) {

   // Setup some default values
   $defaults = array( 
	'file'    => '',
	'version' => '1.0',
	'postid'  => '',
	// Add more!

   // Get the post meta
  $download = get_post_meta( $object->ID, 'download', true );

  // Merge them
  $download = wp_parse_args( $download, $defaults );

This part will set up some default values and merge them with the download post meta using the wp_parse_args() function. Some of the values are set to empty by default to prevent PHP notices when there’s no download meta.

Next part is the fields

<input type="hidden" name="jayj-meta-box-download" value="<?php echo wp_create_nonce( basename( __FILE__ ) ); ?>" />

<br />

<table class="form-table">

   <thead><tr><th><span class="description">None of the fields are required</span></th></tr></thead>

      <th><label for="download-hide">Hide download</label></th>
         <?php $hide = get_post_meta( $object->ID, '_hide_download', true ); ?>
         <label><input type="checkbox" name="download-hide" id="download-hide" value="1" <?php checked( '1', $hide ); ?> />
         <span class="description">Check this if you don't want to show the download at the archive page</span></label>

      <th><label for="download-file">File</label></th>
         <input type="text" id="download-file" size="60" name="download-file" value="<?php echo esc_url( $download['file'] ); ?>" />
         <input type="button" id="upload-file-button"  class="button" value="Upload file" />
         <label for="download-file"><span class="description">Upload or link to download file</span></label>

      <th><label for="download-version">Version</label></th>
         <input type="text" id="download-version" name="download-version" value="<?php echo esc_attr( $download['version'] ); ?>" size="3" />

      <th><label for="download-count">Download count</label></th>
         <?php $count = isset( $object->ID ) ? get_post_meta( $object->ID, 'download_count', true ) : 0; ?>
         <input type="number" id="download-count" name="download-count" value="<?php echo absint( $count ); ?>" size="7" min="0" />
         <?php printf( '<p>This file has been downloaded <b>%d</b> times</p>', absint( $count ) ); ?>

      <th><label for="download-postid">Post ID</label></th>
         <label><input type="text" id="download-postid" name="download-postid" value="<?php echo absint( $download['postid'] ); ?>" size="3" />
         <span class="description">ID of the post which the download is associated with</span></label>
         <?php if ( !empty( $download['postid'] ) ) { ?>
            <p>This download is associated with <a href="<?php echo get_permalink( $download['postid'] ); ?>"><?php echo get_the_title( $download['postid'] ); ?></a></p>
         <?php } ?>


The code might look long and confusing. It adds a table with all the fields and gets the values from the $download array defined earlier.

But we are not done yet. The metabox and the fields has been added, but we haven’t made a function that saves them yet.

 * Save the download information
function jayj_meta_box_download_save( $post_id, $post ) {

   /* Verify that the post type supports the meta box and the nonce before preceding. */
   if ( ! isset( $_POST['jayj-meta-box-download'] ) || ! wp_verify_nonce( $_POST['jayj-meta-box-download'], basename( __FILE__ ) ) )
	return $post_id;

   /* Don't save them if... */
   if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
   if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) return;
   if ( defined( 'DOING_CRON' ) && DOING_CRON ) return;

   /* Get the post type object. */
   $post_type = get_post_type_object( $post->post_type );

   /* Check if the current user has permission to edit the post. */
   if ( ! current_user_can( $post_type->cap->edit_post, $post_id ) )
	return $post_id;

 * Add the download meta
   update_post_meta( $post_id, 'download', array( 
	'file'    => esc_url_raw( $_POST['download-file'] ),
	'version' => strip_tags( $_POST['download-version'] ),
	'postid'   => absint( $_POST['download-postid'] ),
	// Add your own if you've added fields
   ) ); 

   // Seperate the download count and "hide" from the rest
   update_post_meta( $post_id, 'download_count', absint( $_POST['download-count'] ) );
   update_post_meta( $post_id, '_hide_download', ( $_POST['download-hide'] == 1 ? 1 : 0 ) ); 

add_action( 'save_post', 'jayj_meta_box_download_save', 10, 2 );

This saves all the meta data, excerpt the count and “hide”, in a single array in a custom field called download. The download count is saved to download_count and the “hide” option is saved in _hide_download. They’re apart from the others because it’s easier to use them to do other things when they’re alone.

As you might has noticed, there’s a button called “Upload file” in the metabox. Currently it doesn’t do anything but we make it open a lightbox window, just like when you add images, and upload the file.

The jQuery for the upload button needed can either be added inline or in an external file. I’ve decided to add it inline (the final code has the external commented out, almost ready to use)

 * Adds the script needed for the file upload 
function jayj_download_metabox_script() {

   $screen = get_current_screen(); 

   // Make sure we are on the Download screen
   if ( isset( $screen->post_type ) && $screen->post_type == 'jayj_download' ) : ?>

	<script type="text/javascript">

	   var formfield;

	   // Open upload window
	   $('#upload-file-button').click(function() {
		formfield = $('#download-file').attr('name');
		tb_show( '','media-upload.php?type=image&TB_iframe=true' );
		return false;

	   // user inserts file into post. only run custom if user started process using the above process
	   // window.send_to_editor(html) is how wp would normally handle the received data
	   window.original_send_to_editor = window.send_to_editor;

	   window.send_to_editor = function(html) {
		if (formfield) {			
		   // Get the src value from the image
		   fileurl = $('img', html).attr('src');

		   // The upload is not an image! Get the href instead
		   if( fileurl === undefined )
			fileurl = $(html).attr('href');

		   // Insert it into the text box and close

		} else {


add_action( 'admin_footer', 'jayj_download_metabox_script' );

Once the new meta box has been added, it looks like this:

Create the archive

The download archive page can be found at (we defined the slug earlier)

Based on your theme, the archive will probably be using the archive.php template file to display the downloads. But luckily WordPress first looks for the archive-{posttype}.php template, in our case archive-jayj_download.php

Create that in your current theme. The markup I use is based on the Twenty Eleven theme. You should change the markup to fit to your theme and style. So I recommend you copy the code from archive.php into your new file.

Update January 27, 2014: This section has been updated to use the pre_get_posts action instead of query_posts().

First, we’ll have to customize the loop to fit our new post type. This means more “posts” per page and excluding of the hidden downloads. To do that we should use the pre_get_posts action.

The following code should go in the file you have all the previous code:

 * Download archive pages
function jayj_download_cpt_archives( $query ) {

   if ( ( is_post_type_archive( 'jayj_download' ) || is_tax( 'download-categories' ) ) && $query->is_main_query() ) {

      // Limit the number of downloads on one page.
      $query->set( 'posts_per_page', 50 );

      // Exclude hidden downloads
      $query->set( 'meta_query', array(
            'key'     => '_hide_download',
            'value'   => 1,
            'compare' => '!=',


   /* Download categories archive page. */
   if ( is_tax( 'download-categories' ) && $query->is_main_query() ) {
      $query->set( 'post_type', 'jayj_download' );

   return $query;

add_action( 'pre_get_posts', 'jayj_download_cpt_archives' );

Now that our loop is at place, we can return to the markup for the download archives. After <?php while ( have_posts() ) : the_post(); ?> add:

   $download = get_post_meta( $post->ID, 'download', true );
   $download_count = get_post_meta( $post->ID, 'download_count', true );

   <article id="download-<?php the_ID(); ?>" <?php post_class(); ?>>

	<header class="entry-header">
           <h1 class="entry-title"><a href="<?php the_permalink(); ?>" title="<?php printf( esc_attr__( 'Download %s', 'twentyeleven' ), the_title_attribute( 'echo=0' ) ); ?>" rel="bookmark"><?php the_title( '', ' <span>&ra rr;</span>' ); ?></a></h1>

           <div class="entry-meta">
		  $show_sep = false;

		  if ( ! empty( $download['postid'] ) )
		     $show_sep = '<span class="sep">|</span>';

		  // Get the download categories					
		  echo get_the_term_list( $post->ID, 'download-categories', 'Type: ', ' ', $show_sep );

		  // Get the associated post
		  if ( ! empty( $download['postid'] ) )
		     echo '<a href="' . get_permalink( $download['postid'] ) . '" title="Read the post ' . get_the_title( $download['postid'] ) . '">Read the post</a>';
           </div><!-- .entry-meta -->

   		  if ( has_post_thumbnail() )

	<div class="download-description">
           <?php echo $post->post_excerpt; // We can't use the_excerpt because some themes has a "Continue reading..." link ?>

	<footer class="entry-meta">
           <a href="" class="download-btn">Download “<?php the_title(); ?>”</a>
           <span class="download-count">Downloaded <?php echo number_format( $download_count ); ?> times</span>

   </article><!-- #download-<?php the_ID(); ?> -->

Again, this markup is based on Twenty Eleven – you might want to change it. With some styling, here how it can look:

Create the download counter

We have the post type, a custom taxonomy, a metabox and the archive. But we still haven’t created a way for the user to actually download the file.

 * Download counter function
function jayj_count_and_redirect() {

   // Return if not download
   if ( ! is_singular( 'jayj_download' ) )

   $download_id = get_queried_object_id();

   $postmeta = get_post_meta( $download_id, 'download', true );
   $count_meta = get_post_meta( $download_id, 'download_count', true );

   // Get the count
   $count = isset( $download_id ) ? $count_meta : 0;

   // Handle the redirect
   $redirect = isset( $download_id ) ? $postmeta['file'] : '';

   if ( ! empty( $redirect ) ) :
	wp_redirect( esc_url_raw( $redirect ), 301 );
	update_post_meta( $download_id, 'download_count', $count + 1 ); // Update the count
   else :
	wp_redirect( home_url(), 302 );

add_action( 'template_redirect', 'jayj_count_and_redirect' );

Let me explain the code. When a user goes to they normally would see the the single view of the download. But we don’t need that.

What this function does it that it detects when a user vists the single view, it gets the post meta, redirect them to the file and updates the download count. Pretty awesome.

That’s it!

That was it for part 1! Wow. It became quite longer that I imagined – hope I didn’t lose you.

In this part you learned how to

  1. Register the post type
  2. Create a custom “Download Categories” taxonomy
  3. Create a metabox
  4. Create the archive
  5. and create the download counter

Download the code on Github

[This download is no longer available]

The next part will be about creating a download shortcode, a widget and the archive for the custom taxonomy.

If you have any comments, suggestions or have spotted an error please write a comment.

35 responses

  1. Hi,

    Can you please throw any light on adding a “pay to download” method in custom download post type? Or any recommendation of any plugin which can be used to achieve this?


    1. The upkeep would be hard to do on your own. I recommend a plugin, so you have more security and options. The coding to add such a feature is much harder than just downloading one that has it already.

  2. Hi,

    Thanks for this tutorial!

    When I select a file in the media box, it gives an error.

    Warning: Cannot modify header information – headers already sent by (output started at …\wp-content\themes\…\functions.php:864) in C:\…\wp-admin\async-upload.php on line 26

    Could you help me out?

      1. Yes! That was it.

        That’s an excellent tutorial Jesper.

        Thanks for sharing!

  3. Hey there! I know this is kinda off topic

    but I was wondering which blog platform are you using for this site?
    I’m getting fed up of

    WordPress because I’ve had problems with hackers and I’m looking at alternatives for another platform. I

    would be great if you could point me in the direction of a good platform.

  4. Great tutorial Jesper – thank you!

    This tutorial does very close to exactly what I’m after but not quite…

    Basically, I’m trying to create a download custom post type that offers everything you do here and more. Most importantly I would like for users to be able to see the “single” views of the download, where it offers a download link that is then tracked when clicked.

    From playing around with the code you’ve provided here, it seems that the only part I’m struggling with is the download counter function. Any pointers on how to track the clicks without this redirect would be hugely appreciated.


    1. Hello Tim,

      You might want to consider using the Easy Digital Downloads plugin instead. It offers a lot more features than my solution. What you’re requesting is possible with my code but I recommend using that awesome plugin instead.

      – Jesper

    1. And … what means “soon”??
      I mean, it is the 24th of January, your comment was posted on the 5th.

      But I do not want to set you under pressure 😉

      Cheers from Germany,

    1. I haven’t tried it but with minor adjustments to the code I don’t see why not.

      There’s some places where it the code checks for the jayj_download post type – you could change that to post instead. Don’t use register_post_type and you probably could use the category taxonomy instead.

      The biggest question is why?

      1. I’m actually trying to do it right now, been at it for the past few hours, I replaced all the instances of ‘jayj_download’ with ‘post’, I also removed register_post_type. It works except that it turns the post permalink itself into a download link.

        What I’m ultimately trying to achieve is to have a download counter for files uploaded to a post. Your tutorial is the only one that comes remotely close to that I’ve found, all the premium plugins do all these advanced things but yet none of them offer a simple download function for files attached to a post.

      2. Could you create the files in as “posts” in the download post type and then just link to them in the post?

        You can remove the jayj_count_and_redirect function to avoid the turning the permalink into a download link.

        I don’t know what else you can do. Sorry.

  5. Hi Jasper,

    I used this post type for .stp files, when I hit the linked title I see the raw code of the .stp file in the browser, is there a way when I click on the title that the file downloads?

    1. Helo Hans,

      Sorry for the long answer time. Your comment landed in my moderation queue.

      I think you need to edit your .htaccess file.

      Something like this might work:

      <FilesMatch "\.(stp)$">
         ForceType application/octet-stream
         Header set Content-Disposition attachment

      If not, try searching for something like “htaccess force download”

      Hope that helps you.

      – Jesper

  6. Of note, adding thickbox may be required for newer or custom themes, specifically if you’re getting an error when trying to upload the file.

  7. Nice tutorial
    I only have one problem, i need to know how to create a page
    that can display an especific category instead of a single download—-

Comments are closed.