Our WordPress Pro setup

At GRRR we've been using WordPress as one of the CMSes of choice. Over the years, we've created a highly opinionated and ever growing WordPress scaffold. This acts as a boilerplate for our new WordPress-based projects, and we'll share some details and insights here.

There are several reasons why we’ve been using WordPress for a while now. At times it would be a client requesting it, while in other cases it seemed like a great match for the project. Recently we’ve added developer ergonomics to that list, since we’ve been improving our setup along the way. It has become very easy to get things going, and keeping it that way.

But first a bit of backstory.


We think WordPress is pretty great, but ‘out of the box’ it has quite a few limitations and shortcomings. In essence, it’s still a blogging platform, albeit a sophisticated one. It can be horrible at performance, especially when having lots of meta fields and big queries. It’s not that scalable too, and it’s not really multi-environment friendly. Its security reputation is not great either, mostly due to many insecure or outdated plugins. So from a developer standpoint, there are a lot of things that seem wrong.

We’ve tried to fix a lot of these wrongs. Since doing so, we’ve started calling it our ‘WordPress Pro’ setup to clients. Compared to ‘a regular WordPress installation’, it’s a lot more in line with other setups we use, meeting more and more of our professional requirements and getting closer to our coding standards.

Meet Bedrock

So what’s pro about it? It started out a few years ago with a new project based on the Bedrock WordPress setup, which they describe as:

‘[…] a modern WordPress stack that helps you get started with the best development tools and project structure.’

This gave us some basic tools to actually want to work with WordPress in a multi-environment setup, incorporating the Twelve-Factor App methodology.

Some of the initial advantages:

  • All environment variables are stored in .env files, meaning it’s far easier to have separate development, staging and production environments.
  • All dependencies (including WordPress itself and its plugins) are managed by Composer. Clients are prevented from installing plugins, allowing all environments to stay the same and replicable.
  • Improved folder structure, resulting in a more developer-oriented/app-based way of working.
  • Passwords stored with bcrypt instead of the vulnerable md5 algorithm.


We’ve been improving this setup ever since. Most of our work can be found in the lib/Grrr-folder, which is located in our starter theme also included in the boilerplate. The Grrr namespace is added by Composer via PSR-4 autoloading.

Here are some highlights of why we think it makes our setup more ‘pro’:

Post types

Custom post types are the core of most of our projects. They’re WordPress’ models, if WordPress would’ve been classically MVC-based. In our setup, they have their own classes, including some sensible defaults. This makes the registration of post types quite easy and maintainable:

<?php namespace Grrr\PostTypes;

class Project extends PostTypeAbstract {
    protected $_type               = 'project';
    protected $_slug               = 'projects';
    protected $_name               = 'Projects';
    protected $_singular_name      = 'Project';
    protected $_icon               = 'dashicons-portfolio';
    protected $_args = [
        'public' => true,
        'has_archive' => true,

Having classes for each post type allows you to bundle functionality for that post type in that class. This results in more consistent and readable code.

A method to fetch posts with custom filtering, for example, could be added in the Project class. You’d then call it like this:

<?php use Grrr\PostTypes\Project;

$posts = (new Project)->get_filtered_posts(['term' => 'foo-bar']);

Twig templates

All templates are converted to Twig, leveraging the Timber package. This means it’s portable, and prevents you from putting complex logic inside your templates.

There are some handy custom Twig filters and functions, for example to automatically get assets which are versioned during deploy:

<link rel="stylesheet" href="{{ asset('styles/base.css') }}"/>

<link rel="stylesheet" 

Integration with ACF

Without Advanced Custom Fields (or ACF) we probably wouldn’t have ever used WordPress. While ACF is great, it requires some work and strict conventions to make it manageable. Especially when taking multiple developers and environments into account.

Flexible Content

We build all our pages with Flexible Content, following a strict naming convention, resulting in a manageable and modular setup. This means all our pages are empty from scratch, and clients can build them to their likings with all the blocks available.

Screenshot of Advanced Custom Fields Flexible Content blocks in WordPress with custom CMS previews

Flexible Content blocks with custom CMS previews.

We also use clone fields a lot, so we don’t have to repeat ourselves and can simply clone complete blocks into the Flexible Content structure.

Screenshot of Advanced Custom Fields blocks naming convention

Hidden blocks which can be cloned into the Flexible Content group.

Screenshot of Advanced Custom Fields blocks naming convention

We create partials for items that are used often, like a button or page header. This keeps everything DRY, and propagates changes to all groups using that button.


We store all fields and groups as JSON in the repo, so we can spawn them easily. This means ACF fields are always in sync between developers and environments.

Since ACF fields are usually tightly coupled to templates or other logic, you wouldn’t want them to live in the database only. If you commit changes in templates corresponding to changes in ACF fields, you shouldn’t have to manually share the updated database with a colleague or update it on a production server. You want the blueprints for those fields to be committed alongside template changes.

The JSON-stored fields still need to be propagated to the database if you want to make changes to them, which usually is only in development. We’ve built a custom sync warning when new ACF changes are pulled, preventing accidental overwriting of those JSON files.

Screenshot of Advanced Custom Fields custom sync warning in WordPress

Custom sync warning alerting colleagues of unsynced changes.

Front-end setup & tooling

Over the period of about ten years, we’ve built quite a neat front-end setup, incorporating lots of best practices to create performant and accessible front-ends. You could take a look at our base stylesheet or the main JavaScript file to get an impression.

To tie everything together, we’re using our own @grrr/gulpfile to handle all tooling. It can handle a lot of tasks, and is mainly used for:

  • Compiling Sass and automatically prefixing CSS properties
  • Bundling and transpiling JavaScript, so we can use modular ES2019+
  • Image and SVG icon optimization
  • Asset versioning/revisioning and cache busting


We’re using Capistrano to deploy projects. This allows you to deploy specific Git branches or versions. It also generates and syncs front-end assets during deploy, and can reload server processes needed to clear caches. It also does rollbacks, if anything goes wrong.

You can quickly SSH-connect to the server via wp ssh <environment>.

More improvements

  • We’ve disabled all REST routes by default for non-logged in users. Exposing all admin users (via /wp/v2/users) is a pretty bad idea. Custom routes are listed by default, although they’re only usable based on a strict ACL.
  • We’ve eliminated all bloat being output by WordPress and the plugins we’re using. It’s pretty hard to spot a site is using WordPress by looking at the source code alone.
  • There are some defaults for a simple cookie bar, Mailchimp integration, Google Tag Manager loading, error reporting with Sentry and custom admin login screen, making the client feel a tiny bit special.

We’ve created an extensive wiki, describing lots of common actions. Or browse through our theme to see how we’ve been handling things.

Use it!

First of all, our setup is highly opinionated. This boilerplate might require tweaking, but could definitely serve as inspiration.

We’ve created a one-command install, which runs a lot of tasks:

$ composer create-project grrr-amsterdam/wordpress-scaffold <name>

Note: it’s important that you’ve met all requirements. Some tasks are quite specific and opinionated as well, so might fail in specific cases. In that case, create an issue. Pull request are welcome too!

Where to go from here?

Recently we’ve been experimenting with a custom static site strategy, resulting in highly performant websites which are 100% static. This includes generating a static version of the whole website, deploying it to an Amazon S3 bucket and invalidating CloudFront. We’ve been using this in some high-profile projects, with great success. We’re planning to include this somewhere in the future! 🎉

Some stuff still on our list

  • Incorporate the new WordPress static site setup (generating and deployment)
  • Add more default Flexible Content building blocks
  • Possibly look into Gutenberg integration
  • Create proper ACF field migrations (renaming fields and migrating the content)
  • Add proper unit- and end-to-end testing

And finally

We hope this has served as inspiration. Got any feedback or improvements? Please let us know! 👇