Taming a River Theme

Origin Story

Matt worked long hours making an incredible theme for Footprints on the James course.1 It’s in WordPress but a large portion of the site ended up being built by hand as complexity increased and time dwindled.2 That means it’s hard-coded HTML/PHP.

Now because the site was so great, we ended up having a conversation with the faculty involved a few months after the site was finished. They want to run the course on other rivers and want similar sites for those rivers. That means we need to turn this site into something that is more like a traditional WordPress theme. I’m taking the first stab at that and felt it’d be an interesting thing to write about a bit.


My first move was to tackle the front page as it has the most complexity. I drew this on a whiteboard but I included a fancy image below because I wanted to make it nice for you.
Screenshot of the website with annotation.

Now we can see two columns for content on this page type. In the ‘sidebar’ column we see quotes and HTML elements, maybe some more. In the ‘main’ column, we see HTML elements and elements that span the sidebar and main column.

The goal is to figure out how to chop this stuff up so that people can write it using patterns that feel fairly normal in WordPress. We’re still using the original editor so that will also impact how this constructed.

The Aside

Screenshot of the ACF layout for the custom post type 'aside.'
I decided that a clean way to deal with the asides was to create a custom post type. While we’re only seeing two different types at the moment, there’s potential for increased complexity there so putting as an element we can reference seemed to make sense. I didn’t want the main editing area getting too dense. Consider it distributed complexity. The following code makes the custom post type. It’s standard and I use a Sublime code snippet to make it because it’s long and standard. Humans should not have to suffer both of those things.

 * Custom post types
 * @package understrap

//aside custom post type

// Register Custom Post Type aside
// Post Type Key: aside

function create_aside_cpt() {

  $labels = array(
    'name' => __( 'Asides', 'Post Type General Name', 'textdomain' ),
    'singular_name' => __( 'Aside', 'Post Type Singular Name', 'textdomain' ),
    'menu_name' => __( 'Aside', 'textdomain' ),
    'name_admin_bar' => __( 'Aside', 'textdomain' ),
    'archives' => __( 'Aside Archives', 'textdomain' ),
    'attributes' => __( 'Aside Attributes', 'textdomain' ),
    'parent_item_colon' => __( 'Aside:', 'textdomain' ),
    'all_items' => __( 'All Asides', 'textdomain' ),
    'add_new_item' => __( 'Add New Aside', 'textdomain' ),
    'add_new' => __( 'Add New', 'textdomain' ),
    'new_item' => __( 'New Aside', 'textdomain' ),
    'edit_item' => __( 'Edit Aside', 'textdomain' ),
    'update_item' => __( 'Update Aside', 'textdomain' ),
    'view_item' => __( 'View Aside', 'textdomain' ),
    'view_items' => __( 'View Asides', 'textdomain' ),
    'search_items' => __( 'Search Asides', 'textdomain' ),
    'not_found' => __( 'Not found', 'textdomain' ),
    'not_found_in_trash' => __( 'Not found in Trash', 'textdomain' ),
    'featured_image' => __( 'Featured Image', 'textdomain' ),
    'set_featured_image' => __( 'Set featured image', 'textdomain' ),
    'remove_featured_image' => __( 'Remove featured image', 'textdomain' ),
    'use_featured_image' => __( 'Use as featured image', 'textdomain' ),
    'insert_into_item' => __( 'Insert into aside', 'textdomain' ),
    'uploaded_to_this_item' => __( 'Uploaded to this aside', 'textdomain' ),
    'items_list' => __( 'Aside list', 'textdomain' ),
    'items_list_navigation' => __( 'Aside list navigation', 'textdomain' ),
    'filter_items_list' => __( 'Filter Aside list', 'textdomain' ),
  $args = array(
    'label' => __( 'aside', 'textdomain' ),
    'description' => __( '', 'textdomain' ),
    'labels' => $labels,
    'menu_icon' => '',
    'supports' => array('title', 'editor', 'revisions', 'author', 'trackbacks', 'custom-fields', 'thumbnail',),
    'taxonomies' => array(''),
    'public' => true,
    'show_ui' => true,
    'show_in_menu' => true,
    'menu_position' => 5,
    'show_in_admin_bar' => true,
    'show_in_nav_menus' => true,
    'can_export' => true,
    'has_archive' => true,
    'hierarchical' => false,
    'exclude_from_search' => false,
    'show_in_rest' => true,
    'publicly_queryable' => true,
    'capability_type' => 'post',
    'menu_icon' => 'dashicons-admin-comments',
  register_post_type( 'aside', $args );
  // flush rewrite rules because we changed the permalink structure
  global $wp_rewrite;
add_action( 'init', 'create_aside_cpt', 0 );

On the ACF side, I tie in a few things to make the aside easier for people to structure. Initially, I was going to use categories to determine if it was a quote or HTML or whatever. I decided to go with an ACF field instead after trying that. The ACF field gave me the ability to expose additional fields based on the choice of category and I got no benefit for being able to taxonomically locate various asides. So now things are set so that if you write an aside, when you choose the type ‘quote’ an additional field for the quote author appears. I think that kind of situational/necessary expansion of complexity lets us keep things as simple as possible. This attempt can also go wrong (think of the only-if-you-know-to-hover aspects of various WP interface elements) but I think we’re on the right side of that in this choice- complexity hidden but exposed as part of the natural action of creation rather than special knowledge of actions to take within the interface.

The Main Column

In our main column we have two elements. One takes up the full width of that column in conjunction with the aside and one is full width (spans the main column and the aside column). To make this work we have to be able to choose between those two main types and when we choose one that has an aside, we need to be able to assign that aside to that portion of the page content.

To do this we start with a repeater field. This is the field to use when we don’t know how many chunks of content we’ll need. With repeaters we can have as many as we need. Within each repeater field row we will have three fields. The main content block, the content type, and the aside. The main content block is an HTML/WYSIWYG editor. The content type is going to be either regular or full width. When regular is selected the aside shows up and allows you to make an association between that chunk of content and the aside post type of your choice. It’s a post object field and those are pretty handy.

Header Images

I need a list of images to rotate in the banner. This is another repeater field with a single image field inside it. Simple simon with ACF.

Displaying the Data

Now that we’ve built out the data structure and authoring patterns in the backend we have to decide how to display it properly. Matt’s done the hard work of setting up the HTML and CSS. I have to figure out what he did and how I’ll mimic the patterns in PHP.

Header Images

function river_header_images(){
  $images = get_field("header_images");
  $html = '';
  if( $images ){ 
       foreach( $images as $image ){ 
        $html .= '<img class="cover-image" src="' . $image['image']['url'] . '" alt="">';
     return $html;

Getting this from ACF from the repeater field is pretty straight forward.

Main Content

Doing the main content is a bit more complex as we have more complexity in the repeater field and we’re trying to change the HTML wrappers based on different content types. It’s commented up below.

function river_main_text(){
  $main_content = get_field('main_content');
  $html = '';
  if ($main_content){
    foreach ($main_content as $content_block) {
      if ($content_block['content_type'] == 'full-width'){//IS IT FULL WIDTH? GREAT! Super simple display and no need to check for aside
        $html .= '<div>' . $content_block['main_text'] . '</div>';
      } else {
        $html .= '<div class="fotj-sidebar">';//NOT FULL? FINE . . . we want an aside div whether there is content or not
        if ($content_block['aside']){ //HAVE ASIDE CONTENT? 
          if(get_field('aside_type',$content_block['aside']->ID)) { //IS IT A QUOTE? WRAP IT WITH THE QUOTE CLASS
            $html .= '<div class="fotj-quote">';
            $html .=  $content_block['aside']->post_content;
            $html .= '<span class="fotj-quote-author">' . get_field('quote_author',$content_block['aside']->ID) . '</span>'; //get the author
            $html .= '</div>';
          } else {
            $html .=  $content_block['aside']->post_content;
        $html .= '</div>';
        $html .= '<div class="fotj-content">';
        if ($content_block['main_text']){   
          $html .= $content_block['main_text'];
        $html .= '</div>';
    return $html; //RETURN THE HTML 



This seemed like it was going to be much harder to do and explain. Now I feel the post was self-evident and I’m ashamed of my initial trepidation to attempt the project.

1 It’s not just an awesome site. Go see why we’re so excited about the course.

2 I hope to have the reverse happen someday. It may require a tweaking of the space-time continuum.