Edytor Bloków (ang. Block Editor, znany też jako Gutenberg) to dziś serce WordPressa. Każda nowoczesna strona opiera się na blokach — a jako deweloper/freelancer WordPress masz dostęp do jednego z najbardziej rozbudowanych interfejsów programistycznych w ekosystemie PHP i JavaScript. Tworzenie własnych bloków Gutenberg to umiejętność, która radykalnie rozszerza to, co możesz zaoferować klientom: precyzyjne komponenty edytora dopasowane do ich konkretnych potrzeb, zamiast ogólnych rozwiązań ze sklepu z wtyczkami.
Ten przewodnik przeprowadzi Cię przez cały proces — od konfiguracji środowiska programistycznego, przez interfejs API bloków Gutenberga, wzory bloków i bloki wielokrotnego użytku, aż po profesjonalny przepływ pracy programistycznej. Każda sekcja zawiera gotowy do uruchomienia kod.
Czego potrzebujesz przed rozpoczęciem?
Tworzenie własnych bloków Gutenberg wymaga podstawowej znajomości:
- PHP — rejestracja bloków, renderowanie po stronie serwera
- JavaScript / React — interfejs edytora (panel administracyjny)
- Node.js i menadżer pakietów npm (ang. Node Package Manager) — zarządzanie zależnościami i proces kompilacji
- JSX — rozszerzenie składni JavaScript używane przez bibliotekę React
Minimalne wersje: Node.js 18+, WordPress 6.1+, PHP 8.0+.
Filar 1: Interfejs programistyczny Gutenberga (Gutenberg Block API)
Architektura bloku — co składa się na własny blok?
Własny blok Gutenberg składa się z kilku ściśle ze sobą współpracujących plików. Zrozumienie roli każdego z nich to fundament tworzenia własnych bloków Gutenberg.
moj-plugin/
├── moj-plugin.php ← główny plik wtyczki (PHP)
├── package.json ← konfiguracja menadżera pakietów
├── src/ ← kod źródłowy (przed kompilacją)
│ ├── block.json ← plik konfiguracyjny bloku
│ ├── index.js ← punkt wejścia, rejestracja bloku
│ ├── edit.js ← interfejs edytora (co widzi redaktor)
│ ├── save.js ← funkcja zapisu (statyczny HTML)
│ ├── render.php ← renderowanie po stronie serwera (bloki dynamiczne)
│ ├── editor.scss ← style panelu administracyjnego
│ └── style.scss ← style publicznej wersji strony
└── build/ ← skompilowany kod (generowany automatycznie)
Szybki start — narzędzie create-block
Zamiast konfigurować wszystko ręcznie, skorzystaj z oficjalnego narzędzia @wordpress/create-block (narzędzie do rusztowania bloków WordPress), które generuje kompletną strukturę projektu:
# Utwórz nowy blok — jedno polecenie zastępuje godzinę konfiguracji
npx @wordpress/create-block@latest moj-blok --variant=dynamic
# Przejdź do katalogu wtyczki
cd moj-blok
# Uruchom tryb obserwowania (ang. watch mode) — kompilacja przy każdej zmianie
npm start
Przełącznik --variant=dynamic generuje blok dynamiczny (renderowany przez PHP), co jest zalecanym podejściem dla większości produkcyjnych bloków.
Plik konfiguracyjny bloku — block.json
Plik block.json to centrum dowodzenia własnego bloku. Od WordPress 5.8 jest to zalecany i jednocześnie najbardziej wydajny sposób rejestracji bloków — WordPress buforuje (ang. caches — zapisuje w pamięci podręcznej) metadane z tego pliku, co przyspiesza ładowanie strony.
{
"$schema": "<https://schemas.wp.org/trunk/block.json>",
"apiVersion": 3,
"name": "moj-plugin/karta-uslugi",
"version": "1.0.0",
"title": "Karta usługi",
"category": "design",
"icon": "star-filled",
"description": "Elegancki blok prezentujący pojedynczą usługę z tytułem, opisem i przyciskiem.",
"keywords": ["usługa", "karta", "oferta", "cta"],
"textdomain": "moj-plugin",
"supports": {
"html": false,
"align": ["wide", "full"],
"spacing": {
"margin": true,
"padding": true,
"blockGap": true
},
"color": {
"background": true,
"text": true,
"gradients": true
},
"typography": {
"fontSize": true,
"lineHeight": true
}
},
"attributes": {
"tytul": {
"type": "string",
"default": ""
},
"opis": {
"type": "string",
"default": ""
},
"tekstPrzycisku": {
"type": "string",
"default": "Dowiedz się więcej"
},
"pokazPrzycisk": {
"type": "boolean",
"default": true
},
"kolorTla": {
"type": "string",
"default": "#ffffff"
}
},
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"render": "file:./render.php",
"viewScript": "file:./view.js"
}
Kluczowe sekcje pliku konfiguracyjnego:
supports— deklaruje, które wbudowane funkcje WordPress blok obsługuje. Korzystaj z nich zamiast pisać własne kontrolki — dostajesz za darmo integrację z globalnym stylem motywu (ang. theme.json)attributes— dane przechowywane w bloku. Każdy atrybut ma typ i wartość domyślnąrender— wskazuje plik PHP do renderowania (tylko bloki dynamiczne)
Rejestracja bloku po stronie PHP
<?php
/**
* Plugin Name: Mój Plugin z Blokami
* Description: Własne bloki Gutenberg dla projektu.
* Version: 1.0.0
* Requires at least: 6.1
* Requires PHP: 8.0
* Text Domain: moj-plugin
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Wyjście jeśli dostęp bezpośredni
}
/**
* Rejestruje wszystkie bloki z katalogu build/.
* WordPress automatycznie odczyta block.json z każdego podkatalogu.
*/
function moj_plugin_rejestruj_bloki(): void {
$katalog_budowania = __DIR__ . '/build';
// Rejestruje wszystkie bloki znalezione w katalogu build/
foreach ( glob( $katalog_budowania . '/*/block.json' ) as $plik_konfiguracyjny ) {
register_block_type( dirname( $plik_konfiguracyjny ) );
}
}
add_action( 'init', 'moj_plugin_rejestruj_bloki' );
Interfejs edytora — plik edit.js
Funkcja edycji (ang. edit function) definiuje, co deweloper i redaktor widzą w panelu administracyjnym podczas pracy z blokiem.
// src/edit.js
import { __ } from '@wordpress/i18n';
import {
useBlockProps,
RichText, // Tekst Sformatowany
InspectorControls, // Kontrolki Inspektora (panel boczny)
BlockControls, // Kontrolki Bloku (pasek narzędzi)
PanelColorSettings,// Ustawienia Kolorów Panelu
} from '@wordpress/block-editor';
import {
PanelBody, // Treść Panelu
ToggleControl, // Kontrolka Przełącznika
TextControl, // Kontrolka Tekstu
ToolbarGroup, // Grupa Paska Narzędzi
ToolbarButton, // Przycisk Paska Narzędzi
} from '@wordpress/components';
import { useState } from '@wordpress/element';
import './editor.scss';
export default function Edit( { attributes, setAttributes } ) {
const {
tytul,
opis,
tekstPrzycisku,
pokazPrzycisk,
kolorTla,
} = attributes;
// Hook właściwości bloku (ang. useBlockProps) — dodaje wymagane klasy CSS i atrybuty HTML
const wlasciwosciBloku = useBlockProps( {
style: { backgroundColor: kolorTla },
className: 'karta-uslugi',
} );
return (
<>
{/* ── Panel boczny (Inspektor) ─────────────────────────── */}
<InspectorControls>
<PanelBody
title={ __( 'Ustawienia bloku', 'moj-plugin' ) }
initialOpen={ true }
>
<ToggleControl
label={ __( 'Pokaż przycisk', 'moj-plugin' ) }
help={ pokazPrzycisk
? __( 'Przycisk jest widoczny.', 'moj-plugin' )
: __( 'Przycisk jest ukryty.', 'moj-plugin' )
}
checked={ pokazPrzycisk }
onChange={ ( wartosc ) =>
setAttributes( { pokazPrzycisk: wartosc } )
}
/>
{ pokazPrzycisk && (
<TextControl
label={ __( 'Tekst przycisku', 'moj-plugin' ) }
value={ tekstPrzycisku }
onChange={ ( wartosc ) =>
setAttributes( { tekstPrzycisku: wartosc } )
}
/>
) }
</PanelBody>
<PanelColorSettings
title={ __( 'Ustawienia koloru', 'moj-plugin' ) }
colorSettings={ [
{
value: kolorTla,
onChange: ( wartosc ) =>
setAttributes( { kolorTla: wartosc } ),
label: __( 'Kolor tła', 'moj-plugin' ),
},
] }
/>
</InspectorControls>
{/* ── Widok edytora ────────────────────────────────────── */}
<div { ...wlasciwosciBloku }>
<RichText
tagName="h3"
className="karta-uslugi__tytul"
value={ tytul }
onChange={ ( wartosc ) =>
setAttributes( { tytul: wartosc } )
}
placeholder={ __( 'Wpisz tytuł usługi…', 'moj-plugin' ) }
allowedFormats={ [ 'core/bold', 'core/italic' ] }
/>
<RichText
tagName="p"
className="karta-uslugi__opis"
value={ opis }
onChange={ ( wartosc ) =>
setAttributes( { opis: wartosc } )
}
placeholder={ __( 'Opisz usługę…', 'moj-plugin' ) }
/>
{ pokazPrzycisk && (
<div className="karta-uslugi__cta">
<RichText
tagName="a"
className="karta-uslugi__przycisk"
value={ tekstPrzycisku }
onChange={ ( wartosc ) =>
setAttributes( { tekstPrzycisku: wartosc } )
}
allowedFormats={ [] }
/>
</div>
) }
</div>
</>
);
}
Renderowanie po stronie serwera — render.php
Bloki dynamiczne (ang. dynamic blocks) renderują swój kod HTML przy każdym żądaniu strony — dzięki temu zawsze wyświetlają aktualne dane (np. listę wpisów, produkty ze sklepu).
<?php
// src/render.php
// Zmienne dostępne automatycznie przez WordPress:
// $attributes — tablica atrybutów bloku
// $content — zawartość bloków wewnętrznych (jeśli używasz InnerBlocks)
// $block — obiekt klasy WP_Block
$tytul = $attributes['tytul'] ?? '';
$opis = $attributes['opis'] ?? '';
$tekst_przycisku = $attributes['tekstPrzycisku'] ?? __( 'Dowiedz się więcej', 'moj-plugin' );
$pokaz_przycisk = $attributes['pokazPrzycisk'] ?? true;
$kolor_tla = $attributes['kolorTla'] ?? '#ffffff';
// get_block_wrapper_attributes() — generuje wymagane atrybuty HTML bloku,
// uwzględniając klasy CSS, style i dane z block supports
$atrybuty_otoki = get_block_wrapper_attributes( [
'class' => 'karta-uslugi',
'style' => sprintf( 'background-color: %s;', esc_attr( $kolor_tla ) ),
] );
if ( empty( $tytul ) && empty( $opis ) ) {
return; // Nie renderuj pustego bloku
}
?>
<div <?php echo $atrybuty_otoki; ?>>
<?php if ( $tytul ) : ?>
<h3 class="karta-uslugi__tytul">
<?php echo wp_kses_post( $tytul ); ?>
</h3>
<?php endif; ?>
<?php if ( $opis ) : ?>
<p class="karta-uslugi__opis">
<?php echo wp_kses_post( $opis ); ?>
</p>
<?php endif; ?>
<?php if ( $pokaz_przycisk && $tekst_przycisku ) : ?>
<div class="karta-uslugi__cta">
<a class="karta-uslugi__przycisk wp-element-button"
href="#"
role="button">
<?php echo esc_html( $tekst_przycisku ); ?>
</a>
</div>
<?php endif; ?>
</div>
Dlaczego blok dynamiczny zamiast statycznego?
Blok statyczny (plik save.js) zapisuje wygenerowany HTML bezpośrednio w bazie danych. Gdy zmienisz strukturę HTML bloku, WordPress zgłosi błąd walidacji przy wszystkich wcześniej zapisanych blokach — i wymagać będzie ręcznego odzyskania każdego z nich. Blok dynamiczny (render.php) zawsze generuje świeży HTML — zmieniasz PHP, zmiany natychmiast widoczne wszędzie, bez żadnych błędów walidacji.
Bloki wewnętrzne — zagnieżdżanie bloków (InnerBlocks)
Składnik Bloków Wewnętrznych (ang. InnerBlocks) pozwala na zagnieżdżanie bloków wewnątrz własnego bloku — tworząc struktury kontenerowe (sekcje, kolumny, karty).
// src/edit.js — fragment dla bloku kontenerowego
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
// Wzorzec dozwolonych bloków wewnętrznych
const DOZWOLONE_BLOKI = [
'core/paragraph',
'core/heading',
'core/image',
'moj-plugin/karta-uslugi',
];
// Szablon startowy — co pojawia się w bloku przy pierwszym wstawieniu
const SZABLON = [
[ 'core/heading', { level: 2, placeholder: 'Tytuł sekcji' } ],
[ 'core/paragraph', { placeholder: 'Opis sekcji…' } ],
];
export default function Edit() {
const wlasciwosciBloku = useBlockProps();
return (
<div { ...wlasciwosciBloku }>
<InnerBlocks
allowedBlocks={ DOZWOLONE_BLOKI }
template={ SZABLON }
templateLock={ false } // false = redaktor może dowolnie zmieniać układ
/>
</div>
);
}
<?php
// render.php — renderowanie bloków wewnętrznych
$atrybuty_otoki = get_block_wrapper_attributes();
?>
<div <?php echo $atrybuty_otoki; ?>>
<?php
// $content zawiera już wyrenderowany HTML bloków wewnętrznych
echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
?>
</div>
Filar 2: Wzory Bloków (Block Patterns) — gotowe układy do wielokrotnego użycia
Wzory Bloków (ang. Block Patterns) to predefiniowane układy bloków, które redaktor może wstawić jednym kliknięciem z biblioteki wzorów. To doskonały sposób na zapewnienie spójności treści i przyspieszenie pracy redakcyjnej — zamiast budować każdą sekcję od zera, redaktor wybiera gotowy, zaprojektowany układ.
Rejestracja wzoru bloków przez PHP
<?php
// Rejestracja kategorii wzorów
function moj_plugin_kategorie_wzorow(): void {
register_block_pattern_category(
'moj-plugin',
[
'label' => __( 'Moje wzory', 'moj-plugin' ),
'description' => __( 'Predefiniowane układy dla tego projektu.', 'moj-plugin' ),
]
);
}
add_action( 'init', 'moj_plugin_kategorie_wzorow' );
// Rejestracja wzoru
function moj_plugin_zarejestruj_wzory(): void {
register_block_pattern(
'moj-plugin/sekcja-uslugi',
[
'title' => __( 'Sekcja z trzema usługami', 'moj-plugin' ),
'description' => __( 'Trzy karty usług w układzie trójkolumnowym.', 'moj-plugin' ),
'categories' => [ 'moj-plugin', 'featured' ],
'keywords' => [ 'usługi', 'karty', 'kolumny', 'oferta' ],
'blockTypes' => [ 'core/group' ], // Sugeruje wzór przy wstawianiu tego bloku
'content' => moj_plugin_pobierz_wzor( 'sekcja-uslugi' ),
]
);
}
add_action( 'init', 'moj_plugin_zarejestruj_wzory' );
// Pomocnicza funkcja ładująca treść wzoru z osobnego pliku
function moj_plugin_pobierz_wzor( string $nazwa_wzoru ): string {
$plik = plugin_dir_path( __FILE__ ) . "patterns/{$nazwa_wzoru}.php";
if ( ! file_exists( $plik ) ) {
return '';
}
ob_start();
include $plik;
return ob_get_clean();
}
Wzór jako osobny plik PHP — patterns/sekcja-uslugi.php
Przechowywanie wzorów w osobnych plikach (zamiast jako ciągi znaków PHP) znacznie ułatwia ich edycję i utrzymanie.
<?php
// patterns/sekcja-uslugi.php
// Uwaga: to jest treść wzoru bloków w formacie komentarzy bloku WordPress (ang. block markup)
?>
<!-- wp:group {"className":"sekcja-uslugi","layout":{"type":"constrained"}} -->
<div class="wp-block-group sekcja-uslugi">
<!-- wp:heading {"textAlign":"center","level":2} -->
<h2 class="wp-block-heading has-text-align-center">Nasze usługi</h2>
<!-- /wp:heading -->
<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">
Oferujemy kompleksowe rozwiązania dopasowane do Twoich potrzeb.
</p>
<!-- /wp:paragraph -->
<!-- wp:columns {"isStackedOnMobile":true} -->
<div class="wp-block-columns is-not-stacked-on-mobile">
<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:moj-plugin/karta-uslugi {"tytul":"Projektowanie stron","opis":"Tworzymy nowoczesne strony internetowe.","tekstPrzycisku":"Dowiedz się więcej"} /-->
</div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:moj-plugin/karta-uslugi {"tytul":"Sklepy internetowe","opis":"Wdrażamy sklepy WooCommerce.","tekstPrzycisku":"Zobacz ofertę"} /-->
</div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:moj-plugin/karta-uslugi {"tytul":"Obsługa techniczna","opis":"Dbamy o Twoją stronę przez całą dobę.","tekstPrzycisku":"Sprawdź pakiety"} /-->
</div>
<!-- /wp:column -->
</div>
<!-- /wp:columns -->
</div>
<!-- /wp:group -->
Wzory z plikami PHP — automatyczna rejestracja (WordPress 6.0+)
Od WordPress 6.0 wtyczki mogą rejestrować wzory przez pliki PHP w katalogu patterns/ z nagłówkiem metadanych — bez pisania kodu rejestrującego:
<?php
/**
* Title: Sekcja z ofertą
* Slug: moj-plugin/oferta
* Description: Sekcja prezentująca ofertę z nagłówkiem i kartami.
* Categories: moj-plugin, featured
* Keywords: oferta, usługi, sekcja
* Block Types: core/group
* Post Types: page
* Inserter: true
*/
?>
<!-- wp:group ... -->
...
<!-- /wp:group -->
WordPress automatycznie odnajdzie ten plik i zarejestruje wzór — zero dodatkowego kodu PHP.
Wzory bloków zarejestrowane przez JavaScript
Wzory można rejestrować również po stronie JavaScript — przydatne gdy wzór korzysta z danych dostępnych tylko w kontekście edytora:
// src/patterns/index.js
import { registerBlockPattern, registerBlockPatternCategory } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
// Rejestracja kategorii
registerBlockPatternCategory( 'moj-plugin', {
label: __( 'Moje wzory', 'moj-plugin' ),
} );
// Rejestracja wzoru
registerBlockPattern( 'moj-plugin/hero-prosty', {
title: __( 'Prosty hero', 'moj-plugin' ),
description: __( 'Sekcja hero z nagłówkiem, opisem i przyciskiem.', 'moj-plugin' ),
categories: [ 'moj-plugin', 'banner' ],
content: `
<!-- wp:cover {"overlayColor":"black","minHeight":400} -->
<div class="wp-block-cover" style="min-height:400px">
<div class="wp-block-cover__inner-container">
<!-- wp:heading {"textAlign":"center","style":{"color":{"text":"#ffffff"}}} -->
<h2 class="has-text-align-center has-text-color" style="color:#ffffff">
Twój nagłówek
</h2>
<!-- /wp:heading -->
<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
<div class="wp-block-buttons">
<!-- wp:button -->
<div class="wp-block-button">
<a class="wp-block-button__link wp-element-button">Zacznij teraz</a>
</div>
<!-- /wp:button -->
</div>
<!-- /wp:buttons -->
</div>
</div>
<!-- /wp:cover -->
`,
} );
Filar 3: Bloki Wielokrotnego Użytku i Zsynchronizowane Wzory
Ewolucja koncepcji — od Bloków Wielokrotnego Użytku do Zsynchronizowanych Wzorów
W WordPress 6.3 dokonała się ważna zmiana nazewnictwa i funkcjonalna rozbudowa tej koncepcji:
- WordPress < 6.3 — Bloki Wielokrotnego Użytku (ang. Reusable Blocks): bloki, które redaktor mógł zapisać i wstawić w wielu miejscach; zmiana w jednym miejscu automatycznie aktualizowała wszystkie wystąpienia
- WordPress ≥ 6.3 — Zsynchronizowane Wzory (ang. Synced Patterns): rozbudowana wersja tej samej koncepcji, teraz wbudowana w interfejs Wzorów Bloków
Jak działają Zsynchronizowane Wzory?
Zsynchronizowany Wzór to specjalny typ wpisu (ang. custom post type) o nazwie wp_block, przechowywany w bazie danych. Gdy redaktor wstawia go na wielu stronach i zmienia jego treść — zmiana propaguje się automatycznie we wszystkich miejscach.
Analogia: wyobraź sobie „baner promocyjny” używany na 30 podstronach. Bez zsynchronizowanego wzoru — aktualizujesz go 30 razy. Z zsynchronizowanym wzorem — aktualizujesz go raz.
Programowe tworzenie Zsynchronizowanych Wzorów
<?php
/**
* Programowo tworzy Zsynchronizowany Wzór przy aktywacji wtyczki.
* Przydatne gdy chcesz dostarczyć gotowe wzory przy instalacji.
*/
function moj_plugin_utworz_zsynchronizowany_wzor(): void {
// Sprawdź, czy wzór już istnieje
$istniejacy = get_posts( [
'post_type' => 'wp_block',
'post_status' => 'publish',
'posts_per_page' => 1,
'title' => 'Baner promocyjny',
] );
if ( ! empty( $istniejacy ) ) {
return; // Wzór już istnieje — nie twórz duplikatu
}
$id_wzoru = wp_insert_post( [
'post_title' => 'Baner promocyjny',
'post_content' => '<!-- wp:group {"className":"baner-promocyjny","style":{"color":{"background":"#f0f4ff"}}} -->
<div class="wp-block-group baner-promocyjny" style="background-color:#f0f4ff">
<!-- wp:heading {"textAlign":"center"} -->
<h2 class="wp-block-heading has-text-align-center">🎉 Specjalna oferta</h2>
<!-- /wp:heading -->
<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">Skorzystaj z 20% zniżki na wszystkie usługi do końca miesiąca.</p>
<!-- /wp:paragraph -->
</div>
<!-- /wp:group -->',
'post_status' => 'publish',
'post_type' => 'wp_block',
'post_author' => 1,
] );
// Oznacz jako zsynchronizowany (wymagane od WordPress 6.3)
if ( $id_wzoru && ! is_wp_error( $id_wzoru ) ) {
update_post_meta( $id_wzoru, 'wp_pattern_sync_status', '' ); // pusty ciąg = zsynchronizowany
}
}
register_activation_hook( __FILE__, 'moj_plugin_utworz_zsynchronizowany_wzor' );
Wyłączanie synchronizacji — Niezsynchornizowane Wzory
Czasem chcesz wzoru, który redaktor może wstawić jako punkt wyjścia — ale zmieniać niezależnie w każdym miejscu. To Niesynchronizowany Wzór (ang. Unsynced Pattern):
<?php
// Rejestracja Niesynchronizowanego Wzoru (tylko przez PHP, nie przez wp_block)
register_block_pattern(
'moj-plugin/szablon-strony-uslugi',
[
'title' => __( 'Szablon strony usługi', 'moj-plugin' ),
'categories' => [ 'moj-plugin' ],
// Brak 'templateTypes' = wzór niesynchronizowany — każda kopia niezależna
'content' => '<!-- wp:group ... -->...',
]
);
Filar 4: Przepływ Pracy Programistycznej (Development Workflow)
Profesjonalne tworzenie własnych bloków Gutenberg wymaga solidnego środowiska programistycznego. Oto sprawdzony przepływ pracy, który eliminuje typowe problemy.
Lokalne środowisko WordPress — wp-env
Narzędzie @wordpress/env (środowisko WordPress) uruchamia kompletne lokalne środowisko WordPress w kontenerze Docker (oprogramowaniu do konteneryzacji aplikacji) — bez konieczności instalowania PHP, Apache ani MySQL na swojej maszynie.
# Jednorazowa instalacja globalna
npm install -g @wordpress/env
# Uruchom WordPress w katalogu wtyczki
wp-env start
# Twoja strona działa pod adresem: <http://localhost:8888>
# Panel administracyjny: <http://localhost:8888/wp-admin>
# Dane logowania: admin / password
# Zatrzymanie środowiska
wp-env stop
# Całkowity reset — czysta baza danych
wp-env clean all && wp-env start
Konfiguracja środowiska w pliku .wp-env.json:
{
"core": "WordPress/WordPress#6.5",
"phpVersion": "8.2",
"plugins": [
".",
"<https://downloads.wordpress.org/plugin/woocommerce.latest-stable.zip>"
],
"themes": [
"<https://downloads.wordpress.org/theme/twentytwentyfour.latest-stable.zip>"
],
"mappings": {
"wp-content/uploads": "./testy/zasoby"
}
}
Skrypty WordPress — @wordpress/scripts
Pakiet @wordpress/scripts (skrypty WordPress) dostarcza gotową konfigurację narzędzia Webpack (pakowacza modułów) zoptymalizowaną pod tworzenie własnych bloków Gutenberg. Nie musisz konfigurować niczego ręcznie.
// package.json
{
"name": "moj-plugin",
"version": "1.0.0",
"description": "Własne bloki Gutenberg",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start",
"lint:js": "wp-scripts lint-js",
"lint:css": "wp-scripts lint-style",
"lint:md": "wp-scripts lint-md-docs",
"format": "wp-scripts format",
"test:unit": "wp-scripts test-unit-js",
"test:e2e": "wp-scripts test-playwright",
"packages-update": "wp-scripts packages-update",
"plugin-zip": "wp-scripts plugin-zip"
},
"devDependencies": {
"@wordpress/scripts": "^27.0.0"
}
}
Kluczowe polecenia:
# Tryb deweloperski — obserwuje pliki i kompiluje przy każdej zmianie
# Generuje mapy źródłowe (ang. source maps) do debugowania
npm start
# Kompilacja produkcyjna — minifikuje kod, usuwa mapy źródłowe
npm run build
# Sprawdzanie jakości kodu JavaScript (statyczna analiza kodu)
npm run lint:js
# Automatyczne formatowanie kodu
npm run format
# Testy jednostkowe (ang. unit tests)
npm run test:unit
# Spakowanie wtyczki do pliku ZIP
npm run plugin-zip
Niestandardowa konfiguracja Webpacka
Gdy standardowa konfiguracja @wordpress/scripts nie wystarczy, możesz ją rozszerzyć:
// webpack.config.js
const domyslnaKonfiguracja = require( '@wordpress/scripts/config/webpack.config' );
const { resolve } = require( 'path' );
module.exports = {
// Rozszerz domyślną konfigurację zamiast ją zastępować
...domyslnaKonfiguracja,
// Wiele punktów wejścia — dla wtyczek z wieloma blokami
entry: {
'karta-uslugi/index': './src/karta-uslugi/index.js',
'sekcja-hero/index': './src/sekcja-hero/index.js',
'galeria-projektow/index':'./src/galeria-projektow/index.js',
},
// Aliasy importów — krótsze ścieżki w kodzie
resolve: {
...domyslnaKonfiguracja.resolve,
alias: {
...domyslnaKonfiguracja.resolve?.alias,
'@komponenty': resolve( __dirname, 'src/komponenty' ),
'@narzedziownia': resolve( __dirname, 'src/narzedziownia' ),
},
},
};
Współdzielone komponenty między blokami
Przy większej liczbie własnych bloków szybko okazuje się, że wiele z nich powtarza te same kontrolki i pomocnicze funkcje. Rozwiązanie: biblioteka współdzielonych komponentów:
src/
├── komponenty/ ← wspólne komponenty React
│ ├── KontrolkaKoloru.js
│ ├── WyborIkony.js
│ └── PodgladLinku.js
├── narzedziownia/ ← funkcje pomocnicze
│ ├── atrybuty.js ← wspólne definicje atrybutów
│ └── formatowanie.js ← funkcje formatujące
├── karta-uslugi/
│ ├── block.json
│ ├── index.js
│ ├── edit.js
│ └── render.php
└── sekcja-hero/
├── block.json
├── index.js
├── edit.js
└── render.php
// src/narzedziownia/atrybuty.js
// Współdzielone definicje atrybutów — importuj zamiast kopiować
export const ATRYBUTY_KOLORU = {
kolorTekstu: {
type: 'string',
default: '',
},
kolorTla: {
type: 'string',
default: '',
},
};
export const ATRYBUTY_ODSTEPOW = {
wewnetrznyOdstep: {
type: 'object',
default: { top: '1rem', bottom: '1rem' },
},
};
// Użycie w block.json niemożliwe — importuj w edit.js i przekazuj do setAttributes
Testowanie własnych bloków Gutenberg
Testy jednostkowe z biblioteką @wordpress/jest-console
// src/karta-uslugi/edit.test.js
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Edit from './edit';
// Atrapy (ang. mocks) dla modułów WordPress
jest.mock( '@wordpress/block-editor', () => ( {
useBlockProps: () => ( {} ),
InspectorControls: ( { children } ) => children,
RichText: ( { onChange, value, placeholder } ) => (
<input
value={ value }
placeholder={ placeholder }
onChange={ ( e ) => onChange( e.target.value ) }
/>
),
} ) );
describe( 'Blok Karta Usługi — Edit', () => {
const domyslneAtrybuty = {
tytul: '',
opis: '',
tekstPrzycisku: 'Dowiedz się więcej',
pokazPrzycisk: true,
};
it( 'renderuje pole tytułu z poprawnym placeholderem', () => {
render(
<Edit
attributes={ domyslneAtrybuty }
setAttributes={ jest.fn() }
/>
);
expect(
screen.getByPlaceholderText( 'Wpisz tytuł usługi…' )
).toBeInTheDocument();
} );
it( 'wywołuje setAttributes przy zmianie tytułu', async () => {
const ustawAtrybuty = jest.fn();
render(
<Edit
attributes={ domyslneAtrybuty }
setAttributes={ ustawAtrybuty }
/>
);
const poleTytulu = screen.getByPlaceholderText( 'Wpisz tytuł usługi…' );
await userEvent.type( poleTytulu, 'Nowy tytuł' );
expect( ustawAtrybuty ).toHaveBeenCalledWith( { tytul: 'Nowy tytuł' } );
} );
} );
Testy całościowe (ang. end-to-end) z Playwright
// tests/e2e/karta-uslugi.spec.js
const { test, expect } = require( '@playwright/test' );
const { admin } = require( '@wordpress/e2e-test-utils-playwright' );
test.describe( 'Blok Karta Usługi — testy całościowe', () => {
test.use( { storageState: admin.storageStatePath } );
test( 'można wstawić blok i edytować tytuł', async ( { page, editor } ) => {
await editor.openDocumentSettingsSidebar();
await editor.canvas.click( 'role=button[name="Add block"i]' );
// Wyszukaj własny blok
await page.keyboard.type( 'Karta usługi' );
await page.click( 'role=option[name="Karta usługi"i]' );
// Edytuj tytuł
await editor.canvas.click( '.karta-uslugi__tytul' );
await page.keyboard.type( 'Projektowanie stron' );
// Sprawdź czy tytuł się pojawił
await expect(
editor.canvas.locator( '.karta-uslugi__tytul' )
).toContainText( 'Projektowanie stron' );
} );
} );
Debugowanie bloków — praktyczne techniki
// Włącz szczegółowe informacje debugowania w edytorze
// Dodaj do pliku wp-config.php lub .wp-env.json:
// define( 'SCRIPT_DEBUG', true );
// define( 'WP_DEBUG', true );
// W kodzie JavaScript — warunkowe logowanie tylko w trybie deweloperskim
if ( process.env.NODE_ENV === 'development' ) {
console.group( '🧱 Karta Usługi — atrybuty' );
console.log( 'tytul:', attributes.tytul );
console.log( 'opis:', attributes.opis );
console.groupEnd();
}
// Użyj rozszerzenia przeglądarki "React DevTools" do inspekcji komponentów
// Użyj rozszerzenia "Redux DevTools" do śledzenia stanu edytora Gutenberg
Zaawansowane techniki tworzenia własnych bloków Gutenberg
Zmiana kategorii i kolejności bloków w inserterze
<?php
// Dodaj własną kategorię do insertera bloków (ang. block inserter — narzędzie do wstawiania bloków)
function moj_plugin_kategoria_blokow( array $kategorie, WP_Post $post ): array {
return array_merge(
[
[
'slug' => 'moj-plugin',
'title' => __( 'Moje bloki', 'moj-plugin' ),
'icon' => 'screenoptions',
],
],
$kategorie
);
}
add_filter( 'block_categories_all', 'moj_plugin_kategoria_blokow', 10, 2 );
Walidacja atrybutów po stronie serwera
<?php
/**
* Waliduje i oczyszcza atrybuty bloku przed zapisem.
* Podpinamy się pod filtr render_block — stosujemy zasadę
* "ufaj, ale weryfikuj" nawet dla własnych bloków.
*/
function moj_plugin_waliduj_atrybuty( string $tresc_bloku, array $blok ): string {
if ( 'moj-plugin/karta-uslugi' !== $blok['blockName'] ) {
return $tresc_bloku;
}
// Weryfikacja — czy atrybuty mają oczekiwane typy
$atrybuty = $blok['attrs'];
if ( isset( $atrybuty['kolorTla'] ) &&
! preg_match( '/^#[0-9a-fA-F]{3,8}$/', $atrybuty['kolorTla'] ) ) {
// Nieprawidłowy kolor — nie renderuj bloku
return '';
}
return $tresc_bloku;
}
add_filter( 'render_block', 'moj_plugin_waliduj_atrybuty', 10, 2 );
Bloki z danymi zewnętrznymi — pobieranie przez interfejs REST WordPress
// edit.js — pobieranie wpisów przez interfejs REST bezpośrednio w edytorze
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { Spinner } from '@wordpress/components'; // Wskaźnik ładowania
export default function Edit( { attributes } ) {
const wlasciwosciBloku = useBlockProps();
// Hook useSelect — pobiera dane ze sklepu danych WordPress (ang. data store)
const { wpisy, trwaLadowanie } = useSelect( ( wybierz ) => {
const { getEntityRecords, isResolving } = wybierz( coreStore );
const argumenty = [
'postType',
'post',
{
per_page: attributes.liczbaWpisow || 3,
status: 'publish',
_fields: 'id,title,excerpt,link',
},
];
return {
wpisy: getEntityRecords( ...argumenty ),
trwaLadowanie: isResolving( 'getEntityRecords', argumenty ),
};
}, [ attributes.liczbaWpisow ] );
if ( trwaLadowanie ) {
return <div { ...wlasciwosciBloku }><Spinner /></div>;
}
return (
<div { ...wlasciwosciBloku }>
{ wpisy?.map( ( wpis ) => (
<article key={ wpis.id }>
<h3>{ wpis.title.rendered }</h3>
<div
dangerouslySetInnerHTML={ {
__html: wpis.excerpt.rendered,
} }
/>
</article>
) ) }
</div>
);
}
Lista kontrolna — własny blok gotowy do produkcji
Przed wdrożeniem własnego bloku Gutenberg na środowisko produkcyjne (ang. production environment) sprawdź każdy punkt:
Bezpieczeństwo
- Wszystkie dane wyjściowe PHP oczyszczone przez
wp_kses_post(),esc_html()lubesc_attr() - Żadne surowe dane użytkownika nie trafiają bezpośrednio do SQL ani HTML
- Uprawnienia (ang. capabilities) sprawdzone przed operacjami administracyjnymi
- Użyty
get_block_wrapper_attributes()zamiast ręcznego budowania atrybutów HTML
Wydajność
- Blok dynamiczny korzysta z buforowania (ang. caching) dla drogich zapytań do bazy danych
- Zdjęcia i media ładowane leniwie (ang. lazy loading)
- Skrypty JavaScript ładowane tylko tam, gdzie blok jest używany (
viewScriptzamiasteditorScript) - Kod produkcyjny skompilowany poleceniem
npm run build
Dostępność (ang. Accessibility)
- Wszystkie elementy interaktywne dostępne z klawiatury
- Atrybuty ARIA (zestaw atrybutów dostępności treści internetowych) dodane tam, gdzie HTML semantyczny nie wystarczy
- Kontrast kolorów zgodny ze standardem WCAG 2.1 AA
- Blok przetestowany z czytnikiem ekranu (np. NVDA lub VoiceOver)
Jakość kodu
- Kod przeszedł statyczną analizę (
npm run lint:jsinpm run lint:css) - Testy jednostkowe napisane i przechodzące
- Brak ostrzeżeń w konsoli przeglądarki ani w dzienniku PHP
- Atrybut
apiVersion: 3w block.json (najnowsza wersja interfejsu)
Podsumowanie
Tworzenie własnych bloków Gutenberg to dziś jedna z najbardziej wartościowych umiejętności w ekosystemie WordPress. Plik block.json jako centrum konfiguracji, bloki dynamiczne renderowane przez PHP, wzory bloków przyspieszające pracę redakcyjną, zsynchronizowane wzory dla spójnych elementów — to fundamenty nowoczesnego podejścia do rozwijania WordPressa.
Kluczowe wnioski z tego przewodnika: używaj @wordpress/create-block do startu każdego projektu zamiast konfigurować środowisko ręcznie. Preferuj bloki dynamiczne (render.php) nad statycznymi (save.js) — unikniesz problemów z walidacją przy aktualizacjach. Korzystaj z block supports zamiast pisać własne kontrolki kolorów i odstępów — dostajesz automatyczną integrację z globalnym stylem motywu. I testuj — testy jednostkowe i całościowe to inwestycja, która procentuje przy każdej kolejnej aktualizacji wtyczki.
Własne bloki Gutenberg to nie tylko techniczna ciekawostka — to realna przewaga konkurencyjna jako dewelopera WordPress, który dostarcza klientom precyzyjne narzędzia zamiast ogólnych rozwiązań z katalogu wtyczek.
Pracujesz nad własnym blokiem i natknąłeś się na konkretny problem? Opisz go w komentarzach — chętnie pomogę znaleźć rozwiązanie.


