Headless(-ish) WordPress Example

A screenshot of the site on codepen that has the three columns of html, css, and javascript visible.

This is a bit of an odd one but possibly odd enough to be interesting.

The Scenario

We can’t run WordPress on the University servers but we’ve got a bunch of people who have content on WordPress and like that editing experience. They also want VCU domains which are much harder to get for off-site servers.

The Proposal

What if we take one of those sites and see what we can do with the REST API data in a headless1 HTML/JS/CSS only environment? I also wanted to keep it out of Vue or other larger javascript frameworks so it’d be less abstract to explain. At this point I am unsure that was a great choice but I can make a different choice next time.

More Details

The faculty member already had a WordPress site. It had a bunch of pages and had no posts. The faculty member also wanted the header image to change on various pages and for a sub-menu to be created on certain pages.

The Proof of Concept – Step One HTML Shell

I opted to sketch out the HTML portion loosely with Bootstrap 4. Initially, I’d gone to one of the HTML5Up templates but pulled back as it added a chunk more complexity than I needed initially. Plus, I knew Matt was going to handle taking the design to the next level so that took the pressure off me.

You can see the four main elements commented up below.

The menu piece was done manually for this but I think we generated it for the final (or if not, we could).

<!--THE MENU IS THE LARGEST PART OF THE HTML . . . WEIRD-->
<nav class="navbar navbar-toggleable-md navbar-inverse bg-inverse">
  <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
  <a class="navbar-brand" href="#">Navbar</a>

  <div class="collapse navbar-collapse" id="navbarsExampleDefault">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link" href="#home">Home <span class="sr-only">(current)</span></a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#bio" id="bio-menu">Bio</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#join-us" id="join">Join Us</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#about">About</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#science">Science</a>
      </li>
    </ul>
  </div>
</nav>

<!--WHERE THE IMAGE GOES-->
<div class="container-fluid">
  <div class="row header" id="featured">
  </div>
</div>


<!--WHERE WE BUILD THE SECONDARY MENU WHEN NEEDED-->
<div class="secondary-menu">
  <div id="secondary-menu-parent"></div>
  <ul id="sub-menu-js"></ul>
</div>

<!--WHERE THE DATA GOES ON SHOW-->
<div class="container">
  <div class="row">
    <div class="col-md-8 main">
      <div id="theContent"></div>
    </div>
  </div>
</div>

<!--WHERE THE DATA GOES TO HIDE ON LOAD-->
<div id="hidden-data"></div>

The Javascript

The first portion is pretty standard fetch. I’m using the REST API route2 and asking for 90 pages. I can ask for up to 99 with the new(ish) rules without having to get into additional drama to expand that and I anticipate that 90 is plenty. You did have the capability of doing -1 like in wp_query in the older API scenario.3

//GET WORDPRESS JSON CONTENT
fetch(
  "https://rampages.us/kirkwarrenbrown/wp-json/wp/v2/pages?per_page=90&_embed"
)
  .then(function(response) {
  // Convert to JSON
  return response.json();
})
  .then(function(data) {
  for (i = 0; i < data.length; i++) {
    writePages(data[i]); //takes that JSON and writes it to our hidden HTML div
    if (data[i].slug == urlHash()) {
      var slug = urlHash(); //is there a slug? great set it
      setPage(slug); //set the page 
    }
  }
  if (urlHash() === "" || document.getElementById(slug) === null) {
    setPage("home"); //if not slug set the page to home
  }
  buildMenu(slug); //make the menu based on the slug
});

So that’s the big piece that gets the data and then runs it through our functions. Now lets break down some of the functions to see what they do.

The following function takes the JSON data and writes it into the hidden div (just a div with our good CSS-friend display:none). Once it’s there we can show it in other places really quickly because all the data is already loaded. Since we were already loading jQuery for Bootstrap stuff I went ahead and used it.

//writes the pages to a hidden div for later reference
function writePages(data) {
  var post = jQuery("#hidden-data").append(
    jQuery(
      '<div id="' +
      data.slug +
      '" class="post"' +
      getImage(data) +
      getParent(data) +
      getId(data) +
      '><h2 class="page-title">' +
      data.title.rendered +
      '</h2><div class="page-content">' +
      data.content.rendered +
      "</div></div>"
    )
  );
}

Our destination is the #hidden-data div and we’re just appending a div full of stuff for each page from the WordPress JSON. Key things to notice are the data attributes which make it easy to find the right data and pull the right pieces. There are a couple of smaller helper functions that do that formatting for me. I don’t know if it was worth chunking them out like this. I continue to struggle with how small my functions should be.4

function getId(data) {
  return ' data-id="' + data.id + '"';
}

OK, now we’ve got all the data in there but we’re also checking to see if there are any URL parameters (that #something in the URL) set that would decide what content should be actually showing. I’m doing that by looking for a URL hash that matches the page-slug generated from WordPress. I could have used the post ID or something else but slugs are unique values and they’re pretty human readable.

function urlHash() {
  if (window.location.hash) {
    var hash = window.location.hash.substring(1); //Puts hash in variable, and removes the # character
    //console.log(hash);
    return hash;
    // hash found
  } else {
    // No hash found
    return "about"; //no preferences? fine we'll set the page to about
  }
}

Once we’ve got a hash variable, set or assumed, we can then use it to pick our data and load it. That’s what this function does.

function setPage(slug) {
  var getPage = document.getElementById(slug).innerHTML;//get the HTML of our matching hidden post
  var getDestination = document.getElementById("theContent");//this is where it goes
  getDestination.innerHTML = getPage; //this puts the contents of getPage into our destination 

  //set header image
  var getImage = document.getElementById(slug).dataset.img;
  var getFeatured = document.getElementById("featured");
  getFeatured.style.backgroundImage = "url('" + getImage + "')";
}

I then realized we had to watch the URL in case their were changes. If I recall correctly, hitting the back button or something like that wasn’t being caught. Hard to remember at this point but I’m pretty sure this piece was useful.

//lets you do the back and forward properly --
$(window).on("hashchange", function() {
  setPage(urlHash());
  buildMenu(urlHash());
});

Now building the sub-menus . . . we needed to be able to do two things here which weren’t immediately obvious. One was to make a relationship between the page and sub-menu but also to relate the sub-menu pages to one another so the menu would stay there in all the scenarios. Thankfully the API had the parent ID in the JSON so we wrote it as a data attribute into the div. I really like data attributes.

function buildMenu(slug) {
  //get id etc
  var j = 0;
  var theId = parseInt(document.getElementById(slug).dataset.id);
  var secondMenu = document.getElementById("sub-menu-js");

  // var theParent = "";
  if (document.getElementById(slug).dataset.parent != 0) {
    var theParent = parseInt(document.getElementById(slug).dataset.parent); //get parent ID if exists
  }

  var posts = document.getElementsByClassName("post");
  document.getElementById("sub-menu-js").innerHTML = "";

  //default parent category in sub menu
  addParent(theId, theParent);

  for (i = 0; i < posts.length; i++) {
    //MENU BUILDING LOGIC
    if (
      theId == posts[i].dataset.parent ||
      theParent == posts[i].dataset.parent
    ) {
      var urlParam = posts[i].id;
      var menuName = posts[i].querySelector("h2").innerHTML;
      var a = document.createElement("a");
      var newItem = document.createElement("li");

      a.textContent = menuName;
      a.setAttribute("href", "#" + urlParam);
      newItem.appendChild(a);
      secondMenu.appendChild(newItem);
      j++;
    }
  }
  if (j === 0) {
    document.getElementById("sub-menu-js").innerHTML = "";
  }
}

Matt’s taken this all much further by doing things like sorting the menus based on the sort order options in WordPress pages and a chunk of other user interface and visual improvements.

In any case, it was a fun little experiment that might be useful to other people.

The whole non-pretty version is in the CodePen below and I’ll update this to link to the real one once we finalize things there.

See the Pen mindfulness – headless wp json display – hash version by Tom (@twwoodward) on CodePen.


1 I’m probably using this term appropriately but words tend to wander.

2 That’s, like, totally official terminology and stuff.

3 I also remember when Twitter was just 140 characters and they counted URL length against you. Snow hills! Both ways!

4 You’re right. It does sound like a personal problem.