WordPress Extremo
- Como Criar um Plugin WordPress com Composer e PSR-4 – WordPress Extremo Dia 1
- Como Usar Serviços em Plugins para Código Limpo e Desacoplado – WordPress Extremo Dia 2
- Como Usar Repositórios em Plugins para Separar Lógica de Dados – WordPress Extremo Dia 3
- Injeção de Dependência Manual em Plugins WordPress – WordPress Extremo Dia 4
- Hooks Avançados no WordPress: apply_filters, do_action e boas práticas
- Criando Comandos WP-CLI Personalizados para Plugins WordPress
- Criando Blocos Personalizados com Gutenberg e React
- Estilizando Blocos Gutenberg com CSS e Classes Dinâmicas
- Cor, Alinhamento e Estilo Dinâmico com Gutenberg + React
- Ícones, Imagens e Classes Personalizadas no Gutenberg
- Componentes Reutilizáveis e Atributos Compostos em Blocos Gutenberg
Fala dev! No dia 12 da nossa trilha WordPress Extremo, a parada ficou séria: vamos montar um bloco chamado Card, com:
- imagem destacada
- título
- descrição
- botão com texto + link
Tudo isso com:
✅ Componentes React reutilizáveis
✅ Atributos compostos (objeto)
✅ Estrutura modular e segura
✅ Registro correto no PHP
✅ Webpack com build organizado em /dist
✅ O que você vai aprender
- Como criar e registrar blocos personalizados com
@wordpress/scripts
- Como separar partes do bloco em componentes (estilo profissional)
- Como usar
TextareaControl
,MediaUpload
,InspectorControls
- Como configurar corretamente
webpack.config.js
para não apagar arquivos - Como registrar os blocos via PHP usando
register_block_type()
ewp_register_script()
📁 Estrutura do Bloco card
/blocks/card/
├── block.json
├── index.js
├── edit.js
├── save.js
├── style.css
├── editor.css
└── components/
├── ImageUploader.js
└── ButtonLink.js
/dist/
├── card.js
├── card.asset.php
📄 Arquivos do Bloco
📌 block.json
{
"apiVersion": 2,
"name": "wp24h/card",
"title": "Card Personalizado WP24H",
"category": "widgets",
"icon": "format-image",
"description": "Um card com imagem, título, descrição e botão. Com componentes React e atributos compostos.",
"editorScript": "file:../../dist/card.js",
"style": "file:./style.css",
"editorStyle": "file:./editor.css",
"attributes": {
"imagem": { "type": "string", "default": "" },
"titulo": { "type": "string", "default": "Título do card" },
"descricao": { "type": "string", "default": "Descrição do conteúdo do card." },
"botao": {
"type": "object",
"default": {
"texto": "Saiba mais",
"url": "#"
}
}
},
"supports": {
"align": true,
"html": false
},
"keywords": ["card", "wp24h", "imagem", "botão"]
}
📌 index.js
import { registerBlockType } from '@wordpress/blocks';
import edit from './edit';
import save from './save';
import metadata from './block.json';
registerBlockType(metadata.name, {
...metadata,
edit,
save,
});
📌 edit.js
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl, TextareaControl } from '@wordpress/components';
import ImageUploader from './components/ImageUploader';
import ButtonLink from './components/ButtonLink';
export default function Edit({ attributes, setAttributes }) {
const { imagem, titulo, descricao, botao } = attributes;
return (
<>
<InspectorControls>
<PanelBody title="Conteúdo do Card">
<TextControl
label="Título"
value={titulo}
onChange={(val) => setAttributes({ titulo: val })}
/>
<TextareaControl
label="Descrição"
value={descricao}
onChange={(val) => setAttributes({ descricao: val })}
/>
<ButtonLink
texto={botao.texto}
url={botao.url}
onChange={(novo) => setAttributes({ botao: novo })}
/>
</PanelBody>
</InspectorControls>
<div {...useBlockProps()} className="wp-block-wp24h-card">
<ImageUploader imagem={imagem} onChange={(val) => setAttributes({ imagem: val })} />
<h3>{titulo}</h3>
<p>{descricao}</p>
<a href={botao.url} className="btn-link">{botao.texto}</a>
</div>
</>
);
}
📌 save.js
import { useBlockProps } from '@wordpress/block-editor';
export default function save({ attributes }) {
const { imagem, titulo, descricao, botao } = attributes;
return (
<div {...useBlockProps.save()} className="wp-block-wp24h-card">
{imagem && <img src={imagem} alt="" />}
<h3>{titulo}</h3>
<p>{descricao}</p>
<a href={botao.url} className="btn-link">{botao.texto}</a>
</div>
);
}
📌 components/ImageUploader.js
import { MediaUpload } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
export default function ImageUploader({ imagem, onChange }) {
return (
<div className="image-uploader">
{imagem && <img src={imagem} alt="Imagem do card" style={{ maxWidth: '100%' }} />}
<MediaUpload
onSelect={(media) => onChange(media.url)}
allowedTypes={['image']}
render={({ open }) => (
<Button onClick={open} variant="secondary">
{imagem ? 'Trocar imagem' : 'Selecionar imagem'}
</Button>
)}
/>
</div>
);
}
📌 components/ButtonLink.js
import { TextControl } from '@wordpress/components';
export default function ButtonLink({ texto, url, onChange }) {
return (
<div className="button-config">
<TextControl
label="Texto do botão"
value={texto}
onChange={(val) => onChange({ texto: val, url })}
/>
<TextControl
label="Link do botão"
value={url}
onChange={(val) => onChange({ texto, url: val })}
/>
</div>
);
}
📌 style.css
.wp-block-wp24h-card {
padding: 1.5rem;
background-color: #ffffff;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
max-width: 480px;
margin: 1rem auto;
font-family: sans-serif;
text-align: center;
}
.wp-block-wp24h-card img {
max-width: 100%;
border-radius: 6px;
margin-bottom: 1rem;
}
.wp-block-wp24h-card h3 {
font-size: 1.4rem;
margin: 0.5rem 0;
color: #222;
}
.wp-block-wp24h-card p {
font-size: 1rem;
color: #444;
margin-bottom: 1rem;
}
.wp-block-wp24h-card .btn-link {
display: inline-block;
background: #007cba;
color: #fff;
padding: 0.6rem 1rem;
text-decoration: none;
border-radius: 4px;
transition: background 0.3s ease;
}
.wp-block-wp24h-card .btn-link:hover {
background: #005a8d;
}
📌 editor.css
.wp-block-wp24h-card {
outline: 1px dashed #bbb;
background-color: #f9f9f9;
}
.image-uploader img {
max-width: 100%;
margin-bottom: 1rem;
border-radius: 6px;
}
✅ Registro via PHP (organizado no Init.php)
namespace WpArquiteturaExtrema\Hooks;
class Init {
public function register() {
add_action('init', [$this, 'init_plugin']);
add_action('init', [$this, 'register_blocks']);
}
public function init_plugin() {
// lógica do plugin
}
public function register_blocks() {
$blocks = ['card', 'hello'];
foreach ($blocks as $block) {
$asset_path = __DIR__ . "/../../dist/{$block}.asset.php";
if (!file_exists($asset_path)) continue;
$asset = include $asset_path;
wp_register_script(
"wp24h-{$block}",
plugins_url("dist/{$block}.js", dirname(__DIR__, 2)),
$asset['dependencies'],
$asset['version']
);
register_block_type(__DIR__ . "/../../blocks/{$block}", [
'editor_script' => "wp24h-{$block}"
]);
}
}
}
⚙️ Sobre o Webpack e o Build Seguro
Quando usamos @wordpress/scripts
, é o Webpack quem cuida da compilação dos arquivos JS do bloco.
O problema? Se você não configurar direito, ele pode:
- Apagar suas pastas (
blocks/card
) - Ignorar arquivos (se você buildar antes de criá-los)
- Ou gerar JS que o WordPress não entende
🧨 O erro mais comum
Muita gente (e até dev experiente) usa isso no webpack.config.js
:
output: {
path: path.resolve(__dirname, 'blocks'),
filename: '[name]/build/[name].js'
}
Isso faz o Webpack sobrescrever (e até apagar) arquivos dentro da pasta blocks/
a cada build.
✅ A solução profissional
Separar o JS compilado em uma pasta externa:
/dist
📄 Seu webpack.config.js
deve ser assim:
const path = require('path');
const defaultConfig = require('@wordpress/scripts/config/webpack.config');
module.exports = {
...defaultConfig,
entry: {
'card': path.resolve(__dirname, 'blocks/card/index.js'),
'hello': path.resolve(__dirname, 'blocks/hello/index.js')
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
clean: true
}
};
🎯 Por que usar /dist
?
✅ Evita apagar arquivos importantes (como seus .js
, .json
, .css
) dentro de blocks/
✅ Centraliza todos os scripts prontos para uso
✅ Facilita deploy, versionamento e debug
✅ É o padrão em projetos reais (inclusive plugins no repositório oficial)
📌 Depois disso:
- Certifique que no
editorScript
noblock.json
esteja assim:
"editorScript": "file:../../dist/card.js"
- E rode:
npm run build
Ou para build automático enquanto edita:
npm run start
✅ Desse momo temos:
✔️ Bloco seguro, escalável e funcional
✔️ Tudo registrado corretamente
✔️ Componentes reaproveitáveis
✔️ Build confiável com Webpack moderno
✔️ Estrutura pronta pra produção real
🎯 Resultado final
✅ Bloco “Card” aparece no editor
✅ Totalmente funcional
✅ Seguro pra build
✅ Componentes organizados
✅ Setup profissional
Se você gostou desse conteúdo, deixe seu comentário abaixo.