12k
All articles

So erstellen Sie einen eigenen Gutenberg-Block

Erstellen Sie einen eigenen Gutenberg-Block mit React, block.json und PHP. Erfahren Sie, wie Attribute, statische und dynamische Ausgabe sowie veraltete Versionen Fehler vermeiden.

OpenReplay Team
OpenReplay Team
So erstellen Sie einen eigenen Gutenberg-Block

Ein Gutenberg-Block ist eine React-Komponente, die WordPress unter einem Namen mit Namespace registriert. Im Editor wird sie über eine edit-Funktion gerendert. Im Frontend erfolgt die Ausgabe entweder über eine save-Funktion oder ein PHP-Render-Template.

Wenn Sie eine React-Komponente schreiben können, können Sie auch einen Block schreiben. Die eigentliche Schwierigkeit liegt in den WordPress-spezifischen Teilen rund um die Komponente: einem JSON-Manifest, einem PHP-Registrierungsaufruf und einem Validierungsschritt. Dieser meldet „Dieser Block enthält unerwarteten oder ungültigen Inhalt“, sobald Sie Ihr Markup ändern.

Bevor es Blöcke gab, fügten Sie eigenes Verhalten hinzu, indem Sie WordPress-Themes eigenes JavaScript hinzufügten – mit wp_enqueue_script. Blöcke ersetzen diesen Ansatz für Inhalte: Redakteure erhalten eine visuelle, bearbeitbare Komponente statt eines Skripts, das auf statischem HTML ausgeführt wird. In dieser Anleitung bauen wir einen Callout-Block mit einem bearbeitbaren RichText-Textkörper und einem URL-Feld in der Seitenleiste. Sie erklärt jede generierte Datei und zeigt, warum Validierungsfehler auftreten und wie Sie sie beheben.

Das Wichtigste in Kürze

  • npx @wordpress/create-block@latest callout --namespace=openreplay erzeugt ein vollständiges Block-Plugin. @wordpress/scripts baut es mit npm start und npm run build.
  • block.json deklariert den Block. Sowohl der PHP- als auch der JavaScript-Registrierungsaufruf lesen Name, Attribute, Supports und Asset-Pfade aus dieser einen Datei.
  • Attribute ohne source werden als JSON im HTML-Kommentar-Delimiter des Blocks gespeichert. Attribute mit source und selector werden aus dem gespeicherten Markup zurückgelesen.
  • Beim Laden eines Beitrags führt der Editor save() erneut aus und vergleicht das Ergebnis mit dem gespeicherten Markup. Jede Abweichung löst den Fehler wegen ungültigen Inhalts aus – so lange, bis ein deprecated-Eintrag die alte Ausgabe reproduziert.
  • Ein dynamischer Block gibt aus save den Wert null zurück und wird bei jeder Anfrage in PHP gerendert. Setzen Sie ihn für Ausgaben ein, die sich ändern müssen, ohne dass jemand den Beitrag erneut speichert.

Ein Block ist eine React-Komponente

Ein Gutenberg-Block ist eine React-Komponente, deren WordPress-spezifische API sich auf Muster abbilden lässt, die React-Entwicklern bereits vertraut sind:

React-KonzeptBlock-Entsprechung
Komponenten-JSXedit-Funktion
Propsattributes
setStatesetAttributes
Serverseitig gerenderte Ausgabesave-Funktion oder render.php

Ein Gutenberg-Block unterscheidet sich in zwei Punkten von einer gewöhnlichen React-Komponente. Erstens wird die Ausgabe von save als HTML in den Beitragsinhalt geschrieben. Zweitens liegt das Attributschema in JSON statt in PropTypes oder TypeScript.

Das Block-Gerüst mit create-block erzeugen

Das offizielle Paket @wordpress/create-block erzeugt ein lauffähiges Block-Plugin mit bereits konfiguriertem Build-Tooling. Führen Sie es innerhalb von wp-content/plugins aus:

npx @wordpress/create-block@latest callout --namespace=openreplay
cd callout
npm start        # watch mode during development
npm run build    # production build

Wenn Sie --namespace weglassen, erhält der Block den Namen create-block/callout. Sobald npm start läuft, aktivieren Sie das Plugin im Admin-Bereich.

DateiZweck
callout.phpPlugin-Header und Block-Registrierung beim init-Hook
package.jsonnpm-Skripte, die @wordpress/scripts kapseln
src/callout/block.jsonBlock-Deklaration: Name, Attribute, Supports, Assets
src/callout/index.jsRuft registerBlockType mit edit und save auf
src/callout/edit.jsUI-Komponente für den Editor
src/callout/save.jsMarkup, das im Beitragsinhalt gespeichert wird
src/callout/style.scssStyles für Frontend und Editor
src/callout/editor.scssStyles nur für den Editor
build/Kompilierte Ausgabe, die WordPress tatsächlich lädt
build/blocks-manifest.phpMetadaten aller Blöcke, zur Build-Zeit generiert

Die PHP-Datei ist ein gewöhnliches Plugin (siehe ein WordPress-Plugin von Grund auf schreiben) mit einem zusätzlichen Registrierungsaufruf. Das zugrunde liegende Konzept ist register_block_type(). Die Funktion nimmt einen Ordner entgegen, der eine block.json enthält. Das Gerüst kapselt dies in einem Batch-Aufruf, der WordPress 6.8 oder neuer voraussetzt. Der generierte Plugin-Header setzt passend dazu Requires at least: 6.8. Eine vereinfachte Version dieses Aufrufs:

add_action( 'init', function () {
	wp_register_block_types_from_metadata_collection(
		__DIR__ . '/build',
		__DIR__ . '/build/blocks-manifest.php'
	);
} );

Der Leitfaden zur Block-Registrierung erläutert beide Varianten.

block.json: Hier wird der Block deklariert

Die Datei block.json ist die zentrale Deklaration eines Blocks. Sowohl der PHP- als auch der JavaScript-Registrierungsaufruf lesen daraus Name, Attribute, Supports und Asset-Pfade. So beschreibt eine einzige Datei den Block sowohl für den Editor als auch für den Server.

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "openreplay/callout",
  "version": "0.1.0",
  "title": "Callout",
  "category": "text",
  "icon": "info",
  "attributes": {
    "content": { "type": "string", "source": "html", "selector": "p" },
    "url": { "type": "string", "default": "" }
  },
  "supports": { "html": false },
  "textdomain": "callout",
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css"
}

name hat die Form namespace/slug. Betrachten Sie diesen Wert als unveränderlich, denn er wird in das Markup jedes Beitrags geschrieben, der den Block verwendet.

apiVersion: 3 wurde mit WordPress 6.3 eingeführt und deklariert, dass der Block im Editor-iframe funktioniert. Seit WordPress 6.9 akzeptiert das Schema nur noch Version 3. Blöcke, die mit einer älteren Version registriert sind, geben bei aktiviertem SCRIPT_DEBUG eine Warnung in der Konsole aus. Ab WordPress 7.1 läuft der Beitragseditor immer in einem iframe, unabhängig davon, welche apiVersion ein Block deklariert. Daher muss jeder Block darin funktionieren.

attributes definieren die Daten des Blocks. type und default funktionieren wie erwartet. source legt fest, wo der Wert gespeichert wird. Die Attribut-Referenz erklärt, dass ein Wert ohne source im Kommentar-Delimiter landet. source: "html" dagegen liest das innere HTML des passenden Elements aus – darauf setzen RichText-Felder in der Regel.

AttributdefinitionSpeicherort des Werts
Kein source (url)JSON im <!-- wp:... -->-Kommentar
source + selector (content)Wird bei jedem Laden aus dem gespeicherten HTML geparst

So sieht der gespeicherte Beitragsinhalt aus (beispielhaft):

<!-- wp:openreplay/callout {"url":"https://example.com"} -->
<div class="wp-block-openreplay-callout"><p>Deploys freeze Friday at noon.</p></div>
<!-- /wp:openreplay/callout -->

supports aktiviert Editor-Funktionen. html: false blendet die Option „Als HTML bearbeiten“ aus. Schlüssel wie align und color fügen ohne zusätzlichen Code Steuerelemente in Werkzeugleiste und Seitenleiste hinzu. Eine Übersicht finden Sie in der Supports-Referenz.

file:-Pfade werden relativ zur registrierten block.json aufgelöst. Das ist die Kopie in build/callout/, die neben den kompilierten Dateien index.js, index.css und style-index.css liegt. Deshalb verweist PHP auf build und nicht auf src. Die Block-Metadaten-Referenz dokumentiert jeden Schlüssel.

edit.js: RichText, InspectorControls und useBlockProps

Die edit-Komponente wird nur im Editor gerendert. Sie erhält attributes und setAttributes als Props. setAttributes erfüllt dieselbe Aufgabe wie ein State-Setter: Es führt die übergebenen Änderungen mit den bestehenden Attributen zusammen und löst ein erneutes Rendern aus.

import { __ } from '@wordpress/i18n';
import { useBlockProps, RichText, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';

export default function Edit( { attributes, setAttributes } ) {
  const { content, url } = attributes;
  return (
    <>
      <InspectorControls>
        <PanelBody title={ __( 'Link', 'callout' ) }>
          <TextControl
            label={ __( 'URL', 'callout' ) }
            value={ url }
            onChange={ ( value ) => setAttributes( { url: value } ) }
          />
        </PanelBody>
      </InspectorControls>
      <div { ...useBlockProps() }>
        <RichText
          tagName="p"
          value={ content }
          onChange={ ( value ) => setAttributes( { content: value } ) }
          placeholder={ __( 'Write the callout…', 'callout' ) }
        />
      </div>
    </>
  );
}

useBlockProps() fügt die Klassennamen und Data-Attribute hinzu, die der Editor für Auswahl und Styling benötigt. Wenden Sie es daher per Spread-Operator auf das äußerste Element an. RichText ermöglicht es Nutzern, Text direkt im Block zu bearbeiten. InspectorControls verschiebt Einstellungen, die nicht inline bearbeitet werden – etwa eine URL –, in die Seitenleiste. PanelBody gruppiert diese Steuerelemente in einem einklappbaren Abschnitt.

save.js und der Block-Validierungsfehler

Die save-Funktion erzeugt das Markup, das im Beitragsinhalt gespeichert wird. Sie muss eine reine Funktion der Attribute sein: kein State, keine Effekte und keine Hooks außer useBlockProps.save().

import { useBlockProps, RichText } from '@wordpress/block-editor';

export default function save( { attributes } ) {
  return (
    <div { ...useBlockProps.save() }>
      <RichText.Content tagName="p" value={ attributes.content } />
    </div>
  );
}

RichText.Content gibt den gespeicherten Wert als reines Markup aus, und useBlockProps.save() fügt im Frontend die Wrapper-Klasse hinzu.

Warum schlägt die Block-Validierung fehl?

Wenn ein Beitrag im Editor geöffnet wird, führt WordPress die aktuelle save-Funktion des Blocks mit den geparsten Attributen aus. Das Ergebnis wird mit dem im Beitrag gespeicherten Markup verglichen. Laut dem Edit-and-Save-Leitfaden gilt jede Abweichung zwischen dieser frisch erzeugten Ausgabe und dem gespeicherten HTML als ungültiger Block. Der Editor zeigt dann „Dieser Block enthält unerwarteten oder ungültigen Inhalt“ an und protokolliert ein Diff in der Browserkonsole.

Eine Änderung der save-Funktion schreibt Beiträge, die den Block bereits enthalten, nicht neu. Angenommen, v2 fügt eine Klasse und einen Link hinzu:

export default function save( { attributes } ) {
  const { content, url } = attributes;
  return (
    <div { ...useBlockProps.save() }>
      <RichText.Content tagName="p" className="callout__body" value={ content } />
      { url && <a href={ url }>Learn more</a> }
    </div>
  );
}

Jeder bestehende Callout schlägt nun bei der Validierung fehl, weil die neue save-Ausgabe nicht mehr mit dem alten gespeicherten Markup übereinstimmt.

Die Lösung: deprecated

Ein deprecated-Eintrag behebt den Block-Validierungsfehler, indem er die vorherigen Attribute und die vorherige save-Funktion vorhält. Optional kann er eine migrate-Funktion enthalten, die alte Attribute auf eine neue Struktur abbildet. Mit diesem Eintrag erkennt der Editor das alte Markup und aktualisiert es beim nächsten Speichern des Beitrags.

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, RichText } from '@wordpress/block-editor';
import Edit from './edit';
import save from './save';
import metadata from './block.json';

const v1 = {
  attributes: {
    content: { type: 'string', source: 'html', selector: 'p' },
    url: { type: 'string', default: '' },
  },
  save( { attributes } ) {
    return (
      <div { ...useBlockProps.save() }>
        <RichText.Content tagName="p" value={ attributes.content } />
      </div>
    );
  },
};

registerBlockType( metadata.name, { edit: Edit, save, deprecated: [ v1 ] } );

Jeder Eintrag bringt seine eigenen attributes mit, da das Parsen von ihnen abhängt. Hätte v2 zusätzlich den Selektor von p auf .callout__body geändert, bräuchte v1 weiterhin selector: 'p', um alte Beiträge korrekt zu lesen.

Die Deprecation-Referenz legt folgende Regeln fest:

  • Einträge werden absteigend aufgelistet, der neueste zuerst.
  • Der Editor prüft jeden Eintrag einzeln gegen das Markup in seinem gespeicherten Zustand. Ein Eintrag gibt sein Ergebnis nie an den nächsten weiter.
  • Stimmt die save-Ausgabe eines Eintrags nicht mit dem gespeicherten HTML überein, ignoriert der Editor diesen Eintrag. Dessen migrate wird dann nie ausgeführt.
  • Mit einer optionalen isEligible-Funktion lässt sich ein Eintrag auch auf einen Block anwenden, der die Validierung bereits besteht.

Drei Gewohnheiten verhindern die meisten Validierungsfehler:

  1. Verwenden Sie in save niemals nicht-deterministische Werte wie Datumsangaben oder zufällige IDs.
  2. Lesen Sie in save niemals State oder Context aus.
  3. Behandeln Sie die save-Ausgabe wie ein Datenbankschema: Jede Änderung erfordert eine Migration.

Was ist ein dynamischer Block?

Ein dynamischer Block gibt aus save den Wert null zurück, speichert nur seine Attribute im Beitragsinhalt und wird bei jeder Anfrage von PHP gerendert. Wählen Sie diesen Ansatz, wenn sich die Ausgabe ändern muss, ohne dass jemand den Beitrag erneut speichert – etwa bei einer Liste der neuesten Einträge.

Statischer BlockDynamischer Block
Markup liegt inBeitragsinhaltPHP-Template
Aktualisierung beiErneutem Speichern des BeitragsJeder Anfrage
save gibt zurückJSXnull
ServerlastKeine über die Auslieferung des Inhalts hinausPHP-Ausführung bei jedem Rendern

Erzeugen Sie das Gerüst mit npx @wordpress/create-block@latest callout --namespace=openreplay --variant dynamic. Alternativ fügen Sie "render": "file:./render.php" selbst zur block.json hinzu. Das folgende Template listet Einträge eines Custom Post Types auf. Es setzt voraus, dass Sie ein count-Attribut vom Typ number hinzugefügt haben:

<?php
$query = new WP_Query( array(
	'post_type'      => 'project',
	'posts_per_page' => isset( $attributes['count'] ) ? (int) $attributes['count'] : 3,
) );
?>
<ul <?php echo get_block_wrapper_attributes(); ?>>
	<?php while ( $query->have_posts() ) : $query->the_post(); ?>
		<li><a href="<?php echo esc_url( get_permalink() ); ?>"><?php echo esc_html( get_the_title() ); ?></a></li>
	<?php endwhile; wp_reset_postdata(); ?>
</ul>

$attributes enthält die geparsten Attribute des Blocks. get_block_wrapper_attributes() ist das PHP-Gegenstück zu useBlockProps. Wenn Sie eine Funktion einer Template-Datei vorziehen, übergeben Sie register_block_type() einen render_callback. Dynamische Blöcke lösen nie Validierungsfehler aus, da der Editor kein gespeichertes Markup zum Vergleichen hat.

Fazit

Ein Block besteht aus einer React-Komponente, einer JSON-Deklaration und einem Vertrag über das gespeicherte Markup. Die Komponente ist für React-Entwickler der einfache Teil. Am Vertrag dagegen scheitern Blöcke: Sobald echte Beiträge Ihren Block enthalten, erfordert jede Änderung an save einen passenden deprecated-Eintrag. Erzeugen Sie den Callout-Block, fügen Sie seiner save-Ausgabe eine Klasse hinzu und beobachten Sie, wie der Fehler in der Konsole erscheint. Schreiben Sie anschließend die Deprecation, die ihn behebt. Wenn Sie diesen Zyklus einmal durchlaufen haben, ist der Fehler wegen ungültigen Inhalts kein Rätsel mehr.

FAQ

Sollte ein eigener Gutenberg-Block in einem Plugin oder in einem Theme liegen?

In einem Plugin. Block-Markup wird im Beitragsinhalt gespeichert, daher muss der Block so lange registriert bleiben, wie Beiträge ihn verwenden. Wird der Code, der ihn registriert, deaktiviert – etwa durch einen Theme-Wechsel –, zeigt der Editor einen Hinweis an, dass die Website diesen Block nicht unterstützt. Er bietet dann an, den Block unverändert zu lassen oder zu entfernen. Ein Plugin hält den Block auch über Theme-Wechsel hinweg verfügbar.

Was bewirkt „Blockwiederherstellung versuchen“ bei einem ungültigen Block?

„Blockwiederherstellung versuchen“ baut den Block mit der aktuellen save-Funktion aus seinen geparsten Attributen neu auf und ersetzt das gespeicherte Markup. Das repariert jeweils nur einen Block in einem Beitrag und verwirft alle Inhalte, die nicht durch die Attribute erfasst werden. Die Funktion umgeht das Problem im Editor, behebt aber nicht den Code. Mit einem deprecated-Eintrag werden dagegen alle bestehenden Beiträge aktualisiert, ohne dass jemand Blöcke manuell wiederherstellen muss.

Wie füge ich demselben Plugin einen zweiten Block hinzu?

Legen Sie unter src einen neuen Ordner mit eigener block.json, index.js, edit.js und save.js an. Die Befehle start und build von @wordpress/scripts durchsuchen src und seine Unterordner nach block.json-Dateien. Jede davon wird als separater Einstiegspunkt in einen entsprechenden Ordner unter build gebaut, und jeder Block wird in das Blocks-Manifest eingetragen, das das Plugin registriert. Starten Sie npm start neu, nachdem Sie den Ordner hinzugefügt haben.

[TOGGLE question=“Kann ein dynamischer Block verschachtelte Blöcke enthalten

We use cookies to improve your experience. By using our site, you accept cookies.