To develop a Gutenberg block for WordPress, prepare a testing environment, create a wireframe via @wordpress/create-block, describe the settings in block.json and implement editing in edit.js. For a static block, the markup is stored in the post content, while for a dynamic block, it is generated on the server, for example, through render.php.
In this guide, you'll determine the appropriate architecture, configure block attributes, and explore two scenarios: a service card and a dynamic list of posts. Gutenberg block development should be performed on a staging environment with a backup: before deployment, you should separately check the WordPress version, active theme, plugins, permissions, validation, and data escaping.
The place of blocks among other ways of expanding the site is shown in WordPress Custom Development Guide. Below are the steps for creating your own block.
When you need your own block

A custom block is needed if the editor's standard features don't cover the structure, rules, or logic of content display. For example, you want the editor to add a service card using a standard template, rather than manually assembling it each time from a title, image, text, and button.
Developing Gutenberg blocks is justified when the solution will be used regularly, must maintain a consistent design, or requires data from WordPress. For a one-off page, ready-made blocks or a pattern are usually sufficient.
A ready-made block, pattern, or new block
First, determine what exactly the editor is missing. Not every non-standard section requires programming:
- Finished block Suitable if the task can be solved using standard Gutenberg tools without compromising the structure and design.
- Pattern Convenient for reusing a composition of multiple blocks. It sets the initial layout, but the user can usually modify its elements independently.
- Custom block This is useful when you want to limit the set of fields, fix the layout, add special behavior, or retrieve up-to-date data from the system.
For the service card scenario, the choice depends on the requirements. If the editor requires fixed fields—title, description, image, and link—Gutenberg custom blocks help maintain a predictable structure. If the card is purely a visual template and allows for free editing, it's more rational to start with a pattern.
Evaluate alternatives. For example, ACF in WordPress Suitable for structured fields. Choose the architecture based on your editing scenario, plugin dependencies, and team competencies. Test compatibility with your WordPress versions, themes, and extensions.
Static and dynamic output
The key architectural decision is where the markup is generated: when the content is saved or each time the page is opened. This determines how the block responds to data changes and how difficult it will be to maintain.
| Criterion | Static block | Dynamic block |
|---|---|---|
| Forming the output | The markup is saved along with the post content. | The markup is created on the server when the page is displayed. |
| When to use | Content can only be changed through the block editor. | The result depends on the records, settings or other relevant data. |
| A suitable scenario | Service card with manually filled fields | List of recent posts in the selected category |
| The main limitation | Changing the structure may require taking into account previously saved markup. | The output depends on server logic and requires careful data verification. |
For a service card, a static approach is usually sufficient if all information is defined by the editor and should remain part of a specific page. WordPress dynamic block development with Gutenberg is more appropriate for a list of posts: when new content is published, such a list can be updated without manually editing the page.
Before choosing, determine the data source, refresh frequency, and acceptable level of dependency on server-side code. If user input is required, consider access rights checking, input validation, and output escaping in advance. Test the final solution on a staging copy of the site with a backup, not directly on the production resource.
Prepare the development environment

Test WordPress
Gutenberg block development should be performed in an isolated environment, not on a live site. You can use a local WordPress installation or a staging copy of your site. The latter option is useful if you need to test the block's interaction with the actual theme, plugins, and server settings.
Before starting work, create a backup copy of your files and database. Ensure that the test environment does not accept live orders, payments, or user requests. For your store, ensure that payment notifications, customer emails, and CRM data exchange are disabled.
It's best to keep the WordPress, PHP, theme, and key plugin versions on the test environment as close to the production site as possible. A consistent configuration alone doesn't guarantee compatibility: it's checked after each major update. Diagnostics require access to error logs, backups, and PHP settings.
Before installing the tools, record the initial conditions:
- WordPress version and active theme;
- PHP version and available server extensions;
- list of plugins that affect the editor and content output;
- availability of HTTPS, debug log and backup;
- user roles that are allowed to add and edit a block.
For diagnostic purposes, you can enable WordPress error logging on your test site. Avoid displaying technical messages to visitors and don't leave debug mode enabled on your live site, as logs can reveal file paths and other service information.
Assembly tools
Modern custom Gutenberg blocks typically use JavaScript, JSX, and WordPress packages, so you'll need Node.js and npm. Choose a supported version of Node.js and check it against the requirements of the packages you use, rather than installing any outdated build.
Official package @wordpress/create-block helps prepare the plugin structure and configured build system. Before running, check Node.js with the command node --version, and npm - with the command npm --version. The generator itself can be called through npx @wordpress/create-block; you will set the project parameters in the next step.
Also, prepare a code editor, terminal, Git, and browser developer tools. Git will allow you to separate block changes from other developments and return to a stable state. Do not add passwords, API keys, database backups, or configuration files containing secrets to the repository.
If your team works on different operating systems, document the versions of Node.js, the package manager, and the run command in the project documentation. This reduces the risk of a build that works for one developer but isn't reproducible for another. Before deploying to the production site, install dependencies and build only in a controlled environment, and further test the results on staging.
Create a block base

block.json metadata
It is more convenient to describe the basis of the block in a file block.json. WordPress uses it as a single source of metadata: block name, category, included scripts and styles, supported features, and data structure. A project created through @wordpress/create-block, already contains such a file and a base registration point.
For the "service card" scenario, the minimum configuration might look like this:
{ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "name": "project/service-card", "version": "0.1.0", "title": "Service Card", "category": "widgets", "icon": "index-card", "description": "Card with title, description, and link", "supports": { "html": false }, "attributes": { "title": { "type": "string", "default": "" }, "description": { "type": "string", "default": "" }, "url": { "type": "string", "default": "" } }, "editorScript": "file:./index.js", "editorStyle": "file:./index.css", "style": "file:./style-index.css" }
Field name must contain a namespace and a unique name, for example project/service-card. After publishing a block, it's not advisable to change it without migrating it: WordPress uses this name to recognize saved markup.
editorScript points to the JavaScript editor, editorStyle — for styles only for the administrative part, and style — for general block styles. After the build, check the file connections, WordPress version, and infrastructure limitations.
Editorial part and attributes
File edit.js is responsible for the interface you see in Gutenberg. Data should be stored in block attributes if it should be saved with the publication. setAttributes updates these values as you type.
import { useBlockProps, RichText } from '@wordpress/block-editor'; export default function Edit({ attributes, setAttributes }) { const { title, description } = attributes; return (
<div {...useblockprops()}>
<richtext
tagname="h3"
value="{title}"
placeholder="Название услуги"
onchange="{(value)" > setAttributes({ title: value })} />
<richtext
tagname="p"
value="{description}"
placeholder="Краткое описание"
onchange="{(value)" > setAttributes({ description: value })} />
</div>
);
}
useBlockProps() adds the required wrapper properties that allow the editor to select, move, and customize the block. The component RichText Suitable for rich text. For URLs, numbers, or radio buttons, it's best to choose specialized controls and separately restrict the allowed values.
Attribute type in block.json Describes the data format but does not replace validation and safe output. URLs must be checked, text must be escaped according to context, and user permissions must be checked for operations with sensitive data. For a static block, attributes are then converted to storable markup, and when developing dynamic blocks with Gutenberg, they can be passed to the server handler. Before activating your plugin, build on a staging copy of the site and prepare a backup copy: compatibility with the theme, plugins, and the installed WordPress version requires separate testing.
Customize the output and data

Choose the output method based on the data source. If the content changes only when editing a block, save the finished markup in the post. If the result depends on current posts, settings, or the site's state, generate the HTML on the server with each request.
| Option | When to use | Restrictions |
|---|---|---|
| Static block | Service card, warning, formatted text with an image | Changing the saved structure may require migrating the old content. |
| Dynamic block | List of records, data from settings, or query result | The output depends on PHP code and creates a server load when generating the page. |
Static storage
For the service card, define the block attributes in block.json: For example, title, description And URL. IN edit.js you get them through attributes, and you transfer the changes through setAttributes. Check the type of each value: a string should not unexpectedly turn into an array or a boolean value.
Function save Returns the markup that will be written to the page content. It should depend only on block attributes and embedded content, not on an external API or the current date. Otherwise, the saved markup will no longer match the expected schema.
When changing the HTML of an already published block, WordPress may invalidate it. Therefore, avoid changing classes and structure unless necessary. For a new, incompatible version, provide a legacy block schema with attribute transformations or create a separate version. Check previously created pages as well.
Dynamic rendering
For a dynamic list of records, specify in block.json rendering file, for example ""render": "file:./render.php"". The editor saves the block settings - the number of elements, the selected category and order - and render.php receives attributes and generates actual HTML on the server.
In the dynamic block function save usually returns null, if you do not need to save the backup markup. In render.php Normalize the input parameters before the query: convert the number of records to an integer and limit it to an acceptable range, check the category ID as a positive number, and select the sort direction only from a predefined list.
When outputting, escape data according to context: text - through esc_html(), URL - via esc_url(), HTML attribute values - through esc_attr(). If a block accesses restricted content or requires administrative actions, check user permissions and nonce separately. Do not pass block parameters directly to SQL or HTML.
The editor preview may differ from the front-end due to theme styles and enabled plugins. Therefore, don't promise compatibility ahead of time: test both areas on a staging copy with the same WordPress version, theme, and extension set as your production site. Before replacing an existing block, back up your database and files.
Check the block before implementation
Before publishing, test the block on a staging copy of the site and create a backup. This testing should cover not only the appearance but also the preservation of block attributes, access rights, handling of invalid data, and behavior after a page refresh. Compatibility cannot be guaranteed without testing the specific WordPress version, active theme, and installed plugins.
Editor and frontend
First, add the block to the Gutenberg editor and fill in all available fields. Make sure the controls are clear, changes are immediately reflected in the editing area, and the entered values are saved after updating the post. Check for empty fields, long titles, special characters, images of different proportions, and repeated additions of multiple block instances.
Compare the editor view with the published page. For a static block, it's important that the saved markup matches the current implementation. For a dynamic block, check the result generated on the server via render.php: The output must not reveal private records or data not accessible to the current user.
All data must be validated before saving and escaped when output. For text, use appropriate WordPress functions rather than a generic "just in case" processing: plain text, URLs, and legitimate HTML require different escaping methods. If a block performs a protected action, check the user's permissions and nonce. The mere presence of a block in the editor should not grant additional privileges.
Use the following checklist:
- the block can be added, customized, moved, duplicated and deleted;
- block attributes are preserved without loss or unexpected data transformation;
- the empty state contains a clear hint and does not break the page;
- the markup is correct in the editor, preview, and on the frontend;
- dynamic queries are limited to the required data and take into account the publication status;
- validation, escaping, access rights and error handling have been checked;
- there are no block-related errors or warnings in the browser console or PHP log;
- Block styles do not change extraneous elements of the site.
Mobile screen and updates
Test the block on narrow, medium, and wide screens. The service card should maintain a readable structure, and buttons should not extend beyond the container's boundaries. For a dynamic list of entries, evaluate the wrapping of long headings, image sizes, and the behavior when no results are found. Responsive viewing mode alone is not enough: if possible, open the page on a real mobile device and test the touch controls.
Then test updating the block itself. Changing the attribute structure or saved markup can invalidate previously added instances. Open old posts on staging and test them in the editor and on the site. When changing static markup, consider legacy block versions and data migration; dynamic output is usually updated centrally, but also requires template and query validation.
Repeat the tests after updating WordPress, your theme, or dependent plugins. Deploy the block to your live site only after backing it up and with a quick rollback plan.
Official documentation: block creation guide.