Block Plugin minimal Setup

Plugin Ordnername: einhorn-blocks
Namespace: einhorn

einhorn-blocks.php

PHP
<?php
/**
 * Plugin Name: Einhorn Blocks
 */

defined( 'ABSPATH' ) || exit;

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

Minimale package.json

JSON
{
  "name": "einhorn-blocks",
  "version": "1.0.0",
  "scripts": {
    "start": "wp-scripts start --blocks-manifest",
    "build": "wp-scripts build --blocks-manifest"
  },
  "devDependencies": {
    "@wordpress/scripts": "^30.14.0"
  }
}

Empfohlene package.json

JSON
{
  "name": "einhorn-blocks",
  "version": "1.0.0",
  "scripts": {
    "start": "wp-scripts start --blocks-manifest",
    "build": "wp-scripts build --blocks-manifest",
    "lint:js": "wp-scripts lint-js",
    "lint:css": "wp-scripts lint-style",
    "format": "wp-scripts format",
    "packages-update": "wp-scripts packages-update"
  },
  "devDependencies": {
    "@wordpress/scripts": "^30.14.0"
  }
}

lint, format und packages-update machen Sinn – wenn du es sauber machen willst.

Bash
npm run start # live watch modus während entwicklung - generiert on the fly
npm run build # generiert build assets - vor live push
npm run lint:js # js fehler werden angezeigt
npm run lint:css # css/scss fehler werden angezeigt
npm run format # wp standard formatierungen in allen src files
npm run packages-update # alle packages werden aktualisiert

src Ordnerstruktur anlegen bei nur einem Block

block1 kann frei benannt werden, weil in block.json der Befehl name eindeutig gesetzt wird. Auch wenn andere Plugins Blöcke als block1 registrieren, passiert so nichts. Damit alles sauber funktioniert, immer alles in Ordner in src schieben und nicht in oberster Ebene lassen.

Bash
src/block1/block.json

src Ordnerstruktur bei mehreren Blöcken

Bash
src/block1/block.json
src/block2/block.json
src/block3/block.json

#oder veständlicher
src/hero/block.json
src/card/block.json
src/slider/block.json

Intro block.json

Einziger Unterschied zwischen static und dynamic block:

  • Staticsave.js liefert das Frontend-HTML
  • Dynamicrender.php liefert das Frontend-HTML, save.js gibt null zurück

Sowohl static als auch dynamic blocks können attribures und supports haben.

Ich will…Typ
Fester Content, vom Nutzer eingetipptStatic
Aktuelle Daten aus DB (Posts, User…)Dynamic
WooCommerce ProduktlisteDynamic
Nutzer tippt einen Titel einStatic reicht

block.json für einfachen static Block

Static Block

  • Frontend-HTML wird von save.js generiert
  • Wird beim Speichern in der Datenbank gespeichert
  • Kein PHP zur Laufzeit nötig
  • render ist nicht vorhanden

Core Static Blocks

  • core/paragraph
  • core/heading
  • core/image
  • core/button
  • core/separator
JSON
{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "einhorn-blocks/einblock",
  "version": "1.0.0",
  "title": "Einblock",
  "category": "text",
  "description": "",
  "textdomain": "einhorn-blocks",
  "editorScript": "file:./index.js",
  "editorStyle":  "file:./index.css",
  "style":        "file:./style-index.css",
  "attributes": {
    "text": { "type": "string", "default": "Hallo" }
  },
  "supports": {
    "color": { "background": true, "text": true }
  }
}

Mit dem Minimal-Blueprint kannst du:

  • Einen Block im Editor einfügen
  • Statisches HTML im Editor anzeigen (edit.js)
  • Statisches HTML im Frontend ausgeben (save.js)
  • Editor-only Styles und Frontend-Styles laden

Das reicht für rein visuelle, nicht-editierbare Blöcke. Z.B. ein festes Banner, ein Trenner, eine Dekoration. Wenn du aber mehr Optionen willst, musst du attributes und supports hinzufügen.

save.js wird in block.json nicht referenziert. Das wird via index.js reingezogen.

save.js wird direkt in index.js importiert und an registerBlockType übergeben:

WordPress kennt edit und save nur über registerBlockType — nicht über block.json.

JavaScript
// index.js
import { registerBlockType } from '@wordpress/blocks';
import Edit from './edit';
import save from './save';
import metadata from './block.json';

registerBlockType( metadata, {
    edit: Edit,
    save,
} );
DateiReferenziert in
index.jsblock.jsoneditorScript
edit.jsindex.js
save.jsindex.js
index.cssblock.jsoneditorStyle
style-index.cssblock.jsonstyle

block attributes

nutze immer attributes und supports – auch wenn du es gerade nicht brauchst.

JavaScript
{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "einhorn-blocks/einblock",
  "version": "1.0.0",
  "title": "Einblock",
  "category": "text",
  "description": "",
  "textdomain": "einhorn-blocks",
  "editorScript": "file:./index.js",
  "editorStyle":  "file:./index.css",
  "style":        "file:./style-index.css",
  "attributes": {},
  "supports": {}
}
JavaScript
"attributes": {
  "text": { "type": "string", "default": "Hallo" }
}
```

In `edit.js` bekommt der Nutzer ein Eingabefeld — was er tippt wird in `attributes.text` gespeichert. `save.js` liest `attributes.text` aus und gibt es als HTML aus. Das landet dann in der Datenbank.

---

**Konkret bei deinem Static Block:**
```
Nutzer tippt "Willkommen" ins Textfeld

attributes.text = "Willkommen"

save.js gibt <p>Willkommen</p> aus

wird so in der Datenbank gespeichert
WastypeBeispiel
Text eintippenstringTitel, Beschreibung, Button-Text
Zahl eingebennumberAnzahl Posts, Spalten, Größe
An/Aus schaltenbooleanSchatten anzeigen, Link öffnen in neuem Tab
Auswahl treffenstringLayout wählen: "grid" oder "list"
Bild auswählennumberBild-ID aus der Mediathek
URL eingebenstringLink-Ziel
Farbe manuellstringHex-Wert #ff0000
Array von DatenarrayListe von Elementen

supports

WordPress fügt automatisch UI-Felder in der Sidebar ein UND speichert die Werte selbst — du musst dafür keine eigenen attributes definieren.

supports = WordPress macht alles für dich. attributes = du baust es selbst.

Füge bei jedem block attributes und supports hinzu, immer – auch wenn die leer bleiben.

JavaScript
"supports": {
  "color": { "background": true, "text": true }
}

supports — WordPress UI automatisch:

Wassupports-Key
Hintergrund- & Textfarbecolor
Schriftgröße, Zeilenhöhe, Gewichttypography
Margin & Paddingspacing
Breite & Höhedimensions
Ausrichtung (links/mitte/rechts)align
Vollbreite / breite AusrichtungalignWide
Block umbenennen im Editorrenaming
HTML-Anker setzenanchor
Eigene CSS-KlassecustomClassName

Faustregel:

  • Nutzer gibt etwas ein oder wählt etwasattributes
  • WordPress soll UI automatisch bereitstellen → supports

Erst fragen: Gibt es das schon in supports? → Ja → supports nutzen, kein extra Code → Nein → attributes selbst bauen

Spart enorm viel Arbeit. WordPress baut dir Farbe, Spacing, Typo komplett fertig — inklusive UI, Speicherung und CSS-Output. Das selbst nachzubauen wäre sinnlos.

Beides landet in der Datenbank — aber unterschiedlich:

attributes — du speicherst direkt im Block-Markup:

Attributes = Einstellungen des Blocks

  • dropCap: true → Einstellung
  • fontSize: "large" → Einstellung
  • columns: 3 → Einstellung
  • openInNewTab: true → Einstellung

Der eigentliche Inhalt (Text, Überschrift) steht direkt im HTML — nicht in attributes.

HTML
<!-- wp:einhorn-blocks/einblock {"text":"Hallo"} -->
<p>Hallo</p>
<!-- /wp:einhorn-blocks/einblock -->

supports — WordPress speichert als CSS-Klassen und Inline-Styles im Markup:

HTML
<!-- wp:einhorn-blocks/einblock -->
<p class="has-background" style="background-color:#ff0000">Hallo</p>
<!-- /wp:einhorn-blocks/einblock -->

Beides landet als Post-Content in wp_posts.post_content — nicht in separaten Tabellen. WordPress speichert keine Block-Daten in eigenen Tabellen.

block.json für dynamic Block

Dynamic Block

  • Frontend-HTML wird von render.php zur Laufzeit generiert
  • Nichts wird in der Datenbank gespeichert (außer Attributwerte)
  • PHP läuft bei jedem Seitenaufruf
  • render muss vorhanden sein

save.js wird nicht genutzt in dynamic block

JavaScript
// save.js bei dynamic block
export default function save() {
    return null;
}

Alles andere — attributes, supports, edit.js — ist bei beiden identisch.

JSON
{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "einhorn-blocks/einblock",
  "version": "1.0.0",
  "title": "Einblock",
  "category": "text",
  "description": "",
  "textdomain": "einhorn-blocks",
  "editorScript": "file:./index.js",
  "editorStyle":  "file:./index.css",
  "style":        "file:./style-index.css",
  "attributes": {},
  "supports": {},
  "render": "file:./render.php"
}

Einziger Utnterschied zu static block.
„render“: „file:./render.php“

save.js auslassen nur so in dynamic block

JavaScript
/* index.js */
import { registerBlockType } from '@wordpress/blocks';
import Edit from './edit';
import metadata from './block.json';

registerBlockType( metadata, {
    edit: Edit,
    save: () => null,
} );

Dynamic Core Blocks

  • core/latest-posts
  • core/navigation
  • core/query
  • core/site-title
  • core/template-part
JSON
{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "einhorn-blocks/einblock",
  "version": "1.0.0",
  "title": "Einblock",
  "category": "text",
  "icon": "star-filled",
  "description": "",
  "textdomain": "einhorn-blocks",
  "editorScript": "file:./index.js",
  "editorStyle":  "file:./index.css",
  "style":        "file:./style-index.css",
  "attributes": {},
  "supports": {},
  "render": "file:./render.php"
}

Pflicht:

  • name
  • title
  • editorScript

Praktisch immer sinnvoll:

  • $schema — Autocomplete in VS Code
  • apiVersion — immer 3 für WP 6.3+
  • style — Frontend-CSS

Optional / situativ:

  • version — nur relevant für Cache-Busting
  • category — ohne landet der Block in „Ohne Kategorie“
  • icon – ohne erscheint einfach Standardicon
  • description — nur für den Editor-Inserter
  • textdomain — nur bei Übersetzungen
  • editorStyle — nur wenn du Editor-only CSS brauchst
  • attributes — nur wenn Nutzer Einstellungen speichert
  • supports — nur wenn du WP-UI-Features willst
  • render — nur bei Dynamic Block

render.php+

get_block_wrapper_attributes() ist Pflicht in render.php — das fügt automatisch die WordPress Block-Klassen und supports-Styles ein.

PHP
<div <?php echo get_block_wrapper_attributes(); ?>>
    <p>File Download Block  Frontend</p>
</div>

Dynamic Block save.js löschen

Weil du save: () => null direkt in index.js inline hast, brauchst du keine separate save.js Datei. Die kann weg.

src Ordner und Datenfluss

Bash
src/file-download/
├── block.json      Konfiguration: Name, Kategorie, Attribute, Supports
├── index.js        Registrierung: verbindet block.json mit edit.js
├── edit.js         Editor-Ansicht: was der Nutzer im Backend sieht
└── render.php      Frontend-Ausgabe: was der Besucher auf der Seite sieht
Bash
block.json wird geladen  Block erscheint im Inserter
        
Nutzer fügt Block ein  edit.js wird angezeigt
        
Nutzer ändert etwas  attributes werden aktualisiert
        
edit.js reagiert live auf attributes  Vorschau im Editor
        
Nutzer speichert  attributes landen in der Datenbank
        
Besucher lädt Seite  render.php liest attributes  gibt HTML aus

index.js und block.json sind immer gleich — die änderst du kaum. edit.js und render.php sind deine eigentliche Arbeit.

Editor-Ansicht ist immer edit.js — nicht render.php. Die beiden sind strikt getrennt:

  • edit.js → was du im Backend/Editor siehst
  • render.php → was der Besucher im Frontend sieht

Wichtig: render.php läuft nie im Editor — nur im Frontend. Was du im Editor siehst ist immer edit.js. Du baust beide oft ähnlich, aber sie sind zwei separate Dateien.

Sieh dir hier den weiteren Workflow an.