How to easily set up Webpack for a WordPress theme with 10up-toolkit

By Ian Svoboda

In a modern WordPress theme (or any frontend project) one of your first steps is setting up tooling. There are numerous tools to use, but if you’ve done this before you probably have used (or heard of) Webpack. If you haven’t, I’ll explain below.

In this article, I’ll show you how you can easily leverage Webpack to build your theme assets, lint your files, and simplify your theme’s tooling overall.

Already know about Webpack? Click here to skip to the instructions.

Why do I need Webpack (or something like it)?

Webpack is a bundler: a tool that combines multiple dependencies into a file(s) that you can use in the browser.

Usually when you’re working with custom blocks you’re writing React in JSX. If so, you need something to transpile your JSX into JavaScript that the browser understands.

If you’re writing JSX code you’ll need something like Babel or esbuild to transpile your JSX into an appropriate syntax for web browsers. But setting that up isn’t exactly simple. Just transpiling code is fine, but what about optimizing it, or bundling the appropriate dependencies together?

Having the ability to detect formatting or syntax errors in your CSS and JS as you write is very helpful. This can greatly reduce the amount of buggy code you ship to production. Doing that by hand is pretty tedious and time consuming. If you’ve never set it up before, it can feel hard to know where to start (speaking for myself, anyway).

It all can get overwhelming pretty fast. If you’re on Twitter, you may notice frequent conversations about just how annoying this stuff can be to set up:

So for many of us, setting up this stuff feels cumbersome at best or frustrating at worst. It wouldn’t be so bad if you just had something you could easily reuse each time that you could tweak as needed and was easy to keep updated.

The WordPress Core team has had the @wordpress/scripts package for some time, but it isn’t easy to use for theme assets out of the box and is more tailored for a block plugin by default.

So if something like @wordpress/scripts existed but could easily be used for a theme that also wants to include blocks (or overrides/changes), that would be great right? And that is the exact problem that 10up set out to solve.

About 10up

If you don’t know who 10up is, they are one of the top WordPress agencies in the world. They not only work with WordPress daily, but employ several Core contributors whose primary role is working on WordPress Core, the Block Editor, or both!

In addition, they have a strong library of open-source repos and plugins that you can use to level up your WordPress game. Their projects each have their maintenance status so you can see if the project is receiving regular updates or not.

One of these fantastic open-source projects is 10up-toolkit, which you can use to handle bundling and static asset handling for your theme (or even a non-WordPress project)!

How 10up-toolkit can help

The 10up-toolkit project was designed to simplify and standardize the process of building project assets for 10up internal projects.

Since it’s creation it’s been open-source and available on GitHub. This is fantastic because it means you have a solid, reliable build process you can use that is maintained by a team of professional WordPress and JavaScript engineers. Pretty sweet!

10up-toolkit includes configurations for Webpack, ESLint, and Stylelint to help you build and format your code as you’re writing it and before you deploy.

Additionally, 10up-toolkit has a special mode: useBlockAssets that can automatically detect the blocks in your theme and build their assets for you without you having to manually add each one to Webpack or an import somewhere.

Note: This setting requires your blocks to be using block.json (which they probably are if you’ve made them recently).

See the README for more details on this setting.

Our desired outcome

When all is said and done, we want to make sure the following works:

  • Any new blocks we create will be seen by Webpack
  • Changes to CSS/SCSS or JS files will trigger a rebuild
  • HMR (Hot Module Reloading) is enabled so non-PHP changes can be seen without refresh
  • We can override or alter existing blocks
  • We have a special editor-style file for styles that only show in the block editor

Thankfully, 10up-toolkit makes it easy to do all of these things without much guesswork. Let’s get started!

Setting up the project

Here is an outline of the steps we need to take to setup the project:

  1. Installing Dependencies
  2. Creating necessary folders and files
  3. Configure package.json
  4. Enqueue Stylesheets and Scripts
  5. Add HMR Support to the theme

Installing dependencies

Make sure you have Node and npm installed before continuing. If you don’t have a package.json file already you can set up a new one with npm init. You should be using Node 16+ for best results, but 10up-toolkit supports Node 12 and above if you need an older version.

Creating necessary folders and files

By default, 10up-toolkit uses the assets folder for your static assets (CSS, JS, images, fonts, etc) and the includes/blocks folder when scanning for block assets. We’ll use the default folder names here but you can customize these to whatever you like if you prefer (see instructions in the README).

- assets
  - css
    - editor-style.css
    - frontend.css
  - js
    - frontend.js
- includes
  - blocks

The assets folder will store all the static assets (CSS, JS, images, fonts, etc) and includes will store your block definitions and PHP files that contain logic only. If you’re developing custom blocks in your theme, you’ll probably want to enable useBlockAssets, which will automatically add the scripts and stylesheets for your custom blocks (see the README for more details).

The frontend.js file will import the frontend.css, which is required for HMR (see more below):

import '../css/frontend.css';

If you want to use different file names, go for it! You’ll just need to use those filenames in place of what we’re using here.

If you’re shaking your fist at the sky wondering “Why must we import CSS into JavaScript!?”, worry not! Even though frontend.js imports frontend.css, when you build the site it will still generate a separate CSS file that will be enqueued in a later step.

Configuring package.json

If you don’t have a package.json file, create one using the command:

npm init

Just answer the prompts as it guides you. You can leave many of the fields blank or unfilled.

Install 10up/toolkit

Once you’ve created a package.json file, you’ll need to install 10up-toolkit:

npm i -D 10up/toolkit

The additional -D param (equivalent to –save-dev) will add this to your project devDependencies in the package.json file, which are packages you use to build your project itself.

Add build commands

Next, we’ll add additional scripts to run for our npm commands (ex: npm run start). These commands are how you actually use 10up/toolkit to build assets.

We’re going to be adding a total of 4 commands, which you can copy from the code snippet below or otherwise enter into your package.json scripts key:

  "scripts": {
    "start": "10up-toolkit build --watch --hot",
    "build": "10up-toolkit build",
    "format-js": "10up-toolkit format-js",
    "lint-js": "10up-toolkit lint-js",
    "lint-style": "10up-toolkit lint-style"
  },

Here’s a quick explanation of what each does:

  • start: Begins watching for file changes (uses HMR with the –hot flag included)
  • build: Used to build the project when you’re ready to deploy it to production.
  • format-js: Automatically formats JS/JSX files based on the ESLint settings (these are set by default).
  • lint-js: Checks each JS/JSX file for linting errors (does not change the file)
  • lint-style: Checks each CSS/SCSS file for linting errors using Stylelint (does not change the file)

So when you’re ready to start writing code, just navigate to the theme folder on your computer and run npm run start. When it’s time to deploy, just run npm run build before you push the files.

If you’re using CI/CD tools like GitLab or GitHub actions, you can run that as a pipeline job when the production deployment is triggered.

If you’re using VSCode, 10up has provided some settings you can use to automatically let ESLint format your code for you as you write it (see instructions here).

You can read more about integrating with your text-editor/IDE in the README.

Configure entries

Now that we have the commands to tell 10up-toolkit what to do, we need to tell it what files to use as an “entry”. In the context of bundlers (like Webpack) an “entry” refers to a specific stylesheet or script that we’ll be outputting when the build finishes.

So far, we have 2 stylesheets we want: editor-style.css (for the Block editor only) and frontend.css which will have all of the main styles for our theme. So we will add two total entries, one for each stylesheet.

Entries are configured under a special package.json key (10up-toolkit.entry). here’s an example of how this might look:

  "10up-toolkit": {
    "useBlockAssets": true,
    "entry": {
      "frontend": "assets/js/frontend.js",
      "editor-style": "assets/css/editor-style.css"
    }
  }

In the above example, useBlockAssets is set to true. This will ensure your block styles and scripts are automatically processed by Webpack. So unlike the entries, you won’t have to manually specify each one.

Here’s what the complete package.json looks like at this point if you’re just following along with this example:

{
  "name": "example-theme",
  "author": "Your Name Here",
  "scripts": {
    "start": "10up-toolkit build watch --hot",
    "build": "10up-toolkit build",
    "format-js": "10up-toolkit format-js",
    "lint-js": "10up-toolkit lint-js",
    "lint-style": "10up-toolkit lint-style"
  },
  "devDependencies": {
    "10up-toolkit": "^5.2.1"
  },
  "10up-toolkit": {
    "useBlockAssets": true,
    "entry": {
      "frontend": "assets/js/frontend.js",
      "editor-style": "assets/css/editor-style.css"
    }
  }
}

At this point, double-check that you’ve done the following:

Import the frontend.css file into frontend.js

import '../css/frontend.css';

Add some kind of comment in your stylesheets.

This will prevent a warning about an “empty source” (i.e. the file has nothing in it). This shouldn’t stop the build from working though. Here’s an example you can copy/paste (including the newline at the end):

/* Styles Here */

And now, you can run npm run build and your theme should build successfully!

Enqueue Stylesheets and Scripts

Next, we need to set up an enqueue for our stylesheets and script. For front-end stylesheets and scripts, we’ll use the wp_enqueue_scripts hook. The editor-style.css file needs to be added with a different hook: enqueue_block_editor_assets.

The code snippet below is an example of a callback for both hooks.

<?php
add_action('wp_enqueue_scripts', function() {
  $frontend_styles_path = '/dist/css/frontend.css';
  $frontend_scripts_path = '/dist/js/frontend.js';
  wp_enqueue_style('lwtd-style', get_theme_file_uri( $frontend_styles_path ), '', @filemtime(get_template_directory() . $frontend_styles_path));
  wp_enqueue_script('lwtd-scripts', get_theme_file_uri( $frontend_scripts_path ), '', @filemtime(get_template_directory() . $frontend_scripts_path));
});

add_action('enqueue_block_editor_assets', function() {
  $editor_styles_path = '/dist/css/editor-style.css';
  wp_enqueue_style('editor-style', get_theme_file_uri( $editor_styles_path ), '', @filemtime(get_template_directory() . $editor_styles_path));
});

For an existing theme, you can copy/paste this into your functions.php file or another file in your includes/inc directory (it may vary depending on your specific theme).

If not, you can create a file in the includes folder called enqueues.php and then require that file in your functions.php file along with the hmr.php file we created:

<?php
require_once get_stylesheet_directory() . '/includes/enqueues.php';
require_once get_stylesheet_directory() . '/includes/hmr.php';
// Other requires or theme logic here

If you’ve modified the folder name make sure you update it to whatever your customized value is.

Add HMR Support to the theme

Now that we’ve made sure everything is working, we need to do a few extra things to make sure HMR works. HMR will allow you to see changes made to CSS and JS files immediately without a page refresh (including inside the block editor)!

When our start task runs, Webpack will create a series of JS files that represent the changes we’ve made and inject those into the page as we make changes. So just need to tell WordPress how to find those files.

As with everything else, 10up has a simple solution here! They do provide instructions for setting this up as well as some troubleshooting steps that you can review if you wish. I’ve simplified the instructions a bit so things are more copy/paste friendly.

You’ll start by creating a file called hmr.php and place it in your includes folder (or whatever folder name you’ve chosen) and copy/paste the following into that file:

<?php
$is_local_env = in_array( wp_get_environment_type(), [ 'local', 'development' ], true );
$is_local_url = strpos( home_url(), '.test' ) || strpos( home_url(), '.local' ); // Modify this if you're not using .local or .test for a TLD
$is_local     = $is_local_env || $is_local_url;
$dist_url     = get_stylesheet_directory_uri() . '/dist';
$theme_root   = get_stylesheet_directory();
$dist_path    = $theme_root . '/dist';

if ( $is_local && file_exists( $theme_root . '/dist/fast-refresh.php' ) ) {
	require_once $theme_root . '/dist/fast-refresh.php';
	TenUpToolkit\set_dist_url_path( basename( $theme_root ), $dist_url, $dist_path );
}

Note: 10up-toolkit assumes you’re using a .test domain for your local site. If your local domain uses a TLD (Top-Level-Domain, such as .com, .org, etc) that isn’t .test you also need to add a special key in your package.json under 10up-toolkit to make sure it’s detected:

  "10up-toolkit": {
    "devURL": "https://your-local-site.local" 
  }

Once you’ve added this, open the homepage of your local site and check the console to see if you see a message about HMR:

A screenshot of the Chrome Dev Tools with a message indicating the browser is waiting on an update signal from webpack-dev-server

Troubleshooting

At this point, if you’ve read through this and are having some problems, I’ve outlined a few common pitfalls that I’ve experienced setting this up.

I would also strongly recommend you review the project README (as noted at various points above) which goes into some additional details that might be helpful for your specific situation.

My files aren’t minified or optimized

If you just deployed your code and your assets aren’t optimized, you probably forgot to run npm run build. This will build assets for production and perform various optimizations such as minification and tree-shaking.

Stylesheets and/or scripts aren’t being enqueued

The likely explanation is that you didn’t actually enqueue those files using wp_enqueue_style or wp_enqueue_script. If you created a new file (ex: enqueues.php) to house the PHP for the enqueues, make sure you’ve added it to your theme’s functions.php file appropriately:

<?php
// .. other require statements here
require_once get_stylesheet_directory() . '/includes/enqueues.php';

HMR Scripts aren’t being enqueued

If you’re seeing your own enqueues working but you’re not seeing the enqueue for react-fast-refresh and the HMR specific script, make sure you review the troubleshooting section in the 10up-toolkit README.

HMR won’t connect when everything is set up correctly or throws an SSL error

This likely means that you need to explicitly tell Webpack what certificate files to use. In some scenarios where the local site is using a self-signed certificate (ex: Laravel Valet) the websocket connection will fail without any explicit error. I’m not 100% sure why this happens, but I can confirm that it seems to not be an issue in Docker setups I’ve used (using 10up-local-docker).

The simple fix here is to tell Webpack where to find the certificate files. To do this, you need to create a webpack.config.js file in the root of your theme to add this to the configuration.

If you’re using Valet, your can use this example below and just update it to point to the right cert/key files:

const config = require('10up-toolkit/config/webpack.config.js');
const fs = require('fs')

// Update this to your valet path
const certPath = '/Users/YourUser/.config/valet'

// Update these paths to the correct cert for your site
if( typeof config.devServer === 'object' ) {
  config.devServer.https = {
	  key: fs.readFileSync(`${certPath}/Certificates/yoursite.test.key`),
	  cert: fs.readFileSync(`${certPath}/Certificates/yoursite.test.crt`),
	  ca: fs.readFileSync(`${certPath}/CA/LaravelValetCASelfSigned.pem`),
  }
}

module.exports = config;

Note: This setup requires you to set the NODE_ENV variable to make sure it doesn’t try to use the devServer

A quick recap

Here’s a quick TLDR of what we’ve covered above:

  • 10up-toolkit is a preconfigured Webpack setup to build your theme and block assets.
  • 10up-toolkit has preconfigured ESLint and Stylelint configurations to help you format your code
  • You can override the configuration for 10up-toolkit as little or as much as you need to.
  • By default, 10up-toolkit uses the assets folder for static theme assets (stylesheets, scripts, images, etc) and includes/blocks for custom blocks in your theme.
  • 10up-toolkit supports Hot-Module-Reloading (HMR), which allows you to see CSS and JS changes on the frontend and in the block editor without having to refresh the page
  • When using HMR, you must import the stylesheet into a JS file and enqueue them both. A separate CSS file will still be generated.
  • If you’re not using a .test domain, you need to do extra configuration to allow 10up-toolkit to handle it properly.

Happy coding!