Plugin Ordnername: einhorn-blocks
Namespace: einhorn
einhorn-blocks.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
{
"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
{
"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.
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 aktualisiertsrc 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.
src/block1/block.jsonsrc Ordnerstruktur bei mehreren Blöcken
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.jsonIntro block.json
Einziger Unterschied zwischen static und dynamic block:
- Static →
save.jsliefert das Frontend-HTML - Dynamic →
render.phpliefert das Frontend-HTML,save.jsgibtnullzurück
Sowohl static als auch dynamic blocks können attribures und supports haben.
| Ich will… | Typ |
|---|---|
| Fester Content, vom Nutzer eingetippt | Static |
| Aktuelle Daten aus DB (Posts, User…) | Dynamic |
| WooCommerce Produktliste | Dynamic |
| Nutzer tippt einen Titel ein | Static reicht |
block.json für einfachen static Block
Static Block
- Frontend-HTML wird von
save.jsgeneriert - Wird beim Speichern in der Datenbank gespeichert
- Kein PHP zur Laufzeit nötig
renderist nicht vorhanden
Core Static Blocks
core/paragraphcore/headingcore/imagecore/buttoncore/separator
{
"$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.
// 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,
} );| Datei | Referenziert in |
|---|---|
index.js | block.json → editorScript |
edit.js | index.js |
save.js | index.js |
index.css | block.json → editorStyle |
style-index.css | block.json → style |
block attributes
nutze immer attributes und supports – auch wenn du es gerade nicht brauchst.
{
"$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": {}
}"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| Was | type | Beispiel |
|---|---|---|
| Text eintippen | string | Titel, Beschreibung, Button-Text |
| Zahl eingeben | number | Anzahl Posts, Spalten, Größe |
| An/Aus schalten | boolean | Schatten anzeigen, Link öffnen in neuem Tab |
| Auswahl treffen | string | Layout wählen: "grid" oder "list" |
| Bild auswählen | number | Bild-ID aus der Mediathek |
| URL eingeben | string | Link-Ziel |
| Farbe manuell | string | Hex-Wert #ff0000 |
| Array von Daten | array | Liste 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.
"supports": {
"color": { "background": true, "text": true }
}supports — WordPress UI automatisch:
| Was | supports-Key |
|---|---|
| Hintergrund- & Textfarbe | color |
| Schriftgröße, Zeilenhöhe, Gewicht | typography |
| Margin & Padding | spacing |
| Breite & Höhe | dimensions |
| Ausrichtung (links/mitte/rechts) | align |
| Vollbreite / breite Ausrichtung | alignWide |
| Block umbenennen im Editor | renaming |
| HTML-Anker setzen | anchor |
| Eigene CSS-Klasse | customClassName |
Faustregel:
- Nutzer gibt etwas ein oder wählt etwas →
attributes - 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→ EinstellungfontSize: "large"→ Einstellungcolumns: 3→ EinstellungopenInNewTab: true→ Einstellung
Der eigentliche Inhalt (Text, Überschrift) steht direkt im HTML — nicht in attributes.
<!-- wp:einhorn-blocks/einblock {"text":"Hallo"} -->
<p>Hallo</p>
<!-- /wp:einhorn-blocks/einblock -->supports — WordPress speichert als CSS-Klassen und Inline-Styles im Markup:
<!-- 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.phpzur Laufzeit generiert - Nichts wird in der Datenbank gespeichert (außer Attributwerte)
- PHP läuft bei jedem Seitenaufruf
rendermuss vorhanden sein
save.js wird nicht genutzt in dynamic block
// save.js bei dynamic block
export default function save() {
return null;
}Alles andere — attributes, supports, edit.js — ist bei beiden identisch.
{
"$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
/* 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-postscore/navigationcore/querycore/site-titlecore/template-part
{
"$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:
nametitleeditorScript
Praktisch immer sinnvoll:
$schema— Autocomplete in VS CodeapiVersion— immer3für WP 6.3+style— Frontend-CSS
Optional / situativ:
version— nur relevant für Cache-Bustingcategory— ohne landet der Block in „Ohne Kategorie“icon– ohne erscheint einfach Standardicondescription— nur für den Editor-Insertertextdomain— nur bei ÜbersetzungeneditorStyle— nur wenn du Editor-only CSS brauchstattributes— nur wenn Nutzer Einstellungen speichertsupports— nur wenn du WP-UI-Features willstrender— 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.
<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
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 siehtblock.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 ausindex.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 siehstrender.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.