My Content-Powered Projects Page

As alluded to earlier, I’ve cleaned up my projects page which is now powered by a new taxonomy, custom post type and the content I’ve generated here over the last decade plus. Turns out, I’ve generated quite a few projects. 🙂

projects-page-crop

I’m not completely done creating and managing the content, but I’m pretty happy with the improvement. I’ve tried to group them in ways that are useful – especially calling out projects that are no longer active1.

You can see a comparison of the old and new pages in this Flickr set.

So what did I do and how did I do it?

Note: This was written against a pre-release version of WordPress 3.6

1. Create a Projects Taxonomy (and Post Type)

I created two new taxonomies to power the Projects page.

  1. Projects: the names of my various projects.
  2. Project Groups: to group my projects into buckets.


<?php
function akv3_register_projects_tax() {
register_taxonomy(
'projects',
array('post'),
array(
'hierarchical' => true,
'labels' => array(
'name' => __('Projects', 'projects'),
'singular_name' => __('Project', 'projects'),
'search_items' => __('Search Projects', 'projects'),
'popular_items' => __('Popular Projects', 'projects'),
'all_items' => __('All Projects', 'projects'),
'parent_item' => __('Parent Project', 'projects'),
'parent_item_colon' => __('Parent Project:', 'projects'),
'edit_item' => __('Edit Project', 'projects'),
'update_item' => __('Update Project', 'projects'),
'add_new_item' => __('Add New Project', 'projects'),
'new_item_name' => __('New Project Name', 'projects'),
),
'sort' => true,
'args' => array('orderby' => 'term_order'),
'public' => false,
'show_ui' => true,
)
);
register_taxonomy(
'project-groups',
array('project'),
array(
'hierarchical' => true,
'labels' => array(
'name' => __('Project Groups', 'projects'),
'singular_name' => __('Project Group', 'projects'),
'search_items' => __('Search Project Groups', 'projects'),
'popular_items' => __('Popular Project Groups', 'projects'),
'all_items' => __('All Project Groups', 'projects'),
'parent_item' => __('Parent Project Group', 'projects'),
'parent_item_colon' => __('Parent Project Group:', 'projects'),
'edit_item' => __('Edit Project Group', 'projects'),
'update_item' => __('Update Project Group', 'projects'),
'add_new_item' => __('Add New Project Group', 'projects'),
'new_item_name' => __('New Project Group Name', 'projects'),
),
'sort' => true,
'args' => array('orderby' => 'term_order'),
'public' => false,
'show_ui' => true,
)
);
}
add_action('init', 'akv3_register_projects_tax', 999);

I then used the Crowd Favorite taxonomy-to-post binding plugin plugin to create a Project post type that is effectively a child of the Projects taxonomy. This way I will be able to add content, terms, featured images, etc. to my projects (in a very forward compatible way).


<?php
// Create Project post type (bound to Projects taxonomy) to save meta
function akv3_projects_tax_bindings($configs) {
$configs[] = array(
'taxonomy' => 'projects',
'post_type' => array(
'project',
array(
'public' => true,
'show_ui' => true,
'label' => 'Projects',
'rewrite' => array(
'slug' => 'project',
'with_front' => false,
'feeds' => false,
'pages' => false
),
'supports' => array(
'title',
'editor',
'excerpt',
'thumbnail',
'revisions',
'custom-fields',
)
)
),
'slave_title_editable' => false,
'slave_slug_editable' => false,
);
return $configs;
}
add_filter('cftpb_configs', 'akv3_projects_tax_bindings');

2. Convert existing Categories and Threads to Projects

In my existing content structures I had both categories and threads that were dedicated to specific projects. I needed to convert those relationships to the new projects taxonomy I’d created.

Changing the categories was straightforward, but there was a little more work for the threads because I needed to convert the related post types as well. I did separate SQL queries to get the various category and thread ids I needed to convert.


<?php
function akv3_convert_to_projects() {
global $wpdb;
// convert categories
$wpdb->query("
UPDATE wp_term_taxonomy
SET taxonomy = 'projects',
parent = 0
WHERE term_id IN (13, 14, 15, 20, 22, 52, 53, 58, 59)
");
// convert threads
$wpdb->query("
UPDATE wp_term_taxonomy
SET taxonomy = 'projects',
parent = 0
WHERE term_id IN (400, 417, 438, 455)
");
$wpdb->query("
UPDATE wp_posts
SET post_type = 'project'
WHERE ID IN (15866, 15874, 15876, 15878)
");
$wpdb->query("
UPDATE wp_postmeta
SET meta_key = '_cf-tax-post-binding_projects'
WHERE meta_key = '_cf-tax-post-binding_threads'
AND post_id IN (15866, 15874, 15876, 15878)
");
}

Once everything was converted, I then ran a little code to save each term via the WordPress API so that the associated project post types content would be created (via the tax-post-binding library).


<?php
function akv3_touch_projects_terms() {
$projects = get_terms('projects');
foreach ($projects as $project) {
wp_update_term($project->term_id, 'projects', array('slug' => $project->slug));
}
}

The existing terms are now fully converted to projects.

3. Create New Projects (with meta data)

Now that I had converted the existing categories and threads to projects, it was time to create the rest of the projects. I ended up creating a big array with lots of meta data so that I could run and test this locally, then replay it all on my live site.

This code creates the new project groups taxonomy terms, the new project terms, and adds meta data to the associated project post types.


<?php
function akv3_create_projects_terms() {
$terms = array(
'commercial' => 'Commercial',
'deprecated' => 'Deprecated',
'featured' => 'Featured',
'open-source' => 'Open Source',
'wordpress' => 'WordPress',
);
foreach ($terms as $slug => $name) {
wp_insert_term($name, 'project-groups', array(
'slug' => $slug
));
}
$terms = array(
'threads' => array(
'label' => 'Threads',
'meta' => array(
'proj_github_url' => 'https://github.com/crowdfavorite/wp-threads/',
'proj_wp_url' => 'http://wordpress.org/plugins/threads/',
),
'groups' => array(
'featured',
'wordpress',
),
),
'carrington-core' => array(
'label' => 'Carrington Core',
'meta' => array(
'proj_url' => 'http://crowdfavorite.com/wordpress/carrington-core/',
'proj_github_url' => 'https://github.com/crowdfavorite/wp-carrington-core/',
),
'groups' => array(
'featured',
'wordpress',
),
),
'carrington-build' => array(
'label' => 'Carrington Build',
'meta' => array(
'proj_url' => 'http://crowdfavorite.com/wordpress/carrington-build/',
),
'groups' => array(
'featured',
'wordpress',
),
),
'sharethis' => array(
'label' => 'ShareThis',
'meta' => array(
'proj_url' => 'http://sharethis.com',
'proj_wp_url' => 'http://wordpress.org/plugins/share-this/',
),
'groups' => array(
'deprecated',
),
),
// lots more here – you get the idea
);
foreach ($terms as $slug => $data) {
$term = wp_insert_term($data['label'], 'projects', array(
'slug' => $slug
));
$post = cftpb_get_post($term['term_id'], 'projects');
// meta for project page
if (!empty($data['meta'])) {
foreach ($data['meta'] as $k => $v) {
add_post_meta($post->ID, $k, $v);
}
}
// groups for project page
if (!empty($data['groups'])) {
foreach ($data['groups'] as $slug) {
// note, this is a WP 3.6 function
wp_add_object_terms($post->ID, $slug, 'project-groups');
}
}
}
}

I truncated that quite a bit as I inserted a lot of projects.

4. Assign Existing Posts to Projects

At this point we have all of our project terms and post types created, it’s time to use them.

Finding the posts I wanted to put into each project wasn’t an exact science. I basically created a projects list, then did searches on my site to find the posts relevant to that project. Then I manually selected the post IDs for those posts, and put them into a big array.


<?php
function akv3_apply_projects_terms() {
$project_posts = array();
$project_posts['threads'] = array(16176);
$project_posts['carrington-build'] = array(16291, 13738, 5657, 5608, 4956, 4547, 4556, 4509);
$project_posts['carrington-core'] = array(12784, 14179, 13830, 13730, 13602, 12784, 5168, 5058, 4507, 4343, 3968, 3576, 3566, 3549, 3536, 3512, 3454, 3424, 3391, 3314, 3315, 3303, 3294, 3290, 3283, 3260, 3233, 3228, 3219, 3208);
$project_posts['sharethis'] = array(3133, 3012, 2979, 2948, 2603, 2704, 2681, 2627, 2600, 2620, 2595, 2568, 2581, 2577, 2535, 2511, 2032, 2484);
// lots more here, truncated for sanity
foreach ($project_posts as $project => $post_ids) {
foreach ($post_ids as $post_id) {
wp_set_object_terms($post_id, $project, 'projects', true);
wp_set_object_terms($post_id, 'projects', 'category', true);
}
}
}

I didn’t select the posts programatically because there’s a bit more nuance to choosing what is and isn’t applicable to a project than just a keyword search.

As part of that process, I also added a “Projects” category to all of these posts. I use this to show links to the latest posts across all projects on the new page. I needed to add this category to all of the converted categories and threads too.2


<?php
function akv3_add_projects_cat() {
// find all posts in converted categories and threads
$term_ids = array(400, 417, 438, 455, 13, 14, 15, 20, 22, 52, 53, 58, 59);
$query = new WP_Query(array(
'posts_per_page' => -1,
'tax_query' => array(
array(
'taxonomy' => 'projects',
'field' => 'id',
'terms' => $term_ids,
'operator' => 'IN'
)
)
));
// add "projects" cat to those posts
foreach ($query->posts as $post) {
wp_set_object_terms($post->ID, 'projects', 'category', true);
}
}

5. Expose the New Data to Create the Projects Page

With all of my data set up, the last step was to expose it to power the projects page. I decided to do this by creating a shortcode that would output the projects by group.


<?php
function akv3_project_group_shortcode($atts) {
extract(shortcode_atts(array(
'group' => null,
'description' => null,
), $atts));
if (empty($group)) {
return '';
}
$term = get_term_by('slug', $group, 'project-groups');
?>
<h2 class="widget-title clearfix"><?php echo esc_html($term->name); ?></h2>
<div class="project-group clearfix">
<?php
if (!empty($description)) {
echo wpautop(wptexturize($description));
}
$args = array(
'post_type' => 'project',
'posts_per_page' => -1,
'orderby' => 'title',
'order' => 'ASC',
'tax_query' => array(
array(
'taxonomy' => 'project-groups',
'field' => 'slug',
'terms' => $group,
)
)
);
if ($group != 'featured') {
$args['tax_query'][] = array(
'taxonomy' => 'project-groups',
'field' => 'slug',
'terms' => 'featured',
'operator' => 'NOT IN'
);
}
$query = new WP_Query($args);
while ($query->have_posts()) {
$query->the_post();
// AKV#_PATH is defined in my child theme
include(AKV3_PATH.'misc/project-summary.php');
}
?>
</div>
<?php
wp_reset_postdata();
}
add_shortcode('project-group', 'akv3_project_group_shortcode');

This has a bit too much of a mix of template and presentation for my liking, but it works and refactoring it is pretty low on my list at this point. Next time I mess with that code, I’ll probably clean it up a bit.

6. Project Page

The final step in the process was to customize the output of the individual project page. I am outputting the description, related links and featured image from the bound post type, and I converted some of my Threads plugin code to show a timeline of posts in “newest first” order. I also added a “this post is part of a project” banner to the individual posts, again borrowing the code from the Threads plugin.

Here’s an example.

Conclusion

I feel like the new projects page is a better representation of my projects, and will be more meaningful and maintainable in the future than the old static page was. It will also reward me for blogging more frequently by being more up to date as a result.

All good things.


  1. More on this in a future post. 
  2. I probably could have done this earlier in the process, but didn’t think of the feature I wanted it for until later. 

This post is part of the thread: Content Presentation – an ongoing story on this site. View the thread timeline for more context on this post.