Como criar blocos customizados no Gutenberg (com código)

Aprenda a criar blocos customizados no Gutenberg do zero com create-block, block.json e React. Bloco estático vs dinâmico, com código real e portável.

Para criar blocos customizados no Gutenberg, gere o scaffold com @wordpress/create-block, descreva o bloco no block.json (apiVersion 3) e escreva edit e save em React/JSX. O resultado é HTML nativo e portável gravado no banco — sem lock-in de plugin.

Bloco customizado do Gutenberg é um componente próprio, registrado via block.json, que adiciona um tipo de conteúdo ao editor do WordPress e grava HTML padrão no banco de dados — sem depender de plugins de terceiros para renderizar.

Todo tutorial de “blocos customizados” que começa com “instale este plugin de blocos” está resolvendo o problema errado. O jeito que os profissionais fazem — e o que o próprio WordPress documenta — é construir um bloco nativo: um pacote com block.json, edit.js e save.js, compilado pelo toolchain oficial. É mais simples do que parece, não exige ser mago de React, e entrega uma coisa que nenhum page builder entrega: HTML seu, limpo, que sobrevive à troca de tema, plugin ou agência. Este guia constrói um do zero, com código real que roda.

O que é um bloco nativo (e por que não é um plugin de blocos)

Quando você cria um bloco nativo, o WordPress salva no banco de dados um HTML comum, delimitado por um comentário de bloco:

<!-- wp:pixelize/card -->
<div class="wp-block-pixelize-card"><p>Seu conteúdo</p></div>
<!-- /wp:pixelize/card -->

Esse comentário é a chave. Ele é só um marcador — o conteúdo entre as tags é HTML padrão. Se amanhã o bloco deixar de existir, o WordPress ainda renderiza o HTML de dentro. Nada quebra. É o oposto do que acontece com as alternativas populares:

  • ACF Blocks e Lazy Blocks deixam você montar blocos por interface, mas o HTML final é gerado por um template PHP do plugin em cada request. Desative o plugin e o conteúdo evapora.
  • Elementor e outros page builders gravam a estrutura como um blob serializado ou uma montanha de <div> aninhadas com classes próprias. O conteúdo fica refém do produto: sem o plugin ativo, você vê shortcodes órfãos ou uma sopa de divs sem estilo.

A diferença técnica é lock-in. Bloco nativo grava HTML que é do WordPress, não do plugin. É por isso que a arquitetura importa: um bloco nativo é DOM leve, compatível com Full Site Editing (FSE) e theme.json, e portável entre projetos. Essa é a prova técnica de por que recomendamos Gutenberg em vez de page builders — o argumento completo está em Elementor vs Gutenberg.

Vale a distinção: ACF Blocks resolve casos legítimos (protótipos rápidos, times sem front-end). Mas quando o objetivo é performance, portabilidade e manutenção de longo prazo, o bloco nativo é o padrão-ouro — e é o que este guia ensina.

”Preciso ser expert em React?”

Não. Esse é o maior mito que trava desenvolvedores na porta do Gutenberg.

Você não precisa saber Redux, hooks avançados, Suspense, Server Components ou qualquer coisa do ecossistema React moderno. O que você precisa é bem menor:

  1. Entender JSX. JSX é HTML com superpoderes — <div className="card">{ texto }</div>. Se você lê HTML, você lê JSX. As duas pegadinhas são className no lugar de class e chaves { } para inserir valores JavaScript.
  2. Passar props. Seus componentes recebem attributes e setAttributes. Você lê de um, escreve com o outro. É isso.
  3. Usar um punhado de componentes do WordPress. useBlockProps, RichText, InspectorControls, TextControl. Todos documentados, todos com a mesma cara. Você copia o padrão e adapta.

O toolchain faz o trabalho chato: o @wordpress/create-block gera o projeto inteiro e o @wordpress/scripts cuida do build (Webpack, Babel, JSX → JavaScript). Você nunca configura nada disso. Um desenvolvedor que domina HTML, CSS e JavaScript básico registra o primeiro bloco funcional em uma tarde. Bora provar.

Pré-requisitos e ambiente

Você precisa de Node.js e npm instalados. O toolchain do WordPress pede Node 20 ou superior. Confira as versões:

node -v
# v20.11.0  (20.x ou superior)

npm -v
# 10.2.4

Se os comandos retornarem versões válidas, está pronto. Se não, instale o Node pelo site oficial ou via um gerenciador de versões como o nvm. Você também vai precisar de uma instalação WordPress rodando (local, via wp-env, Local, Docker ou staging) para instalar e testar o plugin gerado.

Passo a passo com @wordpress/create-block

A partir daqui é mão na massa. Vamos criar um bloco chamado pixelize/card — um cartão simples com título editável — e evoluí-lo até um bloco com controles e versão dinâmica.

1. Scaffold do projeto

O @wordpress/create-block gera todo o projeto com um comando. Rode-o dentro da pasta wp-content/plugins da sua instalação (ou em qualquer lugar e depois mova a build):

npx @wordpress/create-block pixelize-card --namespace pixelize
cd pixelize-card

O --namespace pixelize define o prefixo do bloco (pixelize/...), evitando colisão com blocos de outros autores. O comando baixa as dependências, cria a estrutura e já deixa um plugin funcional pronto para ativar.

2. Estrutura de arquivos gerada

Ao terminar, você tem algo assim:

pixelize-card/
├── build/                 # saída compilada (gerada pelo build; não edite)
├── src/
   ├── block.json         # manifesto do bloco
   ├── index.js           # registra o bloco (registerBlockType)
   ├── edit.js            # componente do editor
   ├── save.js            # o que é gravado no banco
   ├── style.scss         # estilo do front-end + editor
   ├── editor.scss        # estilo só do editor
   └── view.js            # JS que roda no front-end
├── pixelize-card.php      # plugin: register_block_type no init
├── package.json
└── readme.txt

Você vai editar quase sempre três arquivos: block.json, edit.js e save.js. O resto o toolchain administra.

3. block.json — o manifesto do bloco

O block.json é o coração do bloco na apiVersion 3: a fonte única de metadados. É ele que o WordPress lê para registrar o bloco, enfileirar scripts e estilos sob demanda e manter compatibilidade com FSE. Abra src/block.json e ajuste:

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "pixelize/card",
	"version": "0.1.0",
	"title": "Pixelize Card",
	"category": "design",
	"icon": "id-alt",
	"description": "Um cartão customizado da Pixelize.",
	"textdomain": "pixelize-card",
	"supports": {
		"html": false
	},
	"attributes": {
		"content": {
			"type": "string",
			"source": "html",
			"selector": "p"
		}
	},
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css",
	"viewScript": "file:./view.js",
	"render": "file:./render.php"
}

Os campos que importam:

  • apiVersion: 3 — a versão atual da API de blocos, com melhor isolamento do iframe do editor.
  • name — o identificador único namespace/slug. É o que aparece no comentário <!-- wp:pixelize/card -->.
  • attributes — os dados que o bloco guarda. Aqui, content é lido do HTML da tag <p> gravada pelo save.
  • editorScript / style / viewScript — o WordPress carrega cada asset só quando o bloco está em uso. editorScript roda no editor, style vale para editor e front-end, viewScript roda só no front-end.
  • render — o PHP que gera o HTML no front-end (usado na versão dinâmica; veja mais abaixo).

4. Registrar o bloco

No JavaScript, o src/index.js importa os metadados do block.json e conecta edit e save:

import { registerBlockType } from '@wordpress/blocks';

import './style.scss';
import Edit from './edit';
import save from './save';
import metadata from './block.json';

registerBlockType( metadata.name, {
	edit: Edit,
	save,
} );

No PHP, o pixelize-card.php registra o bloco no hook init, apontando para a pasta build (onde fica o block.json compilado):

<?php
/**
 * Plugin Name: Pixelize Card
 * Description: Bloco customizado nativo da Pixelize.
 * Version:     0.1.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

function pixelize_card_block_init() {
	register_block_type( __DIR__ . '/build/pixelize-card' );
}
add_action( 'init', 'pixelize_card_block_init' );

O register_block_type recebe o caminho da pasta, não um array de config — ele lê o block.json de lá e faz todo o trabalho de registro e enfileiramento.

5. edit.js — a experiência no editor

O edit.js define como o bloco aparece e se comporta dentro do editor. Dois protagonistas: useBlockProps, que injeta as classes e atributos que o WordPress espera no elemento raiz, e RichText, um campo de texto editável com formatação:

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

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

	return (
		<RichText
			{ ...blockProps }
			tagName="p"
			value={ attributes.content }
			onChange={ ( content ) => setAttributes( { content } ) }
			placeholder={ __( 'Escreva o título do cartão…', 'pixelize-card' ) }
		/>
	);
}

O value lê o atributo content; o onChange escreve de volta com setAttributes a cada tecla. Esse é o ciclo de dados do Gutenberg inteiro em quatro linhas.

6. save.js — o que é gravado no banco

O save.js define o HTML que vai para o banco de dados. Aqui usamos useBlockProps.save() (a versão para o HTML salvo) e RichText.Content, que serializa o texto formatado:

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

export default function save( { attributes } ) {
	const blockProps = useBlockProps.save();

	return (
		<RichText.Content
			{ ...blockProps }
			tagName="p"
			value={ attributes.content }
		/>
	);
}

É este HTML — <p class="wp-block-pixelize-card">…</p> — que fica salvo entre os comentários de bloco. Limpo, semântico, portável. Um detalhe da apiVersion 3: como o block.json declara render, o WordPress usa o render.php para montar o front-end e passa este HTML do save como $content. Para um bloco 100% estático, remova a linha render do block.json e o HTML acima é servido direto, sem PHP. Vamos voltar a isso na seção de blocos dinâmicos.

7. Build e teste

Com tudo no lugar, o @wordpress/scripts compila. Em desenvolvimento, use o modo watch, que recompila a cada alteração:

npm start

Ative o plugin Pixelize Card no wp-admin, abra o editor de posts e insira o bloco “Pixelize Card”. Para gerar a versão final otimizada (minificada) para produção:

npm run build

Pronto — seu primeiro bloco nativo está funcionando. Agora vamos deixá-lo profissional.

Adicionando controles com InspectorControls

Todo bloco sério tem opções na barra lateral: cor, link, alinhamento. Isso é o InspectorControls, o painel da direita do editor. Você o preenche com PanelBody (seções colapsáveis) e controles como TextControl.

Primeiro, declare o novo atributo no block.json, dentro de attributes:

"attributes": {
	"content": {
		"type": "string",
		"source": "html",
		"selector": "p"
	},
	"buttonUrl": {
		"type": "string",
		"default": ""
	}
}

Depois, atualize o edit.js para renderizar o painel e escrever no atributo:

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 blockProps = useBlockProps();

	return (
		<>
			<InspectorControls>
				<PanelBody title={ __( 'Configurações do cartão', 'pixelize-card' ) }>
					<TextControl
						label={ __( 'Link do botão', 'pixelize-card' ) }
						value={ attributes.buttonUrl }
						onChange={ ( buttonUrl ) => setAttributes( { buttonUrl } ) }
					/>
				</PanelBody>
			</InspectorControls>

			<div { ...blockProps }>
				<RichText
					tagName="p"
					value={ attributes.content }
					onChange={ ( content ) => setAttributes( { content } ) }
					placeholder={ __( 'Escreva o título do cartão…', 'pixelize-card' ) }
				/>
			</div>
		</>
	);
}

O padrão se repete para qualquer controle: value lê o atributo, onChange chama setAttributes. Troque TextControl por ColorPalette, ToggleControl ou SelectControl e você tem qualquer opção que imaginar. O fragmento <>…</> envolve o painel e o conteúdo porque o InspectorControls é “teletransportado” para a barra lateral — ele não aparece dentro do bloco.

Bloco estático vs dinâmico

Essa é a decisão de arquitetura mais importante ao criar um bloco. A regra:

Use estático quando o HTML é fixo no momento em que se escreve (cartão, destaque, CTA, citação): o save.js grava o HTML final no banco e ele é servido como está, sem PHP. Use dinâmico quando o conteúdo muda a cada request (últimos posts, preço, contador, dados do usuário logado): o save retorna null e um render.php gera o HTML na hora.

Estático é mais rápido (nada de PHP por request) e mais robusto. Dinâmico é indispensável quando o dado não existe no momento de salvar. A renderização server-side de blocos via render.php é suportada de forma nativa desde o WordPress 6.1.

Convertendo para dinâmico

Duas mudanças. Primeiro, o save.js deixa de gravar HTML e retorna null — o conteúdo passa a ser responsabilidade do PHP:

export default function save() {
	return null;
}

Segundo, garanta que o block.json aponte para o arquivo de render (já incluído no nosso manifesto):

{
	"render": "file:./render.php"
}

Agora crie o src/render.php. Ele recebe três variáveis prontas: $attributes (os atributos do bloco), $content (o conteúdo interno, quando há) e $block (a instância). Use get_block_wrapper_attributes() para gerar as classes e atributos que o WordPress espera no wrapper — o equivalente PHP do useBlockProps:

<?php
/**
 * @var array    $attributes Atributos do bloco.
 * @var string   $content    Conteúdo interno do bloco.
 * @var WP_Block $block      Instância do bloco.
 */

$card_content = $attributes['content'] ?? '';
$button_url   = $attributes['buttonUrl'] ?? '';
?>
<div <?php echo get_block_wrapper_attributes(); ?>>
	<p><?php echo wp_kses_post( $card_content ); ?></p>

	<?php if ( $button_url ) : ?>
		<a class="wp-block-pixelize-card__button" href="<?php echo esc_url( $button_url ); ?>">
			<?php esc_html_e( 'Saiba mais', 'pixelize-card' ); ?>
		</a>
	<?php endif; ?>
</div>

Repare no cuidado com segurança: wp_kses_post sanitiza o HTML do conteúdo, esc_url valida a URL, esc_html_e escapa e traduz o texto. Toda saída dinâmica passa por escape — regra de ouro do WordPress. O get_block_wrapper_attributes() garante que o bloco continue compatível com os suportes do theme.json (cor, espaçamento, tipografia) sem você reescrever nada.

Por que isso importa: HTML portável, zero lock-in e Core Web Vitals

Recapitulando o que você construiu e por que ele vence um page builder em cada eixo que importa:

  • HTML portável. O bloco grava markup padrão no banco, dentro de um comentário <!-- wp:pixelize/card -->. Trocou de tema? O HTML continua lá. Desativou o plugin? O conteúdo estático permanece renderizável. Não há blob serializado nem shortcode órfão. Isso é zero lock-in — o oposto de amarrar o site a um produto.
  • DOM leve e Core Web Vitals. Você controla exatamente o HTML gerado — sem as camadas de <div> aninhadas que os page builders empilham. Menos DOM significa menos trabalho de layout e paint, o que melhora diretamente LCP e CLS. Some a isso o carregamento condicional de assets (o block.json só enfileira o CSS/JS quando o bloco está na página) e você tem uma base rápida por design.
  • Compatível com FSE e theme.json. Porque é nativo, o bloco participa do Full Site Editing e herda os estilos globais do theme.json — cores, espaçamentos e tipografia definidos uma vez, aplicados em tudo.

É por isso que blocos nativos são a fundação técnica de sites WordPress bem construídos — e a razão de a Pixelize preferir Gutenberg. Se o seu projeto precisa de blocos sob medida, performance de verdade e um site que você (e não um plugin) controla, é exatamente o trabalho que fazemos no serviço de desenvolvimento WordPress. E se o objetivo é evitar as dores do caminho, vale conhecer também os erros comuns do WordPress.

Perguntas frequentes

Preciso ser expert em React para criar um bloco no Gutenberg?

Não. Você precisa entender JSX (que é quase HTML), passar props e usar um punhado de hooks e componentes do WordPress — useBlockProps, RichText, setAttributes. O @wordpress/create-block gera todo o scaffold e o build; você só edita edit.js e save.js. Um dev que conhece HTML, CSS e JavaScript básico registra o primeiro bloco em uma tarde.

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

Um bloco estático grava o HTML final no banco de dados através do save.js — é servido exatamente como foi salvo, sem PHP em cada request. Um bloco dinâmico tem save retornando null e um render.php que gera o HTML a cada carregamento, usando $attributes e $content. Use estático para conteúdo fixo (cartão, CTA) e dinâmico para conteúdo que muda (últimos posts, preço, dados do usuário).

Bloco nativo é a mesma coisa que ACF Blocks ou Lazy Blocks?

Não. ACF Blocks, Lazy Blocks e blocos de Elementor dependem do plugin instalado para renderizar. Se você desativa o plugin, o conteúdo quebra ou vira shortcode órfão. Um bloco nativo registrado via block.json grava HTML padrão do WordPress no banco (com o comentário <!-- wp:pixelize/card -->) e não depende de nenhum plugin de terceiros para existir.

O que é o block.json e por que ele é obrigatório na apiVersion 3?

O block.json é o manifesto do bloco: define nome, título, categoria, ícone, atributos e quais scripts e estilos carregar (editorScript, style, viewScript, render). A partir da apiVersion 3, é a fonte única de metadados — o WordPress lê o arquivo para registrar o bloco, enfileirar assets sob demanda e manter compatibilidade com Full Site Editing e theme.json.

Preciso de um plugin para registrar o bloco ou dá para colocar no tema?

Os dois funcionam. O @wordpress/create-block gera um plugin com um register_block_type no init, que é o caminho recomendado para blocos reutilizáveis e portáveis entre temas. Você também pode registrar a partir do functions.php do tema apontando register_block_type para a pasta build. Plugin isola a lógica do design; tema acopla o bloco ao visual.

Por que a Pixelize recomenda blocos nativos em vez de Elementor?

Porque bloco nativo gera HTML limpo e portável no banco, DOM leve, zero dependência de plugin e compatibilidade total com Full Site Editing e theme.json. Isso se traduz em Core Web Vitals melhores, manutenção previsível e nenhum lock-in — se o tema ou a agência mudam, o conteúdo continua íntegro. Page builders empilham divs e amarram o site ao produto.

Serviços relacionados

Continue com a Pixelize

Do diagnóstico à execução — a Pixelize resolve o que este guia só mostra o caminho.