DesenvolvedorWordPress WhatsApp

Criar blocos Gutenberg dinâmicos (render no PHP)

A maioria dos tutoriais ensina blocos estáticos, cujo HTML fica "congelado" no conteúdo. Mas quando a saída depende de dados que mudam — últimos posts, preço, estoque — você precisa de um bloco dinâmico, renderizado pelo PHP a cada requisição. Veja como montá-lo com block.json e render_callback.

Precisa de desenvolvimento sob medida?

Atualizado em 19/06/2026

No editor de blocos (Gutenberg), existem dois jeitos fundamentalmente diferentes de um bloco produzir o seu HTML. Confundir os dois é a origem de quase todo bug de "meu bloco não atualiza" ou "o conteúdo sumiu depois que mudei o código". Neste artigo eu separo de uma vez o bloco estático do dinâmico, mostro a abordagem moderna baseada em block.json e foco no que interessa para quem vem do PHP: gerar o markup no servidor, com segurança, via render_callback.

Estático × dinâmico: onde o HTML nasce

A diferença está em quem gera o markup e quando:

Bloco estáticoBloco dinâmico
Quem gera o HTMLsave (JavaScript)render_callback (PHP)
Quando é geradoAo salvar o postA cada requisição da página
O que fica no bancoO HTML completoSó os atributos (save = null)
Reflete dados que mudam?Não (fica congelado)Sim, sempre atualizado
Quando usarConteúdo fixoListas, preços, dados dinâmicos

Por que o estático "quebra" ao mudar o código: como o HTML do bloco estático fica salvo no post, se você alterar a função save o markup gravado deixa de bater com o que o JavaScript agora espera, e o editor mostra o aviso de "este bloco contém conteúdo inesperado". Blocos dinâmicos não sofrem disso — eles não gravam HTML.

A abordagem moderna: tudo gira em torno do block.json

Desde o WordPress 5.8, o registro de blocos é centralizado em um arquivo de metadados, o block.json. Ele descreve o bloco (nome com namespace, título, categoria, atributos) e aponta para os assets — e é a partir dele que o WordPress faz o enfileiramento automático de scripts e estilos. O name precisa ter namespace (vendor/bloco) para não colidir com outros plugins.

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "meu-plugin/ultimos-posts",
  "version": "1.0.0",
  "title": "Últimos posts",
  "category": "widgets",
  "icon": "list-view",
  "description": "Lista os posts mais recentes, renderizada no PHP.",
  "attributes": {
    "quantidade": { "type": "number", "default": 5 },
    "mostrarData": { "type": "boolean", "default": true }
  },
  "supports": {
    "html": false
  },
  "textdomain": "meu-plugin",
  "editorScript": "file:./index.js",
  "style": "file:./style-index.css"
}

O editorScript aponta para o JavaScript que registra o bloco no editor (gerado pelo build). Para renderizar no front-end há duas opções: o campo render (a partir do WordPress 6.1) no block.json, apontando um arquivo PHP, ou o render_callback em register_block_type(). Use uma ou outra; neste exemplo usamos o render_callback, por isso o block.json acima não traz o campo render.

Registrando o bloco no PHP, no hook init

Do lado do PHP, o registro é minimalista: aponte register_block_type() para a pasta que contém o block.json. O WordPress lê os metadados e cuida do resto. Se preferir definir o render em PHP (em vez do campo render do JSON), passe o render_callback aqui.

<?php
add_action( 'init', 'meu_plugin_registrar_blocos' );

function meu_plugin_registrar_blocos() {
    // __DIR__ deve apontar para a pasta com o block.json gerado no build (ex.: build/).
    register_block_type(
        __DIR__ . '/build/ultimos-posts',
        array(
            'render_callback' => 'meu_plugin_render_ultimos_posts',
        )
    );
}

Se você declarou "render": "file:./render.php" no block.json, pode chamar apenas register_block_type( __DIR__ . '/build/ultimos-posts' ), sem o segundo argumento — o WordPress usa o arquivo de render automaticamente. O render_callback explícito é útil quando a lógica fica em uma classe ou método.

O render_callback em PHP, com saída escapada

A assinatura é render_callback( $attributes, $content, $block ). Ele recebe os atributos do bloco (o array vindo do block.json), o conteúdo interno (relevante em blocos com InnerBlocks) e o objeto WP_Block. Ele deve retornar (não imprimir) o HTML — e todo dado deve ser escapado na saída. Use get_block_wrapper_attributes() para que o bloco respeite as classes e estilos definidos pelo usuário no editor (alinhamento, cor, etc.).

<?php
function meu_plugin_render_ultimos_posts( $attributes, $content, $block ) {
    $quantidade   = isset( $attributes['quantidade'] ) ? absint( $attributes['quantidade'] ) : 5;
    $mostrar_data = ! empty( $attributes['mostrarData'] );

    $query = new WP_Query( array(
        'post_type'           => 'post',
        'posts_per_page'      => $quantidade,
        'no_found_rows'       => true,
        'ignore_sticky_posts' => true,
    ) );

    if ( ! $query->have_posts() ) {
        return '';
    }

    // Atributos do wrapper: classes/estilos que o editor aplicou ao bloco.
    $wrapper = get_block_wrapper_attributes();

    $itens = '';
    while ( $query->have_posts() ) {
        $query->the_post();

        $data = $mostrar_data
            ? ' <time>' . esc_html( get_the_date() ) . '</time>'
            : '';

        $itens .= sprintf(
            '<li><a href="%1$s">%2$s</a>%3$s</li>',
            esc_url( get_permalink() ),
            esc_html( get_the_title() ),
            $data
        );
    }
    wp_reset_postdata();

    // %1$s já vem escapado por get_block_wrapper_attributes().
    return sprintf( '<ul %1$s>%2$s</ul>', $wrapper, $itens );
}

Escape sempre na saída. Mesmo dados vindos do seu próprio banco passam por esc_html(), esc_url() e esc_attr(). Concatenar título de post ou URL sem escapar é uma porta aberta para XSS. Note ainda que o callback deve retornar a string — fazer echo dentro dele desordena a saída da página. Os princípios estão no artigo de segurança no código.

O lado do editor: edit em React, save retornando null

No JavaScript, o edit desenha a interface do bloco no editor usando useBlockProps() (que aplica as classes e atributos corretos ao elemento raiz). Para um bloco dinâmico, o save simplesmente devolve null — sinalizando que o HTML virá do PHP. Este é o trecho que o wp-scripts transpila do JSX:

// index.js — registra o bloco no editor (transpilado por wp-scripts)
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps } from '@wordpress/block-editor';
import metadata from './block.json';

registerBlockType( metadata.name, {
    edit: ( { attributes } ) => {
        const blockProps = useBlockProps();
        return (
            <div { ...blockProps }>
                Pré-visualização: últimos { attributes.quantidade } posts.
            </div>
        );
    },
    // Bloco dinâmico: nada é gravado no conteúdo; o PHP renderiza.
    save: () => null,
} );

Em produção, o edit normalmente traz controles (um RangeControl para a quantidade, um ToggleControl para a data) no painel lateral (InspectorControls) e até uma prévia real via ServerSideRender, que chama o seu PHP dentro do editor. O ponto que não muda: save retorna null.

Scaffolding e build com @wordpress/scripts

Você não escreve esse esqueleto à mão. O pacote oficial @wordpress/create-block gera a estrutura completa (com block.json, index.js, render.php e package.json já configurados), e o @wordpress/scripts faz o build. A flag --variant=dynamic já cria o esqueleto de um bloco dinâmico:

# Cria o plugin/bloco já no formato dinâmico (gera render.php)
npx @wordpress/create-block meu-bloco-dinamico --variant=dynamic

cd meu-bloco-dinamico

# Build de produção: transpila o JSX e gera a pasta build/
npm run build

# Durante o desenvolvimento: recompila a cada mudança
npm run start

A etapa de build não é opcional. O navegador não executa JSX nem entende import de @wordpress/* diretamente. Sem rodar npm run build (que por baixo chama wp-scripts build), a pasta build/ não existe, o editorScript aponta para um arquivo inexistente e o bloco simplesmente não aparece no inserter. Em servidores de produção, faça o build antes do deploy e versione a pasta build/ — ou rode o build no pipeline.

Armadilhas comuns

Boas práticas

Blocos dinâmicos são a ponte natural entre o editor moderno e o seu conhecimento de PHP: o React cuida da edição, mas a renderização — onde entram WP_Query, transients e a sua regra de negócio — continua no servidor, exatamente onde você já sabe trabalhar.

Quer um bloco Gutenberg sob medida para a sua necessidade?

Desenvolvo blocos dinâmicos (render no PHP) com build em @wordpress/scripts, seguindo os padrões oficiais do editor. Me chame no WhatsApp.

WhatsApp: (43) 99932-9697

ou e-mail: [email protected]

Perguntas frequentes

Qual a diferença entre bloco estático e dinâmico?

Um bloco estático grava o HTML final dentro do conteúdo do post no momento em que você salva — quem gera o markup é a função save em JavaScript. Um bloco dinâmico não grava HTML: o seu save retorna null e o markup é montado pelo PHP a cada requisição, via render_callback. Use dinâmico sempre que a saída depender de dados que mudam (últimos posts, preço, estoque, data, dados do usuário logado).

Por que o save de um bloco dinâmico retorna null?

Porque, se o save gravasse HTML, esse HTML ficaria "congelado" no conteúdo do post e não refletiria mudanças nos dados. Retornando null, nada é persistido além dos atributos do bloco (em um comentário HTML), e o WordPress chama o seu render_callback em PHP toda vez que a página é exibida — garantindo conteúdo sempre atualizado.

Preciso de Node e npm para criar um bloco?

Para a abordagem moderna com React/JSX, sim: você precisa de Node/npm para rodar @wordpress/scripts (o comando wp-scripts build), que transpila o JSX e gera os assets que o navegador entende. O npx @wordpress/create-block cria todo o esqueleto. O navegador não executa JSX diretamente; sem a etapa de build o editor não carrega o bloco.

O que é o apiVersion no block.json?

É a versão da API de marcação/comportamento do bloco no editor. A versão atual recomendada é a 3, que adota o uso de useBlockProps() e prepara o bloco para o editor iframe. Omitir apiVersion faz o bloco cair na versão 1, com comportamento antigo. Em blocos novos, declare sempre "apiVersion": 3.

Como o WordPress sabe quais scripts e estilos enfileirar?

Pelo próprio block.json. Ao registrar o bloco com register_block_type( __DIR__ ) apontando para a pasta com o block.json, o WordPress lê campos como editorScript, script, viewScript e style e cuida do enfileiramento automaticamente — inclusive carregando o viewScript só nas páginas onde o bloco aparece. Veja enfileirar scripts e estilos.

Onde devo registrar o bloco?

No hook init. O registro depende de APIs (tipos de post, REST) que só estão prontas a partir desse momento, e o editor de blocos espera que os tipos estejam disponíveis cedo. Registrar fora do init — direto no carregamento do arquivo, por exemplo — causa avisos e blocos que não aparecem no inserter.

Referências oficiais

  1. WordPress.org — Block Editor Handbook
  2. WordPress.org — Create a Block (Getting Started)
  3. WordPress.org — Creating dynamic blocks
  4. WordPress.org — Block registration (Block API)
  5. WordPress.org — register_block_type() (Code Reference)
Tem alguma dúvida? Ficarei feliz em ajudar. Clique Aqui