Campos personalizados (custom fields) são a forma nativa de anexar dados estruturados a um artigo, página ou CPT no WordPress. Ficam na tabela wp_postmeta e leem-se com a Metadata API - em especial get_post_meta(). Em projetos de agência em Portugal (lojas WooCommerce com NIF e morada de faturação, listagens imobiliárias, agendas de WordCamps ou sites institucionais) quase tudo o que não é título nem conteúdo acaba como meta.
Este guia cobre o ciclo completo: leitura com get_post_meta versus o legado the_meta, escape de saída, escrita segura, ACF quando faz sentido, temas de blocos com render_block, registo REST com register_post_meta, e o cache que o núcleo já faz por si. Sem framework «para o dia em que precise»: primeiro a API nativa, depois a camada de conveniência.
Saiba mais sobre desenvolvimento WordPress profissional na WPPoland.
Fundamentos: get_post_meta()
A função get_post_meta() é o alicerce do trabalho com campos personalizados:
// Valor único
$value = get_post_meta( $post_id, 'meta_key', true );
// Array de valores com a mesma chave
$values = get_post_meta( $post_id, 'meta_key', false );
// Todos os metadados do artigo
$all_meta = get_post_meta( $post_id );Parâmetros
| Parâmetro | Tipo | Descrição |
|---|---|---|
$post_id | int | ID do artigo |
$key | string | Nome da chave meta |
$single | bool | true = um valor, false = array |
O terceiro argumento é fonte clássica de bugs. Com $single = true, meta em falta devolve string vazia. Com false, recebe sempre um array - mesmo com um só elemento. Código que faz if ( $value ) comporta-se de forma diferente em string e em array.
Prefixe as chaves com o nome do projeto (wppoland_nif, cliente_morada_faturacao). Evite nomes genéricos como price, date ou city sem namespace: WooCommerce, Yoast ou um plugin de eventos acabam por colidir. Em sites PT que já passaram por três agências, um inventário GROUP BY meta_key na base de dados mostra depressa que chaves são suas e quais ficaram de plugins removidos.
get_post_meta() versus the_meta()
the_meta() é uma função legada do núcleo que imprime uma lista HTML (<ul class="post-meta">) com todas as chaves meta do artigo atual no loop. Não recebe chave específica, não escapa com a mesma disciplina que um template moderno exige, e mistura apresentação com dados. A documentação em developer.wordpress.org marca o padrão: para qualquer output controlado no tema, use get_post_meta() (ou wrappers seus) e decida o markup.
// Legado: imprime tudo o que existir em postmeta do post atual
the_meta();
// Preferido: lê uma chave, decide o HTML, escapa no contexto certo
$nif = get_post_meta( get_the_ID(), 'wppoland_nif', true );
if ( $nif ) {
echo '<p class="nif">' . esc_html( $nif ) . '</p>';
}Reserve the_meta() para temas antigos que ainda a chamam, ou para uma vista de debug em staging. Em produção, templates de arquivo, single CPT e partials de cartão de produto devem ler chaves explícitas. Assim evita expor meta interna (IDs de ERP, tokens de integração, notas de redação) que alguém gravou sem prefixo e sem register_post_meta com restrições.
Exibir campos personalizados no tema
No loop WordPress
<?php if ( have_posts() ) : while ( have_posts() ) : the_post(); ?>
<h2><?php the_title(); ?></h2>
<?php
$sku = get_post_meta( get_the_ID(), 'wppoland_sku', true );
if ( $sku ) :
?>
<p class="sku"><?php echo esc_html( $sku ); ?></p>
<?php endif; ?>
<?php endwhile; endif; ?>Fora do loop
$post_id = 42;
$author_name = get_post_meta( $post_id, 'custom_author', true );
echo esc_html( $author_name );Em arquivos de CPT não confie no $post global «às cegas» fora do loop: use get_queried_object_id() ou o ID passado ao partial. É um motivo frequente de «está a sair a meta do artigo anterior» em listagens com sidebar partilhada.
Para partials reutilizáveis (cartão de evento, ficha de imóvel), passe o ID como argumento da função ou do bloco PHP incluído. Templates de tema clássico e padrões de tema de blocos misturam-se facilmente em 2026; o ID explícito evita surpresas quando o mesmo partial corre dentro de um Query Loop.
Segurança: escape sempre a saída
Nunca imprima metadados em bruto no HTML:
// Mau - vulnerável a XSS
echo get_post_meta( $post_id, 'user_input', true );
// Bom - seguro
echo esc_html( get_post_meta( $post_id, 'user_input', true ) );
// URLs
echo esc_url( get_post_meta( $post_id, 'website_url', true ) );
// Atributos HTML
echo esc_attr( get_post_meta( $post_id, 'css_class', true ) );A regra prática: o contexto escolhe a função. Texto visível - esc_html. Atributo - esc_attr. URL - esc_url. HTML editorial de confiança - wp_kses_post. Meta preenchida por formulário no front (pedido de orçamento, candidatura, upload de CV) trata-se sempre como não confiável, mesmo num «intranet» de escritório em Lisboa ou Porto.
Do lado da gravação: sanitize_text_field, absint, sanitize_email, ou um callback próprio em register_post_meta. Não assuma que o metabox «só existe no admin» - a REST API e o bulk edit também gravam. Em projetos com redatores externos, um campo de «notas internas» sem escape no front vira vetor XSS clássico em auditorias.
Guardar campos personalizados
Programaticamente
update_post_meta( $post_id, 'meta_key', 'meta_value' );
add_post_meta( $post_id, 'meta_key', 'meta_value' );
delete_post_meta( $post_id, 'meta_key' );update_post_meta cria a linha se não existir. add_post_meta com $unique = false (predefinição) permite várias linhas com a mesma chave - decisão consciente, não acidente de importação CSV.
Metabox no editor
function wppoland_add_sku_metabox() {
add_meta_box(
'product_sku_box',
'Código de catálogo',
'wppoland_render_sku_metabox',
'post',
'side'
);
}
add_action( 'add_meta_boxes', 'wppoland_add_sku_metabox' );
function wppoland_render_sku_metabox( $post ) {
wp_nonce_field( 'save_sku', 'sku_nonce' );
$sku = get_post_meta( $post->ID, 'wppoland_sku', true );
?>
<label for="wppoland_sku">SKU:</label>
<input type="text" id="wppoland_sku" name="wppoland_sku"
value="<?php echo esc_attr( $sku ); ?>">
<?php
}
function wppoland_save_sku_metabox( $post_id ) {
if ( ! isset( $_POST['sku_nonce'] ) ||
! wp_verify_nonce( $_POST['sku_nonce'], 'save_sku' ) ) {
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
if ( isset( $_POST['wppoland_sku'] ) ) {
update_post_meta(
$post_id,
'wppoland_sku',
sanitize_text_field( wp_unslash( $_POST['wppoland_sku'] ) )
);
}
}
add_action( 'save_post', 'wppoland_save_sku_metabox' );Nonce, capability e proteção contra autosave: três itens que tutoriais curtos omitem e que auditorias de segurança marcam primeiro. Inclua também wp_unslash antes de sanitizar dados de $_POST.
Trabalhar com ACF (Advanced Custom Fields)
O ACF simplifica a UI e os tipos de campo (repeater, flexible content, relationship). Em muitas redações portuguesas o painel ACF é o contrato entre developer e editor: o código lê get_field(), a equipa de conteúdo não toca em PHP.
$sku = get_field( 'wppoland_sku' );
$image = get_field( 'hero_image' );
the_field( 'product_description' );
$value = get_field( 'field_name', $post_id );Lembre-se: o ACF continua a assentar em postmeta (com variantes de storage em versões recentes). Consultas meta_query referem muitas vezes as mesmas chaves que o API nativo. Em listagens grandes, evite chamar get_field em cada cartão sem prefetch de meta (update_meta_cache / WP_Query que já carrega meta).
Se o tema tiver de sobreviver com ACF desativado, mantenha um fallback:
$sku = function_exists( 'get_field' )
? get_field( 'wppoland_sku', $post_id )
: get_post_meta( $post_id, 'wppoland_sku', true );Para campos de imagem ACF, get_field pode devolver ID, URL ou array conforme a configuração do campo - alinhe o return format com o que o template espera antes de passar a wp_get_attachment_image ou esc_url.
Temas de blocos e render_block
Em temas baseados em blocos (FSE), muita da apresentação deixa o single.php clássico e passa por templates HTML e pelo filtro render_block. Meta continua a ser lida em PHP; o que muda é o ponto de injeção.
add_filter( 'render_block', 'wppoland_inject_sku_after_title', 10, 2 );
function wppoland_inject_sku_after_title( $block_content, $block ) {
if ( ( $block['blockName'] ?? '' ) !== 'core/post-title' ) {
return $block_content;
}
if ( ! is_singular( 'product' ) ) {
return $block_content;
}
$sku = get_post_meta( get_the_ID(), 'wppoland_sku', true );
if ( ! $sku ) {
return $block_content;
}
$extra = '<p class="sku">' . esc_html( $sku ) . '</p>';
return $block_content . $extra;
}Alternativa mais limpa quando a meta está registada com show_in_rest: Block Bindings (WordPress 6.5+) liga atributos de blocos core a post meta sem filtro ad hoc. Ainda assim, bindings não substituem escape no servidor para HTML customizado que escreva à mão.
Em padrões de tema (theme patterns) evite hardcodar IDs de posts de staging. Leia meta do contexto atual (get_the_ID() dentro do Query Loop) ou passe atributos do bloco. Misturar conteúdo de demo de Lisboa com IDs de produção é um erro típico em handoffs entre agências.
Desempenho e cache
Num WP_Query standard o WordPress chama update_meta_cache() e carrega a meta dos posts em lote. Várias chamadas a get_post_meta() no mesmo ID, no mesmo pedido, não geram SELECTs extra - desde que o object cache esteja a funcionar (Redis/Memcached em hosting sério, ou o cache em memória do pedido).
Antipadrão: num arquivo com 48 cartões dispara um WP_Query adicional por cartão só para ler uma meta. Prefira um query com meta_query, ou dois passos (IDs, depois get_posts com meta já em cache).
$args = [
'post_type' => 'product',
'meta_query' => [
[
'key' => 'wppoland_sku',
'value' => 'A',
'compare' => 'LIKE',
],
],
];
$query = new WP_Query( $args );Evite ORDER BY por meta_value em tabelas grandes sem índice: o MySQL sofre com Joins EAV em centenas de milhares de linhas. Por vezes taxonomia ou tabela própria é a denormalização correta. Autoload não se aplica a postmeta; se copiar padrões de get_option, não ligue autoload «porque é mais fácil» - é outra classe de problemas de TTFB.
Plugins de cache de página (LiteSpeed, WP Rocket, etc.) comuns em alojamento partilhado em Portugal servem HTML estático: meta só é reavaliada quando a página é regenerada. Se a meta mudar por cron ou webhook de ERP, limpe a cache desse URL ou use fragment caching consciente.
Integração com Gutenberg e REST
Para a meta aparecer no editor de blocos e na REST API, registe-a:
register_post_meta( 'post', 'wppoland_sku', [
'show_in_rest' => true,
'single' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'auth_callback' => function () {
return current_user_can( 'edit_posts' );
},
] );Sem show_in_rest, o JavaScript do bloco não vê o campo. Sem auth_callback, pode abrir escrita a um grupo mais amplo do que pretendia. O type tem de coincidir com os dados reais - number versus string no JSON é fonte de bugs silenciosos na sidebar.
Para campos compostos (objetos, listas), configure show_in_rest com schema; caso contrário o Gutenberg recebe uma string plana e «perde» a estrutura. Campos sensíveis (tokens de gateway, IDs internos de faturação) devem ficar com show_in_rest desligado ou schema que não os exponha publicamente em /wp-json/. O tema pode lê-los em PHP no render sem os publicar na API.
Erros típicos em auditorias
- Imprimir meta sem
esc_html,esc_attrouesc_url. - Gravar a partir de
$_POSTsem nonce e semcurrent_user_can. - A mesma chave meta usada por dois plugins (ou por WooCommerce e pelo tema).
get_fieldem loop de arquivo sem cache / sem meta em lote.- Assumir que
$single = truedevolve sempre string (arrays serializados de importações antigas). - Faltar
register_post_metaao construir um bloco - «funciona em PHP, falha no editor». - Usar
the_meta()em produção e expor chaves internas na página pública.
Migração e nomenclatura de chaves
Ao herdar um tema antigo, inventarie chaves: SELECT meta_key, COUNT(*) FROM wp_postmeta GROUP BY meta_key ORDER BY COUNT(*) DESC LIMIT 50. Vê de imediato que prefixos são do projeto e quais sobraram de plugins desinstalados. Um prefixo de projeto (wppoland_, cliente_) protege contra colisão com ACF ou WooCommerce.
Na migração de ACF para register_post_meta nativo, não copie às cegas field_xxxxx. Mantenha uma chave de negócio legível e, se necessário, um script único que mapeia IDs antigos para novos. Staging primeiro: compare o número de posts com meta não vazia antes e depois. Se exportar CSV para a redação, guarde uma coluna com o ID do post - o título não é único.
Em multisite, get_post_meta opera sempre no blog atual. switch_to_blog à volta de leituras de meta sem restore_current_blog num finally é forma clássica de misturar dados de outra loja da rede.
Serialização e importações antigas
get_post_meta( $id, $key, true ) pode devolver um array se na base estiver um array PHP serializado de uma migração antiga. Código que assume string gera notices ou imprime a palavra «Array». Antes de esc_html, verifique o tipo ou normalize na gravação.
Evite serialize() / unserialize() manuais em dados de utilizador. O WordPress serializa valores compostos em meta; você passa o array a update_post_meta. Em integrações externas, mapeie campos para chaves escalares em vez de um único blob - facilita meta_query depois.
Teste a gravação de meta como editor, não só como administrador. Metabox ou grupo ACF deve ter uma linha de ajuda para a redação: de onde vem o valor, se é obrigatório, o que acontece no front quando está vazio. No template, trate o estado vazio de propósito - oculte a secção ou mostre fallback - em vez de renderizar tags vazias.
Checklist antes do merge
- Cada saída usa
esc_html,esc_attr,esc_urlouwp_kses_postconforme o contexto. - Formulários de gravação têm nonce e
current_user_can. - Meta usada em REST tem
show_in_resteauth_callback. - Não há
ORDER BY meta_valueem arquivos grandes sem justificação e índice. - Chaves antigas de plugins estão documentadas ou removidas com script de limpeza em staging.
- Nenhum template de produção chama
the_meta()para conteúdo público.
Resumo
Campos personalizados são a base de CPT e integrações. O nativo get_post_meta() chega longe; the_meta() fica para legado. O ACF acrescenta UX de edição. As regras não mudam: escape a saída, sanitize a entrada, use o cache de meta do núcleo em vez de SELECTs próprios no loop, e registe meta com register_post_meta quando o editor de blocos ou a REST precisarem dela. Em temas de blocos, render_block ou Block Bindings são o sítio certo para injetar valores - não um echo solto no meio do HTML do padrão.
Se herda um tema com centenas de chaves soltas ou migra de ACF para blocos nativos, dá para ordenar isso no âmbito de desenvolvimento WordPress. Escreva pelo formulário de contacto com uma descrição curta do CPT e se a meta tem de ser editável no Gutenberg.






