Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 021 / 100

Gutenberg blocks: your first custom block for production

Build blocks with React.

Expona Admin7 min read

Introduction

Most WordPress projects reach a point where page builders stop being enough—you need something custom, something that fits your exact workflow. Gutenberg blocks: your first custom block for production will teach you to build React-powered blocks that integrate seamlessly with WordPress, giving your team a composable editing experience without the framework overhead. By the end of this guide, you'll have a production-ready block that validates input, persists attributes, and renders consistently on the front end.

Why custom Gutenberg blocks matter

The block editor changed WordPress development. Instead of writing custom post types and metaboxes, you now compose functionality as reusable, self-contained units. A custom block can encapsulate your business logic—validation, data fetching, conditional rendering—and expose it as a drag-and-drop component for editors who may never touch code.

Out-of-the-box blocks cover basic needs: paragraphs, images, columns. Custom blocks handle the specifics: a testimonial carousel with your design system, a pricing table that pulls from your API, an embedded video player with analytics tracking. The investment in learning block development pays back immediately across every project using the same plugin.

Scaffold your block with create-block

WordPress provides @wordpress/create-block, a CLI tool that bootstraps a fully-functional block in seconds. This saves hours of wiring up build tools, dependencies, and boilerplate configuration.

Run this in your plugins directory:

php
npx @wordpress/create-block my-testimonial

This generates a folder structure:

php
my-testimonial/
├── src/
│   ├── edit.js        # React component for editor UI
│   ├── save.js        # React component for front-end output
│   ├── editor.scss    # Editor-only styles
│   ├── style.scss     # Front-end and editor styles
│   └── index.js       # Block registration entry point
├── block.json         # Block metadata and configuration
├── my-testimonial.php # Plugin file
└── package.json       # Node dependencies

The scaffolder includes webpack, Babel, and PostCSS preconfigured. You get fast rebuilds and modern JavaScript without manual toolchain setup. Install dependencies and start development with:

php
cd my-testimonial
npm install
npm start

Your changes auto-compile as you edit. In a browser, navigate to a WordPress page, add a block, search for "Testimonial," and you'll see your block live.

Understand block.json: metadata and configuration

The block.json file is the heart of any modern WordPress block. It declares metadata that WordPress reads during registration, without requiring PHP registration code.

Here's a realistic example:

php
{
  "$schema": "https://schemas.wp.org/wp/6.0/block.json",
  "apiVersion": 2,
  "name": "my-plugin/testimonial",
  "title": "Testimonial",
  "category": "widgets",
  "icon": "format-quote",
  "description": "Display a customer testimonial with name and role",
  "keywords": ["testimonial", "quote", "social-proof"],
  "supports": {
    "html": false,
    "align": ["wide", "full"]
  },
  "textdomain": "my-plugin",
  "attributes": {
    "quote": {
      "type": "string",
      "default": ""
    },
    "author": {
      "type": "string",
      "default": ""
    },
    "role": {
      "type": "string",
      "default": ""
    },
    "imageId": {
      "type": "number",
      "default": 0
    },
    "imageUrl": {
      "type": "string",
      "default": ""
    }
  },
  "editorScript": "file:./index.js",
  "editorStyle": "file:./editor.scss",
  "style": "file:./style.scss"
}

Key sections:

  • name: Namespaced identifier (vendor/block-name format prevents collisions).
  • attributes: Data shape stored in the block's markup. Each attribute has a type and default value.
  • supports: Enable/disable editor features (alignment, colors, typography, etc.).
  • editorScript and style: Point to compiled assets; WordPress handles enqueuing automatically.

Attributes are crucial. They define what data the block holds and how it's serialized in post content. If you later rename an attribute, you need a deprecation handler to migrate old blocks. Plan your attribute shape early.

Edit and save: two components for two contexts

Every block has two render functions: edit runs in the editor, save

The edit component

This is where editors interact with your block. Use WordPress component libraries for UI consistency:

php
import { useState } from '@wordpress/element';
import { 
  RichText, 
  MediaUpload, 
  InspectorControls,
  BlockControls,
  AlignmentToolbar
} from '@wordpress/block-editor';
import { 
  Panel, 
  PanelBody, 
  TextControl, 
  Button 
} from '@wordpress/components';

export default function Edit({ attributes, setAttributes }) {
  const { quote, author, role, imageUrl } = attributes;

  return (
    <>
      
         setAttributes({ align })}
        />
      

      
        
           setAttributes({ author })}
          />
           setAttributes({ role })}
          />
        
      

      <div>
         setAttributes({ quote })}
          placeholder="Enter testimonial text"
        />
         setAttributes({ 
            imageId: media.id, 
            imageUrl: media.url 
          })}
          allowedTypes={['image']}
          render={({ open }) => (
            
              {imageUrl ? 'Change Image' : 'Add Image'}
            
          )}
        />
        {imageUrl && <img src="{imageUrl}" alt="Author" />}
      </div>
    
  );
}

Use RichText for content that needs formatting (bold, italic, links). Use TextControl for plain strings. The InspectorControls sidebar keeps advanced options out of the main canvas, reducing cognitive load for editors.

The save component

This outputs the static HTML that gets stored in post content and rendered on the front end. Avoid interactive elements or client-side logic here:

php
import { RichText } from '@wordpress/block-editor';

export default function Save({ attributes }) {
  const { quote, author, role, imageUrl, align } = attributes;

  return (
    <figure>
      {imageUrl && (
        <img src="{imageUrl}" alt="{author}" />
      )}
      <blockquote>
        
      </blockquote>
      <figcaption>
        <strong>{author}</strong>
        {role && <span>{role}</span>}
      </figcaption>
    </figure>
  );
}

RichText.Content outputs formatted content from a RichText editor. For static fields, output them directly—no need for WordPress components on the front end.

The HTML from save is serialized inside HTML comments in post_content. When a user edits the block again, WordPress parses these comments to hydrate the editor component with the stored attributes.

Register your block in PHP

Modern WordPress (5.9+) handles most registration automatically if block.json is valid. In your main plugin file, call register_block_type:

php
<?php
/**
 * Plugin Name: My Testimonial Block
 * Version: 1.0.0
 */

add_action('init', function() {
  register_block_type(__DIR__ . '/build');
});

This tells WordPress to find block.json in the build directory (where your npm build process outputs compiled files) and register the block with all metadata from that file.

If you need dynamic rendering (server-side PHP generating part of the output), specify a render callback in your plugin or block.json:

php
add_action('init', function() {
  register_block_type(__DIR__ . '/build', [
    'render_callback' => function($attributes, $content) {
      // Optional: render server-side if needed
      return $content;
    }
  ]);
});

For most cases, the static save component is enough. Reserve server-side rendering for when you need to fetch fresh data or apply security-sensitive logic.

Build and test before deploying

When you're ready to ship, run the production build:

php
npm run build

This minifies assets and outputs to a build folder (usually ./build). Verify the build folder contains compiled JS and CSS, plus a copy of block.json.

Test on a staging WordPress install. Activate the plugin, create a page, verify the block appears in the inserter, and test all editor interactions. Then check the front end: ensure styles load, content renders correctly, and there are no console errors.

Common issues: image paths breaking in build output, editor and front-end styles not aligning, attributes not persisting after reload. A staging environment catches these before production.

Common mistakes

  • Mixing editor logic into save: If you add interactive hooks or state logic to save, it won't work on the front end where React isn't loaded. Keep save pure and static.
  • Forgetting to define attributes in block.json: Every field you set in edit must have a corresponding attribute or it won't persist. Attributes are your data contract.
  • Ignoring the deprecation handler: If you rename attributes later, old blocks will break. Use deprecated in block.json to map old attributes to new ones.
  • Over-scoping the initial block: Start simple. A testimonial with quote, author, and image is easier to test and maintain than a testimonial with 20 configurable options. Add features based on real editor feedback.
  • Not validating on the back end: Even though you validate in React, treat all input on the server as untrusted. Sanitize and validate in PHP before storing or output.

Key takeaways

  • Use @wordpress/create-block to scaffold modern blocks with zero config overhead.
  • block.json is your block's manifest: define metadata, attributes, and asset paths there, and WordPress registers it automatically.
  • Separate concerns: edit components build the editor UI, save components output static HTML; they run in different contexts.
  • Attributes are immutable until you deprecate them, so design your data shape deliberately.
  • Always test on staging before pushing custom blocks to production; check both editor and front-end rendering.
  • Keep blocks focused and simple at first; iterating on a block used by 50 editors is faster than shipping over-engineered features nobody asked for.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles