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

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

posted Originally published at medium.com 9 min read

FSE (Full Site Editing): Blessing and Curse

The introduction of Full Site Editing (FSE) represents one of the most significant evolutions in WordPress history. The aim is to provide users with a visual builder-like experience, allowing them to see a direct preview in the dashboard of what will happen on the frontend.

Like any evolutionary process, this journey has seen its share of rapid transformations. With FSE and Block Themes in particular, the change has been radical:

  • No more PHP in the theme root.
  • Strange HTML filled with comments that aren’t comments.
  • A configuration file in JSON format.

For an introduction to these topics, WordPress Developer Resources is a good starting point. Here, the structure of the new themes is explained.

In terms of styling, an FSE theme maintains its own style.css, while global styles for typography, colors, padding, and other site-wide elements are declared in the theme.json file. These styles are accessible to users in the sidebar of the selected block.

A notable innovation is an integrated build process in FSE. Do we want to add JavaScript in ES6? Do we want to use SASS? WordPress provides the WordPress scripts package (built on top of webpack) to compile and include our files.

Block Editor: It Gets Even Better

Block Themes allow the integration of custom Blocks that can be used anywhere in the theme. I’m not talking about Patterns, which are essentially simplified versions of a Block designed to create a reusable graphic template (somewhat like the old reusable blocks). We’re talking about entities that use React and require a build process.

The anatomy of a block

A block consists of several components. Some are for declaring the block, others for how the block behaves in the dashboard, and others for the block’s display on the frontend.

  • index.js is our entry point and initializes the block
  • block.json declares the block properties, including custom attributes or additional JavaScript to be run on the frontend
  • edit.js and edit.scss (or css) handle the dashboard-related part
  • save.js and save.scss (or css) handle the frontend-related part

Additional JavaScript or PHP files can be incorporated for specific functionalities, such as animating elements using GSAP or adjusting server-side rendering for the block.

To use these blocks, they need to be registered in our functions.php file.

Example block: gallery with SwiperJS

Now, finally, some code. This block is moderately complex but perfect for explaining the integration between PHP and React. We will create a gallery with a management interface on the dashboard side. We’ll use SwiperJS library for the gallery’s JavaScript and import it into our block. The build process will be explained in the second part of the article because it differs somewhat from the standard process.

Let’s start with index.js:

import { registerBlockType } from "@wordpress/blocks";
import "./edit.scss";
import "./save.scss";
import Edit from "./edit";
import save from "./save";
import metadata from "./block.json";
registerBlockType(metadata.name, {
  edit: Edit,
  save,
});

Nothing more than an entry point for the WordPress scripts package build system.

In block.json, we start to see something more interesting

{
  "apiVersion": 2,
  "name": "blocks/gallery",
  "title": "Gallery",
  "version": "1.0.0",
  "category": "custom-blocks",
  "icon": "format-gallery",
  "description": "A block with a gallery on the left, and text on the right.",
  "supports": {
    "html": false,
  },
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./index.css",
  "viewScript": "file:./gallery.js",
  "attributes": {
    "slideCount": {
      "type": "number",
      "default": 1
    }
  },
  "example": {
    "innerBlocks": [
      {
        "name": "core/image",
        "attributes": {
          "url": "http://localhost/site/wp-content/themes/my-theme/blocks/custom-blocks/gallery/gallery.png",
          "alt": "gallery preview"
        }
      }
    ]
  }
}

In addition to the standard declarations, we have stated that we will use the gallery.js file on the frontend.

"viewScript": "file:./gallery.js",

And we have declared a new attribute called slideCount

"attributes": {
    "slideCount": {
      "type": "number",
      "default": 1
    }
  },

which we will use. We have also included a preview image to replace the standard preview system. This part is optional.

"example": {
    "innerBlocks": [
      {
        "name": "core/image",
        "attributes": {
          "url": "http://localhost/site/wp-content/themes/my-theme/blocks/custom-blocks/gallery/gallery.png",
          "alt": "gallery preview"
        }
      }
    ]
  }

Alright, now React comes into play. Let’s look at edit.js It’s a bit long, but if you prefer, you can skip directly to the explanations below the code.

import { Button } from "@wordpress/components";
import { useDispatch, useSelect } from "@wordpress/data";
import { InnerBlocks, useBlockProps } from "@wordpress/block-editor";
import { useEffect, useRef } from "@wordpress/element";
import Swiper from "swiper";
import { Navigation, Pagination } from "swiper/modules";

export default function Edit({ attributes, setAttributes, className, clientId }) {
  const ref = useRef();
  const swiperRef = useRef();
  const blockProps = useBlockProps({ ref });

  const { blockOrder, rootClientId, areBlocksInserted } = useSelect(
    (select) => {
      const { getBlockOrder, getBlockHierarchyRootClientId } = select("core/block-editor");
      const blockOrder = getBlockOrder(clientId);
      const rootClientId = getBlockHierarchyRootClientId(clientId);
      return {
        blockOrder,
        rootClientId,
        areBlocksInserted: blockOrder.length === blockOrder.length,
      };
    },
    [clientId]
  );

  const { insertBlock, removeBlock } = useDispatch("core/block-editor");

  useEffect(() => {
    if (
      areBlocksInserted &&
      ref.current &&
      !ref.current.querySelector(".block-editor-inner-blocks").classList.contains("swiper-container-initialized")
    ) {
      let swiperElement = ref.current.querySelector(".block-editor-inner-blocks");
      let swiperWrapper = ref.current.querySelector(".block-editor-block-list__layout");
      swiperElement.classList.add("swiper");
      swiperWrapper.classList.add("swiper-wrapper");
      let swiper_pagination = ref.current.querySelector(".swiper-pagination");
      let swiper_prev = ref.current.querySelector(".swiper-prev");
      let swiper_next = ref.current.querySelector(".swiper-next");

      swiperRef.current = new Swiper(swiperElement, {
        modules: [Navigation, Pagination],
        observer: true,
        observeParents: true,
        pagination: {
          el: swiper_pagination,
          clickable: true,
        },
        navigation: {
          nextEl: swiper_next,
          prevEl: swiper_prev,
        },
        slidesPerView: 1,
        speed: 800,
        touchStartPreventDefault: false,
      });
    }
  }, [areBlocksInserted, ref.current]);

  const TEMPLATE = [
    [
      "core/columns",
      { className: "gallery-cont swiper-slide" },
      [
        ["core/column", {}, [["core/image", {}]]],
        [
          "core/column",
          {},
          [
            ["core/heading", { placeholder: "Insert title", level: 2 }],
            ["core/heading", { placeholder: "Insert small title", level: 3 }],
            ["core/paragraph", { placeholder: "Insert content" }],
          ],
        ],
      ],
    ],
  ];

  const addSlide = () => {
    const newSlideCount = attributes.slideCount + 1;
    setAttributes({ slideCount: newSlideCount });
    const slideBlock = wp.blocks.createBlock(
      "core/columns",
      { className: `gallery-cont swiper-slide slide-${attributes.slideCount}` },
      [
        wp.blocks.createBlock("core/column", {}, [wp.blocks.createBlock("core/image", {})]),
        wp.blocks.createBlock("core/column", {}, [
          wp.blocks.createBlock("core/heading", { placeholder: "Insert title", level: 2 }),
          wp.blocks.createBlock("core/heading", { placeholder: "Insert small title", level: 3 }),
          wp.blocks.createBlock("core/paragraph", { placeholder: "Insert content" }),
        ]),
      ]
    );
    insertBlock(slideBlock, blockOrder.length, clientId);
    swiperRef.current.update();
    swiperRef.current.slideTo(attributes.slideCount);

    setTimeout(() => {
      swiperRef.current.slideTo(attributes.slideCount);
    }, 300);
  };

  const removeSlide = () => {
    const newSlideCount = attributes.slideCount - 1;
    setAttributes({ slideCount: newSlideCount });
    if (swiperRef.current) {
      const activeIndex = swiperRef.current.activeIndex;
      const blockToRemove = blockOrder[activeIndex];
      removeBlock(blockToRemove, rootClientId);
      swiperRef.current.update();
    }
  };

  return (
    <div
      {...blockProps}
      className={`${className || ""} my-block gallery edit`}
    >
      <div className="swiper">
        <div className="swiper-wrapper">
          <InnerBlocks
            template={TEMPLATE}
            templateInsertUpdatesSelection={false}
            templateLock={false}
          >
        </div>
      </div>
      <div className="swiper-pagination"></div>
      <div
        className="swiper-navigation"
        style={{ height: attributes.slideCount === 1 ? "0" : "50px" }}
      >
        <div className="swiper-prev"></div>
        <div className="swiper-next"></div>
      </div>
      <div className="button-wrapper">
        <Button
          isSecondary
          onClick={addSlide}
        >
          Add a Slide
        </Button>
        <Button
          isSecondary
          onClick={removeSlide}
          style={{ display: attributes.slideCount > 1 ? "inline-flex" : "none" }}
        >
          Remove Current Slide
        </Button>
      </div>
    </div>
  );
}

First, let’s import some built-in WordPress functions

import { Button } from "@wordpress/components";
import { useDispatch, useSelect } from "@wordpress/data";
import { InnerBlocks, useBlockProps } from "@wordpress/block-editor";
import { useEffect, useRef } from "@wordpress/element";
import Swiper from "swiper";
import { Navigation, Pagination } from "swiper/modules";

Some of these are familiar to those who use React. Here we import a version of those functions that is usable in the editor.

As attributes of Edit function, we use, among others, our custom attribute that we declared in block.json

export default function Edit({ attributes, setAttributes, className, clientId }) {

Then we set up Refs in the React way and use useDispatch and useSelect to manage the dynamic data flow from the server.

Inside useEffect, we handle SwiperJS. This is intriguing because we are launching a gallery that will be displayed directly in the editor. If we optimize the block interface effectively, our user will see the gallery almost exactly as it will appear to site visitors (on the frontend). Of course, in the editor, we’ll also include buttons to add more slides, which won’t be visible to visitors. This marks a notable shift in perspective.

Now, what does TEMPLATE refer to?

const TEMPLATE = [
    [
      "core/columns",
      { className: "gallery-cont swiper-slide" },
      [
        ["core/column", {}, [["core/image", {}]]],
        [
          "core/column",
          {},
          [
            ["core/heading", { placeholder: "Insert title", level: 2 }],
            ["core/heading", { placeholder: "Insert small title", level: 3 }],
            ["core/paragraph", { placeholder: "Insert content" }],
          ],
        ],
      ],
    ],
  ];

It’s a part of InnerBlocks, a very handy system for importing elements already present in WordPress into our custom block, such as images (including the media library handling) and text elements. Optionally, we can insert videos, quotes, and many other blocks, each with their integrated management system. This syntax closely resembles what we find in the strange HTML of FSE themes, akin to what we see when copying a block and pasting it into a text file.

The template will be rendered with minimal code in save.js, while here it is invoked within InnerBlocks in our return statement (yes, exactly, we're in React logic).

The functions addSlide and removeSlide manage the insertion of new slides. The slide count is managed through the attribute we added. Why isn't it an internal variable? Because we need the slide count to be stored in the database, and that's exactly what custom attributes do. We use createBlock, insertBlock, and removeBlock to insert and remove the slides, which are structured exactly like our initial TEMPLATE. Let’s take a look:

const slideBlock = wp.blocks.createBlock(
      "core/columns",
      { className: `gallery-cont swiper-slide slide-${attributes.slideCount}` },
      [
        wp.blocks.createBlock("core/column", {}, [wp.blocks.createBlock("core/image", {})]),
        wp.blocks.createBlock("core/column", {}, [
          wp.blocks.createBlock("core/heading", { placeholder: "Insert title", level: 2 }),
          wp.blocks.createBlock("core/heading", { placeholder: "Insert small title", level: 3 }),
          wp.blocks.createBlock("core/paragraph", { placeholder: "Insert content" }),
        ]),
      ]
    );

In the end, our render function mirrors the HTML structure needed by SwiperJS, with our TEMPLATE inserted as the first slide.

<InnerBlocks
  template={TEMPLATE}
  templateInsertUpdatesSelection={false}
  templateLock={false}
>

And we include buttons for inserting or removing slides:

<Button
  isSecondary
  onClick={addSlide}
  >
  Add a Slide
</Button>
<Button
  isSecondary
  onClick={removeSlide}
  style={{ display: attributes.slideCount > 1 ? "inline-flex" : "none" }}
>
  Remmove Current Slide
</Button>

The frontend part is much simpler. The file responsible for rendering our block on the site is save.js

import { InnerBlocks, useBlockProps } from "@wordpress/block-editor";

export default function Save({ className }) {
  const blockProps = useBlockProps.save();

  return (
    <div
      {...blockProps}
      className={`${className || ""} my-block gallery`}
    >
      <div className="swiper">
        <div className="swiper-wrapper">
          <InnerBlocks.Content >
        </div>
      </div>
      <div className="swiper-pagination"></div>
    </div>
  );
}

We import InnerBlocks to display the content of our InnerBlocks as defined in edit.js.

Finally, we add the JavaScript code to launch the gallery with SwiperJS on the frontend in gallery.js.

import Swiper from "swiper";
import { Pagination } from "swiper/modules";

function gallery() {
  document.querySelectorAll(".my-block.gallery").forEach(function (el) {
    var swiper_pagination = el.querySelector(".swiper-pagination");

    var swiper = new Swiper(el.querySelector(".swiper"), {
      modules: [Pagination],
      pagination: {
        el: swiper_pagination,
        clickable: true,
      },
      slidesPerView: 1,
      speed: 800,
    });
  });
}

document.addEventListener("DOMContentLoaded", gallery);

Create your own Blocks

Creating your own blocks following this structure is straightforward at this point. Here, I’ve shown a block with a dynamic interface for managing slides, but you can apply the same method to create blocks that are simpler or much more complex. What remains to be defined is the folder structure of the project (whether it’s a theme or a plugin) and, of course, the build process. This will be the topic of the second part of the article.

Another function we can employ when we want to fetch dynamic data is render_callback, which we pass as an argument when registering the block in functions.php. render_callback allows us to retrieve a list of posts, an archive, or navigation menus directly from the server. For instance, in my ZenFSE theme, I've developed a custom header that allows users to choose between two different menus—one for desktop and one for mobile. These menus are fetched using render_callback.

Even more: Modifying Core Blocks

Now that we understand how a block works, keep in mind that WordPress Core Blocks operate in the same way (but with many more features). If you want to take a look at the source code of the blocks, you can go to the WordPress Repository on GitHub.

All blocks, including Core blocks, can be customized using hooks. In particular, we’re interested in addFilter. With this hook, for example, we can enhance an existing block’s functionality without having to rewrite it from scratch. In ZenFSE, I've used GSAP to add an entrance animation to all my blocks.

Conclusion — Part One

Conclusion — Part One
This level of interactivity with components truly leaves ample room for creativity and imagination, both from the developer and the user perspectives. Another advantage is the modular nature of the code. Each block resides within its own folder with its files, and in terms of dependencies, each block has its own CSS and JavaScript. Moreover, InnerBlocks inherit the properties of the blocks they are composed of, so it’s possible to apply colors, typography, and all globally declared properties from the theme.json file.

WordPress continues to evolve with new solutions, the latest of which, at the moment I am writing, is the Interactivity API, which allows defining block interactions in an even more immediate manner.

There is no rest for developers.

If you read this far, tweet to the author to show them you care. Tweet a Thanks
I like FSE theme very much.

More Posts

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

Silvia Malavasi - Oct 8, 2024

Handling Errors and Job Lifecycles in Rails 7.1: Mastering `retry_on`, `discard_on`, and `after_discard`

ShahZaib - Nov 24, 2024

How To Create Custom Middlewares in ASP.NET Core

Anton Martyniuk - May 13, 2024

Mastering Trace Analysis with Span Links using OpenTelemetry and Signoz (A Practical Guide, Part 1)

NOIBI ABDULSALAAM - Oct 24, 2024
chevron_left