Published by Dan Cunning on Jan 3, 2020

Headless WordPress

The benefits of a worldclass CMS without the dependencies and restrictions that make custom work more painful.

The Problem

I've run this website for years via a custom Rails application (think Jekyll without hiding the Ruby), but over the past few years I haven't written anything about several projects, fixes, and ideas, mainly because writing was too much hassle.

This is exactly the problem Content Management Systems (CMS) are built to solve, but I choose not to use one back in 2012 because when you build on top of a CMS, you inherit all its dependencies, rules, and limitations. This website is my playground to try new things, and running my site on a CMS like WordPress seemed too restrictive:

  • What if I want very specific URLs for my posts?
  • Can I do inline demos of JavaScript features and server-side syntax highlighting? (example)
  • Could I do one-off lists with a custom data schema? (example)
  • How would it render data-heavy pages with years and years of information? (example)
  • Can it be run soley via Amazon S3 and CloudFront?

WordPress plugins and custom theming could handle a lot of these, but the end result would be a disorganized site that I wouldn't enjoy maintaining. How can I use WordPress' world-class CMS without it restricting what my site can do?

The Solution

WordPress would handle the creation of posts and pages, but another application will publish the content to https://dan.cunning.cc. The technical term for this approach is a Headless CMS because the "head" (the visitor facing site) is managed elsewhere.

WordPress Configuration

  • Run behind an nginx server with basic authentication enabled, so all PHP files are restricted to only authorized users, protecting the site from security vulnerability scanners.
  • Installed the Advanced Custom Fields and ACF to REST API plugins to allow pages and posts to expose custom settings specific to my site
  • Appended editor-style-block.css with my CSS so WordPress' visual editor would display a better preview of how it will look when published

Publisher Workflow

I created a custom "publisher" application that performs the following workflow:

  1. Read WordPress content (posts, pages, tags, categories) via the REST API
  2. Enhance the WordPress content with custom behavior via Liquid
  3. Identify stale content via timestamps
  4. Render the content using standard Rails controllers and views
  5. Publish HTML, CSS, JavaScript, and images to Amazon S3

Content meets Code

Routing

Rails' powerful routing engine allows the publisher to define my site's structure exactly how I want it:

Rails.application.routes.draw do
  root to: "pages#show", slug: "home", as: :root
  get "index.html", to: "pages#show", slug: "home"
  get "sitemap.xml", to: "sitemap#show"

  get "wp-content/uploads", to: "uploads#index", as: :uploads
  get "wp-content/uploads/*path", to: "uploads#show", as: :upload

  get ":category_slug/index", to: "categories#show", as: :category

  get ":category_slug/:tag_slug", to: "tags#show", as: :category_tag, constraints: TagConstraint
  get ":category_slug/:post_slug", to: "posts#show", as: :post

  get ":category_slug/:post_slug/index", to: "posts#show", as: :parent_post
  get ":category_slug/:parent_slug/:post_slug", to: "posts#show", as: :child_post

  get "*slug", to: "pages#show", as: :page
end

Example Post

Why the Thrashers left Atlanta is one of the most popular pages on this website, and it's 100% WordPress-driven content. Here's how it looks like inside WordPress' editor:

The Home Page

A more complicated page is the home page which organizes the WordPress categories and links to featured articles:

The Advanced Custom Fields allows me to choose the section order and what posts are featured, and the liquid code renders the HTML using a custom Ruby view component:

class Pages::SectionLinks
  attr_reader :template

  def initialize(template, page)
    @template = template
    @page = page
  end

  def render
    sections = (1..6).collect { |i| read_section(i) }

    template.render(
      partial: "pages/components/section_links",
      locals: {
        groups: sections.in_groups_of(3),
      },
    )
  end

private

  def read_section(i)
    acf = @page.data["acf"]
    category_id = acf["section_#{i}"]
    category_id = category_id.first if category_id.is_a?(Array)
    return unless category_id

    category = @page.wp.categories.find(category_id)

    posts = %w[a b c d e f g h i j k l].collect do |c|
      post = acf["link_#{i}#{c}"]
      id = post["ID"] if post
      @page.wp.posts.find(id) if id
    end

    OpenStruct.new(
      category:,
      posts: posts.compact,
    )
  end
end
<% groups.each do |group| %>
  <div class="row section-group">
    <% group.each do |section| %>
      <div class="section">
        <% if section %>
          <h2>
            <%= link_to section.category.name, category_path(section.category) %>
            <span class="label label-default"><%= number_with_delimiter section.category.root_posts.length %></span>
          </h2>

          <% section.posts.each do |post| %>
            <p><%= link_to post.title, page_path(post) %></p>
          <% end %>
        <% end %>
      </div>
    <% end %>
  </div>
<% end %>

Wrap-Up

As evident in the Home Page example, a headless CMS requires coding to render the more complicated content, but I like that the code isn't bogged down in the WordPress platform. I think this solution balances the power of the CMS and the power of custom coding pretty well, but there are still some avenues I'd like to explore:

  • Could I create a custom block type instead of leaning on liquid components?
  • Could I replace the "Document settings" options with only the settings my site supports?
  • Could I repurpose the HTML/CSS/JavaScript under /wp-admin to work without the MySQL/PHP backend?