Skip to Content

Pressing Words, With Your Friend, Wordpress

A con­tem­po­rary de­vel­op­ers guide to build­ing things on Wordpress 4.x and not hav­ing it be ter­ri­ble.

TL:DR; Start here. Install this thing and con­nect it to your ac­count on here. Buy a li­cense of this (it’s worth it). Read some docs for this and start build­ing. Wordpress 5 and Gutenberg will prob­a­bly break all of this ex­cept the en­vi­ron­ments.

When I first started work­ing as a de­vel­oper, Wordpress was the preva­lent plat­form for pretty much any pro­ject. Ten years later and … Wordpress is still pretty much most of the in­ter­net. In gen­eral, Wordpress will be my last choice of a plat­form. I pre­fer to build sta­tic sites, use a head­less CMS, or al­most any­thing else at all.

That said, as the Technical Director at Fuzzco — a de­sign stu­dio that re­lies al­most ex­clu­sively on Wordpress for their web­sites — Wordpress was hap­pen­ing. Fuzzco is rare among stu­dios in that we man­age and host pro­jects for our clients, and of­ten have main­te­nance rid­ers that can last for years. This means that in the course of a year, not only did we build a half dozen new pro­jects on Wordpress, but we main­tained and triaged is­sues on over 100 legacy pro­jects.

Very quickly I re­al­ized I had one op­tion: make Wordpress not ter­ri­ble.

Terrible is pretty harsh

If you’re com­fort­able with Wordpress, you might find some fight­in’ words here. What’s my prob­lem with Wordpress and what am I try­ing to solve for? My biggest is­sue with Wordpress de­vel­op­ment as I’ve en­coun­tered it in the past is a lack of clar­ity around the re­quire­ments of the en­tire sys­tem. What does the pro­ject need to run in an en­vi­ron­ment, and why? How do we move from a repos­i­tory to a lo­cal en­vi­ron­ment and start work­ing on a code­base? How does that code­base get de­ployed to a server?

I’ve seen Wordpress sys­tems that are frozen in time in 2006 — FTP to the server and edit a CSS file on pro­duc­tion, or deploy” your theme by up­load­ing a .zip. I’m in­ter­ested in how we can lower the cog­ni­tive over­head for get­ting a Wordpress pro­ject up and run­ning, and join in with pre-pro­cess­ing, com­pil­ing, con­tainer­iz­ing, test­ing, and all the re­ally ex­cel­lent things that we’ve come to ex­pect from our web stacks over the past few years.

Another is­sue I have with Wordpress is its com­mit­ment to auto-mag­i­cal routes and ren­der­ing tem­plates with ob­scure and com­pli­cated .php pat­terns that ba­si­cally con­cate­nate strings. I’m in­ter­ested in ex­plicit routes — ei­ther hard-coded or pa­ra­me­ter­ized — and sep­a­rat­ing con­cerns be­tween logic and tem­plate.

A lot of this boils down to a dis­agree­ment be­tween what Wordpress thinks a site should be and what I end up us­ing it for. Wordpress as de­signed dis­tin­guishes be­tween your site” and your theme”. Your site” is the con­tent in the data­base, the op­tions you’ve saved, and the menus and wid­gets you’ve in­stalled. It ex­pects themes” to be pre­sen­ta­tions of this real web­site stuff. This model of web­sites per­pet­u­ates that design” is some­thing that can be ap­plied over a web­site, a kind of dress­ing up of the real things. This is the in­verse, and per­haps a corol­lary to, the con­cept that de­sign­ing a web­site is just de­cid­ing what it looks like. It’s an idea that lives within the sys­tem of si­los be­tween de­sign and de­vel­op­ment, and that we can design” a web­site in Photoshop or Sketch and hand off the comps to a de­vel­oper to build it. Which is how a lot of Wordpress pro­jects are built.

In short, I dis­agree with this con­cept of web­sites. My po­si­tion is that de­sign­ing a web­site is both how it looks, how it works, and how the data and struc­tures are com­posed. Taking this ap­proach, con­trol­ling the ob­ject mod­els, the in­for­ma­tion ar­chi­tec­tures, and the tem­plates are all of equal im­por­tance. In my line of work, a Wordpress theme can not be ap­plied to any other site than the one that it was de­signed for, a site where the struc­ture was de­signed for the theme.

So why use Wordpress?

There are still a num­ber of re­ally good, com­pelling rea­sons to use Wordpress as a plat­form for build­ing web­sites. It’s got a ro­bust built-in com­ment­ing sys­tem with user ac­counts. It’s re­ally good for things that are shaped like blogs. It’s got a huge, well-main­tained ecosys­tem of plu­g­ins. It’s free. And since it’s most of the Internet, clients are re­ally, re­ally com­fort­able with it.

There are a cou­ple of rea­sons not to use Wordpress right now. Mostly these cen­ter around the im­pend­ing re­lease of Wordpress 5.0 and the Gutenberg ed­i­tor, which has a num­ber of con­cerns around plu­gin com­pat­i­bil­ity and ac­ces­si­bil­ity for au­thors.

But that’s okay, since we’ve de­cided to use Wordpress 4.x. As we all know, pick­ing a ver­sion of Wordpress and then never up­grad­ing it is one of the time hon­ored tra­di­tions of Wordpress de­vel­op­ment.

How does this work even

Let’s start at the end.

We’re go­ing to be host­ing our pro­duc­tion Wordpress site on a Digital Ocean droplet — the small­est one they have — for $5 per month. Depending on the pro­ject life­cy­cle, we can set up more droplets for a stag­ing server and a de­vel­op­ment server. At Fuzzco we used dev servers to show sites to the in­ter­nal team, stag­ing servers to show sites to the client, and pro­duc­tion servers to show sites to the pub­lic.

I don’t know about you, but I per­son­ally don’t su­per love man­ag­ing my vir­tual pri­vate servers man­u­ally. In or­der to de­ploy our code­bases to Digital Ocean we’ll use the phe­nom­e­nal tool Nanobox. Nanobox is an op­er­a­tions layer that han­dles con­tainer­iz­ing ap­pli­ca­tions and de­ploy­ing them ag­nos­ti­cally to a cloud ser­vice provider. Nanobox will de­ploy our code from the com­mand line to any one of our droplets.

Nanobox will also con­tainer­ize and run an ap­pli­ca­tion in a vir­tual ma­chine lo­cally. This means we’ll use it to run our de­vel­op­ment en­vi­ron­ment, and en­sure that all of our en­vi­ron­ments are iden­ti­cal. No more wor­ry­ing about PHP ver­sions and ex­ten­sions and plu­g­ins. No more run­ning MAMP or MySQL or Apache or what­ever on your lo­cal ma­chine be­fore any­thing works. Nanobox de­fines the server in a .yaml file, and it will al­ways be the same. It also han­dles all the sync­ing be­tween our lo­cal disk and our vir­tual en­vi­ron­ment.

So now that we know how our code is go­ing from lo­cal to pro­duc­tion, we can think for a sec­ond about how it’s go­ing to do that, and how we’re go­ing to man­age our data.

The data­base on the pro­duc­tion server is canonical”. That means that the data­base the client in­ter­acts with is the one true data­base, and we must treat it with care and at­ten­tion. We’ll never change that data­base our­selves, and we’ll move that data­base down­stream from pro­duc­tion to stag­ing to dev to lo­cal in or­der to de­velop against our real data. Importantly, we don’t want to mi­grate the data­base man­u­ally ei­ther. It’s a lit­tle ex­pen­sive but us­ing Migrate DB Pro is an in­cred­i­ble re­source for this part. I guess one could also look into al­ter­na­tives for per­sonal pro­jects.

The canon­i­cal code­base lives in ver­sion con­trol, and moves the other di­rec­tion. From Github to lo­cal to dev to stag­ing to pro­duc­tion, amen. The only things we need to track in ver­sion con­trol are what makes our pro­ject unique. Practically, this means we need to track our theme and our plu­g­ins. Wordpress core files are not spe­cial, and we should not fill our repos­i­to­ries with them.

Getting started

At this point it’s worth get­ting started with Nanobox. I back the con­tain­ers with VirtualBox, since at the time I started this it was slightly more sta­ble than Docker on MacOS High Sierra. Once Nanobox & Virtualbox/Docker is in­stalled, set up Digital Ocean as your provider. Once that’s done, we have every­thing we need to get started!

I’ll be talk­ing through a pro­ject I built in or­der to fa­cil­i­tate build­ing other pro­jects. This will be more in­tense than you might need for a sin­gle build, but this was de­signed a tool that any­one can use to get started quickly. Here’s the ba­sic struc­ture of our repo:

📁 /project-name
⮑ 📄 .gitignore    # includes /wp
⮑ 📄 package.json  # tooling lives here
⮑ 📄     # be nice, write docs    
⮑ 📁 theme         # our theme codebase
⮑ 📁 plugins       # vendor plugins
⮑ 📁 scripts       # some helpers

The crux of the pro­ject is our boxfile.yml con­fig­u­ra­tion file. This is what Nanobox uses to cre­ate our con­tain­ers. It looks like this!

# /boxfile.yml                
run.config:                    # 
  engine: php                  #
  engine.config:               #
    runtime: php-7.0           # Defines PHP version
    document_root: 'wp/'       # Dir to serve app from
    extensions:                # PHP extensions we need
      - gd                     #
      - mysqli                 #
      - curl                   #
      - zlib                   #
      - ctype                  #
web.wp:                        #
  start: php-server            #
  network_dirs:                #              #
      - wp/wp-content/uploads/ #
data.db:                       #
  image: nanobox/mysql:5.6     # Nanobox DB magic
                               #                  #
  image: nanobox/unfs:0.9      #

As noted above, we’ll be serv­ing our en­tire in­stal­la­tion of Wordpress in the /wp di­rec­tory. This will hold all the Wordpress core files and com­piled theme code, none of of which we need or want in ver­sion con­trol. As such, make sure this is listed along­side node_modules in the .gitignore.

Since we’ve de­cided that we don’t want to track these files, but we need them to ac­tu­ally have a pro­ject, we can write a helper script to take care of the gap be­tween those two ideas.

Here are the scripts we’re go­ing to write to help us han­dle this process:

📁 /project-name
⮑ 📁 scripts
   ⮑ 📄 # Installs Wordpress core files.
   ⮑ 📄          # Runs our setup helper.
   ⮑ 📄      # Checks if we need to init.
   ⮑ 📄 setup.js         # Cute lil' CLI helper.

The first thing we’ll do is write a script that checks if /wp ex­ists. If it does­n’t, throw an er­ror that we need to ini­tial­ize the pro­ject since we don’t have any of the core files we need.

echo 'Check to make sure wordpress is here at all'
if test -d ./wp/
  echo 'yup we good'
  exit 0
  echo 'Project not initialized: Run `$ npm run init`'
  exit 1

I’m call­ing this prestart be­cause I want to run it be­fore npm start. Many times I’ll be on au­topi­lot, and af­ter cloning a repo sim­ply run npm install and npm start. This in­ter­rupts that process and lets me know I need a third step, npm run init. Let’s put this in our package.json scripts:

# package.json
  "scripts": {
    "init": "./scripts/",
    "prestart": "./scripts/",
    "start": "npm run dev"

We’ll get to our dev tool­ing later. Lets take a look at what our script does:

node ./scripts/setup.js  

Not much! This just runs our setup CLI helper. You might not need all this, but since I built this sys­tem to help a team of de­vel­op­ers work on many many pro­jects you’re gonna get it any­way.

// setup.js

// some nice deps for making a CLI.
const prompt = require('prompt')
const exec = require('child_process').exec
const colors = require("colors/safe")

// Run and log a bash command
const bash = cmd => {
  msg('green', `Running: ${cmd}`)
  return new Promise(function(resolve, reject) {
    exec(cmd, (err, stdout, stderr) => {
      if (err) reject(err)
      resolve(stdout, stderr)

// Log a message
const msg = (color, text) => {

// do the magic
const setup = (err, result) => {
  if (err) msg(`red`, err)

  msg('yellow', 'WordPress configuration values ☟')

  for (let key in result) {
    msg('yellow', `${key}: ${result[key]};`)
  // run our check-install script.
  .then(ok => {
    // add our project to hostfile
    bash(`nanobox dns add local ${}.local`)
  .then(ok => {
    // explain the next step
    msg('green', `Run npm start, then finish setting up WordPress at ${}.local/wp-admin`)

msg('green', 'Making Progress!')
  properties: {
    name: {
      description: colors.magenta("Project name:")
}, setup);

This will open a CLI ask­ing for the name of the pro­ject, run the script, cre­ate the host­file line for our lo­cal DNS at <project-name>.local, and log the next ac­tion that you need to take to fin­ish in­stalling Wordpress.

Lets take a peek at our file:

echo 'Check to make sure wordpress is here at all'
if test -d ./wp/
  echo 'yup we good'
  echo 'nope we need that'
  degit wp
rsync -va --delete ./plugins/ ./wp/wp-content/plugins/
rsync -va --delete ./theme/ ./wp/wp-content/themes/my-theme

Very sim­i­lar to prestart! The biggest dif­fer­ence is the bit where we use degit to clone Nanobox’s of­fi­cial Wordpress repo into our un­tracked /wp di­rec­tory. Degit will only get the head files, and none of the git his­tory. Nor will it keep the .git file, ba­si­cally mak­ing this a su­per clean, su­per fast way to down­load a di­rec­tory of files. It’s great. The last thing this does is wipe out any themes or plu­g­ins that we don’t want our need in the core files and syncs out own tracked di­rec­to­ries to the cor­rect places in the Wordpress core file struc­ture.

Now would be a time to talk about plu­g­ins.

What’s up with plu­g­ins?

Wordpress has a mil­lion plu­g­ins. We’re go­ing to fo­cus on some of the ba­sic ones that al­most every Wordpress pro­ject ever needs, and should hon­estly be part of Wordpress. Building sites with­out these is a pain. Here they are:

📁 /project-name
⮑ 📁 plugins
  ⮑ 📁 advanced-custom-fields-pro
  ⮑ 📁 custom-post-types-ui
  ⮑ 📁 timber-library
  ⮑ 📁 wp-migrate-db-pro

There are a cou­ple more in my repo to do things like or­der posts in the CMS and im­port CSVs. Not su­per nec­es­sary, so we won’t talk about theme here.

Advanced Custom Fields

ACF is a sta­ple of Wordpress de­vel­op­ment. It lets us de­fine new key/​value pairs to ex­tend the data model of things like posts and pages, and al­lows us to cre­ate a set of global vari­able avail­able from any­where. Sounds sim­ple, sur­pris­ing it’s not part of Wordpress.

Custom Post Types UI

CPT-UI cre­ates an in­ter­face in the ad­min panel for cre­at­ing new post types. Out of the box, Wordpress comes with Posts and Pages. CPT-UI lets us build new types like Projects or Case Studies or what­ever need for our data model. Again, sur­pris­ing that this is­n’t just part of Wordpress. C’est la vivre.

WP Migrate DB

Migrate DB lets us … mi­grate … our … DB. This gives us the abil­ity to sync our data­bases across en­vi­ron­ments and get me­dia up­loads and things with­out need­ing to write magic MySQL queries while tun­neled into open data­base ports on vir­tual ma­chines. This is bet­ter. Believe me.


The Timber li­brary from Upstatement is the great­est thing to hap­pen to Wordpress de­vel­op­ment, af­ter those plu­g­ins that should just be part of Wordpress. Timber in­tro­duces the con­cept of lay­out tem­plates to Wordpress. This lets us write PHP to ma­nip­u­late data, and pass that data to a tem­plate file where we can write Twig tem­plates rather than com­pos­ing strings in PHP. Basically …

<?php echo $myvar ?>

Turns in to:

{% raw %}

{{ myvar }}

{% en­draw %}

This lets us write tem­plates with a tem­plat­ing lan­guage, and write server-side busi­ness logic in a server-side pro­gram­ming lan­guage. Truly rev­o­lu­tion­ary.

What we talk about when we talk about Wordpress de­vel­op­ment: or, The Theme.

With all this ini­tial work around Wordpress core, de­vel­op­ment en­vi­ron­ments, and a ba­sic plu­gin ecosys­tem in place we can start talk­ing about the good stuff: the theme!

📁 /project-name
⮑ 📁 theme
   ⮑ 📁 es6              # Source JS
   ⮑ 📁 scss             # Source SCSS
   ⮑ 📁 routes           # PHP route logic files
      ⮑ 📄 index.php
      ⮑ 📄 page.php
      ⮑ 📄 post.php
   ⮑ 📁 views            # Twig templates
      ⮑ 📁 layouts
      ⮑ 📁 pages
      ⮑ 📁 partials
   ⮑ 📄 functions.php    # This includes routing.
   ⮑ 📄 screenshot.png   # Theme preview image.
   ⮑ 📄 index.php        # Need this, but it's empty.¯\_(ツ)_/¯

We won’t get too deep into this, since we’re get­ting into more con­ven­tional ter­ri­tory here. Basically our es6 di­rec­tory holds source JS that will get com­piled into a bun­dle. Same with the scss di­rec­tory, which gets com­piled into css. We han­dle that with npm scripts in the package.json.

# package.json
  "scripts": {
    "css": "node-sass ./theme/scss/style.scss theme/style.css --watch",
    "js": "rollup -c -w",

Hopefully none of this is to un­usual — if it’s is I rec­om­mend read­ing Paul Pederson’s ex­cel­lent ar­ti­cle on npm scripts.

There is one part of this I want to touch on be­fore mov­ing on:

# package.json
  "scripts": {
    "sync:plugins": "rsync -va --delete ./plugins/ ./wp/wp-content/plugins/",
    "sync:theme": "rsync -va --delete ./theme/ ./wp/wp-content/themes/fuzzco",    
    "watch": "rerun-script",
  "watches": {
    "sync:plugins": "plugins/**/*.*",
    "sync:theme": "theme/**/*.*"

This bit sets up a watcher on our theme and plugins di­rec­tory, which sync our tracked work­ing files to the cor­rect place in our Wordpress core file struc­ture.

Functions, Routes, and Views

The last thing I want to touch on is the ba­sic struc­ture of us­ing Timber to match routes with views.

/** functions.php */
Routes::map('/', function($params){
  Routes::load('routes/page.php', $params, null, 200);
Routes::map('/:page', function ($params) {
  $page = get_page_by_path($params['page']);
  if ($page) {
      Routes::load('routes/page.php', $params, null, 200);
  } else {
      Routes::load('routes/404.php', $params, null, 404);
Routes::map('/blog/:post', function($params){
  Routes::load('routes/post.php', $params, null, 200);

These are Timber routes de­fined in the functions.php file. This re­places the stan­dard rout­ing of Wordpress, and we have change the struc­ture of the Wordpress perma­links to any­thing other than the de­fault to have it work. This is doc­u­mented in Timber.

When our server gets a re­quest at a route of /page-name, it will call the page.php file and pass it the params as­so­ci­ated with the route.

/** page.php */
  $context = Timber::get_context();
  $post = new TimberPost();
  $context['page'] = $post;
  Timber::render( array(
    'views/pages/page-' . $post->post_name . '.twig',
  ), $context );

The page.php file as­signs some vari­ables, in­ter­acts with Wordpress to get and shape our data, and then ren­ders the twig file as­so­ci­ated with the page. In this case, it’s go­ing to see if there’s a tem­plate that matches the name of our page, oth­er­wise it will ren­der the de­fault page tem­plate.

Back to the be­gin­ning

You’ve built your theme! Maybe it’s a sim­ple hello world, maybe it’s a heavy duty big ol’ thing. Either way, it’s time to de­ploy.

You can use Nanobox to cre­ate a droplet for your server. Nanobox will give your pro­ject a name in their sys­tem, and ex­pose the URL for the server at <your-project> I like to use the con­ven­tion project-dev, project-stage, and project-prod. Once you cre­ate your pro­ject in Nanobox, the hard part is over and you can let them do the heavy lift­ing:

$ nanobox deploy project-dev

Or we can map this to our NPM script:

$ npm run deploy:dev  

This will con­tainer­ize our ap­pli­ca­tion, push it to our droplet, hy­drate the en­tire thing, and serve! Now we can use Migrate DB to move our data­base around, and we’re in busi­ness.

Putting it all to­gether

The pro­ject repo is a turnkey, ready to roll ver­sion of all the above. It con­tains all the tool­ing needed to get started, and if you’ve fol­lowed along with this guide, you should be able to get started in no time.

As al­ways, feel free to reach out to me in your venue of choice to talk about any of this — I would be happy to help you set this up for your own Wordpress pro­ject!