DEV Community

Silvia Malavasi
Silvia Malavasi

Posted on

Let’s Get Hands-On with WordPress FSE Theme and Custom Blocks — Part 2

Image description

A Complete Ecosystem

While we can create and compile our own blocks for integration into any FSE theme individually, the optimal efficiency is achieved by constructing an ecosystem that:

  • Automates all build processes
  • Establishes a clear folder structure
  • Allows for scalable expansion
  • Ensures stylistic consistency and facilitates the reuse of common elements across all blocks

So, let’s focus on the theme and develop a build system for our blocks, as well as for our SASS and JavaScript files. It’s important to note that all block-related computations occur only when the block is present on the page we’re viewing. Additionally, any globally used JavaScript functions should have their own file within the theme, following the same approach as traditional themes.

The Structure

In contrast to a basic FSE theme structure, we introduce a few additional folders:

src: this is where we place our JavaScript and styles that need to be compiled
blocks/custom-blocks: each block is housed in its own folder within this directory.
dist: this directory is designated for the compiled theme files

And the two files that pertain to the build process:

package.json
webpack.config.js

Therefore, before building, our structure resembles the following. If you manage the header and footer exclusively through the editor, you can remove the parts folder.



my-theme/
│
├── blocks/
│   └── custom-blocks/
│       └── first-block/
│           ├── block.json
│           ├── edit.js
│           ├── edit.scss
│           ├── index.js
│           ├── save.js
│           ├── save.scss
│       └── second-block/
│           ├── block.json
│           ├── edit.js
│           ├── edit.scss
│           ├── index.js
│           ├── save.js
│           ├── save.scss
│       └── ...
│
├── dist/
│
├── parts/
│   ├── footer.html
│   ├── header.html
│
├── src/
│   ├── css/
│   │   ├── style.scss
│   │   ├── dashboard-style.scss
│   ├── js/
│       ├── site.js
│
├── templates/
│   ├── index.html
│
├── functions.php
├── package.json
├── theme.json
└── webpack.config.js


Enter fullscreen mode Exit fullscreen mode

Let’s briefly discuss the dashboard-style.scss file.

In WordPress, styles are enqueued in various ways. Block styles, as previously mentioned, are handled directly within each block. Frontend styles are enqueued in functions.php using wp_enqueue_style within the wp_enqueue_scripts action.



function zenfse_enqueue_style()
{
  wp_enqueue_style('zenfse-style', get_stylesheet_directory_uri() . '/style.css', array(), filemtime(get_stylesheet_directory() . '/style.css'), false);
}
add_action('wp_enqueue_scripts', 'zenfse_enqueue_style');


Enter fullscreen mode Exit fullscreen mode

However, if we need to enqueue parts of our CSS, such as resets, these won’t be loaded within the editor by default. Therefore, in my themes, I include a dashboard-style.scss file to be loaded on the editor side through the admin_enqueue_scripts action. This approach ensures that these styles are applied specifically within the WordPress editor.



function zenfse_admin_styles()
{
  wp_enqueue_style('zenfse-admin-style', get_template_directory_uri() . '/dist/dashboard-style.css');
}
add_action('admin_enqueue_scripts', 'zenfse_admin_styles');


Enter fullscreen mode Exit fullscreen mode

If I need to customize my dashboard, I can add specific styles to dashboard-style.scss. To ensure our resets and styles don't impact WordPress's default styles, we should wrap our declarations within the .wp-admin .editor-styles-wrapper selector. This confines our styles to apply only within the editor interface.

As a final touch, we apply our global styles to the iframe within the editor. This approach ensures consistency in how our theme appears across different parts and templates modified within the WordPress dashboard.



function zenfse_editor_style()
{
  add_editor_style(array('style.css',));
}

add_action('after_setup_theme', 'zenfse_editor_style');


Enter fullscreen mode Exit fullscreen mode

End of digression.

The Toolbox

We are operating within a Node environment, so our first step is to initialize our project by creating a package.json where we define dependencies and commands. Personally, I use both webpack (on top of which @wordpress/scripts is built) and Parcel. While using two different build engines may lack elegance, Parcel’s user-friendly approach compensates for this compared to webpack. Its commands are straightforward, requiring no additional configuration files. However, if you prefer using webpack exclusively, you can achieve this with just a few additions to your webpack.config.js.

Within the js and css folders in src, you can get creative with importing modules, exporting and importing functions, creating mixins, etc. In short, you have a modern development environment at your disposal, allowing you to tailor your workflow as needed.

The build process

My package.json looks something like this:



{
  "name": "zenfse",
  "version": "1.3.0",
  "description": "blank fse developer theme",
  "scripts": {
    "dev:js": "parcel watch src/js/site.js",
    "dev:css": "sass --watch src/css/style.scss:style.css --style=expanded",
    "dev:css-dashboard-style": "sass --watch src/css/dashboard-style/dashboard-style.scss:dist/dashboard-style.css --style=expanded",
    "dev:wp-scripts-custom-blocks": "wp-scripts start --webpack-src-dir=blocks/custom-blocks/ --output-path=blocks/build-custom-blocks/",
    "dev": "concurrently --kill-others \"npm run dev:js\" \"npm run dev:css\" \"npm run dev:css-dashboard-style\" \"npm run dev:wp-scripts-core-blocks\" \"npm run dev:css-gsap-animations-frontend\" \"npm run dev:css-image-frontend\" \"npm run dev:css-image-backend\" \"npm run dev:css-image-style\" \"npm run dev:wp-scripts-custom-blocks\"",
    "clean": "rimraf ./blocks/build-custom-blocks ./dist",
    "build:js": "parcel build src/js/site.js --no-source-maps --no-content-hash",
    "build:css": "sass src/css/style.scss:style.css --style=compressed",
    "build:css-dashboard-style": "sass src/css/dashboard-style/dashboard-style.scss:dist/dashboard-style.css --style=compressed",
    "build:wp-scripts-custom-blocks": "NODE_ENV=production wp-scripts build --webpack-src-dir=blocks/custom-blocks/ --output-path=blocks/build-custom-blocks/",
    "build:css-autoprefixer": "postcss **/*.css --use autoprefixer --replace",
    "build": "npm run clean && run-s build:*"
  },
  "author": "Silvia Malavasi",
  "license": "ISC",
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ],
  "devDependencies": {
    "@parcel/transformer-sass": "^2.10.3",
    "@wordpress/scripts": "^26.17.0",
    "parcel": "^2.10.3",
    "rimraf": "^5.0.5"
  },
  "dependencies": {
    "concurrently": "^8.2.2",
  }
}


Enter fullscreen mode Exit fullscreen mode

While webpack.config.js will be simply:



const defaultConfig = require("@wordpress/scripts/config/webpack.config");
module.exports = [defaultConfig];


Enter fullscreen mode Exit fullscreen mode

When I run npm run build, all of these commands will be executed:



"clean": "rimraf ./blocks/build-custom-blocks ./dist",
"build:js": "parcel build src/js/site.js --no-source-maps --no-content-hash",
"build:css": "sass src/css/style.scss:style.css --style=compressed",
"build:css-dashboard-style": "sass src/css/dashboard-style/dashboard-style.scss:dist/dashboard-style.css --style=compressed",
"build:wp-scripts-custom-blocks": "NODE_ENV=production wp-scripts build --webpack-src-dir=blocks/custom-blocks/ --output-path=blocks/build-custom-blocks/",
"build:css-autoprefixer": "postcss **/*.css --use autoprefixer --replace",
"build": "npm run clean && run-s build:*"


Enter fullscreen mode Exit fullscreen mode

First, I clean up the project from the old build using rimraf. Then, I compile all my files using Parcel along with its parcel/transformer-sass, plugin, and I process CSS using autoprefixer.

wp-scripts handles the compilation of all my custom blocks based on the configuration specified in webpack.config.js.

This build process setup is highly convenient because whenever I create a new custom block, I just need to place its folder in blocks/custom-blocks, and it will be automatically compiled during the next npm run build.

Credit for this approach goes to the WebberZone article: “WordPress block development: Building multiple blocks.

Similarly, npm run dev uses concurrently to manage all the various watch commands simultaneously.

Let’s import everything into functions.php

Now that our files are ready, we can proceed with importing them into our functions.php. We've already discussed some aspects related to styles. The complete process for importing global scripts and styles (which apply across the entire theme) is as follows:



// Enqueue scripts

function zenfse_enqueue_script()
{
  wp_enqueue_script('zenfse-script', get_stylesheet_directory_uri() . '/dist/site.js', array(), filemtime(get_stylesheet_directory() . '/dist/site.js'), true);
}
add_action('wp_enqueue_scripts', 'zenfse_enqueue_script');

// Enqueue styles

function zenfse_enqueue_style()
{
  wp_enqueue_style('zenfse-style', get_stylesheet_directory_uri() . '/style.css', array(), filemtime(get_stylesheet_directory() . '/style.css'), false);
}
add_action('wp_enqueue_scripts', 'zenfse_enqueue_style');

// Enqueue Dashboard styles

function zenfse_admin_styles()
{
  wp_enqueue_style('zenfse-admin-style', get_template_directory_uri() . '/dist/dashboard-style.css');
}
add_action('admin_enqueue_scripts', 'zenfse_admin_styles');

// Enqueue Editor styles

function zenfse_editor_style()
{
  add_editor_style(
    array(
      'style.css',
    )
  );
}

add_action('after_setup_theme', 'zenfse_editor_style');


Enter fullscreen mode Exit fullscreen mode

Now let’s move on to importing our custom blocks, which we compiled into the directory blocks/build-custom-blocks:



function zenfse_register_blocks()
{

  add_filter('block_categories_all', 'zenfse_blocks_categories');
  function zenfse_blocks_categories($categories)
  {
    array_unshift($categories, array(
      'slug'  => 'zenfse-blocks',
      'title' => 'ZenFSE Blocks'
    ));
    return $categories;
  };


  $blocks = array(
    'navigation' => 'zenfse_render_navigation_block',
    'gallery' => '',
  );

  foreach ($blocks as $block => $render_callback) {
    $args = array();
    if (!empty($render_callback)) {
      $args['render_callback'] = $render_callback;
    }
    register_block_type(__DIR__ . '/blocks/build-custom-blocks/' . $block, $args);
  }
}
add_action('init', 'zenfse_register_blocks');


Enter fullscreen mode Exit fullscreen mode

In this example, I have imported two blocks: one is the gallery block that we saw in Part 1 of the article. The other is a navigation block, included here to illustrate its render_callback.

Every time we want to add a new block, we simply add it to the $blocks array, optionally with its callback function. It’s straightforward and efficient.

Conclusion — for real

We’ve delved into setting up a custom FSE theme in WordPress, organizing its structure, and managing the compilation process. Now, the next step is to innovate and create new blocks.

While the process I’ve described may seem somewhat intricate, in a way, it is. Building my ZenFse starter theme involved a lengthy journey of trial and error.

Initially, working with FSE left me feeling disoriented. The absence of PHP was unsettling. However, as I grasped the logic behind the block system and understood the unique integration between PHP and React, I discovered a powerful, versatile, and… enjoyable system. The capability to craft interfaces for nearly any attribute of my theme or blocks enables the development of intricate custom systems, fulfilling the visual builder objective of FSE in WordPress.

Maintaining custom blocks in separate environments proves remarkably convenient. To replicate blocks from one theme to another, you simply copy a folder and add a line in the $blocks array within functions.php.

From a developer’s viewpoint, once the theme is structured, we enjoy an intuitive environment. But what about users? Granted, every CMS demands some training for end users, but does FSE simplify matters? Yes and no. For existing WordPress users, starting anew can pose challenges. Moreover, the decision to develop a site using an FSE or classic theme depends on the client’s requirements.

For example, if I’m constructing a site for an agency that requires precise control over colors, spacing, and enjoys experimenting with diverse block combinations on each project page, FSE is your choice. If a client wishes to incorporate an unexpected video or lengthy text into an existing block, they can experiment and preview the final outcome directly from the dashboard.

Conversely, for a newbie needing a more structured system, I might opt for a classic theme, potentially integrating a series of fields with ACF to limit theme flexibility.

In conclusion, from my perspective, I find developing with FSE much more enjoyable. It feels cleaner and more elegant to me, though these preferences are subjective. Additionally, since FSE is still evolving, I’m excited to see what new tools the WordPress team will introduce in the future.

Let’s also create a dedicated block category for our custom blocks, making it easier to locate and insert them into pages.

Top comments (0)