Most Gutenberg block tutorials start with npx @wordpress/create-block, which sets up React, JSX, and a webpack build pipeline. That’s the right call for a block you’re publishing or maintaining long-term. For a one-off custom block on a client site, it’s often more setup than the block itself needs.

A block using wp.blocks directly, no build step

// blocks/testimonial.js — loaded directly, no compilation
(function(blocks, element, blockEditor) {
    const { registerBlockType } = blocks;
    const { createElement: el } = element;
    const { RichText, InspectorControls } = blockEditor;

    registerBlockType('myplugin/testimonial', {
        title: 'Testimonial',
        icon: 'format-quote',
        category: 'common',
        attributes: {
            quote: { type: 'string', source: 'html', selector: 'blockquote' },
            author: { type: 'string', source: 'html', selector: 'cite' },
        },
        edit: function(props) {
            const { attributes, setAttributes } = props;
            return el('div', { className: 'testimonial-block' },
                el(RichText, {
                    tagName: 'blockquote',
                    value: attributes.quote,
                    onChange: (quote) => setAttributes({ quote }),
                    placeholder: 'Enter quote...',
                }),
                el(RichText, {
                    tagName: 'cite',
                    value: attributes.author,
                    onChange: (author) => setAttributes({ author }),
                    placeholder: 'Author name',
                })
            );
        },
        save: function(props) {
            const { attributes } = props;
            return el('div', { className: 'testimonial-block' },
                el(RichText.Content, { tagName: 'blockquote', value: attributes.quote }),
                el(RichText.Content, { tagName: 'cite', value: attributes.author })
            );
        },
    });
})(window.wp.blocks, window.wp.element, window.wp.blockEditor);

Registering it in PHP

add_action('enqueue_block_editor_assets', function() {
    wp_enqueue_script(
        'myplugin-testimonial-block',
        plugin_dir_url(__FILE__) . 'blocks/testimonial.js',
        ['wp-blocks', 'wp-element', 'wp-block-editor'],
        filemtime(plugin_dir_path(__FILE__) . 'blocks/testimonial.js')
    );
});

Where this stops being the right call

No JSX means every element is a nested el() call, which gets genuinely hard to read past 3-4 nesting levels. For a simple two-field block like this, it’s fine. For a block with variations, dynamic previews, or complex conditional UI, the readability cost outweighs the build-step savings — that’s when it’s worth switching to the standard tooling.