Voltar ao blog
wordpressheadlessnuxtphpacfrest-apimu-pluginsmigracao

Um campo de galeria múltipla no WordPress sem ACF PRO

12 de setembro de 2026 às 10:20 - Por Larissa Santos

Um campo de galeria múltipla no WordPress sem ACF PRO

Nos dois artigos anteriores dessa série contei a decisão de fazer o site da Teciklar headless, com um Nuxt 4 na Vercel na frente e o WordPress atrás servindo só conteúdo pela REST API, e depois onde as customizações desse WordPress precisavam morar para não sumirem numa troca de tema.

Este é sobre o problema que quem publica conteúdo mais sentia no dia a dia: a galeria de imagens e vídeos estava presa em dez campos numerados do ACF. Conto como escrevi o campo que substituiu aquilo sem comprar o ACF PRO, o que decidi dentro dele, e como migrei o conteúdo dos posts com o site no ar.


Dez campos numerados

Galeria de imagem e vídeo eram campos ACF numerados, a solução que eu mesma tinha dado antes: imagem_1 até imagem_10 na home e nos posts, video_1 até video_10 nos posts. Na prática isso significava um teto arbitrário de dez, que não dava para passar sem criar campo novo, preencher um por um abrindo dez seletores diferentes, e ordem presa à numeração, então mudar a posição de uma foto era reatribuir campos.

O que se quer é óbvio: um campo só, seleção múltipla, quantidade livre, reordenação arrastando.

Por que não comprei o campo pronto

O ACF tem exatamente esse campo, chamado Gallery. Ele é exclusivo do ACF PRO, que é licença paga anual, e o projeto usa a versão free.

As opções reais eram três:

CaminhoCusto
Comprar ACF PROlicença anual, resolve em dois cliques
Instalar outro plugin de campos (Meta Box, Carbon Fields)grátis, mas mais um plugin para manter, e o conteúdo passa a viver em dois sistemas diferentes
Escrever o campocódigo próprio para manter, zero dependência nova

Escolhi escrever por consistência. Já existia um lugar concentrando as customizações do site, e o campo passa a viver ali junto do resto, sem plugin novo para atualizar, sem licença para renovar, usando APIs do WordPress que praticamente não mudam.

O único argumento contra essa escolha foi de que se quem mantiver o site no futuro não mexe com PHP, um plugin com interface gráfica seria mais fácil de administrar.

A anatomia do campo

O campo é feito de três arquivos, com papéis bem separados. Eles nasceram no tema filho e hoje vivem no plugin próprio, pelo motivo que contei no artigo anterior.

A lógica, em PHP

Os campos são configuração, não código repetido. A primeira versão era um campo de galeria e só, com o nome do meta fixo no código e uma função dizendo em que tipo de conteúdo ele aparecia. Funcionou bem até chegar a vez dos vídeos, que é a mesma mecânica inteira trocando o filtro da biblioteca e o rótulo da caixa. Duplicar duzentas linhas para mudar duas era o caminho errado, então em vez disso extraí o que variava para um array que descreve os campos:

php
'galeria' => [
    'meta' => 'teciklar_galeria',
    'library' => 'image',
    'titles' => [
        'page' => 'Imagens da Esfera',
        'post' => 'Galeria do post',
    ],
],
'videos' => [
    'meta' => 'teciklar_videos',
    'library' => 'video',
    'titles' => [
        'post' => 'Vídeos do post',
    ],
],

Cada entrada diz onde guardar, que tipo de arquivo a biblioteca deve filtrar, e com que nome a caixa aparece em cada tipo de conteúdo. A chave titles faz dois trabalhos ao mesmo tempo: define o rótulo e define onde o campo existe. O campo de vídeo só tem post, então ele simplesmente não aparece em páginas. Um campo novo no futuro é uma entrada nova nesse array.

A caixa aparece só onde faz sentido. A home é uma página específica, e não faria sentido poluir a edição de todas as páginas do site:

php
if ($post->post_type === 'page' && (int) $post->ID !== TECIKLAR_HOME_PAGE_ID) {
    return [];
}

O que é gravado são IDs, não URLs. Essa é a decisão mais importante do arquivo inteiro e ela é invisível na interface. O campo guarda uma lista ordenada de IDs de anexo no post meta, e as URLs só são resolvidas na hora de responder a API. A consequência prática: se o arquivo de um anexo mudar, seja editado no próprio editor de imagem do WordPress ou substituído por um plugin de troca de mídia, o site reflete a mudança sem ninguém precisar reeditar o post. Se as URLs estivessem gravadas, cada post carregaria uma cópia congelada do caminho do arquivo.

O salvamento valida antes de gravar. Isso está recebendo dado vindo do navegador, então tem nonce, checagem de permissão, guarda contra autosave e contra revisão, e cada valor passa por intval:

php
$raw = sanitize_text_field($submitted[$key]);
$ids = array_values(array_filter(array_map('intval', explode(',', $raw))));

if (empty($ids)) {
    delete_post_meta($post_id, $field['meta']);
    continue;
}

update_post_meta($post_id, $field['meta'], $ids);

Repare que lista vazia apaga o meta em vez de gravar um array vazio. Deixar lixo no banco é o tipo de coisa que depois aparece como bug fantasma.

A exposição na API entra como campo registrado, com schema declarado, e não como um apêndice improvisado:

php
$items[] = [
    'src' => wp_get_attachment_url($id),
    'alt' => (string) get_post_meta($id, '_wp_attachment_image_alt', true),
    'mime' => (string) get_post_mime_type($id),
];

São três decisões embutidas em três linhas. O src vai como URL pronta, porque o front só usa o endereço do arquivo. O alt vem da Biblioteca de Mídia, porque o texto alternativo é propriedade do arquivo e não do post, então preencher uma vez na biblioteca deixa o texto disponível em toda resposta que devolver aquela imagem, sem depender de quem a inseriu. E o mime entra porque é barato e deixa a resposta autodescritiva: quem consome sabe o que recebeu sem inferir pela extensão do arquivo.

O campo funciona com o parâmetro _fields da API, que é como se pede uma resposta enxuta. Guarde esse detalhe, ele volta mais adiante.

O comportamento no editor

O WordPress já tem a janela de biblioteca de mídia pronta e ela aceita seleção múltipla. O JavaScript aqui é basicamente cola:

js
frame = wp.media({
  title: $add.data('frame-title'),
  button: { text: teciklarMedia.button },
  library: { type: $add.data('library') },
  multiple: 'add'
})

Duas coisas nesse trecho fazem diferença de uso. O library.type vem da configuração do campo, então o campo de vídeo abre a biblioteca já filtrada, sem imagens no meio. E a opção é multiple: 'add': com add, cada clique numa miniatura acrescenta à seleção, enquanto com true o clique troca a seleção anterior, a menos que o editor segure Shift ou Ctrl/Cmd, o que é uma forma silenciosa de fazer o editor perder trabalho.

A reordenação usa o jQuery UI Sortable, que já vem no admin do WordPress, então não entrou biblioteca nenhuma no projeto. Depois de qualquer mudança, seja adicionar, remover ou arrastar, uma função lê a ordem das miniaturas na tela e reescreve o campo escondido que vai ser submetido. A ordem visual é a fonte da verdade.

Um detalhe pequeno melhorou muito o campo de vídeo: a miniatura é um elemento <video preload="metadata"> apontando para o arquivo. O atributo é só uma dica e o padrão varia entre navegadores, mas no painel o navegador desenha o primeiro quadro, então o editor vê o vídeo em vez de um ícone genérico de arquivo, e consegue distinguir um do outro.

As miniaturas

Poucas dezenas de linhas de CSS: grade flexível, miniaturas quadradas com object-fit: cover, cursor de mover para sinalizar que arrasta, e um botão redondo de remover no canto de cada item.

Como ficou para quem publica

Antes: abrir dez campos, escolher uma imagem em cada, e conviver com a ordem definida pela numeração. Depois: um botão, a biblioteca de mídia abre filtrada pelo tipo certo, seleção múltipla de uma vez, arrastar para ordenar, X para remover, sem teto de quantidade.

OndeCaixaTipo
HomeImagens da Esferaimagens
PostGaleria do postimagens
PostVídeos do postvídeos

O lado do Nuxt

Do lado do front, o conteúdo passou a chegar na raiz da resposta, ao lado do bloco do ACF e não dentro dele:

json
{
  "acf": { "...": "..." },
  "galeria": [
    { "src": "https://.../foto.jpg", "alt": "", "mime": "image/jpeg" }
  ],
  "videos": [{ "src": "https://.../clipe.mp4", "alt": "", "mime": "video/mp4" }]
}

O componente da galeria simplificou bastante. Antes ele precisava desfazer a numeração na mão, porque os campos chegavam como um objeto com chaves imagem_1, imagem_2 e assim por diante:

js
Object.entries(campos)
  .sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }))
  .map(([, value]) => value)

Esse sort com comparação numérica existia só para imagem_10 não vir logo depois de imagem_1, que é o que a ordenação alfabética faria. Hoje o array já chega na ordem que a pessoa arrastou no painel, e o componente só repassa.

A armadilha do _fields

Essa merece destaque porque é a falha mais provável de acontecer no futuro e a mais difícil de perceber.

A REST API do WordPress aceita um parâmetro _fields para pedir só os campos que interessam, o que deixa a resposta bem menor, e o front usava isso. Ao adicionar campos novos, eles precisam entrar nessa lista, senão a API simplesmente não os devolve.

O modo de falhar é silencioso: não dá erro e não aparece nada no console. O front recebe uma lista vazia e a galeria some da tela. Alguém vai olhar o componente, olhar o painel, onde as imagens estão lá salvas, e não vai entender. Se um campo novo não chega no front, confira a chamada antes de procurar problema no PHP.

Migrar com o site no ar

Os campos antigos ainda tinham conteúdo e o site estava publicado. Trocar tudo de uma vez significaria um intervalo com galerias vazias.

Fiz o front ler o campo novo e, quando ele viesse vazio, cair no campo antigo. Com isso a ordem das etapas deixa de importar: dá para publicar o código novo antes de migrar o conteúdo, e migrar post por post sem pressa, sem nenhum momento de galeria quebrada.

A conferência foi pela própria API, comparando campo novo e campo antigo em todos os posts de uma vez. Isso revelou duas coisas que eu não saberia olhando o site. Dos nove posts, só cinco tinham galeria, então o trabalho de migração era bem menor do que parecia. E num deles o campo novo ficou com sete imagens enquanto o antigo tinha oito, porque uma imagem escapou na hora de reselecionar.

Esse segundo achado é o argumento a favor de conferir por dados em vez de conferir no olho. As duas galerias renderizam igual, e ninguém percebe uma imagem a menos numa galeria de oito olhando a página.

Essa divergência ainda acabou virando o único jeito confiável de testar se o front estava mesmo lendo o campo novo. Como as duas fontes apontavam para as mesmas imagens, o resultado na tela era idêntico e não provava nada. No post divergente, sete significava campo novo e oito significava fallback.

Na mão ou por script

Considerei escrever um script de migração e descartei, porque eram cinco posts.

Escrever a conversão de URL para ID de anexo, arranjar como executar aquilo no servidor e depois conferir post a post custaria mais que a meia hora de fazer na mão, com mais risco, porque é código de escrita em massa num banco de produção. E existe um argumento a favor do trabalho manual: o script preservaria a ordem numérica antiga, enquanto reselecionar deixa escolher a ordem que faz sentido hoje.

Se fossem cinquenta posts, a conta seria o contrário.

Depois de tudo migrado e verificado, os campos antigos saíram do ACF e o fallback saiu do código.


Conclusões

Campo pago nem sempre é a fronteira que parece. O que o ACF PRO entrega nesse caso específico é uma interface em cima de duas coisas que o WordPress já tem de graça: a janela de biblioteca de mídia com seleção múltipla e o jQuery UI Sortable. A decisão de escrever ou comprar depende de quem vai manter aquilo depois, não do preço da licença.

A decisão que mais importa costuma ser a que não aparece na tela. Gravar IDs em vez de URLs não muda nada no painel e não muda nada no site renderizado. Muda o que acontece quando alguém mexe no arquivo de um anexo dois anos depois.

Fallback transforma migração arriscada em migração sem pressa. Enquanto o front sabe ler as duas fontes, publicar código e migrar conteúdo deixam de ser uma coisa só, e nenhuma das duas precisa acontecer numa janela específica.

Conferir por dados acha o que o olho não acha. A imagem que faltava numa galeria de oito só apareceu porque comparei os dois campos pela API, em todos os posts de uma vez. Na página, as duas versões eram indistinguíveis.


Leituras relacionadas

Perguntas frequentes

Dá para ter um campo de galeria com seleção múltipla no WordPress sem o ACF PRO?

Dá. O campo Gallery do ACF é exclusivo da versão PRO, mas a janela de biblioteca de mídia do WordPress já aceita seleção múltipla e o jQuery UI Sortable já vem no admin. Um campo próprio é uma metabox que guarda uma lista ordenada de IDs de anexo no post meta, mais um JavaScript de poucas dezenas de linhas que liga as duas coisas.

Devo guardar URLs ou IDs de anexo num campo de galeria do WordPress?

IDs. A URL só deve ser resolvida na hora de responder a API. Guardando IDs, qualquer mudança no arquivo do anexo, seja uma edição no editor de imagem do WordPress ou uma substituição por plugin, se reflete em todos os posts que o usam, sem reeditar nada. Guardando URLs, cada post carrega uma cópia congelada do caminho do arquivo.

Por que meu campo novo não aparece na resposta da REST API do WordPress?

Se o front usa o parâmetro _fields para pedir uma resposta enxuta, todo campo novo precisa entrar nessa lista, senão a API não o devolve. A falha é silenciosa: não dá erro e não aparece nada no console, o front só recebe uma lista vazia. Antes de procurar problema no PHP, confira a chamada.

Como migrar o conteúdo de campos antigos sem tirar o site do ar?

Fazendo o front ler o campo novo e, quando ele vier vazio, cair no campo antigo. Com o fallback no lugar, a ordem das etapas deixa de importar: dá para publicar o código novo antes de migrar o conteúdo e migrar post por post sem pressa. Depois de tudo migrado e conferido, o campo antigo e o fallback saem juntos.

Migração de conteúdo na mão ou por script?

É uma conta de volume. Para um punhado de posts, escrever a conversão, arranjar como executá-la no servidor e conferir o resultado custa mais que o trabalho manual, com mais risco, porque é escrita em massa num banco de produção. A partir de algumas dezenas de posts a conta inverte.

LarissaSantos

Desenvolvedora Frontend apaixonada por criar experiências digitais incríveis

Navegação

2026 © Larissa Santos Feito com Vue.js