Webpack for WordPress

With the rise of React in WordPress, there are more and more developers turning to webpack for managing and bundling front end assets. In this post, I’ll cover webpack at a high level (what it is, and why we might reach for it), and show you how you can unleash the power of webpack in your WordPress projects to manage and bundle front end assets.

What is webpack and why should I consider using it?

At first glance, it’s really tempting to classify webpack as yet another front end task runner. Not unlike Grunt or Gulp, it can perform tasks such as JavaScript and Sass compilation and minification. But the way in which webpack works with these files is what makes webpack stand out.

When you run webpack within your project, it looks through the project for the types of files you asked it to process. This could include JavaScript, styles (e.g. CSS / Sass), icons (svg), images, etc. When webpack processes these files, it looks through the code and bundles only what is used in the source code. In webpack, this is known as the dependency graph. The result of the dependency graph is that your final bundle only includes the files required for your application, and nothing more.

Getting started with webpack

The main requirements for getting started with webpack include:

  • Node & NPM
    Follow the instructions for installing Node on the Node site (or if you’re on a Mac, it’s super easy to manage with brew). NPM comes bundled with Node, but understanding the Node package ecosystem is helpful.
  • A little JavaScript knowledge
    Webpack is JavaScript-driven, and understanding JS basics will make working with weback easier.
  • Patience
    The webpack ecosystem takes some time to wrap one’s head around. Research and experimentation will serve you well in your journey to learn webpack.

Creating a webpack file for a WordPress theme

For the demo portion of this post, I’ll be taking you through the process of adding webpack support to _s. If you’d like to code along, go ahead and clone the repo locally and checkout the webpack-start branch. To save time and headache, all the required npm packages are listed in package.json so you will only need to install them.

git clone https://github.com/carrieforde/_s-with-webpack.git _s
git checkout -b webpack-start origin/webpack-start
cd _s
npm install

Side note: the above assumes you already have a local WordPress development environment set up. If you’re not familiar with local WordPress development, I highly suggest checking out Local by Flywheel.

Before we get into the nitty gritty of setting up webpack for _s, let’s talk a little about what would be useful:

  • JavaScript compilation
    There are a handful of JavaScript files in the theme, and as we build out projects on _s, it would be nice to have a way to easily concatenate and bundle our JavaScript files.
  • Sass compilation
    _s comes with Sass, so including functionality to bundle our stylesheet(s) would be really helpful.
  • Code Sniffing / Linting
    Since _s already ships with phpcs.xml.dist, a configuration file for PHP code sniffing, let’s go ahead and include code sniffing / linting for our Sass and JavaScript code according to the WordPress Coding Standards.
  • Browser reloading
    During development, it’s super helpful to immediately see our changes in the browser. We’ll use BrowserSync to reload our local site in the browser every time we save changes in within the theme.
  • Icon & image management
    Finally, it would be a good idea to have a way to manage icons and images used within the theme. For this, we’ll add some functionality to bundle, concatenate, and minify our SVG and image files.

Basic webpack setup

Technically speaking, webpack 4 doesn’t actually require any sort of configuration file. webpack will automatically look for src/index.js as your application’s entry point. Running node_modules/.bin/webpack (or simply, webpack, if you have it installed globally on your machine) within your _s repo, will then produce dist/main.js.

And this is pretty great if you only need to deal with JavaScript. In fact, you could easily build an entire JavaScript or React application without any additional configuration. But that’s not how the real world of front end development works, and we’ll want to have a bit more control over our setup.

Go ahead and open up webpack.config.js within the _s directory. The first thing we’re going to do is declare a variable at the top of the file:

const path = require('path');

path is a built in Node module, that will allow us to declare relative paths to files throughout _s, and prevent any clashes with paths in our operating system. During this tutorial, you’ll often see path used with the __dirname global.

Next, let’s start building our configuration object. Since webpack works in a Node environment, we’ll assign our configuration object to module.exports. According to TutorialsTeacher.com

module is a variable that represents current module and exports is an object that will be exposed as a module. So, whatever you assign to module.exports or exports, will be exposed as a module.

Within our configuration object let’s declare the entry and output properties.

module.exports = {
  context: __dirname,
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'bundle.js'
  }
};
  • context is an absolute path to your application so webpack always knows where to run. It’s not strictly required, but means that you can run webpack from anywhere in your app without issue.
  • entry defines the starting point of our application. The content of your entry file may vary greatly. It may be used to import other JavaScript files using ES6 import, or may even be a regular JS file.
  • output specifies the file(s) generated by the application.
    • path defines the directory in which we’ll find our bundled assets.
    • filename is the name of the JavaScript file generated by the application.

Now that we’ve defined our basic configuration, let’s open index.js and import the files in our js directory using the import statement.

import './js/customizer';
import './js/navigation';
import './js/skip-link-focus-fix';

Go ahead and entry node_modules/.bin/webpackin your terminal (within the _s directory), and you should see a minified file in the public directory that looks something like this:

!function(e){var t={};function n(i){if(t[i])return t[i].exports;var a=t[i]={i:i,l:!1,exports:{}};return e[i].call(a.exports,a,a.exports,n),a.l=!0,a.exports}n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var a in e)n.d(i,a,function(t){return e[t]}.bind(null,a));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";n.r(t);n(1),n(2),n(3)},function(e,t){!function(e){wp.customize("blogname",function(t){t.bind(function(t){e(".site-title a").text(t)})}),wp.customize("blogdescription",function(t){t.bind(function(t){e(".site-description").text(t)})}),wp.customize("header_textcolor",function(t){t.bind(function(t){"blank"===t?e(".site-title, .site-description").css({clip:"rect(1px, 1px, 1px, 1px)",position:"absolute"}):(e(".site-title, .site-description").css({clip:"auto",position:"relative"}),e(".site-title a, .site-description").css({color:t}))})})}(jQuery)},function(e,t){!function(){var e,t,n,i,a,o;if((e=document.getElementById("site-navigation"))&&void 0!==(t=e.getElementsByTagName("button")[0]))if(void 0!==(n=e.getElementsByTagName("ul")[0])){for(n.setAttribute("aria-expanded","false"),-1===n.className.indexOf("nav-menu")&&(n.className+=" nav-menu"),t.onclick=function(){-1!==e.className.indexOf("toggled")?(e.className=e.className.replace(" toggled",""),t.setAttribute("aria-expanded","false"),n.setAttribute("aria-expanded","false")):(e.className+=" toggled",t.setAttribute("aria-expanded","true"),n.setAttribute("aria-expanded","true"))},a=0,o=(i=n.getElementsByTagName("a")).length;a<o;a++)i[a].addEventListener("focus",s,!0),i[a].addEventListener("blur",s,!0);!function(e){var t,n,i=e.querySelectorAll(".menu-item-has-children > a, .page_item_has_children > a");if("ontouchstart"in window)for(t=function(e){var t,n=this.parentNode;if(n.classList.contains("focus"))n.classList.remove("focus");else{for(e.preventDefault(),t=0;t<n.parentNode.children.length;++t)n!==n.parentNode.children[t]&&n.parentNode.children[t].classList.remove("focus");n.classList.add("focus")}},n=0;n<i.length;++n)i[n].addEventListener("touchstart",t,!1)}(e)}else t.style.display="none";function s(){for(var e=this;-1===e.className.indexOf("nav-menu");)"li"===e.tagName.toLowerCase()&&(-1!==e.className.indexOf("focus")?e.className=e.className.replace(" focus",""):e.className+=" focus"),e=e.parentElement}}()},function(e,t){/(trident|msie)/i.test(navigator.userAgent)&&document.getElementById&&window.addEventListener&&window.addEventListener("hashchange",function(){var e,t=location.hash.substring(1);/^[A-z0-9_-]+$/.test(t)&&(e=document.getElementById(t))&&(/^(?:a|select|input|button|textarea)$/i.test(e.tagName)||(e.tabIndex=-1),e.focus())},!1)}]);

Setting up development modes

The bundle.js file above is a bit gnarly, right? By default, wepack runs in production mode. This means that every time we run webpack our files are not only bundled, but they’re also minified.

Minification is the process of stripping out whitespace, and replacing other unneeded characters in our source code. So, for example, instead of seeing variables with sensible names in our code, like container, we see e. And while minification is great for production ready code, it’s not so great during development. Let’s update our config to run in development mode by default instead.

module.exports = {
  context: __dirname,
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'bundle.js'
  },
  mode: 'development'
};

In the code above, you’ll see I added the mode property. Since webpack defaults to production, I set it to development. Now when you run node_modules/.bin/webpack in your project, you’ll see a verbose, human-readable file.

We can enhance this a step further, however. Let’s say we had a bug in our navigation.js. If an error is thrown in the developer console of our browser, it’s going to point to a line in bundle.js, not our navigation.js. And while it’s true that in development mode, our file is more human readable, I don’t think anyone wants to waste their time tracking down a bug in one file just to find fix it in another. That’s where source maps come to the rescue.

A source map helps map the code we write in our files, to what is read by the browser (i.e. bundle.js). Now, if I leave a console.log() statement on line 10 of navigation.js, I’ll be able to see that in my developer tools.

Source maps are added using the devtool property:

module.exports = {
  context: __dirname,
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'bundle.js'
  },
  mode: 'development',
  devtool: 'source-map'
};

source-map is one among a wide variety of source map options for the devtool property. I recommend reviewing the webpack documentation on devtool to learn more about your options.

Now that we have the basics of our webpack config laid out. let’s try running node_modules/.bin/webpack again. If everything worked correctly, we should see a human-readable bundle.js, and if we have errors or console.log() statements, we’ll see where they are in our source code.

An example of source maps in action. The developer tools show that I have a console.log() statement on line 10 of navigation.js, and a problem with jQuery in customizer.js.
Source maps show us that we have a console.log() on line 10 of navigation.js, and a problem with jQuery in our customizer.js.

Adding Node scripts

Typing node_modules/.bin/webpack or webpack every time we want to bundle our files is a little tiresome. Let’s go ahead and open up our package.json file and add a few scripts that will help automate our webpack processes.

Within the "scripts" object of package.json, add "dev": "webpack --watch". Now, you can type npm run dev in your terminal, and every time you save a index.js or one of the files in the js directory, webpack will automatically rebundle.

And since we turned production mode off by default, it would be good to add a script that will bundle our files for production (including minification). Let’s also add "build": "webpack --mode=production" to our script object, which will not only bundle our code, but it will also minify it.

Go ahead and run npm run build and have a peek at your bundle.js file to ensure that you have a minified file, and that it’s working as expected.

Your final "scripts" object should look something like this:

{
  "scripts": {
    "build": "webpack --mode=production",
    "dev": "webpack --watch",
    "eslint": "eslint \"src/**/*.{js,jsx}\" --quiet",
    "eslint:fix": "eslint \"src/**/*.{js,jsx}\" --quiet --fix",
    "format": "prettier --write \"src/**/*.{js,jsx,scss}\"",
    "stylelint": "stylelint \"src/**/*.{css,scss}\"",
    "stylelint:fix": "stylelint \"src/**/*.{css,scss}\" --fix",
    "wp-pot": "wp-pot --src '*.php' --dest-file 'languages/_s.pot' --domain '_s'"
  }
}

We can’t use npm run dev while updating webpack.config.js. You’ll have to either stop then restart the dev script after you’ve made a change, or continue typing our node_modules/.bin/webpack or webpack until your configuration is complete.

Loaders

Now that we have our webpack config set up to bundle JavaScript files, let’s talk about loaders. As we’ve discovered so far, webpack is able to understand and process JavaScript files by default. But even this handling of JavaScript files is limited to compilation and minification. Let’s say you are using the latest and greatest in JS, for example, ES2019. As developers, we know it takes time for new features to be available in all browsers, and we’ll typically need some way of transpiling, or converting, our modernized code into a version understandable by older browsers (e.g. ES6 into ES5).

Loaders help extend the functionality of webpack to allow it to further process JS files (e.g. using Babel for transpilation), or to process other types of files (e.g. Sass, images, etc.), which are converted into other modules needed by our application. Loaders are the secret 🗝to wepack’s magic. ✨

Let’s first take a look at using the Babel loader to help us transpile our ES Next code into something that browsers down to IE11 will be able to understand. You’ll see I have included a Babel configuration file within the project that looks like this:

{
  "presets": [
    [
      "@babel/env",
      {
        "targets": {
          "browsers": ["last 2 versions"]
        }
      }
    ]
  ]
}

Without going too deep in the weeds, the basic idea here is that Babel will look through the source code we create, and during the transpilation process, will turn our JavaScript into code supported by the last two versions of all the major browsers (including IE11). But before this actually happens, we need to set up our babel-loader in the webpack config.

Loaders live within a rules array that is nested within a module object. Each loader is an object that consists of a test property, which determines which files will be targeted for the transform, and a loader property, which tells webpack which loader to use for the actual transformation. Here’s our webpack file with the Babel loader:

const path = require('path');

module.exports = {
  context: __dirname,
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'bundle.js'
  },
  mode: 'development',
  devtool: 'cheap-eval-source-map',
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: 'babel-loader'
      }
    ]
  }
};

Let’s take a closer look at the object for Babel:

{
  test: /\.jsx?$/,
  loader: 'babel-loader'
}
  • test uses a regular expression to match files ending in either .js or .jsx. In this expression, ? means that the proceeding character (the x) is optional, and $ means that the file we’re evaluating should end with this string.
  • loader tells webpack to use babel-loader to perform the transformation.

If you want to learn more about regular expressions, I wrote a lengthy post about using regex to create a basic JSON validator and a link harvester.

We can test that our new rule is working by refactoring navigation.js to use ES6 features including const, let, arrow functions, and forof loops:

/**
 * File navigation.js.
 *
 * Handles toggling the navigation menu for small screens and enables TAB key
 * navigation support for dropdown menus.
 */
(function() {
  let container, button, menu, links, i, len;

  container = document.getElementById('site-navigation');
  if (!container) {
    return;
  }

  button = container.querySelector('button'); // querySelector will automatically return the first match.
  if ('undefined' === typeof button) {
    return;
  }

  menu = container.querySelector('ul');

  // Hide menu toggle button if menu is empty and return early.
  if ('undefined' === typeof menu) {
    button.style.display = 'none';
    return;
  }

  menu.setAttribute('aria-expanded', 'false');
  if (!menu.classList.contains('nav-menu')) {
    menu.classList.add('nav-menu');
  }

  button.onclick = () => {
    if (container.classList.contains('toggled')) {
      container.classList.remove('toggled');
      button.setAttribute('aria-expanded', 'false');
      menu.setAttribute('aria-expanded', 'false');
    } else {
      container.classList.add('toggled');
      button.setAttribute('aria-expanded', 'true');
      menu.setAttribute('aria-expanded', 'true');
    }
  };

  // Get all the link elements within the menu.
  links = menu.querySelectorAll('a');

  // Each time a menu link is focused or blurred, toggle focus.
  for (const link of links) {
    link.addEventListener('focus', toggleFocus, true);
    link.addEventListener('blur', toggleFocus, true);
  }

  /**
   * Sets or removes .focus class on an element.
   */
  function toggleFocus() {
    const self = this;

    // Move up through the ancestors of the current link until we hit .nav-menu.
    while (!self.classList.contains('nav-menu')) {
      // On li elements toggle the class .focus.
      if ('LI' === self.tagName) {
        if (self.classList.contains('focus')) {
          self.classList.remove('focus');
        } else {
          self.classList.add('focus');
        }
      }

      self = self.parentElement;
    }
  }

  /**
   * Toggles `focus` class to allow submenu access on tablets.
   */
  (function(container) {
    const parentLink = container.querySelectorAll('.menu-item-has-children > a, .page_item_has_children > a');
    let touchStartFn, i;

    if ('ontouchstart' in window) {
      touchStartFn = e => {
        const menuItem = this.parentNode;
        let i;

        if (!menuItem.classList.contains('focus')) {
          e.preventDefault();
          for (i = 0; i < menuItem.parentNode.children.length; ++i) {
            if (menuItem === menuItem.parentNode.children[i]) {
              continue;
            }
            menuItem.parentNode.children[i].classList.remove('focus');
          }
          menuItem.classList.add('focus');
        } else {
          menuItem.classList.remove('focus');
        }
      };

      for (const child of parentLink) {
        child.addEventListener('touchstart', touchStartFn, false);
      }
    }
  })(container);
})();

Now after running npm run dev, we see that the arrow function has been transformed into a regular function, the forof loop became a standard for loop, and usage of let and const has been transformed into var.

The bundle.js file after the JS has been run through Babel. We no longer see arrow functions, and the for of loop was transformed into a regular for loop.
The arrow function is now a standard function, and the forof was transformed into a for loop.

We can enhance our JavaScript processing a bit further by adding the eslint-loader to lint our files, and print errors in the console as we save.

const path = require('path');

module.exports = {
  context: __dirname,
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'bundle.js'
  },
  mode: 'development',
  devtool: 'cheap-eval-source-map',
  module: {
    rules: [
      {
        enforce: 'pre',
        exclude: /node_modules/,
        test: /\.jsx$/,
        loader: 'eslint-loader'
      },
      {
        test: /\.jsx?$/,
        loader: 'babel-loader'
      }
    ]
  }
};

Going beyond JavaScript

As I alluded earlier, loaders enable us to work with more than just JavaScript. And even though _s ships with [optional] Sass, it doesn’t ship with a way to compile that Sass. Let’s set up some loaders that will help us compile styles.

Processing Sass with webpack is a bit more complex than working with JavaScript, and we’ll need more than one loader to get our Sass to compile to a usable format.

Let’s start by adding this object below our babel-loader object:

{
  test: /\.s?css$/,
  use: ['style-loader', 'css-loader', 'sass-loader']
}

You’ll notice that instead of the loader property, we’re using the use property, which is able to take an array of loaders. Now, we can add an import line to our index.js file to tell webpack where our styles live:

import './sass/style.scss';

If we run npm run build, we should find that everything compiles successfully, but if you take a peak in the public folder, you won’t see a compiled CSS file. When working in a static environment, webpack and review the DOM and inline only the styles needed for an individual page. But because WordPress works in a server environment, this doesn’t quite work for us. We need a way of extracting our styles to a separate file so we can enqueue theme in the theme’s function.php file.

Plugins

To help us process our Sass, we’re going to enlist the help of MiniCssExtractPlugin.

Plugins in webpack aren’t too dissimilar from loaders. They fill in the gaps in our build and bundle processes by doing the work that loaders can’t. When it comes to working with webpack’s loader and plugin ecosystem, the general rule of thumb I follow is to see if there is a loader to do the job first, and if not, enlist the help of a plugin.

The purpose of the MiniCssExtractPlugin extract the CSS from our source files into a separate file (or files) from the rest of our bundle. In our case, we’ll extract our styles the Sass partials, compile them, and then extract them into a separate stylesheet for enqueueing.

The first thing we’ll need to do is require the MiniCssExtractPlugin in the top of the file, and then we can add the plugins property to the configuration object.

const path = require('path'),
  MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  context: __dirname,
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'bundle.js'
  },
  mode: 'development',
  devtool: 'source-map',
  module: {
    rules: [
      {
        enforce: 'pre',
        exclude: /node_modules/,
        test: /\.jsx$/,
        loader: 'eslint-loader'
      },
      {
        test: /\.jsx?$/,
        loader: 'babel-loader'
      }
    ]
  },
  plugins: [new MiniCssExtractPlugin({ filename: '../style.css' })]
};
  • const MiniCssExtractPlugin = require('mini-css-extract-plugin') saves MiniCssExtractPlugin to a variable for use in our configuration object.
  • plugins is an array, and each plugin added to the array will require the new keyword to instantiate the plugin. Plugin are usually configurable and can take a configuration object.

The only option we need to pass to MiniCssExtractPlugin at this time is an object with a filename property, and a string value that points to our style.css in the root of our project.

const path = require('path'),
  MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  context: __dirname,
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'bundle.js'
  },
  mode: 'development',
  devtool: 'source-map',
  module: {
    rules: [
      {
        enforce: 'pre',
        exclude: /node_modules/,
        test: /\.jsx$/,
        loader: 'eslint-loader'
      },
      {
        test: /\.jsx?$/,
        loader: 'babel-loader'
      },
      {
        test: /\.s?css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
      }
    ]
  },
  plugins: [new MiniCssExtractPlugin({ filename: '../style.css' })]
};

The only difference between our previous loader for managing styles, and our updated loader is that we added MiniCssExtractPlugin.loader as the first item in the use array.

It’s important to note that when we’re using multiple loaders together, the loaders need to be arranged in the use array from last to first to process.

Now we can run npm run dev or npm run build, and we’ll see that our styles have been successfully compiled to style.css in the root of our project. However, if you’ve run npm run build, you’ll notice that our style file isn’t minified. We’ll have to add a separate plugin to minify our style file.

Using plugins for bundle optimization

It’s really great that webpack minifies our bundle by default when we run npm run build. But webpack also gives us flexibility when it comes to optimization. So if the standard, built-in minifier doesn’t cut it, as is the case with our styles, then we can add our own optimization tools.

We need to require two more plugins and set up an optimization object to process and minify our files.

const path = require('path'),
  MiniCssExtractPlugin = require('mini-css-extract-plugin'),
  UglifyJSPlugin = require('uglifyjs-webpack-plugin'),
  OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
  context: __dirname,
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'bundle.js'
  },
  mode: 'development',
  devtool: 'source-map',
  module: {
    rules: [
      {
        enforce: 'pre',
        exclude: /node_modules/,
        test: /\.jsx$/,
        loader: 'eslint-loader'
      },
      {
        test: /\.jsx?$/,
        loader: 'babel-loader'
      },
      {
        test: /\.s?css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
      }
    ]
  },
  plugins: [new MiniCssExtractPlugin({ filename: '../style.css' })],
  optimization: {
    minimizer: [
      new UglifyJSPlugin(),
      new OptimizeCssAssetsPlugin()
    ]
  }
};

You’ll notice that I required both the UglifyJSPlugin and OptimizeCSSAssetsPlugin. In webpack, when you use a custom minifier, the default minifier no longer works, so we need to include UglifyJSPlugin to take care of our JavaScript minification.

We also added an optimization object to the webpack configuration. This contains an array of the minimizers we’ll use and instantiates them. The optimization object is ignored when we’re in development mode.

Browser Reloading with BrowserSync

webpack comes with it’s own auto-reloading devServer, but it’s difficult to get it working with WordPress. So instead, we’ll utilize the BrowserSync plugin to handle reloading our browser every time we make a change.

As before, our first step is to require the BrowserSyncPlugin.

const path = require('path'),
  MiniCssExtractPlugin = require('mini-css-extract-plugin'),
  UglifyJSPlugin = require('uglifyjs-webpack-plugin'),
  OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'),
  BrowserSyncPlugin = require('browser-sync-webpack-plugin');

module.exports = {
  context: __dirname,
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'bundle.js'
  },
  mode: 'development',
  devtool: 'source-map',
  module: {
    rules: [
      {
        enforce: 'pre',
        exclude: /node_modules/,
        test: /\.jsx$/,
        loader: 'eslint-loader'
      },
      {
        test: /\.jsx?$/,
        loader: 'babel-loader'
      },
      {
        test: /\.s?css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: '../style.css' }),
    new BrowserSyncPlugin({
      files: '**/*.php',
      proxy: 'http://underscores.test'
    })
  ],
  optimization: {
    minimizer: [new UglifyJSPlugin(), new OptimizeCssAssetsPlugin()]
  }
};

Then, we instantiate the BrowserSyncPlugin in the plugins array, and pass a few values within the configuration object:

  • files: BrowserSync will automatically watch for changes to any files connected to our entry, including both JS and Sass files. We can use this property to tell BrowserSync to watch for other types of files, in this case PHP files, in our project.
  • proxy: this is the URL we’re using for local development. If you’ve set up your local environment to use a different URL, be sure to update the proxy property.

Now when you run npm run dev, BrowserSync will open up a new tab in your browser, and refresh the page every time you save a file. 🎉

Multiple entries & outputs

Whew. We’ve gotten through the basics of webpack, and just scratched the surface of what webpack can do to improve our development workflow. I wanted to revisit the entry and output properties quickly.

Right now, our index.js file looks like this:

// Theme Sass.
import './sass/style.scss';

// Theme JavaScript.
import './js/customizer';
import './js/navigation';
import './js/skip-link-focus-fix';

Notice anything amiss here?

There’s no reason we need to load customizer.js on the front end (in fact, it’s enqueue through customizer.php in _s by default). Enqueuing customizer.js as-is doesn’t reap the same minification benefits that we have for the rest of our JavaScript. The great news is that webpack can actually take multiple entry files, and produce multiple output bundles.

The first thing we’ll need to do is move customizer.js to the root of src. This isn’t strictly necessary, but I feel like it keeps our bundle sources a little cleaner. (Make sure you remove the import from index.js, too!)

Next, we’ll update our webpack file.

const path = require('path'),
  MiniCssExtractPlugin = require('mini-css-extract-plugin'),
  UglifyJSPlugin = require('uglifyjs-webpack-plugin'),
  OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'),
  BrowserSyncPlugin = require('browser-sync-webpack-plugin');

module.exports = {
  context: __dirname,
  entry: {
    frontend: './src/index.js',
    customizer: './src/customizer.js'
  },
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: '[name]-bundle.js'
  },
  mode: 'development',
  devtool: 'source-map',
  module: {
    rules: [
      {
        enforce: 'pre',
        exclude: /node_modules/,
        test: /\.jsx$/,
        loader: 'eslint-loader'
      },
      {
        test: /\.jsx?$/,
        loader: 'babel-loader'
      },
      {
        test: /\.s?css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: '../style.css' }),
    new BrowserSyncPlugin({
      files: '**/*.php',
      injectChanges: true,
      proxy: 'http://underscores.test'
    })
  ],
  optimization: {
    minimizer: [new UglifyJSPlugin(), new OptimizeCssAssetsPlugin()]
  }
};

entry now takes an object, and I’ve updated output to take the [name] placeholder. When webpack runs, we’ll get an output bundle for each entry, and the property for each entry will be placed in the [name] placeholder. So customizer.js will compile to customizer-bundle.js.

If you’re using React in your theme, you may want to considering adding babel-polyfill so you can support IE11. Each key in the entry object can take either a string, or an array. One of the methods Babel suggests for adding babel-polyfill is to add it as the first item in an array for one of the entry points:

const path = require('path'),
  MiniCssExtractPlugin = require('mini-css-extract-plugin'),
  UglifyJSPlugin = require('uglifyjs-webpack-plugin'),
  OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'),
  BrowserSyncPlugin = require('browser-sync-webpack-plugin');

module.exports = {
  context: __dirname,
  entry: {
    frontend: ['babel-polyfill', './src/index.js'],
    customizer: './src/customizer.js'
  },
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: '[name]-bundle.js'
  },
  mode: 'development',
  devtool: 'source-map',
  module: {
    rules: [
      {
        enforce: 'pre',
        exclude: /node_modules/,
        test: /\.jsx$/,
        loader: 'eslint-loader'
      },
      {
        test: /\.jsx?$/,
        loader: 'babel-loader'
      },
      {
        test: /\.s?css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: '../style.css' }),
    new BrowserSyncPlugin({
      files: '**/*.php',
      injectChanges: true,
      proxy: 'http://underscores.test'
    })
  ],
  optimization: {
    minimizer: [new UglifyJSPlugin(), new OptimizeCssAssetsPlugin()]
  }
};

Finishing touches

At this point, we have a fairly robust webpack configuration. We can compile and minify our JavaScript and Sass, reload the browser when making changes in development mode, and split our bundles out depending upon their use. Let’s add a few finishing touches to round out our front end development experience.

We’ve already added linting for our JavaScript files, but let’s also add linting for our Sass files:

const path = require('path'),
  MiniCssExtractPlugin = require('mini-css-extract-plugin'),
  UglifyJSPlugin = require('uglifyjs-webpack-plugin'),
  OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'),
  BrowserSyncPlugin = require('browser-sync-webpack-plugin'),
  StyleLintPlugin = require('stylelint-webpack-plugin');

module.exports = {
  context: __dirname,
  entry: {
    frontend: ['babel-polyfill', './src/index.js'],
    customizer: './src/customizer.js'
  },
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: '[name]-bundle.js'
  },
  mode: 'development',
  devtool: 'source-map',
  module: {
    rules: [
      {
        enforce: 'pre',
        exclude: /node_modules/,
        test: /\.jsx$/,
        loader: 'eslint-loader'
      },
      {
        test: /\.jsx?$/,
        loader: 'babel-loader'
      },
      {
        test: /\.s?css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
      }
    ]
  },
  plugins: [
    new StyleLintPlugin(),
    new MiniCssExtractPlugin({ filename: '../style.css' }),
    new BrowserSyncPlugin({
      files: '**/*.php',
      injectChanges: true,
      proxy: 'http://underscores.test'
    })
  ],
  optimization: {
    minimizer: [new UglifyJSPlugin(), new OptimizeCssAssetsPlugin()]
  }
};

Adding StyleLint is pretty easy. We started by requiring the package at the top of the file, and instantiating it within our plugins array. Now when you run npm run dev or npm run build you’ll see some errors appear in the console.

StyleLint errors in the terminal.
StyleLint errors in the terminal.

Finally, let’s add a loader to manage image assets, and a loader and plugin to manage SVGs.

const path = require('path'),
  MiniCssExtractPlugin = require('mini-css-extract-plugin'),
  UglifyJSPlugin = require('uglifyjs-webpack-plugin'),
  OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'),
  BrowserSyncPlugin = require('browser-sync-webpack-plugin'),
  StyleLintPlugin = require('stylelint-webpack-plugin'),
  SpriteLoaderPlugin = require('svg-sprite-loader/plugin');

module.exports = {
  context: __dirname,
  entry: {
    frontend: ['babel-polyfill', './src/index.js'],
    customizer: './src/customizer.js'
  },
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: '[name]-bundle.js'
  },
  mode: 'development',
  devtool: 'source-map',
  module: {
    rules: [
      {
        enforce: 'pre',
        exclude: /node_modules/,
        test: /\.jsx$/,
        loader: 'eslint-loader'
      },
      {
        test: /\.jsx?$/,
        loader: 'babel-loader'
      },
      {
        test: /\.s?css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
      },
      {
        test: /\.svg$/,
        loader: 'svg-sprite-loader',
        options: {
          extract: true,
          spriteFilename: 'svg-defs.svg'
        }
      },
      {
        test: /\.(jpe?g|png|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              outputPath: 'images/',
              name: '[name].[ext]'
            }
          },
          'img-loader'
        ]
      }
    ]
  },
  plugins: [
    new StyleLintPlugin(),
    new MiniCssExtractPlugin({ filename: '../style.css' }),
    new SpriteLoaderPlugin(),
    new BrowserSyncPlugin({
      files: '**/*.php',
      injectChanges: true,
      proxy: 'http://underscores.test'
    })
  ],
  optimization: {
    minimizer: [new UglifyJSPlugin(), new OptimizeCssAssetsPlugin()]
  }
};

Resources

  • https://babeljs.io/docs/en/babel-polyfill
Spinning up Aurora Theme using underscores.me

Building a theme vs. a decoupled front end

Today is the day we start talking about dev shit™, and start getting into the nitty gritty of building Aurora Theme. In the project plan, I laid out the benefits of using the REST API, and that one of those benefits is that we can break out of the whole theme paradigm. But, I’ve chosen not to decouple my front end and, instead, am building a React-powered theme. And you may be wondering why I’ve chosen to do this. Well, let’s remember this nugget from the project plan:

Aurora will finally make REST API-driven sites and technology more accessible to the everyday WordPress user and developer.

I strongly believe the best way to make the REST API more accessible to the everyday WordPress user is to build the front end using the current theme paradigm. Let’s face it, the current system of changing the look of one’s site is pretty damn nifty. And we can take customization further by adding options to the customizer.

But the beauty is that Aurora Theme could just as easily be repurposed as a decoupled theme. Just take out the PHP and WordPress-specific bits, and you’re left with a decoupled app.

Determining file requirements

Even though Aurora Theme is mean to be a starter theme, it’s still going to be an attractive, functional theme. And I have every intention of submitting it to the theme repository, so it’s important to understand up front what the requirements must be met. According to the WordPress Theme Developer Handbook, themes submitted to the theme repository must contain the following files:

  • style.css
  • index.php (as a fallback if all other files fail)
  • comments.php
  • screenshot.png

Interestingly, Foxhound, which is also a React-powered theme, doesn’t have the comments.php template, but it does have the other files. So following their lead, I think I’ll include the above, minus comments.php, and add the following:

  • functions.php (for managing script & style enqueues, and some other functionality)
  • header.php (for all the stuff that lives in <head />)
  • footer.php (for all the scripts that come before the closing <body />)
  • inc/
    • customizer.php
    • rest-api.php (waffling on this, but I think I’ll need to make a few tweaks to endpoints)
    • template-tags.php (maybe?)

Building a foundation with Underscores

To give myself a head start, I’m using Underscores as the foundation for Aurora. Underscores doesn’t look like much from the start, but it’s packed with a lot of stuff, and it will save me time enqueuing things, getting the file structure set up, and getting a theme that looks like a theme.

Before: Aurora with the full set of files from Underscores
Before: Aurora with the full set of files from Underscores

Before: Aurora home page with full Underscores
Before: Aurora home page with full Underscores

But as you can see from the file structure screenshot, there are a lot of files. So first things, first, buh-bye to all the stuff we don’t need. 💣

After: Aurora with only the files we need to start.
After: Aurora with only the files we need to start.

Ah, that’s better. Less cruft to cut through to get started, and I didn’t completely break the theme.

After: Aurora after removing a bunch of files.
After: Aurora homepage after removing a bunch of files.

Clearly, obliterating the template-parts/ directory prevents any content from loading, but that’s fine. We’ll replace it with JavaScript soon enough.

Cleaning up what’s left

There is still a lot of PHP-specific theme stuff hanging out that won’t be necessary for this project. I tried to handle this in a semi-methodical way:

  1. Clean up functions.php
    • Add theme constants to save on cognitive load / extra typing
    • Add a social menu for eventually managing social media links
    • General code cleanup / rearranging
  2. Remove a bunch of unneeded functionality
    • Wipe template-tags.php completely clean (jury’s out on whether I need this file at all)
    • Clean up the customizer file
  3. Blast away a bunch of markup & templating in header.php, index.php, and footer.php 💥
    • Since this is a JS theme, we’ll let React handle the markup and templating for those sections

Since everything above left me with a literal blank canvas, I added a temporary JavaScript header. It just creates a header tag, an h1 for the title, and a p for the tagline, brings everything together, and slaps it into the DOM. I also discovered at this point that I was a little overzealous with my clean up and accidentally removed get_footer() from index.php, which is responsible for footer scripts and pulling in the admin bar. 😬

Integrating Aurora Core is next on the list. I’m really looking forward to sharing a bit about that project and wrestling Webpack to work with WordPress. 😉

Building a React-powered starter theme

As I near the final stretch in my Bov Academy studies, it’s time for me to choose a final project. For my final project, I have decided to capitalize on my love of JavaScript + WordPress. So, in this post, I want to introduce Aurora Theme–a React-powered starter theme for WordPress. (If you click the link shortly after this is posted, the joke’s on you. I haven’t deployed anything yet, and that is, indeed, the current default theme.)

So maybe you’re thinking, “why is she sharing something she hasn’t even started?” That’s a great question. I thought it’d be pretty neat to document the entire journey of building this thing. To many of us in the WordPress community, this whole REST API thing is still a foreign concept. Breaking our traditional PHP theme paradigm is going to take time. It’s going to take research. And it may not be pretty. I already have questions about implementation of things in the back of my mind, and I’m hoping to answer these questions as I go, and share them with the community. Of course, if you’re reading one of these posts, and you have questions, speak up and ask in the post comments, or @me on Twitter. Let’s help each other. With that, let’s get this thing started. Read on below for a detailed project plan.

Aurora Theme Project Plan

Business Justification

The WordPress REST API was merged into WordPress Core in December 2015. The REST API gives developers a modern way of interacting with WordPress using modern JavaScript techniques and technologies. By default, WordPress ships with endpoints to interact with standard post types (posts and pages), but it can be extended to create custom endpoints which may include post meta, or anything else one might need from the database.

Implementation of the REST API gives WordPress users and developers several advantages. WordPress can continue to operate as a familiar and robust backend for data storage and delivery, while making the data itself more accessible to other platforms. Decoupling data delivery from the content management system (CMS) makes WordPress more accessible to a broader audience. For example, building a frontend to consume WordPress data no longer requires knowledge of the WordPress theming system. It frees developers up to consume their data in whatever way is necessary from use in websites, to apps, and other third party services. Instead of WordPress being the solution for a website, it can be a building block in the foundation of a user’s larger application.

Even though the REST API has been part of WordPress for nearly two years, adoption of the API has been slow, and there are few themes utilizing the features of the REST API. There are even fewer “starter” themes built on the REST API. To date, most REST API-driven projects have been limited to the “high end” spectrum of client sites. But with the imminent integration of Gutenberg, JavaScript technologies are becoming an increasingly integral part of the WordPress experience.

Aurora will be a starter theme built to utilize the features of the standard endpoints in the WP REST API, and will use modern JavaScript technologies including ES Next and React. Aurora will finally make REST API-driven sites and technology more accessible to the everyday WordPress user and developer.

Requirements & Objectives

Any starter theme built for WordPress needs to give developers a leg up in the development process, but should not include so many features that it becomes burdensome to work with. To that end, it is incredibly important that Aurora remain relatively un-opinionated, yet provide the basic features one might expect of a functional WordPress theme (i.e. if you install and activate the theme, it shouldn’t look like a dumpster fire). Ideally, any opinionated elements (e.g. styling) could easily be blasted away, giving the next developer a clean slate.

To that end, Aurora will support modern JavaScript development techniques, and give developers a solid foundation for starting a REST API project. Including documentation support is especially integral to the success of Aurora. Since WordPress has long been in the PHP-based theme paradigm, it’s important to have a well-documented theme to help PHP developers transition to writing JavaScript-driven themes.

Aurora will be a clean, modern theme with the following features:

  • Sass
  • ES Next + React
  • Icon (SVG) management
  • Linting according the WordPress Coding Standards using PHPCS, ESLint, and Stylelint
  • Documentation support

Requirements

  • Support default post types (post, page, nav-menu, comments)
  • Well-documented
  • Easy to understand
  • Extensible
  • Performant
  • Accessible
  • Gutenberg-ready

Objectives

  • Adheres to WordPress Coding Standards for PHP, styling (CSS), and JavaScript.
  • Complies with Section 508 and WCAG 2.0AA accessibility standards.
  • Meets WordPress internationalization and localization guidelines.
  • Use techniques including Atomic Design to keep the theme files organized and easy to understand.

Project Scope / Deliverables

At this time, Aurora will only consist of the starter theme. Future implementations may include:

  • Extensibility via the command line–easily spin up reusable components via a command line integration (e.g. Yeoman), which will be hosted indepently from the theme (likely via a branch of Aurora Core).
  • Gutenberg components. Support for default Gutenberg components will exist, but new components are not in scope for the initial phase of the project.
  • A plugin for interacting with Custom Post Types / Custom Endpoints.

Aurora

Aurora will be a clean, elegant starter theme with simple styling, and basic components, to help developers get started on their WordPress REST API theme projects.

  • Use Aurora Core (the non-theme boilerplate) as the base setup for the theme
    • Tweak Aurora Core Webpack setup to work with WordPress (keeping in mind that hot module replacement doesn’t work in WordPress).
      • Browser Sync
  • Use Underscores for some of the base theme files
    • header.php (pulls in styles and head scripts)
    • footer.php (pulls in scripts)
    • index.php (a fall back if our JS fails)
    • 404.php?
    • functions.php
  • Theme components (React components)
    • Header
    • Navigation (this might be a challenge given that there is no clear way of interacting with menus via the default endpoints)
    • Footer
    • Archive (home, categories, tags, etc.)
    • Single post / page
    • Sidebar (I’m really curious about supporting widgets via the REST API)
    • 404
    • Search results (maybe this is the same as the archives?)
    • Social icons (menu)
    • Comments
  • How should the theme interact with the Customizer?
    • Custom logo
    • Site title
    • Site tagline
    • Site icon (favicon)
    • Sidebar (no sidebar, sidebar right, sidebar left)
    • Widget area(s) for the footer (see Alcatraz)
    • Footer copyright text
  • Testing (Mocha? Jest? other?)
  • Performance metrics
  • Type checking
    • Flow?
    • TypeScript?
  • Documentation
    • Sass documentation
    • Component documentation (KSS? Storybook?)

Resources

Creating Themes with Atomic Design

I gave my first WordCamp talk in Sacramento last October, and it has recently been posted to WordCamp TV. In the talk I covered:

  • the basics of atomic design,
  • an ideal vs. realistic atomic design workflow,
  • strategies for building themes with atomic design.

Presentation Slides

An example of theming using Atomic design

I have had more time to experiment and create themes with atomic design since my WordCamp talk. I am actively working on adding a pattern library to Alcatraz, which will allow front-end developers to quickly style-up atoms according to a client’s style guide. There are a host of ready-made molecules that developers can modify, or give a coat of polish to bring them up to standard with a client project. I’m working on a big overhaul of Alcatraz’s navigation, and I am planning on including some organisms soon after.

View a live example of the Alcatraz pattern library (it’s very much a work-in-progress, so yes, it’s kinda ugly, 😬).