12k
All articles

Como criar um bloco personalizado do Gutenberg

Crie um bloco Gutenberg personalizado com React, block.json e PHP. Entenda atributos, renderização estática e dinâmica e versões antigas para evitar erros de validação.

OpenReplay Team
OpenReplay Team
Como criar um bloco personalizado do Gutenberg

Um bloco do Gutenberg é um componente React que o WordPress registra com um nome dentro de um namespace. No editor, ele é renderizado por uma função edit. No front-end, a saída vem de uma função save ou de um template de renderização em PHP.

Se você sabe escrever um componente React, sabe escrever um bloco. A maior parte da dificuldade está nas partes específicas do WordPress ao redor do componente: um manifesto JSON, uma chamada de registro em PHP e uma etapa de validação que exibe a mensagem “Este bloco contém conteúdo inesperado ou inválido” (This block contains unexpected or invalid content) assim que você altera a marcação.

Antes dos blocos, o comportamento personalizado era adicionado incluindo JavaScript personalizado em temas do WordPress com wp_enqueue_script. Para o conteúdo, os blocos substituem essa abordagem: em vez de um script executado sobre HTML estático, os editores têm um componente visual e editável. Este guia constrói um bloco de destaque (callout) com um corpo editável em RichText e um campo de URL na barra lateral. Ele explica cada arquivo gerado pelo scaffold e mostra por que os erros de validação acontecem e como corrigi-los.

Principais conclusões

  • npx @wordpress/create-block@latest callout --namespace=openreplay gera um plugin de bloco completo, e o @wordpress/scripts faz o build com npm start e npm run build.
  • O block.json declara o bloco. As chamadas de registro em PHP e em JavaScript leem desse único arquivo o nome, os atributos, os supports e os caminhos dos assets.
  • Atributos sem source são armazenados como JSON no comentário delimitador HTML do bloco. Atributos com source e selector são extraídos da marcação salva.
  • Quando um post é carregado, o editor executa novamente o save() e compara o resultado com a marcação armazenada. Qualquer diferença dispara o erro de conteúdo inválido, até que uma entrada deprecated reproduza a saída antiga.
  • Um bloco dinâmico retorna null no save e é renderizado em PHP a cada requisição. Use esse tipo de bloco para saídas que precisam mudar sem que alguém salve o post novamente.

Um bloco é um componente React

Um bloco do Gutenberg é um componente React cuja API específica do WordPress corresponde a padrões que desenvolvedores React já conhecem:

Conceito do ReactEquivalente no bloco
JSX do componenteFunção edit
Propsattributes
setStatesetAttributes
Saída renderizada no servidorFunção save ou render.php

Um bloco do Gutenberg difere de um componente React comum em dois pontos. Primeiro, a saída do save é gravada no conteúdo do post como HTML. Segundo, o schema dos atributos fica em JSON, e não em PropTypes ou TypeScript.

Crie a estrutura do bloco com o create-block

O pacote oficial @wordpress/create-block gera um plugin de bloco funcional com as ferramentas de build já configuradas. Execute-o dentro de wp-content/plugins:

npx @wordpress/create-block@latest callout --namespace=openreplay
cd callout
npm start        # watch mode during development
npm run build    # production build

Se você omitir --namespace, o bloco será chamado create-block/callout. Com o npm start em execução, ative o plugin na tela de administração.

ArquivoFinalidade
callout.phpCabeçalho do plugin e registro do bloco no hook init
package.jsonScripts npm que encapsulam o @wordpress/scripts
src/callout/block.jsonDeclaração do bloco: nome, atributos, supports, assets
src/callout/index.jsChama registerBlockType com edit e save
src/callout/edit.jsComponente da interface do editor
src/callout/save.jsMarcação armazenada no conteúdo do post
src/callout/style.scssEstilos para o front-end e para o editor
src/callout/editor.scssEstilos apenas para o editor
build/Saída compilada que o WordPress de fato carrega
build/blocks-manifest.phpMetadados de todos os blocos, gerados no momento do build

O arquivo PHP é um plugin comum (veja como escrever um plugin do WordPress do zero) com uma chamada de registro a mais. O conceito por trás dele é o register_block_type(), que aceita uma pasta contendo o block.json. O scaffold encapsula essa função em uma chamada de registro em lote que exige o WordPress 6.8 ou superior. Por isso, o cabeçalho do plugin gerado define Requires at least: 6.8. Esta é uma versão simplificada dessa chamada:

add_action( 'init', function () {
	wp_register_block_types_from_metadata_collection(
		__DIR__ . '/build',
		__DIR__ . '/build/blocks-manifest.php'
	);
} );

O guia de registro de blocos explica as duas formas.

block.json: onde o bloco é declarado

O arquivo block.json é a declaração única de um bloco. As chamadas de registro em PHP e em JavaScript leem dele o nome, os atributos, os supports e os caminhos dos assets. Assim, um único arquivo descreve o bloco tanto para o editor quanto para o servidor.

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "openreplay/callout",
  "version": "0.1.0",
  "title": "Callout",
  "category": "text",
  "icon": "info",
  "attributes": {
    "content": { "type": "string", "source": "html", "selector": "p" },
    "url": { "type": "string", "default": "" }
  },
  "supports": { "html": false },
  "textdomain": "callout",
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css"
}

name segue o formato namespace/slug. Trate-o como permanente, porque ele é gravado na marcação de todos os posts que usam o bloco.

apiVersion: 3 surgiu no WordPress 6.3 e declara que o bloco funciona dentro do editor em iframe. Desde o WordPress 6.9, o schema aceita apenas a versão 3, e blocos registrados com uma versão anterior geram um aviso no console quando SCRIPT_DEBUG está ativado. A partir do WordPress 7.1, o editor de posts sempre roda em um iframe, qualquer que seja o apiVersion declarado pelo bloco. Por isso, todo bloco precisa funcionar dentro dele.

attributes definem os dados do bloco. type e default funcionam como esperado. source controla onde o valor fica armazenado. A referência de atributos explica que um valor sem source vai parar no comentário delimitador. Já source: "html" lê o HTML interno do elemento correspondente, que é o que os campos RichText normalmente usam.

Definição do atributoOnde o valor fica armazenado
Sem source (url)JSON dentro do comentário <!-- wp:... -->
source + selector (content)Extraído do HTML salvo a cada carregamento

É assim que o conteúdo armazenado do post fica (exemplo ilustrativo):

<!-- wp:openreplay/callout {"url":"https://example.com"} -->
<div class="wp-block-openreplay-callout"><p>Deploys freeze Friday at noon.</p></div>
<!-- /wp:openreplay/callout -->

supports ativa recursos do editor. html: false oculta a opção “Editar como HTML”. Chaves como align e color adicionam controles na barra de ferramentas e na barra lateral sem código adicional, conforme listado na referência de supports.

Caminhos file: são resolvidos em relação ao block.json que está sendo registrado. No caso, é a cópia em build/callout/, ao lado dos arquivos compilados index.js, index.css e style-index.css. É por isso que o PHP aponta para build, e não para src. A referência de metadados do bloco documenta todas as chaves.

edit.js: RichText, InspectorControls e useBlockProps

O componente edit é renderizado apenas dentro do editor. Ele recebe attributes e setAttributes como props. O setAttributes cumpre o mesmo papel de um setter de estado: mescla o objeto parcial que você passa e dispara uma nova renderização.

import { __ } from '@wordpress/i18n';
import { useBlockProps, RichText, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';

export default function Edit( { attributes, setAttributes } ) {
  const { content, url } = attributes;
  return (
    <>
      <InspectorControls>
        <PanelBody title={ __( 'Link', 'callout' ) }>
          <TextControl
            label={ __( 'URL', 'callout' ) }
            value={ url }
            onChange={ ( value ) => setAttributes( { url: value } ) }
          />
        </PanelBody>
      </InspectorControls>
      <div { ...useBlockProps() }>
        <RichText
          tagName="p"
          value={ content }
          onChange={ ( value ) => setAttributes( { content: value } ) }
          placeholder={ __( 'Write the callout…', 'callout' ) }
        />
      </div>
    </>
  );
}

O useBlockProps() adiciona os nomes de classe e os atributos de dados de que o editor precisa para seleção e estilização, então aplique-o com spread no elemento mais externo. O RichText permite que os usuários editem o texto dentro do próprio bloco. O InspectorControls move para a barra lateral as configurações que não são editadas diretamente no conteúdo, como uma URL. O PanelBody agrupa esses controles em uma seção recolhível.

save.js e o erro de validação de bloco

A função save produz a marcação armazenada no conteúdo do post. Ela precisa ser uma função pura dos atributos: sem estado, sem efeitos e sem hooks, com exceção de useBlockProps.save().

import { useBlockProps, RichText } from '@wordpress/block-editor';

export default function save( { attributes } ) {
  return (
    <div { ...useBlockProps.save() }>
      <RichText.Content tagName="p" value={ attributes.content } />
    </div>
  );
}

O RichText.Content gera o valor armazenado como marcação simples, e o useBlockProps.save() adiciona a classe do wrapper no front-end.

Por que a validação do bloco falha?

Quando um post é aberto no editor, o WordPress executa a função save atual do bloco com os atributos extraídos e compara o resultado com a marcação armazenada no post. O guia de Edit e Save considera inválido o bloco sempre que houver qualquer diferença entre essa nova saída e o HTML armazenado. O editor então exibe a mensagem “Este bloco contém conteúdo inesperado ou inválido” e registra um diff no console do navegador.

Alterar uma função save não reescreve os posts que já contêm o bloco. Suponha que a v2 adicione uma classe e um link:

export default function save( { attributes } ) {
  const { content, url } = attributes;
  return (
    <div { ...useBlockProps.save() }>
      <RichText.Content tagName="p" className="callout__body" value={ content } />
      { url && <a href={ url }>Learn more</a> }
    </div>
  );
}

Todos os callouts existentes agora falham na validação, porque a nova saída do save não corresponde mais à marcação antiga armazenada.

A solução: deprecated

Uma entrada deprecated corrige o erro de validação de bloco guardando os atributos anteriores e a função save anterior. Ela também pode incluir uma função migrate opcional, que converte os atributos antigos para um novo formato. Com essa entrada, o editor consegue reconhecer a marcação antiga e atualizá-la na próxima vez que o post for salvo.

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, RichText } from '@wordpress/block-editor';
import Edit from './edit';
import save from './save';
import metadata from './block.json';

const v1 = {
  attributes: {
    content: { type: 'string', source: 'html', selector: 'p' },
    url: { type: 'string', default: '' },
  },
  save( { attributes } ) {
    return (
      <div { ...useBlockProps.save() }>
        <RichText.Content tagName="p" value={ attributes.content } />
      </div>
    );
  },
};

registerBlockType( metadata.name, { edit: Edit, save, deprecated: [ v1 ] } );

Cada entrada carrega seus próprios attributes, porque a extração dos valores depende deles. Se a v2 também tivesse alterado o seletor de p para .callout__body, a v1 ainda precisaria de selector: 'p' para ler corretamente os posts antigos.

A referência de depreciação define estas regras:

  • Liste as entradas da mais recente para a mais antiga.
  • O editor testa cada entrada separadamente contra a marcação no estado em que foi salva. Uma entrada nunca repassa seu resultado para a próxima.
  • Se a saída do save de uma entrada não corresponder ao HTML armazenado, o editor ignora essa entrada, e o migrate dela nunca é executado.
  • Uma função opcional isEligible pode fazer uma entrada ser aplicada a um bloco que já passa na validação.

Três hábitos evitam a maioria dos erros de validação:

  1. Nunca coloque valores não determinísticos no save, como datas ou IDs aleatórios.
  2. Nunca leia estado ou contexto no save.
  3. Trate a saída do save como um schema de banco de dados: toda alteração precisa de uma migração.

O que é um bloco dinâmico?

Um bloco dinâmico retorna null no save, armazena apenas seus atributos no conteúdo do post e é renderizado pelo PHP a cada requisição. Escolha esse tipo quando a saída precisar mudar sem que alguém salve o post novamente, como em uma lista das entradas mais recentes.

Bloco estáticoBloco dinâmico
A marcação fica emConteúdo do postTemplate PHP
É atualizado quandoO post é salvo novamenteA cada requisição
save retornaJSXnull
Custo no servidorNenhum além de servir o conteúdoExecuta PHP a cada renderização

Crie um com npx @wordpress/create-block@latest callout --namespace=openreplay --variant dynamic ou adicione você mesmo "render": "file:./render.php" ao block.json. O template abaixo lista entradas de um custom post type. Ele pressupõe que você adicionou um atributo count do tipo number:

<?php
$query = new WP_Query( array(
	'post_type'      => 'project',
	'posts_per_page' => isset( $attributes['count'] ) ? (int) $attributes['count'] : 3,
) );
?>
<ul <?php echo get_block_wrapper_attributes(); ?>>
	<?php while ( $query->have_posts() ) : $query->the_post(); ?>
		<li><a href="<?php echo esc_url( get_permalink() ); ?>"><?php echo esc_html( get_the_title() ); ?></a></li>
	<?php endwhile; wp_reset_postdata(); ?>
</ul>

$attributes contém os atributos extraídos do bloco. get_block_wrapper_attributes() é o equivalente em PHP do useBlockProps. Se preferir uma função a um arquivo de template, passe um render_callback para register_block_type(). Blocos dinâmicos nunca geram erros de validação, porque o editor não tem marcação armazenada para comparar.

Conclusão

Um bloco é um componente React, mais uma declaração em JSON e um contrato de marcação armazenada. Para um desenvolvedor React, o componente é a parte fácil. É no contrato que os blocos quebram: quando posts reais passam a conter o seu bloco, toda alteração no save precisa de uma entrada deprecated correspondente. Crie a estrutura do callout, adicione uma classe à saída do save e observe o erro aparecer no console. Em seguida, escreva a depreciação que o elimina. Depois de ver esse ciclo uma vez, o erro de conteúdo inválido deixa de ser um mistério.

Perguntas frequentes

Um bloco personalizado do Gutenberg deve ficar em um plugin ou em um tema?

Coloque-o em um plugin. A marcação do bloco é salva no conteúdo do post, então o bloco precisa continuar registrado enquanto houver posts que o utilizem. Se o código que o registra for desativado, por exemplo ao trocar de tema, o editor exibe uma mensagem informando que o site não oferece suporte a esse bloco e oferece a opção de mantê-lo intacto ou removê-lo. Um plugin mantém o bloco disponível mesmo com trocas de tema.

O que a opção Tentar recuperar o bloco (Attempt Block Recovery) faz com um bloco inválido?

A opção Tentar recuperar o bloco reconstrói o bloco a partir dos atributos extraídos, usando a função save atual, e substitui a marcação armazenada. Ela corrige um bloco em um post por vez e descarta qualquer conteúdo que não esteja representado nos atributos. É uma forma de contornar o problema no editor, mas não corrige o código. Uma entrada deprecated permite que todos os posts existentes sejam atualizados sem que ninguém precise recuperar blocos manualmente.

Como adiciono um segundo bloco ao mesmo plugin?

Crie uma nova pasta dentro de src com seus próprios arquivos block.json, index.js, edit.js e save.js. Os comandos start e build do @wordpress/scripts procuram arquivos block.json em src e em suas subpastas, fazem o build de cada um como um entry point separado em uma pasta correspondente dentro de build e incluem todos os blocos no manifesto que o plugin registra. Reinicie o npm start depois de adicionar a pasta.

Um bloco dinâmico pode conter blocos aninhados?

Sim, mas nesse caso o save não pode retornar null. Renderize a área dos blocos filhos com InnerBlocks ou useInnerBlocksProps no edit e retorne InnerBlocks.Content no save, para que a marcação dos filhos seja armazenada no conteúdo do post. O template PHP recebe essa marcação já renderizada na variável $content, ao lado de $attributes. Cada bloco aceita apenas uma área de InnerBlocks, e o allowedBlocks limita quais blocos filhos os editores podem inserir.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

Nota: a tradução usa o português do Brasil, a variante mais comum em conteúdo técnico sobre WordPress. Se preferir o português europeu, posso adaptar o texto (por exemplo, “ficheiro” em vez de “arquivo” e “ecrã” em vez de “tela”).

We use cookies to improve your experience. By using our site, you accept cookies.