WordPress vs. CraftCMS

I’ve known and loved WordPress for years, and I’ve used it personally and professionally for a number of projects. But a friend recently introduced me to CraftCMS, and suggested it might be better. The only way to find out is to try them both out and compare!

For the purpose for this comparison, I’m creating a simple website that should look and work identical on both platforms. This will allow me to compare like-for-like more easily. The site itself should be pretty simple, in this case a static site with some categorised product pages. I’ll want the content editing to be as customisable as possible, within certain bounds. For ease of development, since design isn’t what we’re comparing, I’ll be using UIKit to build the components. For the sake of this exercise, I’ll be building a site to showcase various geometric shapes.


As a professional web developer, I like to have all my projects created in a way that makes them easy to update later. I track everything in Git, and use Composer to manage all my dependencies. When setting up our site, I want to ensure I can get up and running quickly and easily, without adding too much of the platform itself into my repo, so that I can easily update it later.

Installing CraftCMS

CraftCMS 3 is a doddle to install, as it’s fully integrated with Composer. A simple command gets everything in place:

composer create-project -s RC craftcms/craft shapemastercraft

There are a couple of extra parameters in here compared to normal, simply because version 3 is currently only in Release Candidate status, but that will change in the coming weeks/months. This command installs CraftCMS and gives me a nice clean directory structure to work with.

Another nice feature is that CraftCMS uses .env files for its environment-specific settings, which is basically the standard now for web projects – you’ll find it used in Laravel too, amongst many other platforms. I can put my database connection details in there, and of course that’s not added to my Git repo, so there’s no conflict with production setup.

I’ve already set up my local server environment to point to the right place, so I can go ahead and navigate to http://dev.shapemastercraftcms.com to load up my site. Immediately I see an error page, but apparently that’s normal because I haven’t set up a template yet. I can however navigate to /admin, which then guides me through the rest of the installation process, which happens to be a breeze. In moments, it’s all set up and I’m logged into the back end.

Installing WordPress

Getting WordPress installed feels like going back in time. A zip file? Seriously? I don’t want the WordPress core in my Git repo, I want to be able to keep it separate. Thankfully, there is a solution that converts WordPress into a Composer dependency (yay!). There are some instructions on http://composer.rarst.net/. I say “instructions”, it’s more of a rough guide, and there is still a lot of trial and error needed to get it working. I ended up referring to a couple of other blogs as well, which filled in some of the gaps. In the end, I created this composer.json file which eventually did the magic install:

  "name": "tbt/shapemasterwp",
  "description": "Test project for WordPress stack via Composer",
  "type": "project",
  "repositories": [
      "type": "composer",
      "url": "https://wpackagist.org"
  "config": {
    "vendor-dir": "wp-content/vendor"
  "require": {
    "composer/installers": "1.5.*",
    "johnpbloch/wordpress": ">=4.9"
  "require-dev": {
  "extra": {
    "wordpress-install-dir": "public/wp",
    "installer-paths": {
      "public/wp-content/plugins/{$name}/": ["type:wordpress-plugin"],
      "public/wp-content/themes/{$name}/": ["type:wordpress-theme"]

With this in place I was finally able to run composer install and have Composer download WordPress into its own directory as a dependency. I also have a public folder, which means I can potentially hide away settings and important files so that they’re not accessible on the web. It also installs themes and plugins in a separate folder, so that updating the WordPress core doesn’t overwrite or delete my work. I have a /public/wp folder that contains the WordPress core, and I’ll never need to change anything in there.

Next, I create /public/index.php:


define('WP_USE_THEMES', true);

And /public/wp-config.php:


ini_set( 'display_errors', 0 );

// ===================================================
// Load database info and local development parameters
// ===================================================
if ( file_exists( dirname( __FILE__ ) . '/../production-config.php' ) ) {
    define( 'WP_LOCAL_DEV', false );
    include( dirname( __FILE__ ) . '/../production-config.php' );
} else {
    define( 'WP_LOCAL_DEV', true );
    include( dirname( __FILE__ ) . '/../local-config.php' );

// ========================
// Custom Content Directory
// ========================
define( 'WP_CONTENT_DIR', dirname( __FILE__ ) . '/wp-content' );
define( 'WP_CONTENT_URL', 'http://' . $_SERVER['HTTP_HOST'] . '/wp-content' );

// ================================================
// You almost certainly do not want to change these
// ================================================
define( 'DB_CHARSET', 'utf8' );
define( 'DB_COLLATE', '' );

// ================================
// Language
// Leave blank for American English
// ================================
define( 'WPLANG', '' );

// ======================
// Hide errors by default
// ======================
define( 'WP_DEBUG_DISPLAY', false );
define( 'WP_DEBUG', false );

// =========================
// Disable automatic updates
// =========================

// =======================
// Load WordPress Settings
// =======================
$table_prefix  = 'wp_';

if ( ! defined( 'ABSPATH' ) ) {
    define( 'ABSPATH', dirname( __FILE__ ) . '/wp/' );
require_once( ABSPATH . 'wp-settings.php' );

This is a handy setup (which I found online somewhere, and sadly can’t remember where) which allows me to specify an environment file in my project root:


define( 'DB_NAME', 'shapemasterwp' );
define( 'DB_USER', 'homestead' );
define( 'DB_PASSWORD', 'secret' );
define( 'DB_HOST', 'localhost' );

ini_set( 'display_errors', E_ALL );
define( 'WP_DEBUG_DISPLAY', true );
define( 'WP_DEBUG', true );

define('AUTH_KEY',         '...');
define('SECURE_AUTH_KEY',  '...');
define('LOGGED_IN_KEY',    '...');
define('NONCE_KEY',        '...');
define('AUTH_SALT',        '...');
define('SECURE_AUTH_SALT', '...');
define('LOGGED_IN_SALT',   '...');
define('NONCE_SALT',       '...');

With all this done, I can finally bring up http://dev.shapemasterwp.com/wp/wp-admin in my browser. The WordPress installation process kicks in, and a few moments later I’m logged into the back end.

But before we go any further we need to quickly sort out the site URL. Because we’ve installed the WP core in its own folder, we need to go into the General settings and change the Site Address so that it doesn’t include the trailing ‘/wp’. The WordPress Address retains it. Now I can visit http://dev.shapemasterwp.com and see my website, which, as expected, is just a blank screen, because I haven’t installed a theme yet.


CraftCMS wins hands down on this bit. With a bit (a.k.a. a lot) of jiggerypokery we can get WordPress installed as a Composer dependency, but it’s not standard and requires manually creating several files. It only needs to be done once, and updates to the WP core can now be done with composer update , but it’s hardly intuitive. CraftCMS installs in moments, giving me a clean developer-friendly directory structure that can easily be tracked via Git.

Creating a custom theme

Off-the-shelf themes are great if you’re not a developer. But (as you’ll have guessed) I am. I want to make my own from scratch. Nothing fancy on this occasion, just something quick and functional. For now, all I want is a home page where I can put some basic content.

Templating in CraftCMS

This is a game of two halves. One the one hand, you’ve got template files using Twig, neatly and logically arranged in a /templates folder. Sweet. Here’s my /templates/_layouts/master.twig file:

<!doctype html>
<html lang="en">
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>{{ siteName }} : {{ pageName|default(entry.title|default('')) }}</title>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.40/css/uikit.min.css" integrity="sha256-VyWNo3nreq7kl76bp/ETa0Tbq3FVqCd6wCMF49aGP4c=" crossorigin="anonymous" />
        {{ head() }}

        <nav class="uk-navbar-container uk-margin" uk-navbar>

{% block content %}{% endblock %}
https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.40/js/uikit.min.js https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.40/js/uikit-icons.min.js {{ endBody() }} </body> </html>

I’m including UIKit and jQuery from a CDN, just for convenience. There are a few CraftCMS-specific tags available, which allows me to insert things like the site name and URL. I’m also defining one block called ‘content’, which I’ll populate in my home page’s template file, /templates/home-page/_entry.twig:

{% extends '_layouts/master.twig' %}

{% block content %}
    'Hello world!'
{% endblock %}

Beautiful. But not yet connected up to my actual content.

In CraftCMS we have the concepts of ‘sections’, ‘entries’ and ‘fields’. A section is an area of your site, which likely contains one or more entries, and each entry is defined by one or more fields. All of these need setting up via CraftCMS’s settings screens. You cannot do this via code, and it’s all stored in the database. I’ll be honest, this wasn’t exactly intuitive, and took some back-and-forth before I finally had it all set up correctly. To begin with I just set up one text field called ‘body’, which meant I could reference it in my template:

{% extends '_layouts/master.twig' %}

{% block content %}
    {{ entry.body }}
{% endblock %}

Now, finally, I can open up the public home page and see my home page.

Templating in WordPress

The first step is to create myself a basic (and compulsory) stylesheet file in /public/wp-content/themes/shapemaster/style.css. Since I’m pulling in UIKit from a CDN I don’t actually have any need of a stylesheet yet, but it’s how WordPress identifies the theme.

Theme Name: ShapeMaster WP

We also need an index.php file. Best practice tells us to separate out the header and footer of our site so that the common elements only need specifying once. Since WordPress doesn’t have any concept of template inheritance, we’re left with includes. Here’s my header.php file:

<!doctype html>
<html lang="en">
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title><?php bloginfo('name'); ?> : <?php the_title(); ?></title>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.40/css/uikit.min.css" integrity="sha256-VyWNo3nreq7kl76bp/ETa0Tbq3FVqCd6wCMF49aGP4c=" crossorigin="anonymous" />
        <?php wp_head(); ?>

        <nav class="uk-navbar-container uk-margin" uk-navbar>


And my footer.php file:

https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.40/js/uikit.min.js https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.40/js/uikit-icons.min.js <?php wp_footer(); ?> </body> </html>

And finally my index.php file:

<?php get_header(); ?>

<?php if (have_posts()) : the_post(); ?>
<?php the_content(); ?>
<?php endif; ?>

<?php get_footer(); ?>

It’s pretty self-explanatory really, if a little clunky. I can now activate the theme in the WordPress admin, view my site in the browser, and it’ll show my site.


Both platforms have their strengths and weaknesses when it comes to templating.

WordPress feels like a dinosaur with its old-school file inclusion approach. But it’s easy to get started, and everything in my theme is defined in my codebase, so I know I can track it and deploy it easily.

CraftCMS uses Twig, meaning it can benefit massively from the advanced features of that templating language. It’s code is neater, and will ultimately be more powerful. And it ensures that no dangerous PHP can sneak in and cause unexpected problems. Where CraftCMS falls down (massively, I think) is that half of its template definition is held in the database, and can only be administered visually. Once you know where everything is, it’s pretty easy to add new sections and fields and so on (and those fields are very powerful, as we’ll see shortly), but I can’t add that structure to my Git repo, which means it’s going to be an absolute headache to deploy.

I’d call this a draw. WordPress is simultaneously fairly good and fairly bad. CraftCMS is simultaneously really really good and really really bad.

Product pages

Next up, we want to create some product pages for our shapes. We’ll need a couple of different ranges, each containing an assortment of products that can be managed in the back end. Each product needs to have some engaging content. And of course we’ll need to integrate some navigation so that people can find the products.

Products in CraftCMS

Setting up product pages and range pages in CraftCMS is really no different from setting up a basic page, just a little more fiddly. It all happens in the admin area, so there’s no code I can show you for the setup.

A really nice feature of CraftCMS is its choice of field types. There are the obvious ones, of course, but there’s also one called ‘Matrix’. This allows the content editor to add additional fields in whatever order and quantity as necessary, but only within the options you specify. This means you can keep close control of how the site looks, while also providing flexibility. Combine this with the Redactor plugin (which provides a rich text editor, which sadly doesn’t come bundled with CraftCMS) and you’ve got a whole world of joy that you can pass on to your content editors.

I ended up creating a reusable Twig file for displaying my content areas, which loops through the different field types available in the matrix and displays them nicely. I can use that Twig template in my product pages and also my home page, allowing me to effortlessly integrate image carousels, hero blocks, and more.

{% for item in productContent.all() %}
    {% if item.type == 'hero' %}

{{ item.heroTitle }}

{{ item.heroSubtitle }}

{% elseif item.type == 'description' %}
{{ item.body }}
{% elseif item.type == 'slider' %} {% include '_components/_slider.twig' with {'slider': imageSlider} only %} {% endif %} {% endfor %}

There’s also a specific sub-template for creating the slider, using UIKit’s functionality:

    {% for item in slider.all() %}
  • {% endfor %}

    My minimal product template now looks like this:

    {% extends '_layouts/master.twig' %}
    {% block content %}

    {{ entry.title }}

    </div> {% include '_components/_productContent.twig' with {'productContent': entry.productContent, 'imageSlider': entry.imageSlider} only %} {% endblock %}

    Finally, to add the navigation into my master template, we can loop through the available productRange category sections we’ve created in CraftCMS:


    I know I haven’t explained absolutely everything line by line, but my general opinion has been that creating the product pages was pretty easy and very effective. It didn’t take me long, even though I started with zero experience of CraftCMS.

    Custom post types in WordPress

    To create the same sort of site structure in WordPress, we’ll need to create our own custom post type. To accomplish this, we’ll need our own plugin, which I’m creating in /public/wp-content/plugins/shapemaster/plugin.php. Here’s the main bit of code that makes it all happen:

    add_action('init', function() {
        register_post_type('product', [
            'label' => 'Product',
            'labels' => [
                'name' => 'Products',
                'singular_name' => 'Product'
            'taxonomies' => ['product_range'],
            'supports' => ['title', 'editor', 'thumbnail', 'revisions'],
            'public' => true,
            'hierarchical' => true,
            'has_archive' => true,
            'rewrite' => [
                'with_front' => false
        register_taxonomy('product_range', 'product', [
            'label' => 'Range',
            'rewrite' => ['slug' => 'range'],
            'hierarchical' => true

    Refresh the WP admin, and as if my magic I have a new section on the left hand navigation for my products, and a new taxonomy for the product ranges (which works much like a category).

    Creating the range page is relatively straightforward:

    <?php get_header(); ?>
    <h1>Range: <?php single_term_title(); ?></h1>
    <?php if (have_posts()) : ?>


    </div> <?php endwhile; ?> </div> <?php else : ?> <p>No products found.</p> <?php endif; ?> <?php get_footer(); ?>

    I could have created a specific template for the products themselves, but on this occasion (since I was getting bored by this point) I decided to reuse the main index.php file. I was intrigued by the power of CraftCMS’s matrix field type, so I installed SiteOrigin’s Page Builder plugin, which provides a similar sort of experience. I had to install their widget bundle plugin as well, and I ended up creating my own widget to allow me to define a hero section. To be honest, this was the hardest part of the whole process, and the experience for a content editor is far from perfect. But I eventually had it working, and to the eyes of an everyday punter the two sites now look pretty much identical.


    Defining the data structure in the code is far more fiddly than in CraftCMS, and takes longer to get working smoothly. On the other hand, it’s all there in the code, making future development and deployment easier. From a content editor’s perspective, CraftCMS is the clear winner here, as even with the Page Builder plugin the user experience is a bit clunky. Another consideration is that I enjoyed creating the product pages in CraftCMS, whereas in WordPress it was more painful.


    This has been a fairly brief look at WordPress and CraftCMS, and neither appears to be 100% perfect. As a developer, I like to define things in code. The trouble is, WordPress’s code is awful. It feels outdated, and doesn’t have the same spirit of elegance that I’m accustomed to with other modern frameworks. Deployment is undoubtedly going to be easier with WordPress, but that alone won’t necessarily make for a better site. And of course we all know about the bloat of third-party plugins and a slew of security considerations…

    CraftCMS on the other hand was a joy to use. It installed in moments, uses Twig for its templating, and encourages creativity and flexibility with its content. All good things. Its reliance on storing content structure in the database is cause for concern though, and I really hope that’s something the community finds a solution to one day. But overall, CraftCMS doesn’t hold your hand too much, and gives a competent developer plenty of control over exactly how the site looks and feels.

    You hack WordPress to make it work the way you want, whereas you carefully develop CraftCMS from the ground up, which I imagine would result in a more predictable and better website. WordPress has undoubtedly changed the internet in the last few years, but unless it completely revamps itself it’s liable to end up falling out of favour, to be replaced by more forward-thinking platforms like CraftCMS.

    Which would I choose for my next project? It depends. Both have their place. But CraftCMS might just pip WordPress to the post…