tworzenie wlasnych blokow Gutenberg

Tworzenie własnych bloków Gutenberg — kompletny przewodnik dla freelancerów WordPress

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() lub esc_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 (viewScript zamiast editorScript)
  • 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:js i npm run lint:css)
  • Testy jednostkowe napisane i przechodzące
  • Brak ostrzeżeń w konsoli przeglądarki ani w dzienniku PHP
  • Atrybut apiVersion: 3 w 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.

Ten wpis powstał w oparciu o moje wcześniejsze doświadczenie zawodowe jako WordPress Developer. Obecnie nie prowadzę już działalności gospodarczej ani nie świadczę tych usług — treść zostawiam jako źródło wiedzy dla osób, które wciąż się tym zajmują.

Bądź na bieżąco...

otrzymuj najnowsze wiadomości, aktualizacje i wiele innych rzeczy co 2 tygodnie.

Zostaw komentarz

Przewijanie do góry