LW IT Solutions logo LW IT Solutions logo LW IT Solutions
« Przegląd bloga /Wtyczki i triki WordPress/Tutorials / Dynamiczny blok Gutenberga z block.json i render.php
Ten artykuł w innych językach:

Dynamiczny blok Gutenberga z block.json i render.php

Dynamiczny blok Gutenberga z block.json i render.php

Management Summary

Dynamiczny blok Gutenberga to wielokrotnie używany element treści WordPressa, którego wygląd powstaje z zapisanych ustawień. W przykładzie wybór kategorii i liczby tworzy listę najnowszych opublikowanych wpisów. Nowe artykuły mogą pojawiać się bez ręcznej edycji każdej strony z listą, ograniczając pracę redakcji. Lista zależy jednak od aktywnej wtyczki, a zapisane kopie stron mogą opóźniać widoczne zmiany. Automatyczna treść nadal wymaga właściwej prezentacji i obsługi języka.

Spis treści
  1. Dlaczego listę tworzy PHP
  2. 1. Szkielet i kompilacja
  3. 2. Jeden opis dla edytora i serwera
  4. 3. Rejestracja bloku
  5. 4. Lista wpisów na podstawie ustawień
  6. 5. Ustawienia i podgląd w edytorze
  7. 6. Zapis, pamięć podręczna i weryfikacja
  8. Pytania i odpowiedzi
  9. Źródła

Dlaczego listę tworzy PHP

Lista najnowszych wpisów może się zmienić bez edycji wpisu zawierającego listę. Blok dynamiczny tworzy HTML na serwerze. Ten przykład zapisuje tylko kategorię i liczbę wpisów; PHP wyszukuje pasujące opublikowane wpisy i tworzy odnośniki. Bloki dynamiczne mogą też zapisywać zastępczy HTML. Tutaj celowo go nie ma: Po wyłączeniu wtyczki wynik pozostaje pusty.

1. Szkielet i kompilacja

Przykład korzysta z ustalonych wersji narzędzi: create-block 4.99.0 i @wordpress/scripts 37.0.0. Odpowiednia jest na przykład wersja Node.js 22.23.3; scripts 37 wymaga co najmniej 22.22.2 w gałęzi 22.x. Polecenie node –version pokazuje zainstalowaną wersję. Poniższe polecenia tworzą szkielet. Po zastąpieniu plików źródłowych konieczne jest ponowne npm run build.

Shell
npx @wordpress/create-block@4.99.0 category-posts --variant dynamic --namespace lw --title "Category Posts"
cd category-posts
npm install --save-dev --save-exact @wordpress/scripts@37.0.0
npm run build

Pliki źródłowe znajdują się w src/category-posts/. Poniższe przykłady zastępują block.json, index.js, edit.js, render.php i główny plik wtyczki. Wygenerowane pliki SCSS pozostają. view.js nie jest ładowany, ponieważ block.json nie zawiera wpisu viewScript.

Tekst
category-posts/                 (abridged)
├── category-posts.php          plugin header and block registration
├── package.json                build/start scripts with --blocks-manifest
├── readme.txt
├── src/
│   └── category-posts/
│       ├── block.json
│       ├── index.js
│       ├── edit.js
│       ├── render.php
│       ├── view.js             not loaded by this example
│       ├── style.scss
│       └── editor.scss
└── build/                      written by npm start / npm run build
    ├── blocks-manifest.php
    └── category-posts/
        ├── block.json
        ├── index.js
        ├── index.asset.php
        ├── index.css
        ├── style-index.css
        └── render.php

Oba skrypty należą do istniejącej sekcji scripts w package.json; pozostałe ustawienia pakietu pozostają. –webpack-copy-php kopiuje pliki PHP, a –blocks-manifest tworzy wspólny plik metadanych. npm start ponawia kompilację po zmianach; npm run build tworzy wersję do publikacji.

JSON
{
	"scripts": {
		"build": "wp-scripts build --webpack-copy-php --blocks-manifest",
		"start": "wp-scripts start --webpack-copy-php --blocks-manifest"
	}
}

2. Jeden opis dla edytora i serwera

block.json opisuje nazwę, atrybuty i pliki. Atrybuty to zapisane ustawienia: categoryId zawiera identyfikator kategorii, a count żądaną liczbę wpisów. Schemat sprawdza typ danych; PHP dodatkowo sprawdza dozwolone zakresy liczb. usesContext udostępnia identyfikator wpisu zawierającego blok, aby lista mogła pominąć ten wpis.

JSON
{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "lw/category-posts",
	"version": "0.1.0",
	"title": "Category Posts",
	"category": "widgets",
	"icon": "list-view",
	"description": "Lists the latest posts of one category, rendered on the server.",
	"attributes": {
		"categoryId": {
			"type": "integer",
			"default": 0
		},
		"count": {
			"type": "integer",
			"default": 5
		}
	},
	"usesContext": [ "postId" ],
	"supports": {
		"html": false,
		"align": [ "wide", "full" ],
		"color": {
			"background": true,
			"text": true,
			"link": true
		},
		"spacing": {
			"margin": true,
			"padding": true
		},
		"typography": {
			"fontSize": true
		}
	},
	"textdomain": "category-posts",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css",
	"render": "file:./render.php"
}

apiVersion 3 oznacza wersję API bloków. editorScript i editorStyle dotyczą edytora, a style — edytora i witryny. supports włącza opcje wyglądu, takie jak kolory i odstępy. Odpowiednie atrybuty HTML dostarczają useBlockProps() i get_block_wrapper_attributes().

3. Rejestracja bloku

Główny plik rejestruje bloki opisane w manifeście kompilacji. Manifest to wygenerowany plik PHP z metadanymi; eliminuje osobne odczytywanie każdego block.json. Ten przykład wymaga WordPressa 6.9 lub nowszego.

PHP
<?php
/**
 * Plugin Name:       Category Posts
 * Description:       Server-rendered block that lists the latest posts of one category.
 * Version:           0.1.0
 * Requires at least: 6.9
 * Requires PHP:      7.4
 * Author:            Lukas Wojcik
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       category-posts
 *
 * @package Lw
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Registers every block listed in build/blocks-manifest.php.
 *
 * The manifest is generated by wp-scripts (--blocks-manifest), so WordPress
 * does not have to read and decode each block.json on every request.
 */
function lw_category_posts_block_init() {
	wp_register_block_types_from_metadata_collection(
		__DIR__ . '/build',
		__DIR__ . '/build/blocks-manifest.php'
	);
}
add_action( 'init', 'lw_category_posts_block_init' );

Dla pojedynczego bloku alternatywą jest rejestracja przez katalog. Ta ścieżka również wskazuje build: Kompilacja nadal jest potrzebna. Alternatywa zastępuje rejestrację w obsłudze init, zamiast działać dodatkowo.

PHP
// Single block, without a manifest: reads build/category-posts/block.json.
register_block_type( __DIR__ . '/build/category-posts' );

Zmiany w src zaczynają działać po kolejnej kompilacji. Dotyczy to również render.php i block.json. Na instalację docelową trafiają główny plik wtyczki i wygenerowany katalog build.

4. Lista wpisów na podstawie ustawień

WordPress udostępnia w render.php trzy zmienne: $attributes zawiera ustawienia, $content zapisany wewnętrzny HTML, a $block kontekst bloku. W tym przykładzie $content jest pusty. Plik tworzy następujący wynik:

PHP
<?php
/**
 * Server-side rendering of the lw/category-posts block.
 *
 * Variables provided by WordPress:
 *
 * @var array    $attributes Block attributes, defaults from block.json applied.
 * @var string   $content    Inner content (always empty for this block).
 * @var WP_Block $block      Block instance: name, context, parsed block.
 *
 * @package Lw
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

$lwcp_category_id = isset( $attributes['categoryId'] ) && is_int( $attributes['categoryId'] )
    ? $attributes['categoryId'] : 0;
$lwcp_count = isset( $attributes['count'] ) && is_int( $attributes['count'] )
    ? $attributes['count'] : 5;
if ( $lwcp_category_id < 1 ) {
    return;
}
if ( $lwcp_count < 1 || $lwcp_count > 10 ) {
    $lwcp_count = 5;
}
$lwcp_term        = $lwcp_category_id ? get_term( $lwcp_category_id, 'category' ) : null;

// Render nothing until a valid category has been chosen.
if ( ! $lwcp_term instanceof WP_Term ) {
	return;
}

// Do not list the post that contains the block (context from "usesContext").
$lwcp_current_id = isset( $block->context['postId'] ) ? absint( $block->context['postId'] ) : 0;

$lwcp_posts = get_posts(
	array(
		'category__in' => array( $lwcp_term->term_id ),
        'suppress_filters' => false,
		'numberposts'  => $lwcp_count,
		'post_status'  => 'publish',
		'has_password' => false,
		'exclude'      => $lwcp_current_id ? array( $lwcp_current_id ) : array(),
	)
);
?>
<div <?php // Core escapes the returned attribute values.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Core-generated wrapper attributes.
echo get_block_wrapper_attributes(); ?>>
	<?php if ( empty( $lwcp_posts ) ) : ?>
		<p><?php esc_html_e( 'No posts in this category yet.', 'category-posts' ); ?></p>
	<?php else : ?>
		<ul class="wp-block-lw-category-posts__list">
			<?php foreach ( $lwcp_posts as $lwcp_post ) : ?>
				<li>
					<a href="<?php echo esc_url( get_permalink( $lwcp_post ) ); ?>"><?php echo esc_html( get_the_title( $lwcp_post ) ); ?></a>
					<time datetime="<?php echo esc_attr( get_the_date( 'c', $lwcp_post ) ); ?>"><?php echo esc_html( get_the_date( '', $lwcp_post ) ); ?></time>
				</li>
			<?php endforeach; ?>
		</ul>
	<?php endif; ?>
</div>

Nieprawidłowa lub brakująca kategoria oznacza brak wyniku. Liczba poza zakresem 1–10 powoduje użycie pięciu wpisów. category__in ogranicza zapytanie do wybranej kategorii; podkategorie nie są automatycznie dołączane. Wpisy chronione hasłem i nieopublikowane pozostają wykluczone.

suppress_filters ma jawnie wartość false, aby mogły działać filtry, na przykład z wtyczek językowych. Nie gwarantuje to jeszcze właściwego języka: Konkretna wtyczka wielojęzyczna i działanie podglądu wymagają osobnego sprawdzenia.

Odnośniki, tytuły i daty korzystają z funkcji zabezpieczających właściwych dla miejsca wyświetlenia. Kodowanie znaków uniemożliwia potraktowanie treści jako HTML lub atrybutów. get_block_wrapper_attributes() zwraca wartości atrybutów zabezpieczone już przez WordPressa. Uzasadniony phpcs:ignore dotyczy wyłącznie tego wyniku.

WordPress ponownie ładuje render.php dla każdego wystąpienia bloku. Bezwarunkowe globalne deklaracje funkcji lub klas kolidowałyby przy drugim załadowaniu. Takie definicje pomocnicze należą do głównego pliku wtyczki, ładowanego jednokrotnie.

5. Ustawienia i podgląd w edytorze

index.js łączy blok z edit.js. save jawnie zwraca null: Zapisywane są wyłącznie komentarz i atrybuty, bez gotowego HTML listy.

JavaScript
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import metadata from './block.json';

// No fallback HTML: only the block comment and attributes are stored.
registerBlockType( metadata.name, {
	edit: Edit,
    save: () => null,
} );

edit.js pobiera nazwy kategorii, wyświetla wybór i ogranicza suwak do zakresu 1–10. Błąd ładowania wywołuje komunikat zamiast niekończącego się wskaźnika ładowania. Zapytanie pobiera wszystkie kategorie; bardzo duże zbiory wymagają wyszukiwania z ograniczoną liczbą wyników na stronie.

JavaScript
import { __ } from '@wordpress/i18n';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import {
	Notice,
	PanelBody,
	Placeholder,
	RangeControl,
	SelectControl,
	Spinner,
} from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
import { ServerSideRender } from '@wordpress/server-side-render';
import metadata from './block.json';
import './editor.scss';

// Same query shape as the core Latest Posts block uses for its category list.
const CATEGORIES_QUERY = { per_page: -1, _fields: 'id,name', context: 'view' };

export default function Edit( { attributes, setAttributes } ) {
	const { categoryId, count } = attributes;
	const blockProps = useBlockProps();

    const { categories, categoryError } = useSelect( ( select ) => {
        const store = select( coreStore );
        const args = [ 'taxonomy', 'category', CATEGORIES_QUERY ];
        return {
            categories: store.getEntityRecords( ...args ),
            categoryError: store.getResolutionError( 'getEntityRecords', args ),
        };
    }, [] );

	const options = [
		{ value: '0', label: __( '— Select a category —', 'category-posts' ) },
		...( categories ?? [] ).map( ( term ) => ( {
			value: String( term.id ),
			label: decodeEntities( term.name ),
		} ) ),
	];

	return (
		<>
			<InspectorControls>
				<PanelBody title={ __( 'Settings', 'category-posts' ) }>
					{ categoryError ? (
                        <Notice status="error" isDismissible={ false }>
                            { __( 'Categories could not be loaded.', 'category-posts' ) }
                        </Notice>
                    ) : categories ? (
						<SelectControl
							__next40pxDefaultSize
							__nextHasNoMarginBottom
							label={ __( 'Category', 'category-posts' ) }
							value={ String( categoryId ) }
							options={ options }
							onChange={ ( value ) =>
								setAttributes( { categoryId: Number( value ) } )
							}
						/>
					) : (
						<Spinner />
					) }
					<RangeControl
						__next40pxDefaultSize
						__nextHasNoMarginBottom
						label={ __( 'Number of posts', 'category-posts' ) }
						value={ count }
						min={ 1 }
						max={ 10 }
						onChange={ ( value ) =>
							setAttributes( { count: value ?? 5 } )
						}
					/>
				</PanelBody>
			</InspectorControls>
			<div { ...blockProps }>
				{ categoryId ? (
					<ServerSideRender
						block={ metadata.name }
						attributes={ attributes }
						skipBlockSupportAttributes
					/>
				) : (
					<Placeholder
						icon="list-view"
						label={ __( 'Category Posts', 'category-posts' ) }
						instructions={ __(
							'A category selection in the block settings enables the preview.',
							'category-posts'
						) }
					/>
				) }
			</div>
		</>
	);
}

ServerSideRender wysyła ustawienia do interfejsu REST WordPressa i wyświetla wynik render.php. REST oznacza tutaj dostęp przez HTTP do danych i funkcji WordPressa. Komponent może łączyć żądania, dlatego jedna zmiana nie musi oznaczać dokładnie jednego żądania.

Oficjalna dokumentacja opisuje ServerSideRender jako rozwiązanie zastępcze dla istniejącego kodu PHP. W nowych rozbudowanych interfejsach często lepiej sprawdza się podgląd tworzony w JavaScript z danych REST. Ten przykład edukacyjny celowo korzysta z tego samego wyniku PHP w podglądzie i witrynie.

skipBlockSupportAttributes usuwa atrybuty wyglądu z żądania podglądu, ponieważ zewnętrzny element edytora już je otrzymuje. Wewnętrzny element PHP pozostaje. Kolory, odstępy i zagnieżdżone elementy wymagają więc osobnych kontroli w edytorze i witrynie.

6. Zapis, pamięć podręczna i weryfikacja

Zapisany komentarz bloku dla kategorii 7 i trzech wpisów wygląda następująco:

Tekst
<!-- wp:lw/category-posts {"categoryId":7,"count":3} /-->

Bloki statyczne zwykle zapisują HTML. Zmiany w save mogą unieważnić istniejący HTML, jeśli nowy wynik nie pasuje do zapisanej struktury. Nie każda zmiana kodu powoduje ten błąd. Definicja deprecated może zachować obsługę starszych prawidłowych struktur.

W tym bloku dynamicznym nie ma walidacji zapisanego HTML listy. Zmiana render.php działa po kompilacji i wdrożeniu; pamięć podręczna stron może jednak nadal zwracać starszy wynik. Wykonanie PHP nie musi też oznaczać nowego zapytania do bazy: Pamięć podręczna obiektów może dostarczyć wynik zapytania.

Poprawiony kod został pomyślnie skompilowany 26.10.2026 z Node.js 22.23.3 i scripts 37.0.0. Udana kompilacja nie potwierdza wygody obsługi edytora ani zgodności z każdym motywem czy wtyczką językową. Audyt wykazał znane podatności w zależnościach środowiska programistycznego; użycie produkcyjne wymaga aktualizacji zależności i ponownych kontroli. node_modules nie należy do wdrażanej wtyczki.

Blok dynamiczny zapisuje ustawienia, tworzy listę wpisów w PHP i wymaga kontroli w edytorze oraz witrynie. Pamięć podręczna może zachować starszy wynik.
Blok dynamiczny zapisuje ustawienia, tworzy listę wpisów w PHP i wymaga kontroli w edytorze oraz witrynie. Pamięć podręczna może zachować starszy wynik.

Pytania i odpowiedzi

Dlaczego zmiana nie działa? Rejestracja i ładowanie korzystają z plików w build. Nowa kompilacja przenosi zmiany z src; później może być potrzebne odświeżenie pamięci podręcznej stron.

Czy render.php może zawierać funkcje pomocnicze? Bezwarunkowe globalne deklaracje są niewłaściwe, ponieważ plik jest ładowany wielokrotnie. Właściwym miejscem są pliki wtyczki ładowane jednokrotnie.

Dlaczego brakuje wpisów z podkategorii? category__in celowo wybiera tutaj wskazaną kategorię. Włączenie podkategorii wymaga innych argumentów zapytania i zgodnego z nimi opisu.

Źródła

Lukas Wójcik

Lukas Wójcik

Systems architect and technology enthusiast specializing in scalable tracking solutions, GMP Stack (GA4 & GTM), and robust backend architectures. Advocate for clean code and privacy-first design.

Kontakt

Masz pytanie lub pomysł na projekt internetowy? Napisz krótko, o co chodzi. Formularz korzysta z reCAPTCHA do ochrony przed spamem.

Napisanie komentarza

Doświadczenia z innymi wtyczkami lub środowiskami hostingowymi oraz pytania o konfigurację są tu mile widziane.

Adres e-mail nie jest publikowany. Pola obowiązkowe oznaczono gwiazdką.