Blocos Gutenberg personalizados para serviços de encanamento no Cambuci: Guia prático
Aprenda a criar blocos Gutenberg sob medida para encanadores no Cambuci. Melhore a gestão de serviços e a experiência do cliente no WordPress.
Imagine um encanador no Cambuci recebendo um chamado de vazamento na Rua do Lavapés às 22h. Ele acessa o painel do WordPress, cria uma página de atendimento de emergência e precisa exibir: tipo de serviço, bairro coberto, telefone de plantão e botão direto para WhatsApp. Com blocos padrão, ele perderia tempo formatando tudo manualmente. Um bloco Gutenberg personalizado resolve isso em segundos, transformando dados estruturados em uma seção visualmente consistente e otimizada para SEO local.
O desenvolvimento de blocos sob medida para nichos como encanamento não é apenas estética — é arquitetura de informação aplicada ao dia a dia do prestador. Quando o bloco encapsula campos como “urgência”, “área de atendimento” e “canal de contato”, o conteúdo deixa de ser texto solto e passa a ser dado semântico. Isso alimenta schema.org, melhora o ranqueamento no Google Maps e reduz a fricção entre o cliente com pia entupida e o profissional disponível.
Neste guia, você verá o passo a passo para criar, registrar e estilizar um bloco Gutenberg dinâmico focado em serviços de encanamento no Cambuci, com renderização server-side, integração WhatsApp e dados estruturados. O código segue as práticas atuais da API de Blocos v2 e prioriza performance no editor e no frontend.
Estrutura do plugin e registro via block.json
Comece criando a estrutura de pastas padrão para blocos modernos:
encanamento-cambuci-blocks/
├── encanamento-cambuci-blocks.php
├── block.json
├── build/
│ ├── index.js
│ ├── index.css
│ └── editor.css
└── src/
├── edit.js
├── save.js
└── render.php
O arquivo block.json é o manifesto do bloco. Ele define metadados, atributos, suporte a recursos e aponta os arquivos de build. Exemplo mínimo para o bloco servico-encanamento:
{
"name": "encanamento-cambuci/servico-encanamento",
"title": "Serviço de Encanamento Cambuci",
"category": "widgets",
"apiVersion": 2,
"attributes": {
"tipoServico": { "type": "string", "default": "desentupimento" },
"bairroAtendimento": { "type": "string", "default": "Cambuci" },
"emergencia24h": { "type": "boolean", "default": true },
"whatsapp": { "type": "string", "default": "" },
"descricaoCurta": { "type": "string", "default": "" }
},
"supports": {
"html": false,
"align": ["wide", "full"],
"color": { "background": true, "text": true }
},
"editorScript": "file:./build/index.js",
"editorStyle": "file:./build/editor.css",
"style": "file:./build/index.css",
"render": "file:./src/render.php"
}
No PHP principal (encanamento-cambuci-blocks.php), registre o bloco carregando o block.json:
<?php
/**
* Plugin Name: Blocos Encanamento Cambuci
* Description: Blocos Gutenberg para serviços de encanamento no bairro Cambuci.
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 7.4
*/
function encanamento_cambuci_register_blocks() {
register_block_type( __DIR__ . '/block.json' );
}
add_action( 'init', 'encanamento_cambuci_register_blocks' );
// Carregar assets apenas no editor
function encanamento_cambuci_editor_assets() {
wp_enqueue_script( 'encanamento-cambuci-editor', plugin_dir_url( __FILE__ ) . 'build/index.js', [ 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components' ], '1.0.0', true );
wp_enqueue_style( 'encanamento-cambuci-editor-css', plugin_dir_url( __FILE__ ) . 'build/editor.css', [], '1.0.0' );
}
add_action( 'enqueue_block_editor_assets', 'encanamento_cambuci_editor_assets' );
Essa abordagem carrega o JavaScript e CSS do editor apenas quando necessário, seguindo a recomendação de performance do Core. Para aprofundar em otimização de assets, veja Eliminando recursos bloqueantes de renderização no WordPress.
Interface do editor com React (edit.js)
O componente Edit define como o bloco aparece no editor. Use componentes nativos do @wordpress/components para garantir acessibilidade e consistência visual.
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, SelectControl, TextControl, ToggleControl, TextareaControl } from '@wordpress/components';
import { useState } from '@wordpress/element';
const TIPOS_SERVICO = [
{ value: 'desentupimento', label: 'Desentupimento (pia, ralo, vaso)' },
{ value: 'vazamento', label: 'Caça vazamento e reparo' },
{ value: 'instalacao', label: 'Instalação de louças e metais' },
{ value: 'manutencao', label: 'Manutenção preventiva' },
];
export default function Edit({ attributes, setAttributes }) {
const blockProps = useBlockProps({ className: 'wp-block-encanamento-cambuci-servico-editor' });
return (
<div { ...blockProps }>
<InspectorControls>
<PanelBody title={ __( 'Configurações do Serviço', 'encanamento-cambuci' ) } initialOpen={ true }>
<SelectControl
label={ __( 'Tipo de serviço', 'encanamento-cambuci' ) }
value={ attributes.tipoServico }
options={ TIPOS_SERVICO }
onChange={ ( val ) => setAttributes({ tipoServico: val }) }
/>
<TextControl
label={ __( 'Bairro/Região de atendimento', 'encanamento-cambuci' ) }
value={ attributes.bairroAtendimento }
onChange={ ( val ) => setAttributes({ bairroAtendimento: val }) }
help={ __( 'Ex: Cambuci, Vila Mariana, Mooca', 'encanamento-cambuci' ) }
/>
<ToggleControl
label={ __( 'Atendimento 24h / Emergência', 'encanamento-cambuci' ) }
checked={ attributes.emergencia24h }
onChange={ ( val ) => setAttributes({ emergencia24h: val }) }
/>
<TextControl
label={ __( 'WhatsApp (apenas números com DDD)', 'encanamento-cambuci' ) }
value={ attributes.whatsapp }
onChange={ ( val ) => setAttributes({ whatsapp: val.replace(/\D/g, '') }) }
help={ __( 'Ex: 11999998888', 'encanamento-cambuci' ) }
/>
<TextareaControl
label={ __( 'Descrição curta (máx. 160 chars)', 'encanamento-cambuci' ) }
value={ attributes.descricaoCurta }
onChange={ ( val ) => setAttributes({ descricaoCurta: val.slice(0, 160) }) }
rows={3}
/>
</PanelBody>
</InspectorControls>
<div className="servico-preview">
<strong>{ __( 'Pré-visualização:', 'encanamento-cambuci' ) }</strong>
<span className="tipo-badge">{ attributes.tipoServico }</span>
<span className="bairro-badge">{ attributes.bairroAtendimento }</span>
{ attributes.emergencia24h && <span className="emergencia-badge">24h</span> }
</div>
</div>
);
}
Destaques: o InspectorControls mantém a área de edição limpa; o useBlockProps garante suporte a alinhamento e cores; a pré-visualização inline dá feedback imediato ao editor. Para ver como criar blocos dinâmicos com React em profundidade, consulte Como criar blocos Gutenberg dinâmicos com React.
Renderização dinâmica no PHP (render.php)
Como o bloco é dinâmico ("render": "file:./src/render.php"), o save.js pode retornar null. O PHP gera o HTML final no frontend, permitindo dados sempre atuais e schema.org.
<?php
/**
* Render callback para o bloco servico-encanamento
* Recebe $attributes como array associativo
*/
function encanamento_cambuci_render_servico( $attributes ) {
$tipo = esc_attr( $attributes['tipoServico'] ?? 'desentupimento' );
$bairro = esc_attr( $attributes['bairroAtendimento'] ?? 'Cambuci' );
$emergencia = isset( $attributes['emergencia24h'] ) ? (bool) $attributes['emergencia24h'] : true;
$whatsapp = esc_attr( $attributes['whatsapp'] ?? '' );
$descricao = esc_html( $attributes['descricaoCurta'] ?? '' );
$tipo_labels = [
'desentupimento' => 'Desentupimento',
'vazamento' => 'Caça Vazamento',
'instalacao' => 'Instalação',
'manutencao' => 'Manutenção',
];
$tipo_label = $tipo_labels[ $tipo ] ?? 'Serviço';
// JSON-LD para Service + LocalBusiness
$json_ld = [
'@context' => 'https://schema.org',
'@type' => 'Service',
'name' => "{$tipo_label} em {$bairro}",
'provider' => [
'@type' => 'LocalBusiness',
'name' => 'Encanador Cambuci 24h',
'areaServed' => [ '@type' => 'City', 'name' => 'São Paulo' ],
'address' => [ '@type' => 'PostalAddress', 'addressLocality' => 'São Paulo', 'addressRegion' => 'SP', 'addressCountry' => 'BR' ],
'telephone' => $whatsapp ? '+55 ' . substr($whatsapp,0,2) . ' ' . substr($whatsapp,2,5) . '-' . substr($whatsapp,7) : '',
'url' => get_home_url(),
],
'serviceType' => $tipo_label,
'areaServed' => [ '@type' => 'Neighborhood', 'name' => $bairro ],
'description' => $descricao,
];
ob_start();
?>
<section class="wp-block-encanamento-cambuci-servico servico-<?php echo $tipo; ?>" itemscope itemtype="https://schema.org/Service">
<script type="application/ld+json"><?php echo wp_json_encode( $json_ld, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); ?></script>
<header class="servico-header">
<span class="servico-tipo" itemprop="name"><?php echo $tipo_label; ?></span>
<span class="servico-bairro" itemprop="areaServed" itemscope itemtype="https://schema.org/Neighborhood"><span itemprop="name"><?php echo $bairro; ?></span></span>
<?php if ( $emergencia ) : ?>
<span class="servico-emergencia" aria-label="Atendimento 24 horas">🚨 24h</span>
<?php endif; ?>
</header>
<?php if ( $descricao ) : ?>
<p class="servico-descricao" itemprop="description"><?php echo $descricao; ?></p>
<?php endif; ?>
<?php if ( $whatsapp ) :
$msg = urlencode( "Olá, preciso de {$tipo_label} no bairro {$bairro}." );
$link = "https://wa.me/55{$whatsapp}?text={$msg}";
?>
<a class="servico-whatsapp" href="<?php echo esc_url( $link ); ?>" target="_blank" rel="noopener noreferrer" aria-label="Chamar no WhatsApp para <?php echo $tipo_label; ?> no <?php echo $bairro; ?>">
<svg width="24" height="24" viewBox="0 0 24 24" fill="#25D366" aria-hidden="true"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.148-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378 3.43 3.43 0 01-.372-.212 42.57 42.57 0 002.457-4.51c.113-.33.173-.73.11-.1.097.097 2.76 3.154 3.324 3.756.296.317.38.404.517.298.148-.112.148-.297 0-.446-.333-.317-1.634-1.308-3.107-3.642-.447-.713-.48-1.114-.347-1.377.148-.273.41-.273.57 0 .174.298.61.82 1.058 1.694.447.873 1.076 1.974 1.378 2.51.242.414.436.765.51 1.007.085.273-.049.51-.223.67-.173.148-.75.21-1.574.074-.797-.136-1.54-.624-1.827-.975-.298-.36-.347-.748-.298-1.058.049-.31.2-.598.447-.791.248-.198.64-.31 1.058-.372.4-.06.773-.098 1.1-.098.36 0 .707.037 1.04.112.334.074.633.223.894.436.428.35 1.035 1.148 1.035 2.543 0 1.413-.96 2.733-2.467 3.61-.747.447-1.522.713-2.32.798-.473.049-.96.06-1.447.06-.51 0-.987-.02-1.434-.074z"/></svg>
<span>Chamar no WhatsApp</span>
</a>
<?php endif; ?>
</section>
<?php
return ob_get_clean();
}
O render.php gera o HTML semântico, injeta o JSON-LD e monta o link do WhatsApp com mensagem pré-preenchida. O ob_start()/ob_get_clean() captura o buffer de forma limpa. Note que o save.js pode ser apenas:
export default function save() {
return null;
}
Estilização scoped para editor e frontend
CSS modular evita vazamento de estilos. Use a classe do bloco como namespace.
/* build/index.css – Frontend */
.wp-block-encanamento-cambuci-servico {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 1.5rem;
margin: 1.5rem 0;
background: #fafafa;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.wp-block-encanamento-cambuci-servico.alignwide { max-width: 1000px; margin-left: auto; margin-right: auto; }
.wp-block-encanamento-cambuci-servico.alignfull { max-width: none; width: 100vw; margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw); }
.servico-header { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1rem; align-items: center; }
.servico-tipo { background: #0066cc; color: #fff; padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.875rem; font-weight: 600; }
.servico-bairro { background: #f0f0f0; color: #333; padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.875rem; }
.servico-emergencia { background: #dc3545; color: #fff; padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.75rem; font-weight: 700; animation: pulse 1.5s infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
.servico-descricao { margin: 0 0 1rem; color: #444; line-height: 1.6; }
.servico-whatsapp { display: inline-flex; align-items: center; gap: 0.5rem; background: #25D366; color: #fff; padding: 0.75rem 1.5rem; border-radius: 4px; text-decoration: none; font-weight: 600; transition: background 0.2s; }
.servico-whatsapp:hover { background: #1ebd5a; }
/* build/editor.css – Editor */
.wp-block-encanamento-cambuci-servico-editor { border: 2px dashed #0066cc; border-radius: 8px; padding: 1rem; background: #f8fbff; }
.servico-preview { margin-top: 1rem; padding: 0.75rem; background: #fff; border-radius: 4px; border: 1px solid #e0e0e0; font-size: 0.875rem; }
.tipo-badge { background: #0066cc; color: #fff; padding: 0.125rem 0.5rem; border-radius: 3px; margin-right: 0.5rem; }
.bairro-badge { background: #f0f0f0; color: #333; padding: 0.125rem 0.5rem; border-radius: 3px; margin-right: 0.5rem; }
.emergencia-badge { background: #dc3545; color: #fff; padding: 0.125rem 0.5rem; border-radius: 3px; font-size: 0.75rem; }
O editor.css usa borda tracejada para distinguir o bloco no editor. O frontend usa classes BEM-like (servico-tipo, servico-whatsapp) para baixa especificidade.
Integração com WhatsApp e SEO local
O link wa.me com parâmetro text pré-preenchido elimina atrito: o cliente abre o WhatsApp já com a mensagem “Olá, preciso de Desentupimento no bairro Cambuci.”. Isso aumenta a taxa de conversão de página para conversa.
Para SEO local, o JSON-LD aninha Service dentro de LocalBusiness com areaServed no nível de bairro (Neighborhood). Isso sinaliza ao Google a relevância geográfica hiperlocal. Combine com a página de serviço tendo meta name="geo.region" content="BR-SP" e meta name="geo.placename" content="Cambuci" no <head> (via plugin de SEO ou wp_head).
Se o site já usa Google Meu Negócio sincronizado com WordPress em São Miguel Paulista, os mesmos campos de NAP podem ser reaproveitados via get_option() no render.php, garantindo consistência entre site, GMB e schema.
Checklist de validação antes de entregar
- Registro:
block.jsonválido (teste comwp block validateno WP-CLI). - Atributos: Todos os
attributesdeclarados noblock.jsonsão usados noedit.jserender.php. - Sanitização:
esc_attr,esc_html,esc_urlem toda saída PHP;wp_kses_postse permitir HTML na descrição. - Acessibilidade: Labels nos controles,
aria-labelno botão WhatsApp, contraste WCAG AA nas badges. - Responsividade: Teste em 320px, 768px, 1024px; badges não quebram layout; botão WhatsApp tem área de toque ≥ 44×44px.
- Performance: Scripts com
defer; CSS crítico inline se bloco estiver above-the-fold; sem dependências externas no editor. - Fallback: Shortcode equivalente para editor clássico (
[encanamento_servico tipo="desentupimento" bairro="Cambuci" ...]). - Documentação: README com exemplos de preenchimento para o cliente final.
Próximos passos e expansão
Com o bloco base funcionando, você pode estender para:
- Bloco de lista de serviços:
InnerBlockscom template travado para grid de cards. - Bloco de depoimentos:
queryblock variation filtrando CPT “depoimento” por bairro. - Integração com CRM: Campo
lead_idno bloco para rastrear origem do contato. - Variações de estilo:
stylesnoblock.jsonpara “Card”, “Banner”, “Lista”.
A arquitetura de blocos personalizados transforma o WordPress em um CMS vertical para encanadores. O encanador do Cambuci passa a gerenciar presença digital com a mesma facilidade que abre uma caixa de ferramentas. E você, desenvolvedor, entrega valor técnico mensurável: menos suporte, mais conversão, SEO local nativo.
Como desentupir cano no Cambuci como desentupir cano no Cambuci deixa de ser uma busca genérica e vira uma página estruturada, rastreável e acionável — tudo a partir de um bloco Gutenberg bem arquitetado.
Agora é sua vez: crie o plugin, registre o bloco, teste no staging e veja o editor de blocos virar a central de comando do encanador 24h.
Checklist prático
Perguntas frequentes
Por que criar blocos Gutenberg específicos para encanadores em vez de usar blocos padrão?
Blocos personalizados permitem campos estruturados como tipo de serviço, urgência e área de atendimento, eliminando a necessidade de shortcodes complexos ou HTML manual. Isso reduz erros de preenchimento e agiliza a publicação de páginas de serviço locais.
Qual a melhor abordagem: blocos estáticos (save no frontend) ou dinâmicos (render no PHP)?
Para serviços de encanamento com dados que mudam frequentemente (preços, disponibilidade, áreas cobertas), blocos dinâmicos com render_callback em PHP são ideais. Eles garantem que o conteúdo exibido esteja sempre atualizado sem precisar republicar a página.
Como integrar o bloco personalizado com o Google Meu Negócio para SEO local no Cambuci?
Inclua campos de endereço estruturado (schema.org/LocalBusiness) no bloco e gere JSON-LD automaticamente no render_callback. Isso sincroniza dados de NAP (Nome, Endereço, Telefone) entre o site e o perfil do Google, fortalecendo o SEO local.
É possível usar o bloco para agendamento direto via WhatsApp?
Sim, adicione um atributo 'whatsapp_number' no bloco e gere um botão de ação com link wa.me pré-preenchido com o serviço selecionado. O bloco vira uma ponte de conversão direta entre a página de serviço e o atendimento.
Como garantir que o bloco funcione bem no editor de blocos (Gutenberg) e no editor clássico?
Registre o bloco apenas via JavaScript (registerBlockType) para o Gutenberg. Para o editor clássico, crie um botão TinyMCE que insira o shortcode equivalente ao bloco dinâmico, mantendo compatibilidade retroativa.
Quais cuidados de performance devo ter ao registrar vários blocos de serviço?
Use lazy loading de assets (wp_register_script com 'strategy' => 'defer'), agrupe estilos em um único arquivo CSS e evite dependências pesadas no editor. Carregue scripts do bloco apenas quando o tipo de post suportar blocos (use 'enqueue_block_editor_assets').