Campos personalizados (custom fields), tecnicamente conhecidos como post meta, 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 eventos 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 a função obsoleta the_meta(), escape de saída, escrita segura, ACF quando faz sentido, temas de blocos com render_block e Block Bindings, registo REST com register_post_meta e o cache que o núcleo já faz por si. Primeiro a API nativa, depois a camada de conveniência.
Saiba mais sobre desenvolvimento WordPress profissional na WPPoland.
Como usar get_post_meta() no WordPress
O WordPress oferece um conjunto de funções para gerir metadados: ler, criar, atualizar e apagar. 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 da função get_post_meta()
| Parâmetro | Tipo | Descrição |
|---|---|---|
$post_id | int | ID do artigo (use get_the_ID() dentro do loop) |
$key | string | Nome da chave meta; vazio devolve todas as chaves |
$single | bool | true = um valor, false = array (predefinição) |
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:
$val = get_post_meta( $id, 'cor', false );
// Devolve: Array ( [0] => "azul" )Código que faz if ( $value ) comporta-se de forma diferente em string e em array. Para campos com um só valor, use $single = true.
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 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.
O que get_post_meta() devolve para números, booleanos e chaves em falta
Tudo o que está em wp_postmeta volta como texto, salvo se tiver sido serializado. O docblock de get_post_meta() no núcleo do WordPress lista as conversões: false volta como string vazia, true como '1', e inteiros e decimais como strings. Arrays e objetos mantêm o tipo, porque o WordPress os serializa ao gravar e os desserializa ao ler.
Casos-limite do mesmo docblock: um ID de artigo inválido (não numérico, zero ou negativo) devolve false. Um ID válido sem a chave devolve '' com $single = true e um array vazio com $single = false. Um default registado com register_meta() (disponível desde o WordPress 5.5) nunca é gravado, por isso volta no tipo registado.
update_post_meta( $post_id, 'wppoland_stock', 0 );
$stock = get_post_meta( $post_id, 'wppoland_stock', true );
var_dump( $stock ); // string(1) "0"
var_dump( 0 === $stock ); // bool(false)
$stock = (int) $stock; // converter antes de aritmética ou comparação estritaUma comparação estrita com um inteiro nunca coincide com um número gravado. Converta com (int), (float) ou absint() antes de fazer contas e compare com '0' === $value quando o zero tem significado.
Como verificar se um campo personalizado existe no WordPress
Uma string vazia de get_post_meta() não diz se a chave foi gravada vazia ou nunca foi gravada. metadata_exists() (desde o WordPress 3.3) responde a essa pergunta e lê o mesmo cache de meta, por isso não acrescenta consulta quando a meta do artigo já está carregada:
if ( metadata_exists( 'post', $post_id, 'wppoland_sku' ) ) {
$sku = get_post_meta( $post_id, 'wppoland_sku', true );
// A chave existe, mesmo que o valor seja uma string vazia.
}A função corre primeiro o filtro get_post_metadata, como mostra o código em meta.php, por isso um plugin que intercete leituras de meta pode mudar a resposta. Use-a quando «vazio» e «não definido» exigem tratamento diferente, por exemplo quando a redação limpa um campo de propósito e o template não deve recorrer a um valor por omissão.
get_post_meta() versus the_meta()
the_meta() está obsoleta desde o WordPress 6.0.2. Chamá-la dispara _deprecated_function() com get_post_meta() como substituta, o que gera um aviso com WP_DEBUG ligado. Imprime no loop uma lista <ul class='post-meta'> com todos os campos personalizados do artigo atual. Escapa chaves e valores com esc_html() e salta chaves protegidas (as que começam por underscore), mas não recebe chave específica e não dá controlo sobre o markup nem sobre que chaves aparecem.
// Obsoleta: imprime todas as chaves não protegidas 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 underscore. O código está em post-template.php.
Exibir campos personalizados no tema
Como mostrar um campo personalizado no loop
<?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; ?>Como obter um campo personalizado 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. Templates de tema clássico e padrões de tema de blocos misturam-se facilmente; o ID explícito evita surpresas quando o mesmo partial corre dentro de um Query Loop.
Como escapar campos personalizados no tema
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 numa intranet de escritório em Lisboa ou no 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 a metabox «só existe no admin»: a REST API e a edição em massa também gravam. Em projetos com redatores externos, um campo de «notas internas» sem escape no front vira vetor XSS clássico em auditorias.
Como guardar campos personalizados no WordPress
Funções update_post_meta(), add_post_meta() e delete_post_meta()
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 e atualiza-a se existir. add_post_meta com $unique = false (predefinição) permite várias linhas com a mesma chave, o que deve ser uma decisão consciente e não um acidente de importação CSV.
Se guardar um array, o WordPress serializa-o automaticamente:
$dados = [ 'cor' => 'vermelho', 'tamanho' => 'XL' ];
update_post_meta( $post_id, 'variantes', $dados );
// Guardado na BD como: a:2:{s:3:"cor";s:8:"vermelho";...}Como adicionar uma metabox com add_meta_box()
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.
Ocultar campos personalizados com prefixo underscore
Alguns campos não aparecem na caixa de campos personalizados do editor. Isso acontece quando o nome da chave começa por underscore _: preco fica visível para o utilizador na interface padrão, _preco fica oculto (protegido).
Plugins como Yoast SEO ou WooCommerce usam _ (por exemplo _yoast_wpseo_title) para que o utilizador não estrague esses dados à mão. Como programador, use a mesma convenção para dados de sistema. Atenção a uma consequência: chaves protegidas também ficam de fora das Block Bindings (secção mais abaixo).
Como ler campos ACF com get_field()
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: nos artigos, o ACF continua a guardar valores em postmeta. Consultas meta_query referem muitas vezes as mesmas chaves que a API nativa. Em listagens grandes, evite chamar get_field em cada cartão sem a meta carregada em lote (update_meta_cache ou um 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 formato de retorno com o que o template espera antes de passar o valor a wp_get_attachment_image ou esc_url.
A saída de the_field() no ACF é escapada
A documentação de get_field() diz que os valores devolvidos devem ser escapados com a função adequada antes de serem impressos, porque o ACF não sabe onde os vai usar. Sempre que fizer echo get_field(), o escape é responsabilidade sua.
the_field() comporta-se de outra forma desde o ACF 6.2.7. O guia de escape de HTML do ACF indica que a partir da 6.2.5 o shortcode do ACF passa a saída por wp_kses(), e a partir da 6.2.7 o mesmo vale para the_field() e the_sub_field(). As notas da versão de segurança 6.2.5 avisam que isto remove HTML inseguro, como scripts ou iframes, da saída de campos textarea e WYSIWYG. Desde o ACF 6.2.6, get_field() aceita ainda um quarto argumento, $escape_html, que devolve o valor escapado e exige $format_value a true.
// Escapado pelo ACF (wp_kses) desde a 6.2.7
the_field( 'event_summary' );
// Sem escape: escolha a função para o contexto
echo esc_html( get_field( 'event_summary' ) );
// Desde a 6.2.6: peça ao tipo de campo um valor escapado
echo get_field( 'event_summary', false, true, true );Se um embed dentro de um campo WYSIWYG desapareceu depois de uma atualização do ACF, a causa costuma ser esta. Decida campo a campo se o HTML é de confiança e imprima-o com wp_kses_post() ou com uma lista própria de tags permitidas, em vez de desligar o escape globalmente.
Como mostrar a caixa de campos personalizados no editor de blocos
A caixa nativa de campos personalizados existe também no editor de blocos, mas fica oculta por predefinição quando ainda não foi usada. A documentação do WordPress.org sobre campos personalizados descreve o caminho. Com a interface em português de Portugal, os nomes vêm do pacote de idioma oficial: guarde primeiro o artigo, abra o menu Opções do editor (os três pontos na barra superior), escolha Preferências, abra o separador Geral, ative Campos personalizados na secção Avançadas e clique em Mostrar e recarregar página. A caixa aparece por baixo do conteúdo, onde Criar novo e Adicionar campo personalizado criam uma chave e o respetivo valor.
Duas coisas escondem campos nessa caixa. As chaves que começam por underscore são protegidas e nunca aparecem na lista. E o ACF remove a caixa por predefinição: a referência de definições do ACF lista remove_wp_meta_box (adicionada no ACF 5.6.0, predefinição true). Para recuperar a caixa nativa com o ACF ativo:
add_filter( 'acf/settings/remove_wp_meta_box', '__return_false' );Reativá-la é útil para depurar importações em staging. Em produção, redatores que veem chaves em bruto ao lado dos campos ACF acabam por editar o mesmo valor em dois sítios, por isso deixe-a desligada.
Como mostrar post meta num tema de blocos com 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. A 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;
}Em padrões de tema (theme patterns) evite fixar 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 com IDs de produção é um erro típico em passagens de projeto entre agências.
Como ligar campos personalizados a blocos com Block Bindings
Desde o WordPress 6.5, a Block Bindings API liga blocos do núcleo a valores de campos personalizados sem criar um bloco próprio. A fonte core/post-meta lê o valor com get_post_meta() no momento do render.
Que blocos suportam Block Bindings
A lista de atributos que aceitam ligação está no núcleo e cresceu a cada versão, como mostram os ramos de versão do wordpress-develop:
| Versão do WordPress | Blocos e atributos que aceitam ligação |
|---|---|
| 6.5 a 6.8 | Parágrafo (content), Título (content), Imagem (id, url, title, alt), Botão (url, text, linkTarget, rel) |
| 6.9 | acrescenta caption da Imagem, Data do artigo (datetime), Ligação e Submenu de navegação (url) |
| 7.1 | acrescenta Item de lista (content) |
Fontes: class-wp-block.php no ramo 6.5 e block-bindings.php no ramo 7.1.
Registar a meta e ligá-la a um parágrafo
add_action( 'init', function () {
register_post_meta( 'post', 'wppoland_nota_autor', [
'show_in_rest' => true,
'single' => true,
'type' => 'string',
'auth_callback' => function () {
return current_user_can( 'edit_posts' );
},
] );
} );<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"wppoland_nota_autor"}}}}} -->
<p>Texto de reserva quando a meta está vazia.</p>
<!-- /wp:paragraph -->A fonte core/post-meta não devolve nada em quatro casos: a chave não está registada com show_in_rest, a chave é protegida (começa por underscore), o artigo não é público e o visitante não o pode ler, ou o artigo está protegido por palavra-passe. Uma chave como _wppoland_nota_autor nunca aparece através de uma ligação, mesmo registada. Use chaves sem underscore para tudo o que quiser ligar. As bindings não substituem o escape no servidor para HTML que escreva à mão.
Como registar uma fonte própria de Block Bindings
Quando o valor precisa de processamento (preço formatado, data num formato próprio do site), registe uma fonte em PHP com register_block_bindings_source() no hook init. O callback recebe os argumentos da ligação, a instância do bloco e o nome do atributo, como descreve o docblock em block-bindings.php:
add_action( 'init', function () {
register_block_bindings_source( 'wppoland/formatted-price', [
'label' => __( 'Preço formatado', 'wppoland' ),
'uses_context' => [ 'postId' ],
'get_value_callback' => function ( array $source_args, $block_instance ) {
$post_id = $block_instance->context['postId'] ?? get_the_ID();
$raw = get_post_meta( $post_id, $source_args['key'] ?? '', true );
return '' === $raw ? null : number_format_i18n( (float) $raw, 2 );
},
] );
} );O markup do bloco usa então "source": "wppoland/formatted-price" com a chave meta em args. O nome da fonte precisa de prefixo de namespace, como wppoland/, e só pode ter letras minúsculas, dígitos, hífenes e barras.
Guardar dados do Gutenberg em atributos de bloco ou em post meta
Ao usar o editor de blocos surge a questão: onde guardar os dados, em atributos de bloco ou em post meta?
- Atributos de bloco: guardados no conteúdo do artigo (HTML). Rápidos de ler ao renderizar, mas impossíveis de consultar em SQL (não pode pedir «mostrar artigos com um bloco de cor vermelha»).
- Post meta: guardada na tabela
wp_postmeta. Exige consulta separada, mas permite filtrar e ordenar (por exemplo em arquivos).
Se os dados dizem respeito à aparência de um elemento específico da página, use um atributo de bloco. Se descrevem o artigo inteiro e quer filtrar por eles, use post meta. Também pode combinar os dois: um bloco pode guardar atributos em post meta para ficarem acessíveis a WP_Query.
Filtrar posts por campo personalizado com meta_query
O WP_Query permite obter artigos com base nos valores dos campos.
Como filtrar posts por valor numérico com meta_query
$args = [
'post_type' => 'produto',
'meta_query' => [
[
'key' => 'preco',
'value' => 100,
'compare' => '>',
'type' => 'NUMERIC', // necessário para comparar como número
],
],
];
$query = new WP_Query( $args );Combinar vários campos em meta_query com AND ou OR
$args = [
'post_type' => 'produto',
'meta_query' => [
'relation' => 'AND', // ou 'OR'
[
'key' => 'preco',
'value' => 100,
'compare' => '>',
'type' => 'NUMERIC',
],
[
'key' => 'cor',
'value' => 'vermelho',
],
],
];Cada critério adicional de meta_query gera mais um JOIN à tabela wp_postmeta. Em bases grandes isto fica lento. Se o site estiver lento, verifique se está a abusar de meta_query; nesses casos considere taxonomias ou tabelas próprias.
Porque os arrays serializados tornam o SQL lento
O WordPress serializa automaticamente arrays e objetos PHP na base de dados. É prático, mas não pode pesquisar nem ordenar eficientemente por dados serializados em SQL. Se guardar ['idade' => 25, 'cidade' => 'Lisboa'] num único campo meta, uma consulta à procura de «pessoas de Lisboa» tem de usar um LIKE '%Lisboa%' lento. Se precisa de pesquisar ou ordenar por um valor (ORDER BY), guarde-o num campo meta separado e não dentro de um array.
Desempenho de campos personalizados e cache de meta
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, porque os valores ficam no object cache (persistente com Redis ou Memcached, 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).
Evite ORDER BY por meta_value em tabelas grandes sem índice: o núcleo indexa meta_key, mas não meta_value, e 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 e outros), comuns em alojamento partilhado em Portugal, servem HTML estático: a 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.
Campos personalizados no Gutenberg e na REST API
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 editor 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 barra lateral.
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 com schema que não os exponha publicamente em /wp-json/. O tema pode lê-los em PHP no render sem os publicar na API.
Porque os campos personalizados não aparecem na REST API num CPT
Num tipo de conteúdo personalizado há mais uma condição, escrita no docblock de register_meta(): o tipo de conteúdo tem de declarar suporte a campos personalizados, caso contrário a meta registada fica fora da resposta REST.
register_post_type( 'evento', [
'public' => true,
'show_in_rest' => true,
'supports' => [ 'title', 'editor', 'custom-fields' ],
] );
register_post_meta( 'evento', 'wppoland_local', [
'show_in_rest' => true,
'single' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'auth_callback' => function () {
return current_user_can( 'edit_posts' );
},
] );O mesmo docblock lista dois argumentos mais recentes. revisions_enabled (WordPress 6.4) guarda a meta com as revisões do artigo e só funciona para o tipo de objeto post. label (WordPress 6.7) dá à chave um nome legível.
Erros comuns com campos personalizados no WordPress
- 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 a meta carregada 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. - Ligar uma chave protegida
_chavecom Block Bindings e não perceber porque nada aparece.
Como nomear chaves meta e migrar do ACF
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 referências 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, porque 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.
Porque get_post_meta devolve um array em vez de string
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() e unserialize() manuais em dados de utilizador. O WordPress serializa valores compostos em meta; o array é passado a update_post_meta. Em integrações externas, mapeie campos para chaves escalares em vez de um único blob, o que facilita meta_query depois.
Teste a gravação de meta como editor, não só como administrador. Uma metabox ou um 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 um fallback) em vez de renderizar tags vazias.
Checklist de campos personalizados antes do deploy
- 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, e os CPT declaram suporte acustom-fields. - 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 testado em staging.
- Nenhum template de produção chama
the_meta()para conteúdo público. - As chaves ligadas com Block Bindings não têm prefixo underscore.
Resumo
Campos personalizados são a base de CPT e integrações. O nativo get_post_meta() chega longe; the_meta() está obsoleta desde a 6.0.2. O ACF acrescenta UX de edição, e desde a 6.2.7 o the_field() passa por wp_kses(). 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, e 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.







