{"id":20866,"date":"2026-10-21T08:29:00","date_gmt":"2026-10-21T06:29:00","guid":{"rendered":"https:\/\/www.lukaswojcik.com\/blog\/?p=20866"},"modified":"2026-10-09T22:33:53","modified_gmt":"2026-10-09T20:33:53","slug":"dynamic-block-block-json","status":"publish","type":"post","link":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/","title":{"rendered":"A dynamic Gutenberg block with block.json and render.php"},"content":{"rendered":"<p>A block whose output depends on data that changes after the post was saved (the newest posts, a price, a list of events) cannot store its final HTML in <code>post_content<\/code>. Every new post in the category would leave the saved markup behind. Static blocks have a second weakness: once the markup of the <code>save<\/code> function changes, older posts no longer match, the editor reports invalid content and the block needs a deprecation entry. A dynamic block avoids both problems. The editor stores only the block comment with its attributes, and PHP renders the markup on every request.<\/p>\n<p>This article builds such a block from scratch: a list of the latest posts of one category, with the attributes <code>categoryId<\/code> and <code>count<\/code>. It describes what <code>@wordpress\/create-block<\/code> generates as of 29.09.2026, how <code>block.json<\/code> connects the editor with the server, why the scaffold now registers blocks through a generated manifest, and which variables <code>render.php<\/code> receives. All code is complete; the plugin runs on WordPress 6.9 or newer, and the current release at the time of writing is 7.1.2.<\/p>\n<h2>What the create-block scaffold produces today<\/h2>\n<p>The official tool is the npm package <code>@wordpress\/create-block<\/code>; the latest published version on the retrieval date is 4.99.0 from 23.09.2026. It needs Node.js 20.10.0 or newer. The standard template knows two variants, <code>static<\/code> and <code>dynamic<\/code>. The dynamic variant differs in exactly three places: <code>block.json<\/code> receives a <code>render<\/code> field pointing to <code>render.php<\/code>, <code>index.js<\/code> registers the block without a <code>save<\/code> function, and the npm scripts copy PHP files into the build folder. The commands for the example look like this:<\/p>\n\n\n<figure style=\"--cbb-digits:1\" class=\"cbb cbb--wrap cbb--numbers wp-block-code-beautifier-block-code\" data-lang=\"bash\"><figcaption class=\"cbb-head\"><span class=\"cbb-lang\">Shell<\/span><span class=\"cbb-actions\"><button type=\"button\" class=\"cbb-button cbb-button--wrap\" data-cbb-action=\"wrap\" aria-pressed=\"true\" title=\"Wrap long lines\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path d=\"M2 4h12M2 8h9.5a2.5 2.5 0 0 1 0 5H8m0 0 1.5-1.5M8 13l1.5 1.5M2 12h3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg><span class=\"cbb-sr\">Wrap long lines<\/span><\/button><button type=\"button\" class=\"cbb-button cbb-button--copy\" data-cbb-action=\"copy\" data-cbb-done=\"Copied\" data-cbb-failed=\"Press Ctrl+C to copy\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8.5\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><path d=\"M10.5 3.5V3A1.5 1.5 0 0 0 9 1.5H4A1.5 1.5 0 0 0 2.5 3v6.5A1.5 1.5 0 0 0 4 11h.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><\/svg><span class=\"cbb-button-label\" aria-live=\"polite\">Copy<\/span><\/button><\/span><\/figcaption><pre class=\"cbb-pre\" tabindex=\"0\"><code class=\"cbb-code language-bash\" style=\"counter-reset:cbb-line 0;\"><span class=\"cbb-line\"><span class=\"cbb-function\">npx<\/span> @wordpress\/create-block@latest category-posts <span class=\"cbb-attr\">--variant<\/span> dynamic <span class=\"cbb-attr\">--namespace<\/span> lw <span class=\"cbb-attr\">--title<\/span> <span class=\"cbb-string\">\"Category Posts\"<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-function\">cd<\/span> category-posts\n<\/span><span class=\"cbb-line\"><span class=\"cbb-function\">npm<\/span> install @wordpress\/components @wordpress\/core-data @wordpress\/data @wordpress\/html-entities @wordpress\/server-side-render <span class=\"cbb-attr\">--save<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-function\">npm<\/span> start<\/span><\/code><\/pre><\/figure>\n\n\n<p>The additional <code>npm install<\/code> line is not strictly required for the build, because <code>@wordpress\/scripts<\/code> treats <code>@wordpress\/*<\/code> imports as externals and maps them to the script handles that WordPress already ships. Installing the packages keeps editor tooling and linting aware of the imports. <code>npm start<\/code> watches <code>src\/<\/code> and writes the result to <code>build\/<\/code>; <code>npm run build<\/code> produces the minified version for release. The resulting plugin folder looks roughly like this:<\/p>\n\n\n<figure style=\"--cbb-digits:2\" class=\"cbb cbb--wrap cbb--numbers wp-block-code-beautifier-block-code\" data-lang=\"plain\"><figcaption class=\"cbb-head\"><span class=\"cbb-lang\">Text<\/span><span class=\"cbb-actions\"><button type=\"button\" class=\"cbb-button cbb-button--wrap\" data-cbb-action=\"wrap\" aria-pressed=\"true\" title=\"Wrap long lines\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path d=\"M2 4h12M2 8h9.5a2.5 2.5 0 0 1 0 5H8m0 0 1.5-1.5M8 13l1.5 1.5M2 12h3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg><span class=\"cbb-sr\">Wrap long lines<\/span><\/button><button type=\"button\" class=\"cbb-button cbb-button--copy\" data-cbb-action=\"copy\" data-cbb-done=\"Copied\" data-cbb-failed=\"Press Ctrl+C to copy\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8.5\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><path d=\"M10.5 3.5V3A1.5 1.5 0 0 0 9 1.5H4A1.5 1.5 0 0 0 2.5 3v6.5A1.5 1.5 0 0 0 4 11h.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><\/svg><span class=\"cbb-button-label\" aria-live=\"polite\">Copy<\/span><\/button><\/span><\/figcaption><pre class=\"cbb-pre\" tabindex=\"0\"><code class=\"cbb-code language-plain\" style=\"counter-reset:cbb-line 0;\"><span class=\"cbb-line\">category-posts\/                 (abridged)\n<\/span><span class=\"cbb-line\">\u251c\u2500\u2500 category-posts.php          plugin header and block registration\n<\/span><span class=\"cbb-line\">\u251c\u2500\u2500 package.json                build\/start scripts with --blocks-manifest\n<\/span><span class=\"cbb-line\">\u251c\u2500\u2500 readme.txt\n<\/span><span class=\"cbb-line\">\u251c\u2500\u2500 src\/\n<\/span><span class=\"cbb-line\">\u2502   \u2514\u2500\u2500 category-posts\/\n<\/span><span class=\"cbb-line\">\u2502       \u251c\u2500\u2500 block.json\n<\/span><span class=\"cbb-line\">\u2502       \u251c\u2500\u2500 index.js\n<\/span><span class=\"cbb-line\">\u2502       \u251c\u2500\u2500 edit.js\n<\/span><span class=\"cbb-line\">\u2502       \u251c\u2500\u2500 render.php\n<\/span><span class=\"cbb-line\">\u2502       \u251c\u2500\u2500 view.js             not needed here, deleted\n<\/span><span class=\"cbb-line\">\u2502       \u251c\u2500\u2500 style.scss\n<\/span><span class=\"cbb-line\">\u2502       \u2514\u2500\u2500 editor.scss\n<\/span><span class=\"cbb-line\">\u2514\u2500\u2500 build\/                      written by npm start \/ npm run build\n<\/span><span class=\"cbb-line\">    \u251c\u2500\u2500 blocks-manifest.php\n<\/span><span class=\"cbb-line\">    \u2514\u2500\u2500 category-posts\/\n<\/span><span class=\"cbb-line\">        \u251c\u2500\u2500 block.json\n<\/span><span class=\"cbb-line\">        \u251c\u2500\u2500 index.js\n<\/span><span class=\"cbb-line\">        \u251c\u2500\u2500 index.asset.php\n<\/span><span class=\"cbb-line\">        \u251c\u2500\u2500 index.css\n<\/span><span class=\"cbb-line\">        \u251c\u2500\u2500 style-index.css\n<\/span><span class=\"cbb-line\">        \u2514\u2500\u2500 render.php<\/span><\/code><\/pre><\/figure>\n\n\n<p>With the default template, the block lives in <code>src\/&lt;slug&gt;\/<\/code>, not directly in <code>src\/<\/code>. The text domain defaults to the slug, here <code>category-posts<\/code>. The scaffold also adds a <code>view.js<\/code> file and a matching <code>viewScript<\/code> entry for front-end JavaScript. A server-rendered list without interaction does not need it, so the example removes both. The two npm scripts that matter for a dynamic block are generated like this:<\/p>\n\n\n<figure style=\"--cbb-digits:1\" class=\"cbb cbb--wrap cbb--numbers wp-block-code-beautifier-block-code\" data-lang=\"json\"><figcaption class=\"cbb-head\"><span class=\"cbb-lang\">JSON<\/span><span class=\"cbb-actions\"><button type=\"button\" class=\"cbb-button cbb-button--wrap\" data-cbb-action=\"wrap\" aria-pressed=\"true\" title=\"Wrap long lines\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path d=\"M2 4h12M2 8h9.5a2.5 2.5 0 0 1 0 5H8m0 0 1.5-1.5M8 13l1.5 1.5M2 12h3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg><span class=\"cbb-sr\">Wrap long lines<\/span><\/button><button type=\"button\" class=\"cbb-button cbb-button--copy\" data-cbb-action=\"copy\" data-cbb-done=\"Copied\" data-cbb-failed=\"Press Ctrl+C to copy\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8.5\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><path d=\"M10.5 3.5V3A1.5 1.5 0 0 0 9 1.5H4A1.5 1.5 0 0 0 2.5 3v6.5A1.5 1.5 0 0 0 4 11h.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><\/svg><span class=\"cbb-button-label\" aria-live=\"polite\">Copy<\/span><\/button><\/span><\/figcaption><pre class=\"cbb-pre\" tabindex=\"0\"><code class=\"cbb-code language-json\" style=\"counter-reset:cbb-line 0;\"><span class=\"cbb-line\">{\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-attr\">\"scripts\"<\/span>: {\n<\/span><span class=\"cbb-line\">\t\t<span class=\"cbb-attr\">\"build\"<\/span>: <span class=\"cbb-string\">\"wp-scripts build --webpack-copy-php --blocks-manifest\"<\/span>,\n<\/span><span class=\"cbb-line\">\t\t<span class=\"cbb-attr\">\"start\"<\/span>: <span class=\"cbb-string\">\"wp-scripts start --webpack-copy-php --blocks-manifest\"<\/span>\n<\/span><span class=\"cbb-line\">\t}\n<\/span><span class=\"cbb-line\">}<\/span><\/code><\/pre><\/figure>\n\n\n<p><code>--webpack-copy-php<\/code> copies all PHP files from <code>src<\/code> to <code>build<\/code>; without the flag, <code>@wordpress\/scripts<\/code> only copies PHP files that a <code>block.json<\/code> names in <code>render<\/code> or <code>variations<\/code>. <code>--blocks-manifest<\/code> writes <code>build\/blocks-manifest.php<\/code>, which the next sections explain. The generated plugin header states \u201cRequires at least: 6.8\u201d and <code>readme.txt<\/code> states \u201cTested up to: 6.8\u201d. Both are template defaults, not statements about the current release, and they should be adjusted before publishing; the header fields are covered in <a href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/readme-header-svn\/\">a separate article of this series about <code>readme.txt<\/code>, the plugin header and SVN<\/a>.<\/p>\n<h2>block.json as the contract between editor and server<\/h2>\n<p><code>block.json<\/code> is read on both sides. PHP uses it to register the block type, its attributes and its assets; the editor bundle imports the same file so that JavaScript knows the name and the attribute schema. For the example, the file in <code>src\/category-posts\/<\/code> has this content:<\/p>\n\n\n<figure style=\"--cbb-digits:2\" class=\"cbb cbb--wrap cbb--numbers wp-block-code-beautifier-block-code\" data-lang=\"json\"><figcaption class=\"cbb-head\"><span class=\"cbb-lang\">JSON<\/span><span class=\"cbb-actions\"><button type=\"button\" class=\"cbb-button cbb-button--wrap\" data-cbb-action=\"wrap\" aria-pressed=\"true\" title=\"Wrap long lines\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path d=\"M2 4h12M2 8h9.5a2.5 2.5 0 0 1 0 5H8m0 0 1.5-1.5M8 13l1.5 1.5M2 12h3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg><span class=\"cbb-sr\">Wrap long lines<\/span><\/button><button type=\"button\" class=\"cbb-button cbb-button--copy\" data-cbb-action=\"copy\" data-cbb-done=\"Copied\" data-cbb-failed=\"Press Ctrl+C to copy\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8.5\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><path d=\"M10.5 3.5V3A1.5 1.5 0 0 0 9 1.5H4A1.5 1.5 0 0 0 2.5 3v6.5A1.5 1.5 0 0 0 4 11h.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><\/svg><span class=\"cbb-button-label\" aria-live=\"polite\">Copy<\/span><\/button><\/span><\/figcaption><pre class=\"cbb-pre\" tabindex=\"0\"><code class=\"cbb-code language-json\" style=\"counter-reset:cbb-line 0;\"><span class=\"cbb-line\">{\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-attr\">\"$schema\"<\/span>: <span class=\"cbb-string\">\"https:\/\/schemas.wp.org\/trunk\/block.json\"<\/span>,\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-attr\">\"apiVersion\"<\/span>: <span class=\"cbb-number\">3<\/span>,\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-attr\">\"name\"<\/span>: <span class=\"cbb-string\">\"lw\/category-posts\"<\/span>,\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-attr\">\"version\"<\/span>: <span class=\"cbb-string\">\"0.1.0\"<\/span>,\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-attr\">\"title\"<\/span>: <span class=\"cbb-string\">\"Category Posts\"<\/span>,\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-attr\">\"category\"<\/span>: <span class=\"cbb-string\">\"widgets\"<\/span>,\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-attr\">\"icon\"<\/span>: <span class=\"cbb-string\">\"list-view\"<\/span>,\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-attr\">\"description\"<\/span>: <span class=\"cbb-string\">\"Lists the latest posts of one category, rendered on the server.\"<\/span>,\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-attr\">\"attributes\"<\/span>: {\n<\/span><span class=\"cbb-line\">\t\t<span class=\"cbb-attr\">\"categoryId\"<\/span>: {\n<\/span><span class=\"cbb-line\">\t\t\t<span class=\"cbb-attr\">\"type\"<\/span>: <span class=\"cbb-string\">\"integer\"<\/span>,\n<\/span><span class=\"cbb-line\">\t\t\t<span class=\"cbb-attr\">\"default\"<\/span>: <span class=\"cbb-number\">0<\/span>\n<\/span><span class=\"cbb-line\">\t\t},\n<\/span><span class=\"cbb-line\">\t\t<span class=\"cbb-attr\">\"count\"<\/span>: {\n<\/span><span class=\"cbb-line\">\t\t\t<span class=\"cbb-attr\">\"type\"<\/span>: <span class=\"cbb-string\">\"integer\"<\/span>,\n<\/span><span class=\"cbb-line\">\t\t\t<span class=\"cbb-attr\">\"default\"<\/span>: <span class=\"cbb-number\">5<\/span>\n<\/span><span class=\"cbb-line\">\t\t}\n<\/span><span class=\"cbb-line\">\t},\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-attr\">\"usesContext\"<\/span>: &#91; <span class=\"cbb-string\">\"postId\"<\/span> &#93;,\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-attr\">\"supports\"<\/span>: {\n<\/span><span class=\"cbb-line\">\t\t<span class=\"cbb-attr\">\"html\"<\/span>: <span class=\"cbb-constant\">false<\/span>,\n<\/span><span class=\"cbb-line\">\t\t<span class=\"cbb-attr\">\"align\"<\/span>: &#91; <span class=\"cbb-string\">\"wide\"<\/span>, <span class=\"cbb-string\">\"full\"<\/span> &#93;,\n<\/span><span class=\"cbb-line\">\t\t<span class=\"cbb-attr\">\"color\"<\/span>: {\n<\/span><span class=\"cbb-line\">\t\t\t<span class=\"cbb-attr\">\"background\"<\/span>: <span class=\"cbb-constant\">true<\/span>,\n<\/span><span class=\"cbb-line\">\t\t\t<span class=\"cbb-attr\">\"text\"<\/span>: <span class=\"cbb-constant\">true<\/span>,\n<\/span><span class=\"cbb-line\">\t\t\t<span class=\"cbb-attr\">\"link\"<\/span>: <span class=\"cbb-constant\">true<\/span>\n<\/span><span class=\"cbb-line\">\t\t},\n<\/span><span class=\"cbb-line\">\t\t<span class=\"cbb-attr\">\"spacing\"<\/span>: {\n<\/span><span class=\"cbb-line\">\t\t\t<span class=\"cbb-attr\">\"margin\"<\/span>: <span class=\"cbb-constant\">true<\/span>,\n<\/span><span class=\"cbb-line\">\t\t\t<span class=\"cbb-attr\">\"padding\"<\/span>: <span class=\"cbb-constant\">true<\/span>\n<\/span><span class=\"cbb-line\">\t\t},\n<\/span><span class=\"cbb-line\">\t\t<span class=\"cbb-attr\">\"typography\"<\/span>: {\n<\/span><span class=\"cbb-line\">\t\t\t<span class=\"cbb-attr\">\"fontSize\"<\/span>: <span class=\"cbb-constant\">true<\/span>\n<\/span><span class=\"cbb-line\">\t\t}\n<\/span><span class=\"cbb-line\">\t},\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-attr\">\"textdomain\"<\/span>: <span class=\"cbb-string\">\"category-posts\"<\/span>,\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-attr\">\"editorScript\"<\/span>: <span class=\"cbb-string\">\"file:.\/index.js\"<\/span>,\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-attr\">\"editorStyle\"<\/span>: <span class=\"cbb-string\">\"file:.\/index.css\"<\/span>,\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-attr\">\"style\"<\/span>: <span class=\"cbb-string\">\"file:.\/style-index.css\"<\/span>,\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-attr\">\"render\"<\/span>: <span class=\"cbb-string\">\"file:.\/render.php\"<\/span>\n<\/span><span class=\"cbb-line\">}<\/span><\/code><\/pre><\/figure>\n\n\n<p><code>apiVersion<\/code> 3 is the current Block API version and was introduced with WordPress 6.3. Without the field, WordPress assumes version 1. Version 3 is tied to the iframed post editor: since 6.3, the editor used an iframe only when every registered block declared version 3 or higher. WordPress 7.0 changed the check to the blocks actually present in the post content, and according to the Block API documentation, WordPress 7.1 always uses the iframe for the post editor regardless of <code>apiVersion<\/code>. For a new block this leaves one consequence: editor styles must work inside the iframe, which they do when they are loaded through <code>editorStyle<\/code> and <code>style<\/code> in <code>block.json<\/code>.<\/p>\n<p>Both attributes have type <code>integer<\/code> and default values. Before rendering, WordPress validates each attribute against its schema, drops values that fail validation and fills in defaults for missing ones. <code>usesContext<\/code> requests the <code>postId<\/code> context, which <code>render_block()<\/code> supplies from the global post. This lets the block exclude the post that contains it.<\/p>\n<p>The <code>supports<\/code> section enables alignment, colours, spacing and font size. <code>get_block_wrapper_attributes()<\/code> and <code>useBlockProps()<\/code> produce the markup for those settings later, so no custom code is needed. <code>editorScript<\/code> and <code>editorStyle<\/code> load only in the editor, <code>style<\/code> loads in both the editor and the front end, and <code>render<\/code> is a server-only field.<\/p>\n<h2>Registration through a manifest instead of single block.json files<\/h2>\n<p>For a long time, <code>register_block_type( __DIR__ . '\/build' )<\/code> or a path to the block folder was the usual registration. It still works and is the right choice for a single block without a build step. On every request, however, it reads a JSON file from disk and decodes it. WordPress 6.7 added <code>wp_register_block_metadata_collection()<\/code>, which registers a PHP manifest with the metadata of several blocks; the dev note by Felix Arntz names the avoided file system and JSON operations as the reason. In 6.7 each block still needed its own <code>register_block_type()<\/code> call. WordPress 6.8 closed that gap with <code>wp_register_block_types_from_metadata_collection()<\/code>: one call registers the collection and all block types in it.<\/p>\n<p>The current scaffold uses exactly this function whenever <code>@wordpress\/scripts<\/code> is enabled, which is the default. The main plugin file of the example therefore looks like this:<\/p>\n\n\n<figure style=\"--cbb-digits:2\" class=\"cbb cbb--wrap cbb--numbers wp-block-code-beautifier-block-code\" data-lang=\"php\"><figcaption class=\"cbb-head\"><span class=\"cbb-lang\">PHP<\/span><span class=\"cbb-actions\"><button type=\"button\" class=\"cbb-button cbb-button--wrap\" data-cbb-action=\"wrap\" aria-pressed=\"true\" title=\"Wrap long lines\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path d=\"M2 4h12M2 8h9.5a2.5 2.5 0 0 1 0 5H8m0 0 1.5-1.5M8 13l1.5 1.5M2 12h3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg><span class=\"cbb-sr\">Wrap long lines<\/span><\/button><button type=\"button\" class=\"cbb-button cbb-button--copy\" data-cbb-action=\"copy\" data-cbb-done=\"Copied\" data-cbb-failed=\"Press Ctrl+C to copy\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8.5\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><path d=\"M10.5 3.5V3A1.5 1.5 0 0 0 9 1.5H4A1.5 1.5 0 0 0 2.5 3v6.5A1.5 1.5 0 0 0 4 11h.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><\/svg><span class=\"cbb-button-label\" aria-live=\"polite\">Copy<\/span><\/button><\/span><\/figcaption><pre class=\"cbb-pre\" tabindex=\"0\"><code class=\"cbb-code language-php\" style=\"counter-reset:cbb-line 0;\"><span class=\"cbb-line\"><span class=\"cbb-meta\">&lt;?php<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\">\/**<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * Plugin Name:       Category Posts<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * Description:       Server-rendered block that lists the latest posts of one category.<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * Version:           0.1.0<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * Requires at least: 6.9<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * Requires PHP:      7.4<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * Author:            Lukas Wojcik<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * License:           GPL-2.0-or-later<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * License URI:       https:\/\/www.gnu.org\/licenses\/gpl-2.0.html<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * Text Domain:       category-posts<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> *<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * @package Lw<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> *\/<\/span>\n<\/span><span class=\"cbb-line\">\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">if<\/span> ( ! <span class=\"cbb-function\">defined<\/span>( <span class=\"cbb-string\">'ABSPATH'<\/span> ) ) {\n<\/span><span class=\"cbb-line\">\texit; <span class=\"cbb-comment\">\/\/ Exit if accessed directly.<\/span>\n<\/span><span class=\"cbb-line\">}\n<\/span><span class=\"cbb-line\">\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\">\/**<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * Registers every block listed in build\/blocks-manifest.php.<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> *<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * The manifest is generated by wp-scripts (--blocks-manifest), so WordPress<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * does not have to read and decode each block.json on every request.<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> *\/<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">function<\/span> <span class=\"cbb-function\">lw_category_posts_block_init<\/span>() {\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-function\">wp_register_block_types_from_metadata_collection<\/span>(\n<\/span><span class=\"cbb-line\">\t\t__DIR__ . <span class=\"cbb-string\">'\/build'<\/span>,\n<\/span><span class=\"cbb-line\">\t\t__DIR__ . <span class=\"cbb-string\">'\/build\/blocks-manifest.php'<\/span>\n<\/span><span class=\"cbb-line\">\t);\n<\/span><span class=\"cbb-line\">}\n<\/span><span class=\"cbb-line\"><span class=\"cbb-function\">add_action<\/span>( <span class=\"cbb-string\">'init'<\/span>, <span class=\"cbb-string\">'lw_category_posts_block_init'<\/span> );<\/span><\/code><\/pre><\/figure>\n\n\n<p>The first parameter is the base folder of the collection, the second the manifest. If the second parameter is left empty, the base folder must refer to a collection registered earlier. Plugins that still support WordPress 6.7 or older need a <code>function_exists()<\/code> check with a fallback, as described in the 6.8 dev note. The example avoids this through \u201cRequires at least: 6.9\u201d, which it needs anyway for the editor import shown below. Without a build step, a single call is enough:<\/p>\n\n\n<figure style=\"--cbb-digits:1\" class=\"cbb cbb--wrap wp-block-code-beautifier-block-code\" data-lang=\"plain\"><figcaption class=\"cbb-head\"><span class=\"cbb-lang\">Text<\/span><span class=\"cbb-actions\"><button type=\"button\" class=\"cbb-button cbb-button--wrap\" data-cbb-action=\"wrap\" aria-pressed=\"true\" title=\"Wrap long lines\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path d=\"M2 4h12M2 8h9.5a2.5 2.5 0 0 1 0 5H8m0 0 1.5-1.5M8 13l1.5 1.5M2 12h3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg><span class=\"cbb-sr\">Wrap long lines<\/span><\/button><button type=\"button\" class=\"cbb-button cbb-button--copy\" data-cbb-action=\"copy\" data-cbb-done=\"Copied\" data-cbb-failed=\"Press Ctrl+C to copy\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8.5\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><path d=\"M10.5 3.5V3A1.5 1.5 0 0 0 9 1.5H4A1.5 1.5 0 0 0 2.5 3v6.5A1.5 1.5 0 0 0 4 11h.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><\/svg><span class=\"cbb-button-label\" aria-live=\"polite\">Copy<\/span><\/button><\/span><\/figcaption><pre class=\"cbb-pre\" tabindex=\"0\"><code class=\"cbb-code language-plain\" style=\"counter-reset:cbb-line 0;\"><span class=\"cbb-line\">\/\/ Single block, without a manifest: reads build\/category-posts\/block.json.\n<\/span><span class=\"cbb-line\">register_block_type( __DIR__ . '\/build\/category-posts' );<\/span><\/code><\/pre><\/figure>\n\n\n<p>One detail is easy to miss: the manifest is a build artefact. A <code>block.json<\/code> changed in <code>src\/<\/code> only takes effect after the next build, even on the server side.<\/p>\n<h2>render.php and its three variables<\/h2>\n<p>When <code>block.json<\/code> contains a <code>render<\/code> field, WordPress creates a <code>render_callback<\/code> during registration. In the core source this is a static closure that starts output buffering, loads the file with <code>require<\/code> and returns the buffer. Inside the file, three variables are available: <code>$attributes<\/code> (array of the block attributes, defaults already applied), <code>$content<\/code> (the saved inner content, relevant for blocks with inner blocks and always empty here) and <code>$block<\/code> (the <code>WP_Block<\/code> instance with name, context and the parsed block). The render file of the example:<\/p>\n\n\n<figure style=\"--cbb-digits:2\" class=\"cbb cbb--wrap cbb--numbers wp-block-code-beautifier-block-code\" data-lang=\"php\"><figcaption class=\"cbb-head\"><span class=\"cbb-lang\">PHP<\/span><span class=\"cbb-actions\"><button type=\"button\" class=\"cbb-button cbb-button--wrap\" data-cbb-action=\"wrap\" aria-pressed=\"true\" title=\"Wrap long lines\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path d=\"M2 4h12M2 8h9.5a2.5 2.5 0 0 1 0 5H8m0 0 1.5-1.5M8 13l1.5 1.5M2 12h3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg><span class=\"cbb-sr\">Wrap long lines<\/span><\/button><button type=\"button\" class=\"cbb-button cbb-button--copy\" data-cbb-action=\"copy\" data-cbb-done=\"Copied\" data-cbb-failed=\"Press Ctrl+C to copy\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8.5\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><path d=\"M10.5 3.5V3A1.5 1.5 0 0 0 9 1.5H4A1.5 1.5 0 0 0 2.5 3v6.5A1.5 1.5 0 0 0 4 11h.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><\/svg><span class=\"cbb-button-label\" aria-live=\"polite\">Copy<\/span><\/button><\/span><\/figcaption><pre class=\"cbb-pre\" tabindex=\"0\"><code class=\"cbb-code language-php\" style=\"counter-reset:cbb-line 0;\"><span class=\"cbb-line\"><span class=\"cbb-meta\">&lt;?php<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\">\/**<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * Server-side rendering of the lw\/category-posts block.<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> *<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * Variables provided by WordPress:<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> *<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * @var array    $attributes Block attributes, defaults from block.json applied.<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * @var string   $content    Inner content (always empty for this block).<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * @var WP_Block $block      Block instance: name, context, parsed block.<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> *<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> * @package Lw<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\"> *\/<\/span>\n<\/span><span class=\"cbb-line\">\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">if<\/span> ( ! <span class=\"cbb-function\">defined<\/span>( <span class=\"cbb-string\">'ABSPATH'<\/span> ) ) {\n<\/span><span class=\"cbb-line\">\texit; <span class=\"cbb-comment\">\/\/ Exit if accessed directly.<\/span>\n<\/span><span class=\"cbb-line\">}\n<\/span><span class=\"cbb-line\">\n<\/span><span class=\"cbb-line\"><span class=\"cbb-variable\">$lw_category_id<\/span> = <span class=\"cbb-keyword\">isset<\/span>( <span class=\"cbb-variable\">$attributes<\/span>&#91;<span class=\"cbb-string\">'categoryId'<\/span>&#93; ) ? <span class=\"cbb-function\">absint<\/span>( <span class=\"cbb-variable\">$attributes<\/span>&#91;<span class=\"cbb-string\">'categoryId'<\/span>&#93; ) : <span class=\"cbb-number\">0<\/span>;\n<\/span><span class=\"cbb-line\"><span class=\"cbb-variable\">$lw_count<\/span>       = <span class=\"cbb-keyword\">isset<\/span>( <span class=\"cbb-variable\">$attributes<\/span>&#91;<span class=\"cbb-string\">'count'<\/span>&#93; ) ? <span class=\"cbb-function\">absint<\/span>( <span class=\"cbb-variable\">$attributes<\/span>&#91;<span class=\"cbb-string\">'count'<\/span>&#93; ) : <span class=\"cbb-number\">5<\/span>;\n<\/span><span class=\"cbb-line\"><span class=\"cbb-variable\">$lw_count<\/span>       = <span class=\"cbb-function\">min<\/span>( <span class=\"cbb-function\">max<\/span>( <span class=\"cbb-variable\">$lw_count<\/span>, <span class=\"cbb-number\">1<\/span> ), <span class=\"cbb-number\">10<\/span> );\n<\/span><span class=\"cbb-line\"><span class=\"cbb-variable\">$lw_term<\/span>        = <span class=\"cbb-variable\">$lw_category_id<\/span> ? <span class=\"cbb-function\">get_term<\/span>( <span class=\"cbb-variable\">$lw_category_id<\/span>, <span class=\"cbb-string\">'category'<\/span> ) : <span class=\"cbb-constant\">null<\/span>;\n<\/span><span class=\"cbb-line\">\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\">\/\/ Render nothing until a valid category has been chosen.<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">if<\/span> ( ! <span class=\"cbb-variable\">$lw_term<\/span> <span class=\"cbb-keyword\">instanceof<\/span> <span class=\"cbb-type\">WP_Term<\/span> ) {\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-keyword\">return<\/span>;\n<\/span><span class=\"cbb-line\">}\n<\/span><span class=\"cbb-line\">\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\">\/\/ Do not list the post that contains the block (context from \"usesContext\").<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-variable\">$lw_current_id<\/span> = <span class=\"cbb-keyword\">isset<\/span>( <span class=\"cbb-variable\">$block<\/span><span class=\"cbb-operator\">-&gt;<\/span>context&#91;<span class=\"cbb-string\">'postId'<\/span>&#93; ) ? <span class=\"cbb-function\">absint<\/span>( <span class=\"cbb-variable\">$block<\/span><span class=\"cbb-operator\">-&gt;<\/span>context&#91;<span class=\"cbb-string\">'postId'<\/span>&#93; ) : <span class=\"cbb-number\">0<\/span>;\n<\/span><span class=\"cbb-line\">\n<\/span><span class=\"cbb-line\"><span class=\"cbb-variable\">$lw_posts<\/span> = <span class=\"cbb-function\">get_posts<\/span>(\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-keyword\">array<\/span>(\n<\/span><span class=\"cbb-line\">\t\t<span class=\"cbb-string\">'category'<\/span>     <span class=\"cbb-operator\">=&gt;<\/span> <span class=\"cbb-variable\">$lw_term<\/span><span class=\"cbb-operator\">-&gt;<\/span>term_id,\n<\/span><span class=\"cbb-line\">\t\t<span class=\"cbb-string\">'numberposts'<\/span>  <span class=\"cbb-operator\">=&gt;<\/span> <span class=\"cbb-variable\">$lw_count<\/span>,\n<\/span><span class=\"cbb-line\">\t\t<span class=\"cbb-string\">'post_status'<\/span>  <span class=\"cbb-operator\">=&gt;<\/span> <span class=\"cbb-string\">'publish'<\/span>,\n<\/span><span class=\"cbb-line\">\t\t<span class=\"cbb-string\">'has_password'<\/span> <span class=\"cbb-operator\">=&gt;<\/span> <span class=\"cbb-constant\">false<\/span>,\n<\/span><span class=\"cbb-line\">\t\t<span class=\"cbb-string\">'exclude'<\/span>      <span class=\"cbb-operator\">=&gt;<\/span> <span class=\"cbb-variable\">$lw_current_id<\/span> ? <span class=\"cbb-keyword\">array<\/span>( <span class=\"cbb-variable\">$lw_current_id<\/span> ) : <span class=\"cbb-keyword\">array<\/span>(),\n<\/span><span class=\"cbb-line\">\t)\n<\/span><span class=\"cbb-line\">);\n<\/span><span class=\"cbb-line\"><span class=\"cbb-meta\">?&gt;<\/span>\n<\/span><span class=\"cbb-line\">&lt;div <span class=\"cbb-meta\">&lt;?php<\/span> <span class=\"cbb-keyword\">echo<\/span> <span class=\"cbb-function\">wp_kses_data<\/span>( <span class=\"cbb-function\">get_block_wrapper_attributes<\/span>() ); <span class=\"cbb-meta\">?&gt;<\/span>&gt;\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-meta\">&lt;?php<\/span> <span class=\"cbb-keyword\">if<\/span> ( <span class=\"cbb-keyword\">empty<\/span>( <span class=\"cbb-variable\">$lw_posts<\/span> ) ) : <span class=\"cbb-meta\">?&gt;<\/span>\n<\/span><span class=\"cbb-line\">\t\t<span class=\"cbb-punct\">&lt;<\/span><span class=\"cbb-tag\">p<\/span><span class=\"cbb-punct\">&gt;<\/span><span class=\"cbb-meta\">&lt;?php<\/span> <span class=\"cbb-function\">esc_html_e<\/span>( <span class=\"cbb-string\">'No posts in this category yet.'<\/span>, <span class=\"cbb-string\">'category-posts'<\/span> ); <span class=\"cbb-meta\">?&gt;<\/span><span class=\"cbb-punct\">&lt;\/<\/span><span class=\"cbb-tag\">p<\/span><span class=\"cbb-punct\">&gt;<\/span>\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-meta\">&lt;?php<\/span> <span class=\"cbb-keyword\">else<\/span> : <span class=\"cbb-meta\">?&gt;<\/span>\n<\/span><span class=\"cbb-line\">\t\t<span class=\"cbb-punct\">&lt;<\/span><span class=\"cbb-tag\">ul<\/span> <span class=\"cbb-attr\">class<\/span><span class=\"cbb-punct\">=<\/span><span class=\"cbb-string\">\"wp-block-lw-category-posts__list\"<\/span><span class=\"cbb-punct\">&gt;<\/span>\n<\/span><span class=\"cbb-line\">\t\t\t<span class=\"cbb-meta\">&lt;?php<\/span> <span class=\"cbb-keyword\">foreach<\/span> ( <span class=\"cbb-variable\">$lw_posts<\/span> <span class=\"cbb-keyword\">as<\/span> <span class=\"cbb-variable\">$lw_post<\/span> ) : <span class=\"cbb-meta\">?&gt;<\/span>\n<\/span><span class=\"cbb-line\">\t\t\t\t<span class=\"cbb-punct\">&lt;<\/span><span class=\"cbb-tag\">li<\/span><span class=\"cbb-punct\">&gt;<\/span>\n<\/span><span class=\"cbb-line\">\t\t\t\t\t<span class=\"cbb-punct\">&lt;<\/span><span class=\"cbb-tag\">a<\/span> <span class=\"cbb-attr\">href<\/span><span class=\"cbb-punct\">=<\/span><span class=\"cbb-string\">\"&lt;?php echo esc_url( get_permalink( $lw_post ) ); ?&gt;\"<\/span><span class=\"cbb-punct\">&gt;<\/span><span class=\"cbb-meta\">&lt;?php<\/span> <span class=\"cbb-keyword\">echo<\/span> <span class=\"cbb-function\">esc_html<\/span>( <span class=\"cbb-function\">get_the_title<\/span>( <span class=\"cbb-variable\">$lw_post<\/span> ) ); <span class=\"cbb-meta\">?&gt;<\/span><span class=\"cbb-punct\">&lt;\/<\/span><span class=\"cbb-tag\">a<\/span><span class=\"cbb-punct\">&gt;<\/span>\n<\/span><span class=\"cbb-line\">\t\t\t\t\t<span class=\"cbb-punct\">&lt;<\/span><span class=\"cbb-tag\">time<\/span> <span class=\"cbb-attr\">datetime<\/span><span class=\"cbb-punct\">=<\/span><span class=\"cbb-string\">\"&lt;?php echo esc_attr( get_the_date( 'c', $lw_post ) ); ?&gt;\"<\/span><span class=\"cbb-punct\">&gt;<\/span><span class=\"cbb-meta\">&lt;?php<\/span> <span class=\"cbb-keyword\">echo<\/span> <span class=\"cbb-function\">esc_html<\/span>( <span class=\"cbb-function\">get_the_date<\/span>( <span class=\"cbb-string\">''<\/span>, <span class=\"cbb-variable\">$lw_post<\/span> ) ); <span class=\"cbb-meta\">?&gt;<\/span><span class=\"cbb-punct\">&lt;\/<\/span><span class=\"cbb-tag\">time<\/span><span class=\"cbb-punct\">&gt;<\/span>\n<\/span><span class=\"cbb-line\">\t\t\t\t<span class=\"cbb-punct\">&lt;\/<\/span><span class=\"cbb-tag\">li<\/span><span class=\"cbb-punct\">&gt;<\/span>\n<\/span><span class=\"cbb-line\">\t\t\t<span class=\"cbb-meta\">&lt;?php<\/span> <span class=\"cbb-keyword\">endforeach<\/span>; <span class=\"cbb-meta\">?&gt;<\/span>\n<\/span><span class=\"cbb-line\">\t\t<span class=\"cbb-punct\">&lt;\/<\/span><span class=\"cbb-tag\">ul<\/span><span class=\"cbb-punct\">&gt;<\/span>\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-meta\">&lt;?php<\/span> <span class=\"cbb-keyword\">endif<\/span>; <span class=\"cbb-meta\">?&gt;<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-punct\">&lt;\/<\/span><span class=\"cbb-tag\">div<\/span><span class=\"cbb-punct\">&gt;<\/span><\/span><\/code><\/pre><\/figure>\n\n\n<p>Several decisions in this file follow from how it is loaded. Because <code>require<\/code> runs once for every block instance on a page, the file must not declare functions or classes; a second instance of the block would end in a fatal error. Helper functions belong in the main plugin file or in a separate include. The variables look global to PHP_CodeSniffer, which is why they carry the prefix <code>lw_<\/code>. That prefix does not satisfy the check, however: WPCS rejects prefixes shorter than four characters, and Plugin Check 2.1.0, which derives the expected prefix from the plugin code (here from the function name <code>lw_category_posts_block_init<\/code>), lists every one of these variables as a warning. The warnings do not affect how the block works. <code>return<\/code> ends the file early and leaves an empty buffer, which is how a block renders nothing.<\/p>\n<p>Attributes are data from <code>post_content<\/code> and therefore input. The schema check guarantees integers, not a reasonable range, so <code>absint()<\/code> together with <code>min()<\/code> and <code>max()<\/code> limits the count to 1 to 10. <code>get_term()<\/code> confirms that the category exists. <code>get_posts()<\/code> is a thin wrapper around <code>WP_Query<\/code> that sets <code>ignore_sticky_posts<\/code>, <code>no_found_rows<\/code> and <code>suppress_filters<\/code> itself, which fits a short list without pagination. How further query arguments such as <code>meta_query<\/code> turn into SQL is shown in <a href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/meta_query-in-wp_query-how-each-clause-becomes-a-join-and-what-it-costs\/\">the article on <code>meta_query<\/code> in <code>WP_Query<\/code><\/a>. Every output passes through the matching escaping function: <code>esc_url()<\/code> for the link, <code>esc_html()<\/code> for title and date, <code>esc_attr()<\/code> for the <code>datetime<\/code> attribute. The broader rules are the topic of <a href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/sanitizing-escaping-nonces-capabilities\/\">a separate article in this series on sanitizing, escaping, nonces and capabilities<\/a>.<\/p>\n<p><code>get_block_wrapper_attributes()<\/code> returns the attribute string for the wrapper: the class <code>wp-block-lw-category-posts<\/code> plus classes and inline styles from the <code>supports<\/code> settings. The function escapes every value with <code>esc_attr()<\/code> itself, as its source shows. The WordPress Coding Standards list of auto-escaped functions does not contain it, however, so a bare <code>echo<\/code> triggers the escaping sniff; Plugin Check 2.1.0 reports it as an error (<code>WordPress.Security.EscapeOutput.OutputNotEscaped<\/code>). <code>wp_kses_data()<\/code> leaves an attribute string without tags unchanged and satisfies the check; a justified <code>phpcs:ignore<\/code> comment is the alternative.<\/p>\n<h2>The editor side: edit.js with ServerSideRender<\/h2>\n<p>In JavaScript, <code>index.js<\/code> registers the block with <code>edit<\/code> only. When no <code>save<\/code> is given, the block API uses a default <code>save<\/code> that returns <code>null<\/code>:<\/p>\n\n\n<figure style=\"--cbb-digits:2\" class=\"cbb cbb--wrap cbb--numbers wp-block-code-beautifier-block-code\" data-lang=\"javascript\"><figcaption class=\"cbb-head\"><span class=\"cbb-lang\">JavaScript<\/span><span class=\"cbb-actions\"><button type=\"button\" class=\"cbb-button cbb-button--wrap\" data-cbb-action=\"wrap\" aria-pressed=\"true\" title=\"Wrap long lines\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path d=\"M2 4h12M2 8h9.5a2.5 2.5 0 0 1 0 5H8m0 0 1.5-1.5M8 13l1.5 1.5M2 12h3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg><span class=\"cbb-sr\">Wrap long lines<\/span><\/button><button type=\"button\" class=\"cbb-button cbb-button--copy\" data-cbb-action=\"copy\" data-cbb-done=\"Copied\" data-cbb-failed=\"Press Ctrl+C to copy\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8.5\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><path d=\"M10.5 3.5V3A1.5 1.5 0 0 0 9 1.5H4A1.5 1.5 0 0 0 2.5 3v6.5A1.5 1.5 0 0 0 4 11h.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><\/svg><span class=\"cbb-button-label\" aria-live=\"polite\">Copy<\/span><\/button><\/span><\/figcaption><pre class=\"cbb-pre\" tabindex=\"0\"><code class=\"cbb-code language-javascript\" style=\"counter-reset:cbb-line 0;\"><span class=\"cbb-line\"><span class=\"cbb-keyword\">import<\/span> { registerBlockType } <span class=\"cbb-keyword\">from<\/span> <span class=\"cbb-string\">'@wordpress\/blocks'<\/span>;\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">import<\/span> <span class=\"cbb-string\">'.\/style.scss'<\/span>;\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">import<\/span> <span class=\"cbb-type\">Edit<\/span> <span class=\"cbb-keyword\">from<\/span> <span class=\"cbb-string\">'.\/edit'<\/span>;\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">import<\/span> metadata <span class=\"cbb-keyword\">from<\/span> <span class=\"cbb-string\">'.\/block.json'<\/span>;\n<\/span><span class=\"cbb-line\">\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\">\/\/ No save(): the default save returns null, so only the block comment<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\">\/\/ with its attributes is stored in post_content.<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-function\">registerBlockType<\/span>( metadata.name, {\n<\/span><span class=\"cbb-line\">\tedit: <span class=\"cbb-type\">Edit<\/span>,\n<\/span><span class=\"cbb-line\">} );<\/span><\/code><\/pre><\/figure>\n\n\n<p><code>edit.js<\/code> provides the settings sidebar and a preview. Categories come from the core data store, with the same query the core Latest Posts block uses. The preview is produced by <code>ServerSideRender<\/code>, which sends the attributes to the REST route <code>\/wp\/v2\/block-renderer\/lw\/category-posts<\/code> and inserts the HTML that <code>render.php<\/code> returns:<\/p>\n\n\n<figure style=\"--cbb-digits:2\" class=\"cbb cbb--wrap cbb--numbers wp-block-code-beautifier-block-code\" data-lang=\"javascript\"><figcaption class=\"cbb-head\"><span class=\"cbb-lang\">JavaScript<\/span><span class=\"cbb-actions\"><button type=\"button\" class=\"cbb-button cbb-button--wrap\" data-cbb-action=\"wrap\" aria-pressed=\"true\" title=\"Wrap long lines\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path d=\"M2 4h12M2 8h9.5a2.5 2.5 0 0 1 0 5H8m0 0 1.5-1.5M8 13l1.5 1.5M2 12h3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg><span class=\"cbb-sr\">Wrap long lines<\/span><\/button><button type=\"button\" class=\"cbb-button cbb-button--copy\" data-cbb-action=\"copy\" data-cbb-done=\"Copied\" data-cbb-failed=\"Press Ctrl+C to copy\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8.5\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><path d=\"M10.5 3.5V3A1.5 1.5 0 0 0 9 1.5H4A1.5 1.5 0 0 0 2.5 3v6.5A1.5 1.5 0 0 0 4 11h.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><\/svg><span class=\"cbb-button-label\" aria-live=\"polite\">Copy<\/span><\/button><\/span><\/figcaption><pre class=\"cbb-pre\" tabindex=\"0\"><code class=\"cbb-code language-javascript\" style=\"counter-reset:cbb-line 0;\"><span class=\"cbb-line\"><span class=\"cbb-keyword\">import<\/span> { __ } <span class=\"cbb-keyword\">from<\/span> <span class=\"cbb-string\">'@wordpress\/i18n'<\/span>;\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">import<\/span> { <span class=\"cbb-type\">InspectorControls<\/span>, useBlockProps } <span class=\"cbb-keyword\">from<\/span> <span class=\"cbb-string\">'@wordpress\/block-editor'<\/span>;\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">import<\/span> {\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-type\">PanelBody<\/span>,\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-type\">Placeholder<\/span>,\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-type\">RangeControl<\/span>,\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-type\">SelectControl<\/span>,\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-type\">Spinner<\/span>,\n<\/span><span class=\"cbb-line\">} <span class=\"cbb-keyword\">from<\/span> <span class=\"cbb-string\">'@wordpress\/components'<\/span>;\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">import<\/span> { useSelect } <span class=\"cbb-keyword\">from<\/span> <span class=\"cbb-string\">'@wordpress\/data'<\/span>;\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">import<\/span> { store as coreStore } <span class=\"cbb-keyword\">from<\/span> <span class=\"cbb-string\">'@wordpress\/core-data'<\/span>;\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">import<\/span> { decodeEntities } <span class=\"cbb-keyword\">from<\/span> <span class=\"cbb-string\">'@wordpress\/html-entities'<\/span>;\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">import<\/span> { <span class=\"cbb-type\">ServerSideRender<\/span> } <span class=\"cbb-keyword\">from<\/span> <span class=\"cbb-string\">'@wordpress\/server-side-render'<\/span>;\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">import<\/span> metadata <span class=\"cbb-keyword\">from<\/span> <span class=\"cbb-string\">'.\/block.json'<\/span>;\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">import<\/span> <span class=\"cbb-string\">'.\/editor.scss'<\/span>;\n<\/span><span class=\"cbb-line\">\n<\/span><span class=\"cbb-line\"><span class=\"cbb-comment\">\/\/ Same query shape as the core Latest Posts block uses for its category list.<\/span>\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">const<\/span> CATEGORIES_QUERY = { per_page: <span class=\"cbb-number\">-1<\/span>, _fields: <span class=\"cbb-string\">'id,name'<\/span>, context: <span class=\"cbb-string\">'view'<\/span> };\n<\/span><span class=\"cbb-line\">\n<\/span><span class=\"cbb-line\"><span class=\"cbb-keyword\">export<\/span> <span class=\"cbb-keyword\">default<\/span> <span class=\"cbb-keyword\">function<\/span> <span class=\"cbb-function\">Edit<\/span>( { attributes, setAttributes } ) {\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-keyword\">const<\/span> { categoryId, count } = attributes;\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-keyword\">const<\/span> blockProps = <span class=\"cbb-function\">useBlockProps<\/span>();\n<\/span><span class=\"cbb-line\">\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-keyword\">const<\/span> categories = <span class=\"cbb-function\">useSelect<\/span>(\n<\/span><span class=\"cbb-line\">\t\t( select ) <span class=\"cbb-operator\">=&gt;<\/span>\n<\/span><span class=\"cbb-line\">\t\t\t<span class=\"cbb-function\">select<\/span>( coreStore ).<span class=\"cbb-function\">getEntityRecords<\/span>(\n<\/span><span class=\"cbb-line\">\t\t\t\t<span class=\"cbb-string\">'taxonomy'<\/span>,\n<\/span><span class=\"cbb-line\">\t\t\t\t<span class=\"cbb-string\">'category'<\/span>,\n<\/span><span class=\"cbb-line\">\t\t\t\tCATEGORIES_QUERY\n<\/span><span class=\"cbb-line\">\t\t\t),\n<\/span><span class=\"cbb-line\">\t\t&#91;&#93;\n<\/span><span class=\"cbb-line\">\t);\n<\/span><span class=\"cbb-line\">\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-keyword\">const<\/span> options = &#91;\n<\/span><span class=\"cbb-line\">\t\t{ value: <span class=\"cbb-string\">'0'<\/span>, label: <span class=\"cbb-function\">__<\/span>( <span class=\"cbb-string\">'\u2014 Select a category \u2014'<\/span>, <span class=\"cbb-string\">'category-posts'<\/span> ) },\n<\/span><span class=\"cbb-line\">\t\t...( categories ?? &#91;&#93; ).<span class=\"cbb-function\">map<\/span>( ( term ) <span class=\"cbb-operator\">=&gt;<\/span> ( {\n<\/span><span class=\"cbb-line\">\t\t\tvalue: <span class=\"cbb-function\">String<\/span>( term.id ),\n<\/span><span class=\"cbb-line\">\t\t\tlabel: <span class=\"cbb-function\">decodeEntities<\/span>( term.name ),\n<\/span><span class=\"cbb-line\">\t\t} ) ),\n<\/span><span class=\"cbb-line\">\t&#93;;\n<\/span><span class=\"cbb-line\">\n<\/span><span class=\"cbb-line\">\t<span class=\"cbb-keyword\">return<\/span> (\n<\/span><span class=\"cbb-line\">\t\t&lt;&gt;\n<\/span><span class=\"cbb-line\">\t\t\t&lt;<span class=\"cbb-type\">InspectorControls<\/span>&gt;\n<\/span><span class=\"cbb-line\">\t\t\t\t&lt;<span class=\"cbb-type\">PanelBody<\/span> title={ <span class=\"cbb-function\">__<\/span>( <span class=\"cbb-string\">'Settings'<\/span>, <span class=\"cbb-string\">'category-posts'<\/span> ) }&gt;\n<\/span><span class=\"cbb-line\">\t\t\t\t\t{ categories ? (\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t&lt;<span class=\"cbb-type\">SelectControl<\/span>\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t\t__next40pxDefaultSize\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t\t__nextHasNoMarginBottom\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t\tlabel={ <span class=\"cbb-function\">__<\/span>( <span class=\"cbb-string\">'Category'<\/span>, <span class=\"cbb-string\">'category-posts'<\/span> ) }\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t\tvalue={ <span class=\"cbb-function\">String<\/span>( categoryId ) }\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t\toptions={ options }\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t\tonChange={ ( value ) <span class=\"cbb-operator\">=&gt;<\/span>\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t\t\t<span class=\"cbb-function\">setAttributes<\/span>( { categoryId: <span class=\"cbb-function\">Number<\/span>( value ) } )\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t\t}\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t\/&gt;\n<\/span><span class=\"cbb-line\">\t\t\t\t\t) : (\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t&lt;<span class=\"cbb-type\">Spinner<\/span> \/&gt;\n<\/span><span class=\"cbb-line\">\t\t\t\t\t) }\n<\/span><span class=\"cbb-line\">\t\t\t\t\t&lt;<span class=\"cbb-type\">RangeControl<\/span>\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t__next40pxDefaultSize\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t__nextHasNoMarginBottom\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\tlabel={ <span class=\"cbb-function\">__<\/span>( <span class=\"cbb-string\">'Number of posts'<\/span>, <span class=\"cbb-string\">'category-posts'<\/span> ) }\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\tvalue={ count }\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\tmin={ <span class=\"cbb-number\">1<\/span> }\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\tmax={ <span class=\"cbb-number\">10<\/span> }\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\tonChange={ ( value ) <span class=\"cbb-operator\">=&gt;<\/span>\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t\t<span class=\"cbb-function\">setAttributes<\/span>( { count: value ?? <span class=\"cbb-number\">5<\/span> } )\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t}\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\/&gt;\n<\/span><span class=\"cbb-line\">\t\t\t\t&lt;\/<span class=\"cbb-type\">PanelBody<\/span>&gt;\n<\/span><span class=\"cbb-line\">\t\t\t&lt;\/<span class=\"cbb-type\">InspectorControls<\/span>&gt;\n<\/span><span class=\"cbb-line\">\t\t\t&lt;div { ...blockProps }&gt;\n<\/span><span class=\"cbb-line\">\t\t\t\t{ categoryId ? (\n<\/span><span class=\"cbb-line\">\t\t\t\t\t&lt;<span class=\"cbb-type\">ServerSideRender<\/span>\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\tblock={ metadata.name }\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\tattributes={ attributes }\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\tskipBlockSupportAttributes\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\/&gt;\n<\/span><span class=\"cbb-line\">\t\t\t\t) : (\n<\/span><span class=\"cbb-line\">\t\t\t\t\t&lt;<span class=\"cbb-type\">Placeholder<\/span>\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\ticon=<span class=\"cbb-string\">\"list-view\"<\/span>\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\tlabel={ <span class=\"cbb-function\">__<\/span>( <span class=\"cbb-string\">'Category Posts'<\/span>, <span class=\"cbb-string\">'category-posts'<\/span> ) }\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\tinstructions={ <span class=\"cbb-function\">__<\/span>(\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t\t<span class=\"cbb-string\">'Choose a category in the block settings.'<\/span>,\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t\t<span class=\"cbb-string\">'category-posts'<\/span>\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\t) }\n<\/span><span class=\"cbb-line\">\t\t\t\t\t\/&gt;\n<\/span><span class=\"cbb-line\">\t\t\t\t) }\n<\/span><span class=\"cbb-line\">\t\t\t&lt;\/div&gt;\n<\/span><span class=\"cbb-line\">\t\t&lt;\/&gt;\n<\/span><span class=\"cbb-line\">\t);\n<\/span><span class=\"cbb-line\">}<\/span><\/code><\/pre><\/figure>\n\n\n<p>In the package version bundled with WordPress 6.9 (6.9.5), <code>@wordpress\/server-side-render<\/code> already exports <code>ServerSideRender<\/code> as a named export; the default export is marked deprecated and survives as a compatibility layer. This named import is the reason for \u201cRequires at least: 6.9\u201d in the plugin header. <code>skipBlockSupportAttributes<\/code> removes the style attributes from the request, because the outer <code>div<\/code> already receives colours and spacing through <code>useBlockProps()<\/code> and they would otherwise appear twice. The component also appends the current post ID to the request, so the <code>postId<\/code> context in <code>render.php<\/code> works in the editor as well. The two <code>__next\u2026<\/code> props opt older WordPress versions into the current control size and spacing; according to the components changelog, both are now defaults and can be dropped once those versions are no longer supported.<\/p>\n<figure class=\"lw-diagram\">\n<img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/diagrams\/wp-dynamischer-block-en.png?v=20260930\" width=\"1120\" height=\"580\" alt=\"Dynamic block: which file is read where and when, in three columns for every PHP request, the block editor and the front end; blocks-manifest.php registers all blocks, the editor loads index.js with a ServerSideRender preview, and render.php builds the markup from the attributes stored in post_content\">\n<figcaption>PHP registers from the manifest, the editor loads the bundle, render.php builds the markup for front end and preview<\/figcaption>\n<\/figure>\n<p>The diagram summarises who reads which file and when: PHP reads the manifest on every request, the editor loads the script bundle and both stylesheets, and <code>render.php<\/code> runs both for the front end and for the editor preview through the REST route.<\/p>\n<h2>Static or dynamic: what ends up in the database<\/h2>\n<p>For the example, the database contains nothing but the block comment. Attributes that equal their default are not serialised:<\/p>\n\n\n<figure style=\"--cbb-digits:1\" class=\"cbb cbb--wrap wp-block-code-beautifier-block-code\" data-lang=\"plain\"><figcaption class=\"cbb-head\"><span class=\"cbb-lang\">Text<\/span><span class=\"cbb-actions\"><button type=\"button\" class=\"cbb-button cbb-button--wrap\" data-cbb-action=\"wrap\" aria-pressed=\"true\" title=\"Wrap long lines\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path d=\"M2 4h12M2 8h9.5a2.5 2.5 0 0 1 0 5H8m0 0 1.5-1.5M8 13l1.5 1.5M2 12h3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg><span class=\"cbb-sr\">Wrap long lines<\/span><\/button><button type=\"button\" class=\"cbb-button cbb-button--copy\" data-cbb-action=\"copy\" data-cbb-done=\"Copied\" data-cbb-failed=\"Press Ctrl+C to copy\"><svg aria-hidden=\"true\" focusable=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8.5\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><path d=\"M10.5 3.5V3A1.5 1.5 0 0 0 9 1.5H4A1.5 1.5 0 0 0 2.5 3v6.5A1.5 1.5 0 0 0 4 11h.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\/><\/svg><span class=\"cbb-button-label\" aria-live=\"polite\">Copy<\/span><\/button><\/span><\/figcaption><pre class=\"cbb-pre\" tabindex=\"0\"><code class=\"cbb-code language-plain\" style=\"counter-reset:cbb-line 0;\"><span class=\"cbb-line\">&lt;!-- wp:lw\/category-posts {\"categoryId\":7,\"count\":3} \/--&gt;<\/span><\/code><\/pre><\/figure>\n\n\n<p>This is the core of the difference. A static block stores the output of its <code>save<\/code> function as HTML between the comment delimiters, and the editor compares that HTML with a fresh <code>save<\/code> call when the post is opened. Any change to the markup (an extra class, a wrapper, a different tag) therefore breaks existing content unless a <code>deprecated<\/code> array describes the old versions. With a dynamic block, <code>save<\/code> returns <code>null<\/code>, the editor skips markup validation, and a change to <code>render.php<\/code> reaches every occurrence of the block after the next deployment. The block editor handbook names exactly these two cases for dynamic rendering: content that should change without the post being updated, and markup that is expected to change.<\/p>\n<p>Dynamic rendering adds work on the server: each page view containing the block runs a database query, and each attribute change in the editor triggers a REST request. This is usually acceptable for a short list. For output that is expensive to generate, a transient or the object cache can store the rendered HTML. Neither is included in the example, and no measurements were made for this article. The block also depends on the plugin being active: without it, the block renders nothing because <code>post_content<\/code> contains no fallback HTML.<\/p>\n<h2>Limits and open points<\/h2>\n<ul>\n<li>The <code>ServerSideRender<\/code> documentation calls the component a fallback or legacy mechanism that is not appropriate for developing new features. For blocks whose preview can be built from REST data in JavaScript, client-side rendering is the recommended path. The example uses it deliberately so that <code>render.php<\/code> remains the single source of the markup; the price is a nested wrapper in the editor (the outer <code>div<\/code> from <code>useBlockProps()<\/code> contains the <code>div<\/code> from <code>render.php<\/code>).<\/li>\n<li>WordPress 7.0 introduced PHP-only block registration: with <code>'autoRegister' =&gt; true<\/code> under <code>supports<\/code> and a <code>render_callback<\/code>, a block appears in the editor without its own JavaScript, and controls are generated for string, integer, boolean and enum attributes. A category selection with names instead of numeric IDs is not covered by that, which is why the example keeps its own <code>edit.js<\/code>.<\/li>\n<li>The scaffold described here corresponds to <code>@wordpress\/create-block<\/code> 4.99.0. The package is released frequently; file names, flags and defaults can change with later versions.<\/li>\n<li>On WordPress 7.1.2, Plugin Check 2.1.0 reports no errors for the example (tested on 30.09.2026). For each copy of <code>render.php<\/code> in <code>src\/<\/code> and <code>build\/<\/code> it lists seven <code>PrefixAllGlobals<\/code> warnings and one warning about the <code>exclude<\/code> parameter of <code>get_posts()<\/code>; a bare <code>echo get_block_wrapper_attributes()<\/code> is reported as an error.<\/li>\n<li>Caching of the rendered list and the performance cost of the REST preview were not measured.<\/li>\n<\/ul>\n\n<div class=\"lw-faq\">\n<h2>Questions and answers<\/h2>\n\n<h3>Why does a change to block.json have no effect although the file is saved?<\/h3>\n<p>Registration reads <code>build\/blocks-manifest.php<\/code>, not <code>src\/<\/code>. The manifest is a build artefact; PHP sees the new version only after <code>npm run build<\/code> or while <code>npm start<\/code> is running.<\/p>\n\n<h3>May render.php declare its own helper functions?<\/h3>\n<p>No. WordPress loads the file with <code>require<\/code> again for every block instance. With two instances on one page, the second declaration of the same function ends in a fatal error. Helper functions belong in the main plugin file or a separate include, with a prefix.<\/p>\n\n<h3>Does a server-rendered block still need JavaScript since WordPress 7.0?<\/h3>\n<p>Not necessarily. With <code>'autoRegister' =&gt; true<\/code> under <code>supports<\/code> and a <code>render_callback<\/code>, a block appears in the editor without its own JavaScript, and controls are generated for string, integer, boolean and enum attributes. A dropdown with category names or a custom placeholder still requires an <code>edit.js<\/code>.<\/p>\n<\/div>\n\n<div class=\"lw-quellen\">\n<h2>Sources<\/h2>\n<ul>\n<li><a href=\"https:\/\/developer.wordpress.org\/block-editor\/reference-guides\/packages\/packages-create-block\/\" target=\"_blank\" rel=\"noopener noreferrer\">@wordpress\/create-block \u2013 Block Editor Handbook<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/gutenberg\/tree\/trunk\/packages\/create-block\/lib\" target=\"_blank\" rel=\"noopener noreferrer\">create-block source: templates.js, init-package-json.js, plugin and block templates (GitHub)<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/www.npmjs.com\/package\/@wordpress\/create-block\" target=\"_blank\" rel=\"noopener noreferrer\">@wordpress\/create-block on npm, version 4.99.0<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/block-editor\/getting-started\/tutorial\/\" target=\"_blank\" rel=\"noopener noreferrer\">Tutorial: Build your first block<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/block-editor\/reference-guides\/block-api\/block-metadata\/\" target=\"_blank\" rel=\"noopener noreferrer\">Metadata in block.json<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/block-editor\/reference-guides\/block-api\/block-api-versions\/\" target=\"_blank\" rel=\"noopener noreferrer\">Block API Versions<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/block-editor\/getting-started\/fundamentals\/static-dynamic-rendering\/\" target=\"_blank\" rel=\"noopener noreferrer\">Static or Dynamic rendering of a block<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/block-editor\/getting-started\/fundamentals\/registration-of-a-block\/\" target=\"_blank\" rel=\"noopener noreferrer\">Registration of a block<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/block-editor\/reference-guides\/block-api\/block-deprecation\/\" target=\"_blank\" rel=\"noopener noreferrer\">Deprecation<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/block-editor\/reference-guides\/packages\/packages-scripts\/\" target=\"_blank\" rel=\"noopener noreferrer\">@wordpress\/scripts<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/gutenberg\/blob\/trunk\/packages\/dependency-extraction-webpack-plugin\/README.md\" target=\"_blank\" rel=\"noopener noreferrer\">Dependency Extraction Webpack Plugin (README)<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/block-editor\/reference-guides\/packages\/packages-server-side-render\/\" target=\"_blank\" rel=\"noopener noreferrer\">@wordpress\/server-side-render<\/a> and <a href=\"https:\/\/github.com\/WordPress\/gutenberg\/blob\/trunk\/packages\/server-side-render\/CHANGELOG.md\" target=\"_blank\" rel=\"noopener noreferrer\">its changelog<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/get_block_wrapper_attributes\/\" target=\"_blank\" rel=\"noopener noreferrer\">get_block_wrapper_attributes()<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/wp_register_block_types_from_metadata_collection\/\" target=\"_blank\" rel=\"noopener noreferrer\">wp_register_block_types_from_metadata_collection()<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/make.wordpress.org\/core\/2024\/10\/17\/new-block-type-registration-apis-to-improve-performance-in-wordpress-6-7\/\" target=\"_blank\" rel=\"noopener noreferrer\">New block type registration APIs to improve performance in WordPress 6.7<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/make.wordpress.org\/core\/2025\/03\/13\/more-efficient-block-type-registration-in-6-8\/\" target=\"_blank\" rel=\"noopener noreferrer\">More efficient block type registration in 6.8<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/make.wordpress.org\/core\/2026\/03\/03\/php-only-block-registration\/\" target=\"_blank\" rel=\"noopener noreferrer\">PHP-only block registration<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/7.1\/src\/wp-includes\/blocks.php\" target=\"_blank\" rel=\"noopener noreferrer\">wordpress-develop, branch 7.1: blocks.php<\/a> and <a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/7.1\/src\/wp-includes\/post.php\" target=\"_blank\" rel=\"noopener noreferrer\">post.php (get_posts)<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/WordPress-Coding-Standards\/blob\/develop\/WordPress\/Helpers\/EscapingFunctionsTrait.php\" target=\"_blank\" rel=\"noopener noreferrer\">WordPress Coding Standards: EscapingFunctionsTrait.php<\/a> (retrieved 29.09.2026)<\/li>\n<li><a href=\"https:\/\/api.wordpress.org\/core\/version-check\/1.7\/\" target=\"_blank\" rel=\"noopener noreferrer\">WordPress version check API (current release 7.1.2)<\/a> (retrieved 29.09.2026)<\/li>\n<\/ul>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>A server-rendered block that lists the latest posts of one category: create-block scaffold, block.json, manifest registration, render.php and an editor preview.<\/p>\n","protected":false},"author":1,"featured_media":20911,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[92642],"tags":[91312,91093,91219,92757,91096],"class_list":["post-20866","post","type-post","status-publish","format-standard","hentry","category-tutorials-en-wordpress-plugins-tricks","tag-javascript","tag-php","tag-tutorial","tag-web-development","tag-wordpress"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>A dynamic Gutenberg block with block.json and render.php | Lukas Wojcik<\/title>\n<meta name=\"description\" content=\"How a dynamic WordPress block is built today: create-block --variant dynamic, block.json with apiVersion 3, blocks-manifest.php and render.php.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"A dynamic Gutenberg block with block.json and render.php | Lukas Wojcik\" \/>\n<meta property=\"og:description\" content=\"How a dynamic WordPress block is built today: create-block --variant dynamic, block.json with apiVersion 3, blocks-manifest.php and render.php.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/\" \/>\n<meta property=\"og:site_name\" content=\"Lukas Wojcik - Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-21T06:29:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20866-dynamic-block-block-json.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Lukas Wojcik\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Lukas Wojcik\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"17 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/dynamic-block-block-json\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/dynamic-block-block-json\\\/\"},\"author\":{\"name\":\"Lukas Wojcik\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"headline\":\"A dynamic Gutenberg block with block.json and render.php\",\"datePublished\":\"2026-10-21T06:29:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/dynamic-block-block-json\\\/\"},\"wordCount\":2236,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"image\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/dynamic-block-block-json\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/fi-20866-dynamic-block-block-json.png\",\"keywords\":[\"JavaScript\",\"PHP\",\"Tutorial\",\"Web Development\",\"WordPress\"],\"articleSection\":[\"Tutorials\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/dynamic-block-block-json\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/dynamic-block-block-json\\\/\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/dynamic-block-block-json\\\/\",\"name\":\"A dynamic Gutenberg block with block.json and render.php | Lukas Wojcik\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/dynamic-block-block-json\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/dynamic-block-block-json\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/fi-20866-dynamic-block-block-json.png\",\"datePublished\":\"2026-10-21T06:29:00+00:00\",\"description\":\"How a dynamic WordPress block is built today: create-block --variant dynamic, block.json with apiVersion 3, blocks-manifest.php and render.php.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/dynamic-block-block-json\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/dynamic-block-block-json\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/dynamic-block-block-json\\\/#primaryimage\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/fi-20866-dynamic-block-block-json.png\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/fi-20866-dynamic-block-block-json.png\",\"width\":1200,\"height\":630,\"caption\":\"A dynamic Gutenberg block with block.json and render.php\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/dynamic-block-block-json\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"A dynamic Gutenberg block with block.json and render.php\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\",\"name\":\"Lukas Wojcik - Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\",\"name\":\"Lukas Wojcik\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"width\":424,\"height\":636,\"caption\":\"Lukas Wojcik\"},\"logo\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\"},\"sameAs\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"A dynamic Gutenberg block with block.json and render.php | Lukas Wojcik","description":"How a dynamic WordPress block is built today: create-block --variant dynamic, block.json with apiVersion 3, blocks-manifest.php and render.php.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/","og_locale":"en_US","og_type":"article","og_title":"A dynamic Gutenberg block with block.json and render.php | Lukas Wojcik","og_description":"How a dynamic WordPress block is built today: create-block --variant dynamic, block.json with apiVersion 3, blocks-manifest.php and render.php.","og_url":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/","og_site_name":"Lukas Wojcik - Blog","article_published_time":"2026-10-21T06:29:00+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20866-dynamic-block-block-json.png","type":"image\/png"}],"author":"Lukas Wojcik","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Lukas Wojcik","Est. reading time":"17 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/#article","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/"},"author":{"name":"Lukas Wojcik","@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"headline":"A dynamic Gutenberg block with block.json and render.php","datePublished":"2026-10-21T06:29:00+00:00","mainEntityOfPage":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/"},"wordCount":2236,"commentCount":0,"publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"image":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/#primaryimage"},"thumbnailUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20866-dynamic-block-block-json.png","keywords":["JavaScript","PHP","Tutorial","Web Development","WordPress"],"articleSection":["Tutorials"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/","url":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/","name":"A dynamic Gutenberg block with block.json and render.php | Lukas Wojcik","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/#primaryimage"},"image":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/#primaryimage"},"thumbnailUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20866-dynamic-block-block-json.png","datePublished":"2026-10-21T06:29:00+00:00","description":"How a dynamic WordPress block is built today: create-block --variant dynamic, block.json with apiVersion 3, blocks-manifest.php and render.php.","breadcrumb":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/#primaryimage","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20866-dynamic-block-block-json.png","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20866-dynamic-block-block-json.png","width":1200,"height":630,"caption":"A dynamic Gutenberg block with block.json and render.php"},{"@type":"BreadcrumbList","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/dynamic-block-block-json\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.lukaswojcik.com\/blog\/"},{"@type":"ListItem","position":2,"name":"A dynamic Gutenberg block with block.json and render.php"}]},{"@type":"WebSite","@id":"https:\/\/www.lukaswojcik.com\/blog\/#website","url":"https:\/\/www.lukaswojcik.com\/blog\/","name":"Lukas Wojcik - Blog","description":"","publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.lukaswojcik.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9","name":"Lukas Wojcik","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","width":424,"height":636,"caption":"Lukas Wojcik"},"logo":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg"},"sameAs":["https:\/\/www.lukaswojcik.com\/blog"]}]}},"_links":{"self":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/20866","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/comments?post=20866"}],"version-history":[{"count":2,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/20866\/revisions"}],"predecessor-version":[{"id":22771,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/20866\/revisions\/22771"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media\/20911"}],"wp:attachment":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media?parent=20866"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/categories?post=20866"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/tags?post=20866"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}