Updating Webpack to use WordPress editor blocks (Gutenberg blocks) on the front end (part 4 of 4)


Getting blocks running on the front end is, or should be, mainly a matter of updating Webpack to create the script to run on the front end, then enqueuing it so it does. However, I’ve always found it requires some fixing of things that don’t work quite right together, that I’ve misnamed or otherwise mismatched so I’ll go through those changes too.

Updating Webpack to build a front end script

Let me start with a caveat here: this may not be the best way to do it. It is definitely not the only way to do it. However. It works! I don’t know the internals of wp-scripts and it’s not super well documented . (This is one of the reasons it would be useful to be able to eject from wp-scripts โ€“ I’d love to have a dummy project and be able to tweak a setting and see what happens. The default Webpack config is here and I suppose I could copy and paste.)

The short of it

The short of it: I’ve put the out of the box editor config in a variable and then added in a front end config and exported both.

JSON
...
const adminConfig = {
...
};

const frontEndConfig = {
...
};

module.exports = [ adminConfig, frontEndConfig ];

You can see the changes I’ve made to webpack.config.js here.

The long of it

The long of it is explaining the front end config. ๐Ÿ˜Š

frontEndConfig looks like this:

JSON
const frontEndConfig = {
	...defaultConfig,
	entry: './src/front-index.js',
	output: {
		...defaultConfig.output,
		path: path.resolve( process.cwd(), 'dist' ), // For some reason, npm run build removes the front bundle from /build so I'm explicitly setting the path here so it doesn't randomly change.
		filename: 'front.bundle.js',
	},
	stats: {
		maxModules: Infinity, // Way too much information but I like it.
		exclude: undefined,
	},
	plugins: [
		// including ...defaultConfig.plugins here causes it not to build the bundle.
		new BrowserSyncPlugin(
			{
				host: 'localhost',
				port: 3124,
				proxy: frontEndEnv,
				open: true,
				files: [ "dist/front.bundle.js", "build/*.css" ],
			},
			{
				injectCss: true,
				reload: false,
			}
		),
		new DependencyExtractionWebpackPlugin(), // This creates the assets php file.
	],
};

It uses the spread operator to add the default config as a starting point. There were a few things I had to change and I’ve added in stats for debugging purposes, you can remove that bit if you don’t like it.

entry

I’ve made a new entry point for the front end, front-index.js:

JAVASCRIPT
/**
 * The entry point for building the front end.
 * This is working around the file structure already set up for the block editor blocks.
 */

import React from 'react';
import ReactDOM from 'react-dom';
import Game from './blocks/click-game/components/game.js';

const elementsToRender = document.getElementsByClassName( 'porchy' );

const blockNameToComponents = {
	'porchy/clickgame': Game,
};

for ( let element of elementsToRender ) {

	const attrString = element.getAttribute( 'data-attr' ); // The JSON string with the attributes.
	const blockName = element.getAttribute( 'data-block' ); // The block's name.
	const attr = JSON.parse( attrString ); // Putting the attributes in a JSON object.
	const ComponentToUse = blockNameToComponents[ blockName ]; // Which component should this use?

	ReactDOM.render( <ComponentToUse attr={ attr } />, element ); // Render the component.
}

The entry point uses the dom elements we created using the render_block filter as I explained in this post.

It picks up everything with a class of “porchy” (this really should be way more specific), then iterates over the elements, going through this process for each one:

  • grabs the data-attr of the element to get the block’s attributes as a string
  • grabs the the data-block of the element to get the blockname
  • parses the attributes string into JSON
  • figures out which component to use by the blockname (additional blocks should be added to blockNameToComponents object with the full blockname as the key and the component as the value โ€“ remember to import the component)
  • renders the component into the element

output

For whatever reason wp-scripts helpfully cleans the build folder of anything it thinks is not supposed to be in there. So I’m using dist as the front end folder. Whatever. I’m bringing in the default config and then explicitly setting the folder and filename even though I found if I left "output" blank it worked the same way. (I’ve learned not to trust default behaviour with WordPress plugins and the block editor seems particularly unstable this way.) This is what is enqueued on the front end.

stats

I like this, I love seeing things scroll down the terminal, but it’s not at all necessary.

plugins

Importing the default config here causes the front end build to fail so I’ve included what I want:

  • Browsersync plugin for front end syncing. It uses an update frontEndEnv variable โ€“ add this to local.json, mine ends up looking like the following โ€“ and you’ll also need to set the variable in your Webpack config.
JSON
{
	"adminURL": "https://idontthink.test/wp-admin/post.php?post=154&action=edit",
	"frontEndURL":  "https://idontthink.test/test-post/"
}
  • DependencyExtractionWebpackPlugin (this needs to be explicitly required at the top of the file or it fails). This is the plugin which builds the php assets file which will end up in the output folder set previously, dist.

Including the built script on the front end

Once I had the script, I needed to include it on the front end. This is done the same way as including it in the editor, albeit on a different hook (there might be a better hook than init ๐Ÿ˜ฌ). Here’s what that looks like:

PHP
function front_end_scripts() {
	wp_enqueue_script(
		'porchy-mjj-block-example-frontend',
		plugins_url( '/dist/front.bundle.js', __FILE__ ),
		front_end_asset_file( 'front.bundle', 'dependencies' ),
		front_end_asset_file( 'front.bundle', 'version' ),
		true
	);
}
add_action( 'init', __NAMESPACE__ . '\front_end_scripts' );

/**
 * Get asset file.
 *
 * @param string $handle Asset handle to reference.
 * @param string $key What do we want to return: version or dependencies.
 */
function front_end_asset_file( $handle, $key ) {
	$default_asset_file = array(
		'dependencies' => array(),
		'version'      => '1.0',
	);

	$asset_filepath = plugin_dir_path( __FILE__ ) . "/dist/{$handle}.asset.php";
	$asset_file     = file_exists( $asset_filepath ) ? include $asset_filepath : $default_asset_file;

	if ( 'version' === $key ) {
		return $asset_file['version'];
	}

	if ( 'dependencies' === $key ) {
		return $asset_file['dependencies'];
	}
}

You may notice in the diff that there is additional bits to make it work on Gatsby, ie here where you’re reading this (most likely). I’m not going to go over that now, this is too long already. I’ll try and post on that later.

Issues fixed

I am incapable of keeping names straight the first time around and there were a number of issues I had to fix to make it all work happily together.

Delicateness and future changes

The way I’m picking up the elements in front-index.js isn’t robust at all and could easily break if someone else uses the same class of the elements picked up (in this case it’s “porchy”). Maybe next time I’ll add the version number to the class for something like porchy-ab7fb51d103d94d2f7f578f1674385ed, set the version number in a localised variable and then pick it up to build the class name in front-index.js.

Let me know what I’ve missed here or anything unclear.

Previous posts on this

Leave a Reply

Your email address will not be published. Required fields are marked *

By submitting this comment, you are agreeing to the use of Akismet which helps reduce spam. You can view Akismet’s privacy policy here. Your email, website and name are also stored on this site.