Lessons learned building a REST API theme for WordPress

Building web applications that use WordPress or other CMS as their backend is quite a common practice these days. But in the vast majority of cases, the integration with the backend functions only via the API. This approach decouples both worlds and makes development easier in many ways. But it also has its downsides. This loose integration limits the use of the CMS. In the case of WordPress, such approach allows you to use the WordPress admin to edit posts, pages, menus, and users. A lot of WordPress functionality is unused: the customizer, custom settings, permalinks, plugins and post preview. Building a tightly integrated theme removes these issues. Most of the WP-Admin can work as usual. And the theme can be distributed via a simple archive.

This article will outline some of the problems that need to be solved when building a REST API theme. If you are interested rather in why than in how, feel free to check our article on client-side rendering first.

Starting up — exporting WordPress settings

Creating a conventional WordPress theme that renders on the server usually consists of using one of the many custom WordPress functions that render specific pieces of content, often in the context of “the loop”. If we are creating a client-side application such functions can’t be used. Therefore if we want the application to respect the WordPress settings, we have to create our own settings interface. Generally, there are several functions that can be used:

get_theme_mods() — for custom theme settings. These can be colors, layout, and others that can usually be changed in the customizer.
get_bloginfo() — essential info about the site, such as name, description
get_option() — more specific WordPress settings

But besides those three functions, a lot of logic still have to be hardcoded with other custom functions.

<?php
$settings = new stdClass();
$settings->theme = get_theme_mods();
$settings->bloginfo = new stdClass();
$settings->bloginfo->name = get_bloginfo('name');
$settings->bloginfo->description = get_bloginfo('description');
$settings->bloginfo->display = get_bloginfo('display');
$settings->isCustomizePreview = is_customize_preview();
$settings->hasNavMenu = has_nav_menu('primary');
$logo_attachment = wp_get_attachment_image_src( $settings->theme['custom_logo'] , 'full' );
$settings->customLogo = (object) $logo_attachment;
$settings->headerImage = get_header_image();
$settings->customHeader = get_custom_header();
$settings->customHeaderSizes = get_attachment_sizes($settings->customHeader->attachment_id);
$settings->siteUrl = get_option('siteurl');
$settings->homeUrl = get_option('home');
?>

Such settings object can be then printed into script element with wp_json_decode function.

<script id="my-wp-settings">
<?php echo wp_json_decode($settings); ?>
</script>

The settings can be retrieved in JavaScript land like this:

const settingsText = document.querySelectorAll('#my-wp-settings').innerText;
const settings = JSON.parse(settingsText);

Now you can use the settings object to correctly set up and render the client side application.

Permalinks compatibility

Single page applications work via client-side routing. There are many client side routers and they differ in various ways, but most of them have similar ways of defining routes and their paths. Routes for pages and categories are easy to set up, but post route can be a bit more difficult because it is completely dynamic and changeable in WP-Admin.

First, we need to add the permalink structure to our settings object.

<?php
$settings->permalinkStructure = get_option('permalink_structure');
?>

This gives us a path like this:

/%year%/%monthnum%/%day%/%postname%/

Such string is incompatible with most client side routers and therefore has to be converted to a different format.

let postFragments = wpSettings.permalinkStructure.replaceAll('/%', '')
                      .replaceAll('%/', '')
                      .replace('post_id', 'id')
                      .replace('postname', 'slug')
                      .split('%');
let postPath = '/:' + postFragments.join('/:');

This piece of code also replaces postname for slug and post_id for id, so that these dynamic fragments match with corresponding properties on the post model.

Router definition in Scribe, which is built with EmberJS, then looks like this:

Router.map(function() {
  this.route('category', {path: '/category/:slug'}, function() {
    this.route('page', {path: '/page/:page_number'}, function() {
      this.route('loading');
    });
    this.route('page-loading');
  });
  this.route('posts-page', {path: '/page/:page_number'});
  this.route('post', {path: postPath});
  this.route('wp-admin');
  this.route('page', {path: '/:slug'});
  this.route('category-loading');
  this.route('posts-page-loading');
  this.route('four-oh-four', {path: '/404'});
});

At this point you can further tweak the router code so it’s more dynamic and that it can support custom page on front and page for posts. So that on ‘/’ there can be custom content and articles can be listed on ‘/blog’ and so on. Scribe does not go this way and does this instead by the dynamic switch of components, so that when the user changes settings in the customiser (changes page for posts for example) the app can react to that in real time.

It is also important to make sure that the URL structure strictly matches the WordPress structure. By default Ember’s router will create an URL like this:

https://blog.meetserenity.com/2017/03/perspectives-of-client-side-rendering

Even though it might seem correct at first, there is no trailing slash in the end. Loading the article from this URL would cause a 301 redirect to 

https://blog.meetserenity.com/2017/03/perspectives-of-client-side-rendering/

And that would increase the initial loading time.

The theme also has to take into account that the website may not run in the root (let’s say the whole blog exists on /blog). Ember’s router can take a rootURL argument which solves this pretty efficiently.

function getURLParts(url){
  return url.replace('http://', '').replace('https://', '').split('/');
}
const homeURLParts = getURLParts(settings.homeUrl);
let rootURL = '/' + homeURLParts[homeURLParts.length - 1] + '/';
if(homeURLParts.length === 1){
  rootURL = '/';
}

And the last necessary thing to consider is, that when the application runs inside the customizer, the URL structure is once again different. First, we need to detect this situation. We can simply check, that the app runs in an Iframe:

let inCustomizer = isPresent(window.parent) && isPresent(window.parent.SerenityCustomizer);

Then, in the case of Ember, we can conditionally set locationType to none (so the URL does not get replaced when transitioning). We can also set initialURL to cover cases when the user visited the customizer via wp admin bar.

if(inCustomizer){
  locationType = 'none';
  let urlQueryParamExists = window.parent.location.search.indexOf('url') > -1;
  if(urlQueryParamExists){
    let initialHost = decodeURIComponent(window.parent.location.search.split('=')[1].split('&')[0]);
    initialURL = initialHost.replace(window.parent.location.origin, '');
  }
}

The client side routing should now behave correctly both in customiser and in the regular environment. (Note that there can also be completely different approaches to client-side routing. Wallace Theme, which is built with Angular, simply relies on the WordPress router).

Integration with Theme Customiser

The customization is another aspect where the single page application can shine. Because of client-side rendering, if it’s correctly data driven, we can only change relevant pieces of data and various parts of the screen will instantly re-render to be up to date. There will be no full page or partial reloads, everything happens in real time.

We have to make sure that to every element in customizer, we pass postMessage as a transform. Then, if needed we can also change the transform on pre-existing elements (not created by the theme):

<?php
//
// Change transform of preset settings
//
$wp_customize->get_setting( 'header_image'  )->transport = 'postMessage';
$wp_customize->get_setting( 'header_image_data'  )->transport = 'postMessage';
$wp_customize->get_setting( 'blogname'  )->transport = 'postMessage';
$wp_customize->get_setting( 'blogdescription'  )->transport = 'postMessage';
$wp_customize->get_setting( 'custom_logo' )->transport = 'postMessage';
  
$wp_customize->get_setting( 'header_textcolor' )->transport = 'postMessage';
$wp_customize->get_setting( 'show_on_front' )->transport = 'postMessage';
$wp_customize->get_setting( 'page_for_posts' )->transport = 'postMessage';
$wp_customize->get_setting( 'page_on_front' )->transport = 'postMessage';
?>

Next, in our customisation script, we have to access a context of our client-side app from the iframe.

appInstance: function(){
  var childWindow = $('iframe')[0].contentWindow;
  this.app = childWindow.Serenity;
  return this.app;
}

Then when we set up our hooks with wp.bind and our function gets called, we can get our appInstance, find a particular service or model and update the data.

'blogname': function(value){
          SerenityCustomizer.appSettingsService().set('server.bloginfo.name', value);
},

Social Media Embeds support

WordPress conveniently converts links to social media sites such as Facebook, Twitter and Instagram to embeds. It also includes particular 3rd party scripts as needed for those embeds to properly initialise. But this is of course not compatible with client side rendering.

Output for Twitter Embeds may look quite like this:

<blockquote class="twitter-tweet" data-width="550">
<p lang="en" dir="ltr">Ember community tells me to learn Elixir. Elixir community tells me to learn Elm. Elm community tells me to learn Haskell. What a ride.</p><p>&mdash; Martin Malinda (@martinmalindacz) <a href="https://twitter.com/martinmalindacz/status/777778013941428224">September 19, 2016</a></p>
</blockquote>
<p><script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script></p>

WordPress prints out a blockquote element with Tweet content and includes a script element with a dependent 3rd party script. Unfortunately, such script is not executed if the content is appended dynamically via JavaScript.

An efficient way to make this work is to check whether the post contains embeds already on the server in the ‘save_post’ hook:

<?php
function set_meta($id, $meta_key, $meta_value){
if ( ! add_post_meta( $id, $meta_key, $meta_value, true ) ) { 
        update_post_meta( $id, $meta_key, $meta_value);
      }
}
function serenity_post_updated($id) {
      $post = get_post($id);
      $content = $post->post_content;
$contains_twitter_status = strpos($content, 'twitter.com') > -1 &&
                                 strpos($content, '/status/') > -1;
set_meta($id, 'contains_twitter_status', $contains_twitter_status);
}
add_action( 'save_post', 'serenity_post_updated', 10, 1);
?>

Then visiting the post route, we can check the post dependencies and load additional scripts as needed:

export default Ember.Service.extend({
loadIfUndefined(objName, url){
if(typeof window[objName] === 'object' || $[objName] === 'object'){
      return RSVP.resolve(window[objName]);
    }
    
    return $.getScript(url);
  },
twitter: computed(function(){
    return this.loadIfUndefined('twttr', '//platform.twitter.com/widgets.js');
  }),
getDependenciesForPost(post){
    if(post.get('containsTwitterStatus')){
      this.get('twitter');
    }
  }
});

When the post is rendered we can initialize the widget:
(in the case of Scribe, it is in the didInsertElement hook in content-renderer component)

initTwitter($contentElement){
    const $tweets = $contentElement.find('blockquote.twitter-tweet');
    this.get('scriptLoader.twitter').then(() => {
      twttr.widgets.load($contentElement[0]);
    });
 },

Future of REST API themes

Most of the snippets above have been simplified and there were also other obstacles I have to get over to get things running. There is definitely a lot of work to create such a theme, especially because there is a lot of unknown. With the power of collaboration, we can create more guides as well as open source solutions to develop solutions like this faster. There are already open sourced REST API themes and Scribe may go this way in the future too. Hopefully, we can create a solid foundation.

Coming next

I’m planning an article on specific details of integrating Ember with WordPress, custom server rendering and performance. Feel free to subscribe below to be notified when it comes out.