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:
- Entender JSX. JSX é HTML com superpoderes —
<div className="card">{ texto }</div>. Se você lê HTML, você lê JSX. As duas pegadinhas sãoclassNameno lugar declasse chaves{ }para inserir valores JavaScript. - Passar props. Seus componentes recebem
attributesesetAttributes. Você lê de um, escreve com o outro. É isso. - 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 úniconamespace/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.editorScriptroda no editor,stylevale para editor e front-end,viewScriptroda 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.jsgrava 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): osaveretornanulle umrender.phpgera 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 (oblock.jsonsó 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.