This is perhaps the most important section when it comes to understanding modern WordPress. Blocks and the block editor have fundamentally changed the way content is created and thought of in WordPress.
Blocks
A block is an abstract component used on the website. Let’s say you want to have a countdown on your website. This countdown may be a block. You can then place it in your templates, on your pages, or in your post’s content. A button is a block. An image is a block. A paragraph and a heading are blocks. All blocks usually give you some attributes to customize in the editor (font size, spacing, text content, etc.).
Blocks are just elements that render some pre-defined HTML on the frontend. You get to decide what that is. It can be something complex with 15 different HTML elements, or it can be something very simple (like the core abstractions over foundational elements like <p>). Here’s what the core paragraph block looks like:
<!-- wp:paragraph -->
<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>
<!-- /wp:paragraph -->And here’s a block from the Ultimate Blocks plugin:
<!-- wp:ub/click-to-tweet {"blockID":"5545214f-81c0-4e12-8325-6f0c79da171c","ubTweet":"Content to tweet","padding":{"top":"0","bottom":"0"}} /-->This is what gets saved in the post_content column in the database when you’re using the block editor. When WordPress renders the content to be displayed on the frontend, it doesn’t just return whatever is stored in the database. It parses the HTML comments (text between arrows) and renders any blocks it finds. These block delimiters transform the pseudo-unstructured blob of HTML into a structured tree of components.
This lets you create reusable, consistent, high-level components, which you can then structure your content with. Blocks are like shortcodes, except they have a much better user experience and get rendered in the editor.
In some way, you could say blocks are like HTML, but at a higher level of abstraction. They also have to get parsed, and just like HTML, they have attributes that modify their content and/or behavior. They are an abstract layer over HTML. This improves the user experience for a typical user, but it is more constraining than writing the HTML by hand. A trade-off that’s beneficial in 95% of cases.
A Technical Deep Dive Into Blocks
This section could be an entire document. I’ll try to explain how blocks work and are created, without going into too much detail. Read the official WordPress Block Editor Handbook if you’re interested in block development. It is surprisingly well written.
Blocks are written either primarily in PHP or JavaScript (with js being the modern standard). The development process deviates greatly from the classic WordPress development paradigm. It utilizes Node.js, ESNext (cutting-edge JavaScript features), React (JSX), and webpack (code compilation).
File Structure
Blocks are almost always added in plugins. A typical project contains a src folder, which includes raw, uncompiled code and other assets. This folder does not have to be included in the final plugin. The contents of the src directory are used in the build process, which generates the final files in a separate build directory. The build directory contains the compiled files used to display the block. Here’s the structure presented visually:

Source: File structure of a block
The index.js file gets loaded in the block editor. It’s responsible for registering the block on the client side and typically imports the edit.js and save.js files to get the functions required for block registration.
The edit.js file contains the React component responsible for rendering the block in the block editor. The save.js file exports the function that returns the static HTML markup that gets saved in the post_content column of the database.
The style.css file contains the styles of the block that will be loaded in both the block editor and on the frontend. The editor.css will be loaded only in the editor. .scss and .sass files can be used instead.
render.php is used for dynamic blocks, which are rendered on the server using PHP instead of being built with JavaScript.
The view.js file will be loaded on the frontend when the block is displayed. You can put any js functionality your block needs in this file.
The names of those files can be changed. The files themselves are specified in block.json or as arguments when registering the block.
Attributes
Block attributes work just like HTML attributes. They allow blocks to be populated with user-provided content or to modify the block’s settings or behavior in any way imaginable. Take a look at the example block ub/click-to-tweet mentioned in the Blocks section above. The attributes are ‘blockID’, ‘ubTweet’, and ‘padding’.
Attributes are usually stored in a JSON format inside the block’s delimiter, just like you would add attributes to an HTML tag. These attributes are ones that have been modified by the user. Default attributes don’t need to be present in the block declaration.
Some attributes can be stored in the content of the block. This is the way the core paragraph block stores the text attribute (the paragraph block was shown in the Blocks section). You can see that it doesn’t need a JSON in its delimiter. The attribute is stored in the generated HTML. This requires additional configuration when defining the attribute in the block.json file, namely the “source” and “selector” properties.
Attributes are defined by the block’s author. When WordPress parses the content and renders the block on the frontend, these attributes are provided as parameters to the block’s code. The block can then change its rendering based on the attributes, like displaying a string attribute inside a <p> tag or changing the padding of a given element. The block’s author has full control of what attributes they define and how they use them in their block.
block.json
This is arguably the most important file for a block. It defines metadata about the block and the files associated with it.
Some of the most important metadata defined in this file are:
- API version
- name
- title
- category
- attributes
- supports (declaring support for certain native features, e.g., changing the text or bg color)
- and many more
block.json also allows you to specify files essential for the block’s functionality. WordPress will then enqueue these files in the correct contexts. The names in brackets are the “standard” names of these files mentioned previously:
- editorScript – js file(s) loaded in the block editor (index.js).
- editorStyle – css file(s) loaded in the block editor (editor.css).
- script – js file(s) loaded both in the editor and on the frontend.
- style – css file(s) loaded both in the editor and on the frontend (style.css).
- viewScript – js file(s) loaded on the frontend (view.js).
- render – php file for a dynamic block (render.php).
These properties work either with a file path prefixed with “file:” or with a handle registered using wp_register_script() or wp_register_style() (more information on handles is available in the section on loading assets).
Registering The Block
Blocks in WordPress should be registered on both the server and the client side.
Server-side registration means registering the block in PHP. This process is pretty much the same as registering custom post types. You basically tell WordPress: “Here’s a block type, it has this name and these parameters, remember it so that you can process it”. WordPress then holds the information that your block type exists, along with its configuration, in memory, which allows it to do different things with that knowledge on the backend.
The registration itself usually takes place in the plugin’s main PHP file. The function used to register a block is register_block_type( $block_type, $args ).
The $block_type argument is a string path to the directory containing your block.json file. This should be the path to the build directory, not the src directory (the block.json file should be copied to the build directory during compilation).
The $args argument is an array of optional parameters. These parameters are the same as in block.json. You can register your block fully with $args, without using the block.json file, but you shouldn’t. It’s considered legacy. Use the JSON file. In practice, the registration would typically look like this:
function register_my_block() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'register_my_block' );Client-side registration means registering the block in JavaScript in the block editor. The concept of “registering” a block is a WordPress-specific thing. It means you create a js object with certain properties needed for your block to work in the block editor. You then pass this object to the editor’s code. Remember, this happens only in the admin panel whenever the block editor is displayed. It doesn’t happen on the user-facing frontend.
The registration happens in the editorScript file (typically index.js). To do it, you use the registerBlockType(blockNameOrMetadata, settings) method from the @wordpress/blocks JavaScript package.
The blockNameOrMetadata argument is either a string or an object. If it’s a string, it’s the name of the block you specified in block.json (e.g., ‘ub/click-to-tweet’). If that’s the case, and the block has been registered on the server, then the metadata will be automatically loaded from the metadata registered on the backend. Instead of passing a string, you can also pass a block.json object. You can literally import your block.json file to the index.js file and pass it as the first argument. The first approach is the best standard.
The settings argument is an object. It has many parameters. As a matter of fact, you could register your block without registering it on the server and passing only the name to the blocknameOrMetadata argument. That’s because the settings argument allows you to specify all of the same metadata you specify in the block.json file. It’s the same as with PHP registration. You could also pass the metadata in the $args argument there, but you shouldn’t.
The settings argument has 2 extremely important properties:
- edit – the React component that gets used in the editor for the block. It’s responsible for making the block function in the block editor.
- save – the function that returns the static HTML of the block to be saved in the database (for static blocks).
That’s the code you write in the edit.js and save.js files. You have to import those files into index.js, and then pass the edit React component and the save function in the settings object. A typical index.js file before compilation might look something like this:
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import save from './save';
import metadata from './block.json';
registerBlockType( metadata.name, {
edit: Edit,
save,
} );You can theoretically register a block on the client side only. While you could do that, you shouldn’t, unless you know what you’re doing and have a very specific reason to do so. Some of the disadvantages that come from not registering the block on the server are:
- The block doesn’t appear in the Block Type REST API endpoint.
- The block has to be static (no server-side rendering).
- The standard best way for registering the block in index.js doesn’t work (you have to pass the block.json file instead of just the name).
- Block hooks don’t work.
- Global styles (from theme.json in block themes) and aren’t applied.
- and probably more…
Static Blocks
A static block doesn’t require any rendering on the server (in PHP). The HTML of the block is generated in the block editor by the save() function (supplied in the index.js file when registering the block on the client side). Static blocks are rendered fully with JavaScript (in the save function), and their final HTML is stored in the post_content column in the database. Their contents are served directly from the database when rendering the page on the frontend – no additional computation is required. One example of a static block is the paragraph block:
<!-- wp:paragraph -->
<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>
<!-- /wp:paragraph -->When WordPress parses the post’s content to render the final HTML, all it does is strip away the block delimiter (the HTML comments), so that only the <p> tag is rendered (the comments are never output on the frontend).
Static blocks don’t have any ability to change after their markup has been rendered in the editor. Only truly static pieces of content that don’t require server-side logic can therefore be static blocks. That being said, static blocks are more performant, precisely because they don’t require server-side computation. You should try to make your blocks static whenever you can.
Block Validation
Block validation is a process the block editor uses to check the validity of static blocks. The existing blocks’ save() function is run every time the editor is loaded. Remember, this function is responsible for generating the block’s HTML from the attributes supplied. If the returned HTML is different than the HTML stored in the database, the block is marked as invalid and the editor displays this:

Let’s break the paragraph block to see how it works. We’ll set the alignment attribute of the paragraph to center, which centers the text. This is what the block looks like in code:
<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>
<!-- /wp:paragraph -->As you can see, the “align: center” property has been added in the block delimiter and the has-text-align-center class has been added to the rendered markup. Let’s now manually delete ‘class=”has-text-align-center”‘ from the <p> tag while keeping the align attribute in the delimiter and let’s refresh the editor. Sure enough, we get a validation error. WordPress prints validation errors to the console so let’s take a look:

As you can see, the markup generated by the save() function did not match the markup stored in the database (which, for some reason, has double <p> tags – likely a quirk of the editor).
You now have 3 options:
- Attempt recovery – WordPress will attempt to recover the correct markup of the block (it’s not always successful). We’ll do that in a second.
- Convert to HTML – converts the markup to a plain HTML block.
- Convert to Classic Block – converts the block to a Classic Block. This block is basically the Classic Editor in the form of a block. It uses the TinyMCE editor just like the classic editor did.
Here’s what we get if we attempt recovery:
<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center"></p>
<!-- /wp:paragraph -->An empty centered paragraph block… Underwhelming, but you can’t really blame WordPress. If you take a look at what the save() function returned (shown on the screenshot from the console above), you will see that it’s exactly what it should be. An empty <p> tag with the “has-text-align-center” class.
As a side note for the curious, the <p> tag was empty because the outside <p> tag of the invalid block stored in the database had no text. It had a <p> tag as its child which had text, but the parent <p> did not have any text itself, so the content attribute for the paragraph block was empty (the attribute is retrieved from the markup using something like document.querySelector(‘p’).textContent).
So why do validation errors happen? Well, one possibility may be what we’ve done. The user plays with the raw code making it invalid. Another one is a flaw in the block’s code resulting in the re-generated HTML not being the same as the stored HTML. But there’s a more problematic reason – the block’s author changed its save() function. If the save() function’s output changes, for example after a plugin’s update, the resulting markup generated might be different, which would mark the stored markup as invalid. What do you do then?
Block deprecation is a technique for updating blocks without making them invalid. The magic depends on creating a new deprecation object (usually in a deprecation.js file). This object needs to contain the old, deprecated save() function. You then register an array of such objects in the ‘settings’ parameter to the registerBlockType method. You can even change the names of your attributes using this method.
Whenever an invalid block is detected in the editor, all the deprecated save() functions are called. If one of them produces HTML matching the one stored in the database, the invalid content warning is not displayed. Instead, the markup of the block is automagically replaced with the new, updated markup when the content is saved (by clicking “Save” in the editor). This means the user has to save the content of the post for the block to upgrade itself to the new version.
It’s worth adding that block validation is the single most important reason why many developers are reluctant to write static blocks and opt for dynamic blocks instead.
Dynamic Blocks
Dynamic blocks are rendered on the server each time the page is requested. Their HTML is not stored in the database. The block displayed in the editor is still written in javascript (in the edit.js file), but the markup returned to the frontend is generated in PHP. Note that this means you have to maintain 2 files, both responsible for displaying the block, but in different contexts. The save() js function should return null (although it can return HTML, which would make the static content act as a fallback, but that’s an advanced topic).
The PHP code to be run is specified either using the legacy ‘render_callback’ method when registering the block with register_block_type or using the ‘render’ property in block.json. The file specified in the ‘render’ property gets included when WordPress parses the content and encounters the block. The file should just echo the block’s markup.
An example of a dynamic block is the aforementioned ub/click-to-tweet:
<!-- wp:ub/click-to-tweet {"blockID":"5545214f-81c0-4e12-8325-6f0c79da171c","ubTweet":"Content to tweet","padding":{"top":"0","bottom":"0"}} /-->