Integrate Twig with Webpack in a WordPress theme

Webpack is an amazing tool to handle assets in a web projects. When I originally started using Webpack, I only used it to bundle JavaScript files in conjunction with Babel. Later I switched from gulp-sass to sass-loader to compile my stylesheets. But what about static assets such as images and fonts?

Since we can use Webpack to process JavaScript and CSS, static assets are missing piece in the puzzle. In my Gulp setup I simply copied files from src to dist. But what about Webpack? If you are using a front end framework like Vue or React, you can simply require assets using a relative path like this:

<img src={require('./images/logo.svg')} alt="Logo">

Webpack can use file-loader to resolve the file to a path like below and automatically copy it over to dist

<img src="/dist/images/logo.svg?ver=0aa5dbf6" alt="Logo">

How about using a font in a SCSS file?

@font-face { font-family: 'Open Sans'; font-style: normal; font-weight: 400; src: url('./fonts/open-sans-v15-latin_latin-ext-regular.woff2') format('woff2'), url('./fonts/open-sans-v15-latin_latin-ext-regular.woff') format('woff'); }

This will be transformed into

@font-face { font-family: 'Open Sans'; font-style: normal; font-weight: 400; src: url(/dist/fonts/open-sans-v15-latin_latin-ext-regular.woff2?ver=83c3deca) format("woff2"), url(/dist/fonts/open-sans-v15-latin_latin-ext-regular.woff?ver=3afbb2a5) format("woff"); }

Note: In a WordPress project, you will want to use publicPath option to specify where the resulting files are located, usually your theme folder

Webpack is a lot smarter than Gulp because it will only copy those files that are references in the templates rather than blindly copying all files. You also don’t need to think about where the files are located on the server because the paths are resolved automatically for you. We can also do many cool transformations to the files, such as compressing images, optimizing SVG files or generating a hash in the URL for long term caching.

This works nicely in an app built using a front end framework like Vue or React. But can we bring the same useful functionality in to files that processed server side? Like Twig templates, if you are using a WordPress framework like Timber or even plain PHP files? With Webpack’s html-loader, we can!

Let’s start with the basics. Here’s an relatively simple Webpack config file for JavaScript, SCSS and static files:

module.exports = { entry: { style: './src/scss.js', script: './src/js/script.js', }, output: { filename: '[name].js', chunkFilename: '[name].js?ver=[chunkhash]', publicPath: '/wp-content/themes/mytheme/dist/', }, resolve: { extensions: ['*', '.js'], }, mode: 'development', performance: { hints: false, }, devtool: 'source-map', module: { rules: [ { test: /\.js$/, use: [ { loader: 'babel-loader', options: { presets: ['env'], }, }, ], }, { test: /\.s?css$/, use: [ MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { sourceMap: true, }, }, { loader: 'postcss-loader', options: { ident: 'postcss', plugins: [autoprefixer({})], sourceMap: true, }, }, { loader: 'sass-loader', options: { sourceMap: true, precision: 10, }, }, ], }, { test: /\.(png|svg|jpg|jpeg|tiff|webp|gif|ico|woff|woff2|eot|ttf|otf|mp4|webm|wav|mp3|m4a|aac|oga)$/, use: [ { loader: 'file-loader', options: { context: 'src', name: '[path][name].[ext]?ver=[md5:hash:8]', }, }, ], }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: '[name].css', chunkFilename: '[id].css', }), ], };

First we need to create an entry point for our twig files. Create a file called twig.js inside you src folder with the following content:

// https://stackoverflow.com/questions/29421409/how-to-load-all-files-in-a-directory-using-webpack-without-require-statements function requireAll(require) { require.keys().forEach(require); } requireAll(require.context('./templates/', true, /\.twig$/));

This file will automatically import all your twig files into the entry point so you don’t have to import them by hand. Add the newly created file in your Webpack config entry section.

module.exports = { // ... entry: { style: './src/scss.js', script: './src/js/script.js', twig: './src/twig.js', }, // ... };

Next we need to install html-loader and extract-loader plugins, this can be done by using npm install html-loader extract-loader. Then you need to add it in the Webpack config under rules.

module.exports = { // ... module: { rules: [ // ... { test: /\.twig$/, use: [ { loader: 'file-loader', options: { context: 'src', name: '[path][name].[ext]', }, }, { loader: 'extract-loader' }, { loader: 'html-loader', options: { minimize: false, interpolate: true, attrs: ['img:src', 'link:href'], }, }, ], }, // ... ], }, };

We first find all Twig files, then use HTML loader to process all references to static files, use Extract loader to convert the templates into string and lastly use file loader to save the templates under dist. Now if you run npx webpack, all templates under src will be processed and copied to dist. In when using Timber, you will also need to specify the template folder to be dist/templates rather than the default templates, you can do this by calling the following function in your WordPress theme:

Timber::$dirname = ['dist/templates'];

Now we are ready to import some assets! In your Twig file, you can simply use <img> tag with a relative path

<img src="./images/logo.svg" alt="Logo">

This will be transformed into

<img src="/wp-content/themes/mytheme/dist/images/logo.svg?ver=0aa5dbf6" alt="Logo">

How about a favicon?

<head> <!-- ... --> <link rel="shortcut icon" href="./images/favicon.ico"> <!-- ... --> </head>

Will be transformed into

<head> <!-- ... --> <link rel="shortcut icon" href="/wp-content/themes/mytheme/dist/images/favicon.ico?ver=431dfbcd"> <!-- ... --> </head>

What about inline SVGs? Because html-loader supports interpolation, we can use JavaScript import to load SVG file in the middle of our template

<div class="icon"> ${require('!raw-loader!./images/logo.svg')} </div>

Will become

<div class="icon"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 774 875.7"><title>icon</title><path fill="#FFF" d="M387 0l387 218.9v437.9L387 875.7 0 656.8V218.9z"/><path fill="#8ed6fb" d="M704.9 641.7L399.8 814.3V679.9l190.1-104.6 115 66.4zm20.9-18.9V261.9l-111.6 64.5v232l111.6 64.4zM67.9 641.7L373 814.3V679.9L182.8 575.3 67.9 641.7zM47 622.8V261.9l111.6 64.5v232L47 622.8zm13.1-384.3L373 61.5v129.9L172.5 301.7l-1.6.9-110.8-64.1zm652.6 0l-312.9-177v129.9l200.5 110.2 1.6.9 110.8-64z"/><path fill="#1c78c0" d="M373 649.3L185.4 546.1V341.8L373 450.1v199.2zm26.8 0l187.6-103.1V341.8L399.8 450.1v199.2zm-13.4-207zM198.1 318.2l188.3-103.5 188.3 103.5-188.3 108.7-188.3-108.7z"/></svg> </div>

Note that we are using raw-loader here with Webpack’s inline loader import syntax so we get the actual file as a string and not just a path to it. The exclamation mark in the beginning tells Webpack to override all other loaders.

To see a full working example of this, check out my timber-starter-webpack repo on GitHub.

That’s cool and all, but how do I import CSS or JavaScript files in my templates?

In a WordPress project I recommend using WordPress’s wp_enqueue_style() and wp_enqueue_script() to add assets. This makes sure the dependencies are loaded in the correct order and the files are registered in WordPress’s assets system. Here’s a snippet I like to use:

add_action('wp_enqueue_scripts', function() { wp_enqueue_style( 'myproject-css', get_template_directory_uri() . '/dist/style.css', false, @md5_file(get_template_directory() . '/dist/style.css') ); });

Overriding all loaders skips my optimization step! What do I do?

Configure your optimization step as a preloader. Here’s an example you can use with SVGO:

module.exports = { // ... module: { rules: [ // ... { test: /\.svg$/, enforce: 'pre', use: [ { loader: 'svgo-loader', options: { precision: 2, plugins: [ { removeViewBox: false, }, ], }, }, ], }, // ... ], }, };

Now we have successfully integrated Twig templates into our Webpack workflow. For an example of a complete webpack config file, check out the timber-starter-webpack repo on GitHub!