LW IT Solutions logo LW IT Solutions logo LW IT Solutions
« Blog-Übersicht /WordPress-Plugins & Tricks/Tutorials / Ein dynamischer Gutenberg-Block mit block.json und render.php
Diesen Artikel in anderen Sprachen lesen:

Ein dynamischer Gutenberg-Block mit block.json und render.php

Ein dynamischer Gutenberg-Block mit block.json und render.php
Inhalt
  1. Was das create-block-Gerüst heute erzeugt
  2. block.json als Vertrag zwischen Editor und Server
  3. Registrierung über ein Manifest statt einzelner block.json-Dateien
  4. render.php und seine drei Variablen
  5. Die Editorseite: edit.js mit ServerSideRender
  6. Statisch oder dynamisch: was in der Datenbank landet
  7. Grenzen und offene Punkte
  8. Fragen und Antworten
  9. Quellen

Ein Block, dessen Ausgabe von Daten abhängt, die sich nach dem Speichern des Beitrags ändern (die neuesten Beiträge, ein Preis, eine Terminliste), kann sein fertiges HTML nicht in post_content ablegen. Jeder neue Beitrag in der Rubrik ließe das gespeicherte Markup veralten. Statische Blocks haben noch eine zweite Schwäche: Ändert sich das Markup der save-Funktion, passen ältere Beiträge nicht mehr dazu, der Editor meldet ungültigen Inhalt, und der Block braucht einen Deprecation-Eintrag. Ein dynamischer Block umgeht beides. Der Editor speichert nur den Block-Kommentar mit den Attributen, das Markup erzeugt PHP bei jedem Aufruf.

Dieser Beitrag baut einen solchen Block von Grund auf: eine Liste der letzten Beiträge einer Rubrik mit den Attributen categoryId und count. Er zeigt, was @wordpress/create-block mit Stand 29.09.2026 erzeugt, wie block.json Editor und Server verbindet, warum das Gerüst Blocks inzwischen über ein generiertes Manifest registriert und welche Variablen render.php erhält. Der Code ist vollständig; das Plugin läuft ab WordPress 6.9, aktuell ist zum Zeitpunkt des Schreibens Version 7.1.2.

Was das create-block-Gerüst heute erzeugt

Das offizielle Werkzeug ist das npm-Paket @wordpress/create-block; am Abrufdatum ist Version 4.99.0 vom 23.09.2026 die neueste veröffentlichte. Vorausgesetzt wird Node.js ab 20.10.0. Die Standardvorlage kennt zwei Varianten, static und dynamic. Die dynamische Variante unterscheidet sich an genau drei Stellen: block.json erhält ein Feld render mit Verweis auf render.php, index.js registriert den Block ohne save-Funktion, und die npm-Skripte kopieren PHP-Dateien in den Build-Ordner. Für das Beispiel sehen die Befehle so aus:

Shell
npx @wordpress/create-block@latest category-posts --variant dynamic --namespace lw --title "Category Posts"
cd category-posts
npm install @wordpress/components @wordpress/core-data @wordpress/data @wordpress/html-entities @wordpress/server-side-render --save
npm start

Die zusätzliche Zeile mit npm install ist für den Build nicht zwingend, denn @wordpress/scripts behandelt Importe aus @wordpress/* als externe Abhängigkeiten und bildet sie auf die Skript-Handles ab, die WordPress ohnehin mitbringt. Mit installierten Paketen kennen Editor-Werkzeuge und Linter die Importe aber. npm start beobachtet src/ und schreibt nach build/, npm run build erzeugt die minimierte Fassung für die Veröffentlichung. Der Plugin-Ordner sieht danach etwa so aus:

Text
category-posts/                 (abridged)
├── category-posts.php          plugin header and block registration
├── package.json                build/start scripts with --blocks-manifest
├── readme.txt
├── src/
│   └── category-posts/
│       ├── block.json
│       ├── index.js
│       ├── edit.js
│       ├── render.php
│       ├── view.js             not needed here, deleted
│       ├── style.scss
│       └── editor.scss
└── build/                      written by npm start / npm run build
    ├── blocks-manifest.php
    └── category-posts/
        ├── block.json
        ├── index.js
        ├── index.asset.php
        ├── index.css
        ├── style-index.css
        └── render.php

Mit der Standardvorlage liegt der Block in src/<slug>/, nicht direkt in src/. Die Text Domain entspricht standardmäßig dem Slug, hier category-posts. Zusätzlich legt das Gerüst eine view.js samt Eintrag viewScript für JavaScript im Frontend an. Eine serverseitig gerenderte Liste ohne Interaktion braucht das nicht, daher entfernt das Beispiel beides. Die beiden npm-Skripte, auf die es bei einem dynamischen Block ankommt, erzeugt das Gerüst so:

JSON
{
	"scripts": {
		"build": "wp-scripts build --webpack-copy-php --blocks-manifest",
		"start": "wp-scripts start --webpack-copy-php --blocks-manifest"
	}
}

--webpack-copy-php kopiert alle PHP-Dateien aus src nach build; ohne den Schalter übernimmt @wordpress/scripts nur PHP-Dateien, die eine block.json unter render oder variations nennt. --blocks-manifest schreibt build/blocks-manifest.php, dazu mehr in den nächsten Abschnitten. Der erzeugte Plugin-Header nennt „Requires at least: 6.8“, die readme.txt „Tested up to: 6.8“. Beides sind Vorgaben der Vorlage, keine Aussage über die aktuelle Version, und sollte vor einer Veröffentlichung angepasst werden. Die Header-Felder behandelt ein eigener Beitrag dieser Reihe zu readme.txt, Plugin-Header und SVN.

block.json als Vertrag zwischen Editor und Server

block.json wird auf beiden Seiten gelesen. PHP registriert daraus Blocktyp, Attribute und Assets; das Editor-Bundle importiert dieselbe Datei, damit JavaScript Namen und Attributschema kennt. Im Beispiel hat die Datei in src/category-posts/ diesen Inhalt:

JSON
{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "lw/category-posts",
	"version": "0.1.0",
	"title": "Category Posts",
	"category": "widgets",
	"icon": "list-view",
	"description": "Lists the latest posts of one category, rendered on the server.",
	"attributes": {
		"categoryId": {
			"type": "integer",
			"default": 0
		},
		"count": {
			"type": "integer",
			"default": 5
		}
	},
	"usesContext": [ "postId" ],
	"supports": {
		"html": false,
		"align": [ "wide", "full" ],
		"color": {
			"background": true,
			"text": true,
			"link": true
		},
		"spacing": {
			"margin": true,
			"padding": true
		},
		"typography": {
			"fontSize": true
		}
	},
	"textdomain": "category-posts",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css",
	"render": "file:./render.php"
}

apiVersion 3 ist die aktuelle Version der Block-API und kam mit WordPress 6.3. Fehlt das Feld, nimmt WordPress Version 1 an. Version 3 hängt am Editor im iframe: Seit 6.3 nutzte der Beitragseditor ein iframe nur, wenn alle registrierten Blocks Version 3 oder höher angaben. WordPress 7.0 stellte die Prüfung auf die Blocks um, die tatsächlich im Beitragsinhalt stehen, und laut Dokumentation der Block-API nutzt WordPress 7.1 für den Beitragseditor immer ein iframe, unabhängig von apiVersion. Für einen neuen Block bleibt davon eine Folge: Editor-Styles müssen im iframe greifen, was sie tun, wenn sie über editorStyle und style in block.json geladen werden.

Beide Attribute haben den Typ integer und einen Standardwert. Vor dem Rendern prüft WordPress jedes Attribut gegen sein Schema, verwirft ungültige Werte und setzt für fehlende Attribute den Standardwert ein. usesContext fordert den Kontext postId an, den render_block() aus dem globalen Beitrag befüllt. So kann der Block den Beitrag auslassen, in dem er selbst steht.

Der Abschnitt supports schaltet Ausrichtung, Farben, Abstände und Schriftgröße frei. Das Markup dafür erzeugen später get_block_wrapper_attributes() und useBlockProps(), sodass kein eigener Code nötig ist. editorScript und editorStyle werden nur im Editor geladen, style im Editor und im Frontend. render ist ein reines Serverfeld.

Registrierung über ein Manifest statt einzelner block.json-Dateien

Lange war register_block_type( __DIR__ . '/build' ) oder ein Pfad zum Block-Ordner die übliche Registrierung. Das funktioniert weiterhin und passt für einen einzelnen Block ohne Build-Schritt. Bei jedem Aufruf liest es allerdings eine JSON-Datei von der Platte und dekodiert sie. WordPress 6.7 brachte wp_register_block_metadata_collection(), das ein PHP-Manifest mit den Metadaten mehrerer Blocks registriert; die Dev-Note von Felix Arntz nennt die eingesparten Dateisystem- und JSON-Operationen als Grund. In 6.7 brauchte trotzdem jeder Block noch einen eigenen Aufruf von register_block_type(). WordPress 6.8 schloss die Lücke mit wp_register_block_types_from_metadata_collection(): Ein Aufruf registriert die Sammlung und alle darin enthaltenen Blocktypen.

Das aktuelle Gerüst nutzt genau diese Funktion, sobald @wordpress/scripts eingebunden ist, und das ist die Vorgabe. Die Plugin-Hauptdatei des Beispiels sieht deshalb so aus:

PHP
<?php
/**
 * Plugin Name:       Category Posts
 * Description:       Server-rendered block that lists the latest posts of one category.
 * Version:           0.1.0
 * Requires at least: 6.9
 * Requires PHP:      7.4
 * Author:            Lukas Wojcik
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       category-posts
 *
 * @package Lw
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Registers every block listed in build/blocks-manifest.php.
 *
 * The manifest is generated by wp-scripts (--blocks-manifest), so WordPress
 * does not have to read and decode each block.json on every request.
 */
function lw_category_posts_block_init() {
	wp_register_block_types_from_metadata_collection(
		__DIR__ . '/build',
		__DIR__ . '/build/blocks-manifest.php'
	);
}
add_action( 'init', 'lw_category_posts_block_init' );

Der erste Parameter ist der Basisordner der Sammlung, der zweite das Manifest. Bleibt der zweite leer, muss der Basisordner auf eine zuvor registrierte Sammlung verweisen. Plugins, die noch WordPress 6.7 oder älter unterstützen, brauchen eine Prüfung mit function_exists() und einen Rückfallweg, wie ihn die Dev-Note zu 6.8 beschreibt. Das Beispiel umgeht das mit „Requires at least: 6.9“, was es für den weiter unten gezeigten Import im Editor ohnehin benötigt. Ohne Build-Schritt reicht ein einzelner Aufruf:

Text
// Single block, without a manifest: reads build/category-posts/block.json.
register_block_type( __DIR__ . '/build/category-posts' );

Ein Detail wird leicht übersehen: Das Manifest ist ein Build-Ergebnis. Eine in src/ geänderte block.json wirkt erst nach dem nächsten Build, auch auf der Serverseite.

render.php und seine drei Variablen

Enthält block.json ein Feld render, legt WordPress bei der Registrierung einen render_callback an. Im Core-Quelltext ist das eine statische Closure, die die Ausgabepufferung startet, die Datei per require lädt und den Puffer zurückgibt. In der Datei stehen drei Variablen bereit: $attributes (Array der Block-Attribute, Standardwerte bereits eingesetzt), $content (der gespeicherte innere Inhalt, wichtig für Blocks mit inneren Blocks, hier immer leer) und $block (die Instanz von WP_Block mit Namen, Kontext und geparstem Block). Die Render-Datei des Beispiels:

PHP
<?php
/**
 * Server-side rendering of the lw/category-posts block.
 *
 * Variables provided by WordPress:
 *
 * @var array    $attributes Block attributes, defaults from block.json applied.
 * @var string   $content    Inner content (always empty for this block).
 * @var WP_Block $block      Block instance: name, context, parsed block.
 *
 * @package Lw
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

$lw_category_id = isset( $attributes['categoryId'] ) ? absint( $attributes['categoryId'] ) : 0;
$lw_count       = isset( $attributes['count'] ) ? absint( $attributes['count'] ) : 5;
$lw_count       = min( max( $lw_count, 1 ), 10 );
$lw_term        = $lw_category_id ? get_term( $lw_category_id, 'category' ) : null;

// Render nothing until a valid category has been chosen.
if ( ! $lw_term instanceof WP_Term ) {
	return;
}

// Do not list the post that contains the block (context from "usesContext").
$lw_current_id = isset( $block->context['postId'] ) ? absint( $block->context['postId'] ) : 0;

$lw_posts = get_posts(
	array(
		'category'     => $lw_term->term_id,
		'numberposts'  => $lw_count,
		'post_status'  => 'publish',
		'has_password' => false,
		'exclude'      => $lw_current_id ? array( $lw_current_id ) : array(),
	)
);
?>
<div <?php echo wp_kses_data( get_block_wrapper_attributes() ); ?>>
	<?php if ( empty( $lw_posts ) ) : ?>
		<p><?php esc_html_e( 'No posts in this category yet.', 'category-posts' ); ?></p>
	<?php else : ?>
		<ul class="wp-block-lw-category-posts__list">
			<?php foreach ( $lw_posts as $lw_post ) : ?>
				<li>
					<a href="<?php echo esc_url( get_permalink( $lw_post ) ); ?>"><?php echo esc_html( get_the_title( $lw_post ) ); ?></a>
					<time datetime="<?php echo esc_attr( get_the_date( 'c', $lw_post ) ); ?>"><?php echo esc_html( get_the_date( '', $lw_post ) ); ?></time>
				</li>
			<?php endforeach; ?>
		</ul>
	<?php endif; ?>
</div>

Mehrere Entscheidungen in dieser Datei folgen daraus, wie sie geladen wird. Da require für jede Block-Instanz auf einer Seite erneut läuft, darf die Datei keine Funktionen oder Klassen deklarieren; eine zweite Instanz des Blocks endete sonst in einem fatalen Fehler. Hilfsfunktionen gehören in die Plugin-Hauptdatei oder in ein eigenes Include. Für PHP_CodeSniffer sehen die Variablen global aus, deshalb tragen sie das Präfix lw_. Die Prüfung stellt das allerdings nicht zufrieden: WPCS lehnt Präfixe mit weniger als vier Zeichen ab, und Plugin Check 2.1.0, das das erwartete Präfix aus dem Plugin-Code ableitet (hier aus dem Funktionsnamen lw_category_posts_block_init), meldet jede dieser Variablen als Warnung. Auf die Funktion des Blocks haben die Warnungen keinen Einfluss. return beendet die Datei vorzeitig und hinterlässt einen leeren Puffer, so rendert ein Block gar nichts.

Attribute sind Daten aus post_content und damit Eingaben. Die Schemaprüfung garantiert ganze Zahlen, aber keinen sinnvollen Bereich, daher begrenzen absint(), min() und max() die Anzahl auf 1 bis 10. get_term() stellt sicher, dass die Rubrik existiert. get_posts() ist eine dünne Hülle um WP_Query, die ignore_sticky_posts, no_found_rows und suppress_filters selbst setzt; das passt zu einer kurzen Liste ohne Seitennavigation. Wie weitere Abfrageargumente wie meta_query zu SQL werden, zeigt der Beitrag zu meta_query in WP_Query. Jede Ausgabe läuft durch die passende Escaping-Funktion: esc_url() für den Link, esc_html() für Titel und Datum, esc_attr() für das Attribut datetime. Die allgemeinen Regeln dazu behandelt ein eigener Beitrag dieser Reihe zu Sanitizing, Escaping, Nonces und Capabilities.

get_block_wrapper_attributes() liefert die Attribut-Zeichenkette für das umschließende Element: die Klasse wp-block-lw-category-posts plus Klassen und Inline-Styles aus den supports-Einstellungen. Die Funktion escapet jeden Wert selbst mit esc_attr(), wie ihr Quelltext zeigt. In der Liste der automatisch escapenden Funktionen der WordPress Coding Standards fehlt sie allerdings, sodass ein nacktes echo den Escaping-Sniff auslöst; Plugin Check 2.1.0 meldet es als Fehler (WordPress.Security.EscapeOutput.OutputNotEscaped). wp_kses_data() lässt eine Attribut-Zeichenkette ohne Tags unverändert und stellt die Prüfung zufrieden; ein begründeter phpcs:ignore-Kommentar ist die Alternative.

Die Editorseite: edit.js mit ServerSideRender

In JavaScript registriert index.js den Block nur mit edit. Fehlt save, setzt die Block-API ein Standard-save ein, das null zurückgibt:

JavaScript
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import metadata from './block.json';

// No save(): the default save returns null, so only the block comment
// with its attributes is stored in post_content.
registerBlockType( metadata.name, {
	edit: Edit,
} );

edit.js liefert die Einstellungen in der Seitenleiste und eine Vorschau. Die Rubriken kommen aus dem Core-Datenspeicher, mit derselben Abfrage, die der Core-Block „Neueste Beiträge“ verwendet. Die Vorschau erzeugt ServerSideRender: Die Komponente schickt die Attribute an die REST-Route /wp/v2/block-renderer/lw/category-posts und fügt das HTML ein, das render.php zurückgibt:

JavaScript
import { __ } from '@wordpress/i18n';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import {
	PanelBody,
	Placeholder,
	RangeControl,
	SelectControl,
	Spinner,
} from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
import { ServerSideRender } from '@wordpress/server-side-render';
import metadata from './block.json';
import './editor.scss';

// Same query shape as the core Latest Posts block uses for its category list.
const CATEGORIES_QUERY = { per_page: -1, _fields: 'id,name', context: 'view' };

export default function Edit( { attributes, setAttributes } ) {
	const { categoryId, count } = attributes;
	const blockProps = useBlockProps();

	const categories = useSelect(
		( select ) =>
			select( coreStore ).getEntityRecords(
				'taxonomy',
				'category',
				CATEGORIES_QUERY
			),
		[]
	);

	const options = [
		{ value: '0', label: __( '— Select a category —', 'category-posts' ) },
		...( categories ?? [] ).map( ( term ) => ( {
			value: String( term.id ),
			label: decodeEntities( term.name ),
		} ) ),
	];

	return (
		<>
			<InspectorControls>
				<PanelBody title={ __( 'Settings', 'category-posts' ) }>
					{ categories ? (
						<SelectControl
							__next40pxDefaultSize
							__nextHasNoMarginBottom
							label={ __( 'Category', 'category-posts' ) }
							value={ String( categoryId ) }
							options={ options }
							onChange={ ( value ) =>
								setAttributes( { categoryId: Number( value ) } )
							}
						/>
					) : (
						<Spinner />
					) }
					<RangeControl
						__next40pxDefaultSize
						__nextHasNoMarginBottom
						label={ __( 'Number of posts', 'category-posts' ) }
						value={ count }
						min={ 1 }
						max={ 10 }
						onChange={ ( value ) =>
							setAttributes( { count: value ?? 5 } )
						}
					/>
				</PanelBody>
			</InspectorControls>
			<div { ...blockProps }>
				{ categoryId ? (
					<ServerSideRender
						block={ metadata.name }
						attributes={ attributes }
						skipBlockSupportAttributes
					/>
				) : (
					<Placeholder
						icon="list-view"
						label={ __( 'Category Posts', 'category-posts' ) }
						instructions={ __(
							'Choose a category in the block settings.',
							'category-posts'
						) }
					/>
				) }
			</div>
		</>
	);
}

In der mit WordPress 6.9 ausgelieferten Paketversion (6.9.5) exportiert @wordpress/server-side-render die Komponente ServerSideRender bereits als benannten Export; der Standard-Export gilt als veraltet und bleibt als Kompatibilitätsschicht erhalten. Dieser benannte Import ist der Grund für „Requires at least: 6.9“ im Plugin-Header. skipBlockSupportAttributes nimmt die Style-Attribute aus der Anfrage, denn das äußere div erhält Farben und Abstände schon über useBlockProps(); sonst erschienen sie doppelt. Die Komponente hängt außerdem die ID des aktuellen Beitrags an die Anfrage, sodass der Kontext postId in render.php auch im Editor greift. Die beiden Props __next… an den Steuerelementen schalten ältere WordPress-Versionen auf die aktuelle Größe und Abstände der Controls; laut Changelog der Components sind beide inzwischen Standard und können entfallen, sobald diese Versionen nicht mehr unterstützt werden.

Dynamischer Block: welche Datei wo und wann gelesen wird, in drei Spalten für jeden PHP-Aufruf, den Block-Editor und das Frontend; blocks-manifest.php registriert alle Blöcke, der Editor lädt index.js mit ServerSideRender-Vorschau, und render.php baut das Markup aus den in post_content gespeicherten Attributen
PHP registriert aus dem Manifest, der Editor lädt das Bundle, render.php erzeugt Markup für Frontend und Vorschau

Das Diagramm fasst zusammen, wer welche Datei wann liest: PHP liest das Manifest bei jedem Aufruf, der Editor lädt das Skript-Bundle und beide Stylesheets, und render.php läuft sowohl für das Frontend als auch für die Editor-Vorschau über die REST-Route.

Statisch oder dynamisch: was in der Datenbank landet

Für das Beispiel steht in der Datenbank nur der Block-Kommentar. Attribute, die ihrem Standardwert entsprechen, werden nicht serialisiert:

Text
<!-- wp:lw/category-posts {"categoryId":7,"count":3} /-->

Hier liegt der Kern des Unterschieds. Ein statischer Block speichert die Ausgabe seiner save-Funktion als HTML zwischen den Kommentar-Begrenzern, und der Editor vergleicht dieses HTML beim Öffnen des Beitrags mit einem frischen Aufruf von save. Jede Änderung am Markup (eine zusätzliche Klasse, ein Wrapper, ein anderes Tag) bricht deshalb bestehende Inhalte, solange kein Array deprecated die alten Fassungen beschreibt. Beim dynamischen Block gibt save null zurück, der Editor überspringt die Markup-Prüfung, und eine Änderung an render.php erreicht nach dem nächsten Deployment jedes Vorkommen des Blocks. Das Block-Editor-Handbuch nennt genau diese beiden Fälle für dynamisches Rendern: Inhalte, die sich ändern sollen, ohne dass der Beitrag aktualisiert wird, und Markup, das sich voraussichtlich ändert.

Dynamisches Rendern verursacht zusätzliche Arbeit auf dem Server: Jeder Seitenaufruf mit dem Block führt eine Datenbankabfrage aus, jede Attributänderung im Editor eine REST-Anfrage. Für eine kurze Liste ist das meist vertretbar. Ist die Ausgabe aufwendig zu erzeugen, kann ein Transient oder der Objekt-Cache das gerenderte HTML speichern. Beides gehört nicht zum Beispiel, und für diesen Beitrag wurde nichts gemessen. Außerdem hängt die Ausgabe vom aktiven Plugin ab: Ohne das Plugin rendert der Block nichts, weil post_content kein Ersatz-HTML enthält.

Grenzen und offene Punkte

  • Die Dokumentation von ServerSideRender bezeichnet die Komponente als Rückfall- oder Altmechanismus, der für die Entwicklung neuer Funktionen nicht geeignet ist. Für Blocks, deren Vorschau sich in JavaScript aus REST-Daten bauen lässt, gilt clientseitiges Rendern als empfohlener Weg. Das Beispiel nutzt die Komponente bewusst, damit render.php die einzige Quelle des Markups bleibt; der Preis ist ein verschachtelter Wrapper im Editor (das äußere div aus useBlockProps() enthält das div aus render.php).
  • WordPress 7.0 hat die Registrierung von Blocks allein in PHP eingeführt: Mit 'autoRegister' => true unter supports und einem render_callback erscheint ein Block ohne eigenes JavaScript im Editor, und für Attribute vom Typ string, integer, boolean und enum entstehen Steuerelemente automatisch. Eine Rubrikenauswahl mit Namen statt numerischer IDs deckt das nicht ab, deshalb behält das Beispiel eine eigene edit.js.
  • Das beschriebene Gerüst entspricht @wordpress/create-block 4.99.0. Das Paket erscheint in kurzen Abständen; Dateinamen, Schalter und Vorgaben können sich mit späteren Versionen ändern.
  • Plugin Check 2.1.0 meldet für das Beispiel unter WordPress 7.1.2 keine Fehler (geprüft am 30.09.2026). Für jede Kopie von render.php in src/ und build/ listet es sieben PrefixAllGlobals-Warnungen und eine Warnung zum Parameter exclude von get_posts(); ein nacktes echo get_block_wrapper_attributes() meldet es als Fehler.
  • Caching der gerenderten Liste und die Kosten der REST-Vorschau wurden nicht gemessen.

Fragen und Antworten

Warum wirkt eine Änderung an block.json nicht, obwohl die Datei gespeichert ist?

Registriert wird aus build/blocks-manifest.php, nicht aus src/. Das Manifest entsteht beim Build; erst nach npm run build oder bei laufendem npm start sieht auch PHP die neue Fassung.

Darf render.php eigene Hilfsfunktionen deklarieren?

Nein. WordPress lädt die Datei per require für jede Block-Instanz erneut. Steht der Block zweimal auf einer Seite, führt eine zweite Deklaration derselben Funktion zu einem fatalen Fehler. Hilfsfunktionen gehören mit Präfix in die Plugin-Hauptdatei oder in ein eigenes Include.

Braucht ein serverseitig gerenderter Block seit WordPress 7.0 überhaupt noch JavaScript?

Nicht zwingend. Mit 'autoRegister' => true unter supports und einem render_callback erscheint ein Block ohne eigenes JavaScript im Editor; Steuerelemente entstehen für Attribute vom Typ string, integer, boolean und enum. Für eine Auswahlliste mit Rubriknamen oder eigene Platzhalter bleibt eine edit.js nötig.

Lukas Wojcik

Lukas Wojcik

Systems architect and technology enthusiast specializing in scalable tracking solutions, GMP Stack (GA4 & GTM), and robust backend architectures. Advocate for clean code and privacy-first design.

Kontakt

Eine Frage oder eine Idee für ein Webprojekt? Schreib mir kurz, worum es geht. Das Formular nutzt reCAPTCHA zum Schutz vor Spam.

Kommentar schreiben

Erfahrungen mit anderen Plugins oder Hosting-Umgebungen und Rückfragen zur Einrichtung sind hier willkommen.

Die E-Mail-Adresse wird nicht veröffentlicht. Pflichtfelder sind mit einem Stern versehen.