One of my favorite additions to WordPress over the last few years was the redesigned media manager. Launched in version 3.5, it was a big improvement, with features like drag-and-drop uploading and an improved gallery function. It is also pretty easy to integrate into WordPress plugins.

To handle the custom image headers on my blog posts, I coded out a small plugin which creates a meta-box on the editing page and uses the WordPress media manager to handle the images.

Here’s what the plugin looks like:

Custom Post Image Screenshot

This code draws from Theme Foundation‘s excellent guide to using WordPress meta fields.

There are three elements of this plugin: PHP to provide the integration with WordPress, jQuery to handle interfacing with the media manager, and CSS to provide some light style.

In a burst of unbridled creativity, I cleverly decided to call my plugin “Custom Post Images”, or “CPI”. You’ll see the abbreviation a lot in the following code, which helps keep the code somewhat namespaced.


This is the most complex looking part of the entire plugin, but once you get into it, the code is fairly straight-forward.

Here are the different components:

The Set-up

First, we start by defining the typical WordPress plugin information. Then we start setting up the PHP class which will house the various functions which will make the plugin work.

In my version, I’ve made an allowance for two images that can be added to the meta box, one for the page header and another for a featured image, which I use on the home and category pages. These are set in the private $cpi_images_array array just after the class declaration. Just add another element to the array if you’d like more custom images.

Then we have the __construct, in which we add the actions to create the meta box, save the data, and en-queue the CSS and JavaScript. This is followed by the functions to en-queue the CSS and JavaScript. For the Javascript, note the wp_localize_script function, which allows us to customize the title and action button on the media manager.

Plugin Name: Custom Post Images
Plugin URI:
Description: Adds custom images to posts
Author: Darren Krape
Version: 1.0
Author URI:

class CustomPostImages {

  // Create array of images (could be added to a settings page instead of hard-coded)
  private $cpi_images_array = array(
    '0' => array(
      'title' => 'Page Header',
      'slug' => 'header'
    '1' => array(
      'title' => 'Featured Image',
      'slug' => 'featured'
  private $cpi_post_types = array('post', 'page'); // Limit meta box to certain post types

  public function __construct() {
    add_action( 'add_meta_boxes', array( $this, 'cpi_add_meta_box' ) ); // Add the meta box
    add_action( 'save_post', array( $this, 'cpi_save' ) ); // Save meta box data
    add_action( 'admin_print_styles', array( $this, 'cpi_admin_styles' ) ); // Add CSS styles
    add_action( 'admin_enqueue_scripts', array( $this, 'cpi_image_enqueue' ) ); // Add JavaScript

   * Adds the image management JavaScript.

  public function cpi_image_enqueue() {

    global $typenow;

    if ( in_array( $typenow, $this->cpi_post_types )) {
      // Registers and enqueues the required javascript.
      wp_register_script( 'cpi-meta-box-image', plugin_dir_url( __FILE__ ) . 'custom-post-images.js', array( 'jquery' ) );
      wp_localize_script( 'cpi-meta-box-image', 'meta_image',
          'title' => __( 'Choose or Upload an Image', 'cpi-textdomain' ),
          'button' => __( 'Use this image', 'cpi-textdomain' ),
      wp_enqueue_script( 'cpi-meta-box-image' );

   * Adds the meta box stylesheet.

  public function cpi_admin_styles() {

    global $typenow;
    if ( in_array( $typenow, $this->cpi_post_types )) {
      wp_enqueue_style( 'cpi_meta_box_styles', plugin_dir_url( __FILE__ ) . 'custom-post-images.css' );

Creating the meta-boxes

With our program set up, we can now create the meta box on the post creation page. This is pretty standard code, though there are a few call-outs:

We’re using the WordPress metadata API to save our images. This is a lot easier than creating a custom table and allows us to use existing WordPress functions, as we’ll see later. One trick here is the use of the underscore in the metadata name, which you see in this line of code: $cpi_type_name = "_cpi-type-" . $cpi_image['slug'];. This will hide the metadataa from the “Custom Fields” widget.

   * Adds the meta box container.
  public function cpi_add_meta_box( $post_type ) {
    if ( in_array( $post_type, $this->cpi_post_types ) ) {
        ,__( 'Post Images', 'cpi-textdomain' )
        ,array( $this, 'cpi_render_meta_box_content' )
   * Render Meta Box content.
   * @param WP_Post $post The post object.
  public function cpi_render_meta_box_content( $post ) {
    // Add an nonce field so we can check for it later.
    wp_nonce_field( basename( __FILE__ ), 'cpi_nonce' );

    $cpi_stored_meta = get_post_meta( $post->ID );

    echo '<ul id="cpi">';

    // Cycle through each CPI image types
    foreach( $this-&gt;cpi_images_array as $cpi_image ) {
      $cpi_type_name = "_cpi-type-" . $cpi_image['slug'];

    <li class="cpi-upload" id="<?php echo $cpi_type_name; ?>">
      <p class="cpi-upload-header"><!--?php echo $cpi_image['title']; ?--></p>
      <div class="cpi-upload-thumbnail">
        if( $cpi_stored_meta[$cpi_type_name] ) {
          echo wp_get_attachment_image( $cpi_stored_meta[$cpi_type_name][0] );

      <input type="button" class="button cpi-button cpi-upload-button" value="<?php _e( 'Choose Image ', 'cpi-textdomain' )?>">
      <input type="button" class="button cpi-button cpi-upload-clear" value="×">
      <input class="cpi-upload-id" type="hidden" name="<?php echo $cpi_type_name ?>" value="<?php if ( isset ( $cpi_stored_meta[$cpi_type_name] ) ) echo $cpi_stored_meta[$cpi_type_name][0]; ?>">


    echo '<ul-->';

Saving our custom images

We have our scripts en-queued and meta box set up. Now, we need a function to save our images into the WordPress database. Since we’re using the WordPress metadata API, the hardest part of this are the security checks to ensure the data is clean and the user has the necessary permissions.

   * Save the meta when the post is saved.
   * @param int $post_id The ID of the post being saved.

  public function cpi_save( $post_id ) {
     * We need to verify this came from the our screen and with proper authorization,
     * because save_post can be triggered at other times

    // Check if our nonce is set.
    if ( ! isset( $_POST['cpi_nonce'] ) )
      return $post_id;

    $nonce = $_POST['cpi_nonce'];

    // Verify that the nonce is valid
    if ( ! wp_verify_nonce( $nonce, basename( __FILE__ ) ) )
      return $post_id;

    // If this is an autosave, our form has not been submitted, so we don't want to do anything
    if ( defined( 'DOING_AUTOSAVE' ) &amp;&amp; DOING_AUTOSAVE ) 
      return $post_id;

    // Check the user's permissions
    if ( 'page' == $_POST['post_type'] ) {

      // Can they edit the page
      if ( ! current_user_can( 'edit_page', $post_id ) )
        return $post_id;

    } else {

      // Can they edit the post
      if ( ! current_user_can( 'edit_post', $post_id ) )
        return $post_id;


    // OK, its safe for us to save the data
    foreach( $this-&gt;cpi_images_array as $cpi_image ) {

      $cpi_type_name = "_cpi-type-" . $cpi_image['slug'];
      // Sanitize the user input
      $cpi_data = sanitize_text_field( $_POST[ $cpi_type_name ] );

      // Update the meta field if we have data, if not remove it
      if( $cpi_data ) {
        update_post_meta( $post_id, $cpi_type_name, $cpi_data );
      } else {
        delete_post_meta( $post_id, $cpi_type_name );

Initializing the PHP

And, finally, we close our class and initialize it.


$custom_post_images = new CustomPostImages();


The JavaScript has two main functions. First, when the user clicks the “Choose image” button in the metabox, the media manager is opened and, if the user selects an image, adds the media’s thumbnail and ID number to the metabox form. Second, if the user clicks the “X” in the metabox, it will clear the the set image.

 * Attaches the image uploader to the input field
  $('#cpi .cpi-upload').each(function() {
    var cpi_image_frame;
    var p = $(this);
    //Choose/upload image
    p.find('.cpi-upload-button').click(function(e) {

      if ( cpi_image_frame ) {;

      // Sets the media manager's title and button text
      cpi_image_frame = ={
        title: meta_image.title,
        button: { text:  meta_image.button }

      // Runs when an image is selected
      cpi_image_frame.on('select', function() {
        // Grabs the attachment selection and creates a JSON representation of the model.
        var media_attachment = cpi_image_frame.state().get('selection').first().toJSON();

        var media_id =;
        var media_thumbnail = media_attachment.sizes.thumbnail.url;
        // Sends the attachment URL to our custom image input field.
        p.find('.cpi-upload-thumbnail').html('<figure><img decoding="async" src="' + media_thumbnail + '"></figure>');

      // Opens the media library frame; 

    // Button to unset current image
    p.find('.cpi-upload-clear').click(function(e) {







And, finally, the CSS. This sets the style for the metabox and is pretty straight-forward.

#cpi_meta_box {
  overflow: hidden;
  padding-bottom: 1em;

#cpi .cpi-upload {
  background-color: #f9f9f9;
  border: 1px solid #dfdfdf;
  float: left;
  margin-right: 12px;

#cpi .cpi-upload-header {
  background-color: #f1f1f1;
  font-weight: bold;
  margin: 0;
  padding: 5px 8px 8px;
#cpi .cpi-upload-thumbnail {
  background: white url('custom-post-image-placeholder.png') no-repeat center center;
  height: 150px;
  margin: 8px 8px 0;
  width: 150px;
#cpi .cpi-button {
  margin: 8px 0 8px 8px;
#cpi .cpi-upload-clear {
  color: #999;
  font-weight: bold;
#cpi .cpi-upload-id {
  background: #eeeeee;
  border: 1px solid silver;
  color: #999;
  display: block;
  margin: 0 8px 8px;
  width: 40px;

Using the images in your theme

With your images set in the admin, it is easy to pull them into your theme. Since we used WordPress’ metadata API, we simply get the ID number of the image we saved then get the URL.

//Get featured image
$featured_meta = get_post_meta( $post-&gt;ID );
$featured_meta_image = wp_get_attachment_url( $featured_meta['_cpi-type-featured'][0] );

Complete code on GitHub

This post has 3 Comments

  1. Hello, I can’t get this to work with pics under 151×151 pixels on my two WP installations (newest version, switched back to default theme twentyfifteen). The image will just not be accepted to be chosen then (even if I can add that pic as “real featured image”).
    As long as one side is below 150px or smaller, it will not work. Already set thumbnails and middle sized images to 1x1px and added with “add_image_size” a 50x50px size + set_post_thumbnail_size to 50×50, too. Still no 150x150px sized image will work.
    Any idea what causes this?

    Thank you, Lola

  2. Cant output images in header outside of a loop of my theme, can you please show me an example of this, tried this but didn’t work?
    //Get featured image
    $featured_meta = get_post_meta( $post->ID );
    $featured_meta_image = wp_get_attachment_url( $featured_meta[‘_cpi-type-featured’][0] );

  3. This was not what I needed, but was absolutely awesome in nudging me along to where I needed to be. For my artists’ site I needed a variable number of detail images in the sidebar, cropped from the original, so when the visitor clicks/taps them opens a larger version of the detail in prettyPhoto. This tut enabled me to sit down and code it in one day.

    I separated it into an admin and front end class, full OOP, and made all the settings configurable in an admin settings page, no hard coded refs anywhere. Multiple images saved as a JSON encoded string in a single meta row.

    I also had to tweak the JS a lot, I used remove() when an image was X’ed and dynamically added a new Add box when one was added so I didn’t have to save/choose/save/choose, but it’s all based on this tut. Thanks for posting, it was very helpful.

Leave a Reply

Your email address will not be published. Required fields are marked *