Build a responsive WordPress portfolio

Web development may change rapidly, but two things that are here to stay are WordPress and responsive design. Knowing how to build responsive WordPress themes and plug-ins is a must. In this tutorial, we will look at building a WordPress plug-in and theme template for a responsive portfolio.

We will work with two mocked-up templates: an archive page, which will list all of the recent projects, and a single page, which will show a specific project. The archive page for the portfolio is a pretty simple one with a header and three columns of projects at full width. This will shrink to two columns, then one column, as the screen gets smaller. The HTML and CSS is available at GitHub. Each project on the page will have this structure.

Watch an exclusive screencast of this tutorial:

This is the HTML that will be generated by the WordPress Loop:

<div class="card">
  <img src="project.png">
    <h3>Name of Site</h3>
    <p>Short description and a link <a href="#">read more...</a></p>

The single page is going to have a similar layout, wrapping all of the text in a container called .project instead of .card. The CSS is also going to be fairly lightweight and scaled. You may also notice in the site files that there is a style.scss file. I've developed all of the CSS using Sass, but fear not: the generated style.css is compiled in a readable way.

Creating a new custom post type

A Custom Post Type is an object in WordPress that allows us to add any types of content we want to the WordPress editor, treating them the same way as posts. In a fresh WordPress install, there are menu options for Posts and Pages. These are both considered post types that handle content differently. With Custom Post Types, we can add options for creating new types of content to the WordPress admin menu. We'll create a Custom Post Type called Portfolio.

We're going to develop this Custom Post Type as part of a bigger WordPress plug-in for portfolio projects. While we could add the functionality to the theme, this is bad practice because then our content is tied to our design: if we change the theme, we lose the portfolio. We will handle display through two methods: templates/template tags, and shortcodes that can be used through the editor.

The first step is to define the plug-in. Create a folder in /wp-content/plugins/ and name it whatever you like. I've named mine /jlc-projects/. Inside that folder, create a file of the same name (for example, jlc-projects.php) and add this code:

Plugin Name: Joe's Portfolio Plugin
Plugin URI:
Description: A simple plugin that creates and display a projects portfolio with WordPress using custom post types!
Author: Joe Casabona
Version: 1.0
Author URI:
define('JLC_PATH', WP_PLUGIN_URL . '/' . plugin_basename(dirname(__FILE__) ) . '/' );
define('JLC_NAME', "Joe's Portfolio Plugin");

There are a few things going on here. The first is the standard plug-in definition for a WordPress plug-in; the next few lines create constants and then include the files the rest of the plug-in needs. At this point, there is only one other file: jlc-project-cpt.php.

You will also notice that I'm using the prefix JLC_ (or jlc-) for everything. You should choose your own prefix to use. Prefixing variables and function names will decrease the chance of your plug-in conflicting with other themes or plug-ins.

The admin page to add a new project. Notice the Project Link section

The admin page to add a new project. Notice the Project Link section

Before we jump into jlc-project-cpt.php, I want to add one more bit of code to jlc-projects.php. The code below will create a new image size, which we will use with our Custom Post Type:

if ( function_exists( 'add_theme_support' ) ) {
  add_theme_support( 'post-thumbnails' );
  add_image_size('jlc_project', 1100, 640, true);

Now it's time to create jlc-project-cpt.php. I'll only be discussing the important code here, but you can find the complete code on the GitHub repo. First (after the opening <?php tag) we define the Custom Post Type:

add_action('init', 'jlc_projects_register');
function jlc_projects_register() {
  $args = array(
    'label' => __('Portfolio'),
    'singular_label' => __('Project'),
    'public' => true,
    'show_ui' => true,
    'capability_type' => 'post',
    'hierarchical' => true,
    'has_archive' => true,
    'supports' => array('title', 'editor', 'thumbnail') ,
    'rewrite' => array('slug' => 'portfolio', 'with_front' => false)
  register_post_type( 'portfolio' , $args );
  register_taxonomy("jlc-project-type", array("portfolio"), array("hierarchical" => true, "label" => "Project Type", "singular_label" => "Project Type", "rewrite" => true));

This is your standard Custom Post Types definition function. We add an action to call it on init , then send our list of arguments to register_post_type(), along with the type's slug, which will be 'portfolio'. After registering the post type, we register the custom taxonomy to go along with the post type. It's important to keep these two functions together. If you don't, and the taxonomy somehow gets registered first, WordPress will throw an error.

After the Custom Post Type is defined, it's time to add the custom metadata we want to use. Our Custom Post Type supports a title, the editor (which will serve as the body text), and a thumbnail, which is where the featured image will go. There is one more thing I like to add to my portfolio pieces: a URL to the website I'm showcasing. First, we'll create the function that will add this box in the admin:

add_action("admin_init", "jlc_projects_admin_init");
function jlc_projects_admin_init(){
  add_meta_box("jlc-projects-meta", __("Project Link"), "jlc_projects_options", "portfolio", "side", "low");
  function jlc_projects_options(){
  global $post;
  if ( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE ) return $post_id;
  $custom = get_post_custom($post->ID);
  $link = $custom["jlc_projects_link"][0];
  <input name="jlc_projects_link" placeholder="http://" value="<?php echo $link; ?>" />

These functions are fairly straightforward. When the admin is initiated (that is, loaded), we'll call a function called jlc_projects_admin_init() that will create a new meta box for portfolio items. In order to generate that box, a new function, jlc_projects_options(), is called.

Once inside the options function, we simply grab the link value, which we've called jlc_projects_link, and print it out. But first, we want to make sure an autosave isn't being performed. If it is, we will probably lose data. After that, we need to actually save the metadata when the post is saved:

add_action('save_post', 'jlc_projects_save');
function jlc_projects_save(){
  global $post;
  if ( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE ){
    return $post_id;
    update_post_meta($post->ID, "jlc_projects_link", $_POST["jlc_projects_link"]);

With the admin section for our Custom Post Types created, it's time to add some frontend functionality to help display our projects to visitors. This consists of an archive template, a single page template and a shortcode (not covered in this tutorial). But before we do that, there is one other function we're going to create for displaying images: picturefill.js.

This piece of JavaScript (you can find the GitHub repo here) allows you to define a set of media queries to switch an image to a version friendlier to the size of the screen it is being viewed on. This also has implications for load time, since you can probably assume that a smaller screen means a mobile device using 4G, 3G, or even EDGE. I know that isn't always the case, but it's better than nothing.

The HTML template used here, at full width. It's a simple header with three columns of projects

The HTML template used here, at full width. It's a simple header with three columns of projects

You can see the markup pattern for a standard picturefill element on the GitHub repo. We can have an unlimited number of <span> elements for each size of the image we have. There is also a fallback for users without JavaScript. As you can imagine, since WordPress creates multiple versions of every image we upload using the Media Uploader, it lends itself nicely to picturefill.js. The first thing we should do is load the script, which is located in the /js/ folder in our plug-in's directory. We add the following code to jlc-projects.php:

function jlc_projects_scripts() {
  wp_enqueue_script( 'picturefill', JLCP_PATH.'js/ picturefill.js', array());
add_action( 'wp_enqueue_scripts', 'jlc_projects_scripts' );

This will load our JavaScript with the scripts being loaded by other plug-ins. It will also ensure that we aren't loading picturefill.js more than once.

Since our projects will be using the featured image section to display the screenshot, we can replace the default featured image markup by using the post_thumbnail_html filter. Note that this function will actually replace all featured image sections on the site. If this could cause a conflict (it probably will), you should add some conditionals to your plug-in to make sure this filter is only being used on portfolio pages.

add_filter( 'post_thumbnail_html', 'jlc_projects_get_featured_image');
function jlc_projects_get_featured_image($html, $aid=false){
  $sizes= array(‚'thumbnail', 'medium', 'large', 'jlc_project', 'full');
    $img= '<span data-picture data-alt="'.get_the_title().'">';
    $ct= 0;
    $aid= (!$aid) ? get_post_thumbnail_id() : $aid;
    foreach($sizes as $size){
      $url= wp_get_attachment_image_src($aid, $size);
      $width= ($ct < sizeof($sizes)-1) ? ($url[1]*0.66) : ($width/0.66)+25;
      $img.= '
        <span data-src="'. $url[0] .'"';
      $img.= ($ct > 0) ? ' data-media="(min-width: '.$width .'px)"></span>' :'></span>';
    $url= wp_get_attachment_image_src( $aid, $sizes[1]);
  $img.= '<noscript>
    <img src="'.$url[0] .'" alt="'.get_the_title().'">
    return $img;

There are a few things going on here. The first is that the function has an array of all the image sizes in WordPress that we want to use. If you have your own sizes defined, you will have to add them here. This is so the picturefill element is accurately populated. After some set up (defining the image sizes, opening the picturefill element, initialising a counter), it moves through the $sizes, printing an image entry for each.

For each entry, wp_get_attachment_image_src() is called to grab the URL of the image based on the image's ID (which get_post_thumbnail_id() returns based on the post ID) and the size. wp_get_attachment_ image_src() returns an array that includes the image, the width, the height, and whether or not it's cropped. There is also a bit of maths going on here to calculate when to determine the breakpoints, as well as how to handle the thumbnail image. I'm not going to discuss this in detail here, but it's worth noting that this is an important problem to solve, and one for which solutions are evolving rapidly.

Now any time we get the post's thumbnail, the HTML returned will be from our function.

Creating the archive page

Next, we will create the archive template for the new Custom Post Type. This is what will be displayed by default and will serve as our main portfolio page.

A portion of the archive template displayed on a mobile-sized screen. The cards shrink to single column with centered images

A portion of the archive template displayed on a mobile-sized screen. The cards shrink to single column with centered images

(Note: we will not be creating the site's homepage in this tutorial, but doing so would require either a template tag or shortcode that will execute a custom Loop using WP_Query.)

Create a new file in whatever theme directory you are using and call it archive-portfolio.php. WordPress's template hierarchy is smart enough to know that, when a user is at the portfolio page, it should display the content using this template. My recommendation at this point is to copy the page.php template for this template. We will simply replace the Loop portion.

I recommend that you use a template without a sidebar, or a single-column template. The CSS referenced here will work a bit more nicely. Here's what our Loop looks like:

<?php while (have_posts()) : the_post(); ?>
  <div class="card">
    <?php the_post_thumbnail('jlc_project'); ?>
    <h3><?php the_title(); ?></h3>
    <p><?php echo get_the_excerpt(); ?> <a href="<?php the_permalink(); ?>">read more...</a></p>
<?php endwhile; ?>

This should be pretty straightforward. Because we are replacing the default HTML for the_post_thumbnail(), the argument of which image to use doesn't matter because all sizes will be returned using picturefill.js markup. I opted to use get_the_excerpt() in order to exclude any markup included by the_excerpt().

When designing a plug-in that includes some CSS, it's important to make it as minimal as possible so that it doesn't butt heads with the theme's CSS or give the user the ability to exclude your CSS completely. Since we're creating templates within the theme, we have a little more wiggle room. Here's a portion of the (Sass-generated) CSS that I've added to each project on the archive page:

.card img {
  display: block;
  margin: 0 auto; }
@media screen and (min-width: 45.88em) {
  .card {
    display: inline-block;
    width: 40%; } }
@media screen and (min-width: 54.62em) {
  .card {
    width: 44%; } }
@media screen and (min-width: 76.38em) {
  .card {
    width: 29%; } }
@media screen and (min-width: 99.4em) {
  .card {
    width: 30%; }

I've determined which breakpoints were best for placing the project cards side by side. I've also made the featured images centre automatically.

Creating the single page

Now we'll create the single template for portfolio projects. Whenever a user visits a single project's page, this is what will display. Create a new file in your theme, call it single-portfolio.php and copy another template to paste in there (I'd recommend whatever you used for archive-portfolio.php). This time we will be replacing the Loop with this code:

<?php while (have_posts()) : the_post(); ?>
  <h2><?php the_title(); ?></h2>
    <?php the_post_thumbnail('jlc_project'); ?>
    <?php the_content('Read the rest of this entry'); ?>
        $jlc_link= jlc_projects_get_link($post->ID);
        <a href="<?php print $jlc_link; ?>"class="button">Visit the Site</a>
      } ?>
<?php endwhile; ?>

This looks similar to the archive template, but we do call an extra function here: jlc_projects_get_link(). We will add this to our plug-in, and it will return the URL for the current project.

The single project view. The project reduces in width as the page grows to keep the text easy to read

The single project view. The project reduces in width as the page grows to keep the text easy to read

In the event there is no URL, false should be returned and no button is displayed. Here's what the function (located in jlc-projects.php) looks like:

function jlc_projects_get_link($id){
$url= get_post_custom_values('jlc_projects_link', $pid);
return ($url[0] != '') ? $url[0] : false;

The CSS for this page will depend largely on the theme: I've used some CSS to generate a nice button. Make sure that whatever CSS you create yourself does not interfere with the main theme.

In conclusion

By now, we've created a plug-in to add a new Custom Post Type for portfolios, integrated picturefill.js to handle images better, and created two theme templates to display the information.

The GitHub repo for the tutorial contains all of the code shown here, plus the theme I used, a shortcode and a template tag.

Words: Joe Casabona

This article originally appeared in net magazine issue 254.

Thank you for reading 5 articles this month* Join now for unlimited access

Enjoy your first month for just £1 / $1 / €1

*Read 5 free articles per month without a subscription

Join now for unlimited access

Try first month for just £1 / $1 / €1

The Creative Bloq team is made up of a group of design fans, and has changed and evolved since Creative Bloq began back in 2012. The current website team consists of eight full-time members of staff: Editor Georgia Coggan, Deputy Editor Rosie Hilder, Deals Editor Beren Neale, Senior News Editor Daniel Piper, Digital Arts and Design Editor Ian Dean, Tech Reviews Editor Erlingur Einarsson and Ecommerce Writer Beth Nicholls and Staff Writer Natalie Fear, as well as a roster of freelancers from around the world. The 3D World and ImagineFX magazine teams also pitch in, ensuring that content from 3D World and ImagineFX is represented on Creative Bloq.