Note: this project journal is not complete. I worked several hours a day for about 2 days in a row then school hit hard and now I’m caught up in work. Portfolio entries are not styled, nor are they linking to any valuable locations at the moment. This will come in time.
My site is finally up, I chose a theme that I liked, installed some plugins, spent a good hour configuring everything, even got to play with the CSS (the search bar in Mantra has some issues by default – not to mention things look ugly if the screen is wider than 1600px). The technical details of how I set up my site will all be available at the About This Site page (soon to come), so I won’t waste time on that here.
Instead, let’s discuss my first project for the new site. I aim to use this site as a central hub for all my programming endeavors. This includes personal projects, hair-brained experiments doomed to failure, and professional examples of my work experience. Of course WordPress isn’t exactly suited to doing more than one thing at a time. It likes to be a blog… and only a blog. But I chose WordPress because it CAN do more than one thing at a time, even if that’s not what it’s meant for.
Entry 1 - The project goal
For my portfolio, I have a couple problems I’ll need to overcome. I’ll update this post as I work in order to keep a running journal of my progress. First, I have three possible “types” of portfolio entries – web applications I’ve developed, desktop applications I’ve developed, and web sites I’ve improved. Note the change of verb on that last one – I’m not an artist, and I don’t design websites. I can, however, take an existing website and re-code it to fix graphical bugs, align content, improve performance, increase usability, and all that fun jazz. Each of these three portfolio types will have a different optimal method for showing it off:
- Web applications: The best way to show off these is with a demo. Click this link, see the application in aciton
- Desktop applications: This is more difficult. Perhaps let them download the application to try it out, but this will cause tin-foil connoisseurs a miniature heart attack. Screenshots are another option, but this may not get across the true usability or utility. For now I think the solution is to say “coming soon” since I have way too many other features of this website to worry about.
- Web sites: The best way to show this off would be a before and after example. Of course a lot of what I do when I improve a website isn’t immediately visible from two screenshots – I’m not an artist, after all. Rather what I do converts a business website from 1.29% text-to-HTML ratio to 27.07%! How can you truly understand the difference this makes in performance from a screenshot? I have a creative solution to this – a custom before/after application I plan to code.
So each type of portfolio entry is going to be viewed entirely differently. One will launch a web application, completely separate from my WordPress site. One will open a static web application for all web sites, displaying the appropriate before and after pages. One we don’t even know what it does! Our first order of business will be getting WordPress to distinguish between these different types of entries and respond accordingly.
The next problem I have is actually displaying the portfolio in a way that’s easy for me to update. I could manually append new projects to a plain HTML page, however that’s unnecessarily tedious and not why I chose to use a Content Management System. Rather I want to post a new portfolio piece as easily as (or more easily than) I post a new blog post. Thinking of solution to this I’m reminded of CouponPress – a premium theme which replaces WordPress posts with “coupons” and categories with “merchants”. Similar code could likely replace posts with “projects” and divide my three types into different categories.
Finally, I want this portfolio to be the primary focus of my site. It needs to be front and center, not this silly little blog. This will take two steps. The first is easy – adjust the WordPress settings to set the home page to “a static page”, and use the portfolio page. Then put a link to the blog where the current link to the portfolio is. The second step is putting recent portfolio updates at the top of my sidebar instead of recent blog posts. This will require a custom widget.
I’m closing this post for now, as I begin my quest with the search for custom post types. If I can create a “portfolio project” post type then I believe the rest will fall into place. If I can do it as a plugin rather than as part of my theme then I won’t have to modify any Mantra core files, which will be nice if the Mantra theme is ever updated.
Entry 2 - Planning phase
I may be in luck! It turns out that as of WordPress 3 there’s actually a post_type attached to all posts, and custom post types are easy to add. Back when I worked with CouponPress WordPress was only on version 2.2, so I guess my procrastination has paid off again!
Yet more luck, possibly, it turns out that WordPress will not display custom post_types in the loop unless you specifically request them. This means that if I create a “portfolio project” post type it likely won’t get mixed in with my blog posts (unless Mantra does something crazy)
I have experience making a WordPress plugin for a client once, however I don’t work with WordPress very often and my experience was limited. I will pull up the files I created for them to get an understanding for what I did and, more importantly, how I did it. Ultimately I want to create a plugin with the following:
- Create a custom “portfolio project” post_type
- Ensure that portfolio projects fit one and only one category of: web site, web app, desktop app
- List all portfolio projects on a specified page (“Portfolio”) using display code unique to each category
- List new portfolio updates in the sidebar with a widget
- Edit details for how it’s displayed in the admin panel
- Adjust the display for mobile devices
Shouldn’t be too hard… right? For now it’s bed time. I’ll tackle this in the morning.
Entry 3 - Framework
I pulled up an old WordPress plugin that I made for a client and it looks like I put a lot of effort into following every best practice with unbelievable detail. I create a single global instance of the plugin class, encapsulating all functions within that class to prevent global namespace clashes. I have functions for not only handling data input and validation, but functions which let you use pseudo-code to display precisely how it will appear prior to hitting “Save” (using jQuery). I even had i18n support!
The plugin itself was rather complicated, as well, having about 20 settings and having to write a custom Walker for menus.
Since the plugin I’m making now is going to be specifically used on my own website and I can personally guarantee no namespace clashes, I think I’ll take the quicker but sloppier method of globally declaring everything. I also don’t feel like anyone will look down on me for this since it’s the same strategy used by Hotfix, W3 Total Cache, Hello Dolly, and many other very popular plugins. In fact, after browsing through about 30 plugins the ONLY one that used the same OOP practices I did was WP Better Security. Good on ya, mate
So let’s get started with our plugin. We’ll begin like any other:
|
1 2 3 4 5 6 7 8 9 10 |
<?php /* Plugin Name: Portfolio Entries Plugin URI: http://stevendesu.com/about/ Description: A custom plugin I made for displaying my own portfolio on my WordPress site. Version: 0.0.1 Author: stevendesu.com Author URI: http://stevendesu.com License: GPL2 */ |
Since most graphical user interfaces provide a beautiful function for precisely moving the insertion pointer, common referred to as clicking, I can start inserting code and code examples and modify as necessary afterwards. For now I have borrowed snippets for basic plugin structure, custom post types, custom widget creation, and adding administration menus.
The code is already a bit wordy, but here’s what I’ve got for now:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
/*********************** * Add Custom Post Type * ***********************/ add_action( 'init', 'create_portfolio_entry_post_type' ); function create_portfolio_entry_post_type() { register_post_type( 'portfolio_entry', array( 'labels' => array( 'name' => __( 'Portfolio Entry' ), 'singular_name' => __( 'Portfolio Entries' ) ), 'public' => true, 'has_archive' => true ) ); } /******************** * Add Custom Widget * ********************/ add_action( 'widgets_init', 'register_portfolio_entries_widget' ); function register_portfolio_entries_widget() { register_widget( 'Portfolio_Widget' ); } class Portfolio_Widget extends WP_Widget { function Portfolio_Widget() { $widget_ops = array( 'classname' => 'example', 'description' => __('A widget that displays the authors name ', 'example') ); $control_ops = array( 'width' => 300, 'height' => 350, 'id_base' => 'example-widget' ); $this->WP_Widget( 'example-widget', __('Example Widget', 'example'), $widget_ops, $control_ops ); } function widget( $args, $instance ) { extract( $args ); //Our variables from the widget settings. $title = apply_filters('widget_title', $instance['title'] ); $name = $instance['name']; $show_info = isset( $instance['show_info'] ) ? $instance['show_info'] : false; echo $before_widget; // Display the widget title if ( $title ) echo $before_title . $title . $after_title; //Display the name if ( $name ) printf( '<p>' . __('Hey their Sailor! My name is %1$s.', 'example') . '</p>', $name ); if ( $show_info ) printf( $name ); echo $after_widget; } function update( $new_instance, $old_instance ) { $instance = $old_instance; //Strip tags from title and name to remove HTML $instance['title'] = strip_tags( $new_instance['title'] ); $instance['name'] = strip_tags( $new_instance['name'] ); $instance['show_info'] = $new_instance['show_info']; return $instance; } function form( $instance ) { //Set up some default widget settings. $defaults = array( 'title' => __('Example', 'example'), 'name' => __('Bilal Shaheen', 'example'), 'show_info' => true ); $instance = wp_parse_args( (array) $instance, $defaults ); ?> <p> <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e('Title:', 'example'); ?></label> <input id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" value="<?php echo $instance['title']; ?>" style="width:100%;" /> </p> <p> <label for="<?php echo $this->get_field_id( 'name' ); ?>"><?php _e('Your Name:', 'example'); ?></label> <input id="<?php echo $this->get_field_id( 'name' ); ?>" name="<?php echo $this->get_field_name( 'name' ); ?>" value="<?php echo $instance['name']; ?>" style="width:100%;" /> </p> <p> <input class="checkbox" type="checkbox" <?php checked( $instance['show_info'], true ); ?> id="<?php echo $this->get_field_id( 'show_info' ); ?>" name="<?php echo $this->get_field_name( 'show_info' ); ?>" /> <label for="<?php echo $this->get_field_id( 'show_info' ); ?>"><?php _e('Display info publicly?', 'example'); ?></label> </p> <?php } } /***************** * Add Admin Menu * *****************/ /** Step 2. */ add_action( 'admin_menu', 'portfolio_entries_menu' ); /** Step 1. */ function portfolio_entries_menu() { add_options_page( 'Portfolio Entries Options', 'Portfolio Entries', 'manage_options', 'portfolio-entries-identifier', 'portfolio_entries_options' ); } /** Step 3. */ function portfolio_entries_options() { if ( !current_user_can( 'manage_options' ) ) { wp_die( __( 'You do not have sufficient permissions to access this page.' ) ); } echo '<div class="wrap">'; echo '<p>Here is where the form would go if I actually had options.</p>'; echo '</div>'; } |
Although I don’t have any custom code yet, everything works thus far:
Entry 4 - Validating posts
I’ll focus on one of these sections at a time. First, let’s get the custom post type functioning properly. Obviously a portfolio entry is not going to look just like a blog post. I need to enforce the selection of a single type at the very least. Ideally I would also require an image of the project. In the case of web sites I may want two images.
Making a taxonomy required turned out to be significantly more complicated than I had hoped. I can always check to see if a portfolio entry is being saved and, if so, make sure they entered one and only one taxonomy and, if not, redirect them to the edit form with an error… I just figured it would be less… hacky
So far here’s the code I’m looking at for the custom post types section:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
/*********************** * Add Custom Post Type * ***********************/ add_action( 'init', 'create_portfolio_entry_post_type' ); add_theme_support('post-thumbnails'); add_action( 'init', 'guarantee_portfolio_entry_taxonomies' ); function create_portfolio_entry_post_type() { register_taxonomy( 'portfolio_entry_type', 'portfolio_entry', array( 'labels' => array( 'name' => __( 'Portfolio Types' ), 'singular_name' => __( 'Portfolio Type' ) ), 'hierarchical' => false, 'rewrite' => array( 'slug' => 'portfolio-type' ), ) ); register_post_type( 'portfolio_entry', array( 'labels' => array( 'name' => __( 'Portfolio Entries' ), 'singular_name' => __( 'Portfolio Entry' ) ), 'public' => true, 'has_archive' => true, 'menu_position' => 5, 'menu_icon' => plugins_url( 'images/menu_icon.png' , __FILE__ ), 'supports' => array( 'title', 'editor', 'thumbnail' ), 'rewrite' => array( 'slug' => 'portfolio' ), ) ); } function guarantee_portfolio_entry_taxonomies() { if( $_POST['action'] == 'editpost' && $_POST['post_type'] == 'portfolio_entry' ) { // Does not handle $_POST['action'] == 'autosave' $types = explode( ',', $_POST['tax_input']['portfolio_entry_type'] ); if( count( $types ) != 1 ) { $_POST['action'] = 'edit'; add_action( 'admin_notices', 'portfolio_entry_taxonomy_error' ); } } } function portfolio_entry_taxonomy_error() { echo '<div class="error"><p><strong>You must select one, and only one, portfolio entry type.</strong></p></div>'; } |
As you’ll notice from the comment, this is not preventing auto-saving of an entry with multiple types. I tried simply amending the if() statement to say something along the lines of “if( action = editpost OR action = autosave)”, but to no avail. It turns out init isn’t being called by autosave.
This time the internet wasn’t so helpful. You can disable autosave, change the autosave interval, and delete post revisions… But how do you run a check at autosave time?
There are a few solutions that came to mind immediately:
- Edit wp-admin/admin-ajax.php
- Read through wp-admin/admin-ajax.php to see what hooks it does call
- Edit wp-includes/js/autosave.js
- Create a custom version of wp-includes/js/autosave.js
Obviously options 1 and 3 are right out since I never want to edit WP core files. Since I don’t want to constantly update my plugin should WordPress update autosave.js, I’ll begin with option 2.
Only a few seconds in and I may have gotten lucky. Line 41: do_action( ‘admin_init’ );
I updated my plugin to work on admin_init instead of init and was sourly disappointed when it still saved. This time I took a look at the POST headers that Chrome sent with the AJAX request. Not a single mention of taxonomies. No wonder it saved, the WordPress autosave feature doesn’t save taxonomies!
I puzzled over this for a minute wondering if it would cause me any trouble, but I’m fairly certain a portfolio entry won’t even be visible until I’ve saved it manually (setting status to Published). Therefore I gave up on my silly autosave quest.
Entry 5 - Admin menus
The post type is looking good and until I start displaying it I won’t really know what features need to be modified or fixed, so let’s get it displaying!
I want the list of portfolio entries to display on the portfolio page. I heavily modified a plugin for a client once named Popshops, in which there was a setting to specify which page you want it to display on. If you visit that page the plugin hijacked the request and filled the page with custom content. The page itself was technically blank.
This is the method I’m going to pursue. There may be a more streamlined method (like a secondary blog listing for a particular post type without having to waste a blank page) but I’ll opt for the method that works rather than hours of research to look for something that may or may not exist.
For now I borrowed the logic from Popshop’s plugin, removing large chunks of unnecessary code and using options instead of a custom database table. The code looks like so:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/********************* * Add Portfolio Page * *********************/ add_option( 'portfolio-entry-page' ); add_filter( 'the_content', 'portfolio_entries_page' ); function portfolio_entries_page( $content = '' ) { global $post; $portfolio_page = get_option( 'portfolio-entry-page' ); if( $post->ID == $portfolio_page && $post->post_type == 'page' ) { ob_start(); echo 'Got here, at least'; $content = ob_get_clean(); } return $content; } |
In theory we’ll see the text “Got here, at least” when we visit my portfolio page. That is, once portfolio-entry-page is set properly. It’s now time to take a look at the admin menu.
Doing just a minute amount of research, I’m pleased to see a lot has changed since WordPress 2.2
No longer must I manually create the form, process everything, handle everything… It seems like using a very standard template and a couple of functions I can get the job done with no hair pulling.
Well, scratch that. Turns out the page is “in transition” (and has been for 3 years) and it’s quite confusing to read. They say one thing then immediately contradict it. For instance, according to the WordPress codex:
After the settings_fields() call, add this function
1 do_settings_fields( 'myoption-group' );This function replaces the form-field markup in the form itself.
However do_settings_fields requires two parameters.
Obviously the instructions in the codex weren’t perfect, so I had to take them with a grain of salt and do some shotgun debugging. Ultimately I had a working solution, though:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
/***************** * Add Admin Menu * *****************/ add_action( 'admin_menu', 'portfolio_entries_menu' ); function portfolio_entries_menu() { add_options_page( 'Portfolio Entries Options', 'Portfolio Entries', 'manage_options', 'portfolio-entries-identifier', 'portfolio_entries_options' ); add_action( 'admin_init', 'register_portfolio_entries_settings' ); } function portfolio_entries_options() { if ( !current_user_can( 'manage_options' ) ) { wp_die( __( 'You do not have sufficient permissions to access this page.' ) ); } ?> <div class="wrap"> <?php screen_icon(); ?> <h2>Portfolio Entries</h2> <form method="post" action="options.php"> <?php settings_fields( 'portfolio-entries-options' ); do_settings_fields( 'portfolio-entries-identifier', 'portfolio-entries-setting-section' ); ?> <table class="form-table"> <tr valign="top"> <th scope="row">Portfolio Page</th> <td> <select name="portfolio-entry-page"> <?php $pages = get_pages(); $portfolio_page = get_option( 'portfolio-entry-page' ); foreach( $pages as $page ) { $selected = ''; if( $page->ID == $portfolio_page ) { $selected = ' selected="selected"'; } echo '<option value="' . $page->ID . '"' . $selected . '>' . $page->post_title . '</option>'; } ?> </select> </td> </tr> </table> <?php submit_button(); ?> </form> </div> <?php } function register_portfolio_entries_settings() { register_setting( 'portfolio-entries-options', 'portfolio-entry-page' ); add_settings_section( 'portfolio-entries-setting-section', 'Setting', 'print_portfolio_entries_section', 'portfolio-entries-identifier' ); } function print_portfolio_entries_section() { print 'Enter your setting below:'; } |
Interestingly enough, print_portfolio_entries_section was never called. Huh. The only reason I included the function is because add_settings_section requires a callback. I can set the portfolio page, though, so I’m happy.
Loading up my Portfolio page, it looks like everything worked!
Entry 6 - Listing posts
Let’s step back and look at the original goals for this project to see how I’m doing:
- Check: Create a custom “portfolio project” post_type
- Check: Ensure that portfolio projects fit one and only one category of: web site, web app, desktop app
- List all portfolio projects on a specified page (“Portfolio”) using display code unique to each category
- List new portfolio updates in the sidebar with a widget
- Check: Edit details for how it’s displayed in the admin panel
- Adjust the display for mobile devices
So now we just have to list the portfolio entries then adjust how they display. Simple enough. To begin, I created 3 sample portfolio entries – one of each type. I’ll start by seeing if I can list these on the Portfolio page and in a sidebar widget. Markup will come later.
Impressively this one worked right first try.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
/********************* * Add Portfolio Page * *********************/ add_option( 'portfolio-entry-page' ); add_filter( 'the_content', 'portfolio_entries_page' ); function portfolio_entries_page( $content = '' ) { global $post; $portfolio_page = get_option( 'portfolio-entry-page' ); if( $post->ID == $portfolio_page && $post->post_type == 'page' ) { ob_start(); $posts = new WP_Query( array( 'posts_per_page' => 10, 'post_type' => 'portfolio_entry' )); while ( $posts->have_posts() ) { $posts->the_post(); ?> <div> <h2><?php the_title(); ?></h2> <?php the_content(); ?> </div> <?php } $content = ob_get_clean(); } return $content; } /******************** * Add Custom Widget * ********************/ add_action( 'widgets_init', 'register_portfolio_entries_widget' ); function register_portfolio_entries_widget() { register_widget( 'Portfolio_Widget' ); } class Portfolio_Widget extends WP_Widget { function Portfolio_Widget() { $widget_ops = array( 'classname' => 'portfolio-widget', 'description' => __('The most recent portfolio entries', 'portfolio-widget') ); $control_ops = array( 'width' => 300, 'height' => 350, 'id_base' => 'portfolio-widget' ); $this->WP_Widget( 'portfolio-widget', __('Portfolio', 'portfolio-widget'), $widget_ops, $control_ops ); } function widget( $args, $instance ) { extract( $args ); //Our variables from the widget settings. $title = apply_filters('widget_title', $instance['title'] ); $posts_per_page = $instance['posts_per_page']; $show_date = isset( $instance['show_date'] ) ? $instance['show_date'] : false; echo $before_widget; // Display the widget title if ( $title ) echo $before_title . $title . $after_title; //Display the portfolio entries echo "<ul>\n"; $posts = new WP_Query( array( 'posts_per_page' => $posts_per_page, 'post_type' => 'portfolio_entry' )); while ( $posts->have_posts() ) { $posts->the_post(); ?> <li> <a href="<?php the_permalink(); ?>" rel="bookmark" title="Permanent link to <?php the_title_attribute(); ?>"><?php the_title(); ?></a> <?php if ( $show_date ) : ?> <span><?php echo "("; ?><?php echo get_the_date(); ?><?php echo ")"; ?></span> <?php endif; ?> </li> <?php } wp_reset_query(); echo "</ul>\n"; echo $after_widget; } function update( $new_instance, $old_instance ) { $instance = $old_instance; //Strip tags from title and name to remove HTML $instance['title'] = strip_tags( $new_instance['title'] ); $instance['posts_per_page'] = strip_tags( $new_instance['posts_per_page'] ); $instance['show_date'] = $new_instance['show_date']; return $instance; } function form( $instance ) { //Set up some default widget settings. $defaults = array( 'title' => __('Portfolio', 'portfolio-widget'), 'posts_per_page' => 5, 'show_date' => false ); $instance = wp_parse_args( (array) $instance, $defaults ); ?> <p> <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e('Title:', 'portfolio-widget'); ?></label> <input id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" value="<?php echo $instance['title']; ?>" style="width:100%;" /> </p> <p> <label for="<?php echo $this->get_field_id( 'posts_per_page' ); ?>"><?php _e('Number of posts to show:', 'portfolio-widget'); ?></label> <input id="<?php echo $this->get_field_id( 'posts_per_page' ); ?>" name="<?php echo $this->get_field_name( 'posts_per_page' ); ?>" value="<?php echo $instance['posts_per_page']; ?>" size="3" /> </p> <p> <input class="checkbox" type="checkbox" <?php if( $instance['show_date'] == true ){ echo 'checked="checked"'; } ?> id="<?php echo $this->get_field_id( 'show_date' ); ?>" name="<?php echo $this->get_field_name( 'show_date' ); ?>" /> <label for="<?php echo $this->get_field_id( 'show_date' ); ?>"><?php _e('Display post date?', 'portfolio-widget'); ?></label> </p> <?php } } |
Entry 7 - The markup
We’re in the home stretch. Now I just need to style these ugly guys. It’s getting late, though, so I’ll finish this up later.
Wrap-up
It’s been a long time since I’ve worked with WordPress, and I found that quite a lot has changed since then. This project gave me a good excuse to research all the wonderful new ways that WordPress has been updated to make life easier, or more difficult, for developers.
Upon learning about custom post types I realized almost immediately that, if set up correctly, my entire plugin may become invalid. I’m certain I can set up an archive widget to display only posts that match my type, and somehow I can create a blog index which only displays my post type. A small tweak to the theme could then change how these posts were displayed and I’d be set. But what’s the fun in that? Through this project I got hands-on experience making not only custom post types, but custom widgets and admin menu pages. I also got to play with over-riding default page functionality using plugins and validating posts in the editor.
Even if my plugin is unnecessary, the lessons I got to learn will prove useful.
