[{"data":1,"prerenderedAt":1168},["ShallowReactive",2],{"blog-wordpress-headless-campo-galeria-acf":3},{"id":4,"title":5,"about":6,"author":7,"body":8,"date":1134,"description":1135,"extension":1136,"faq":1137,"image":1153,"mentions":6,"meta":1154,"navigation":315,"path":1155,"seo":1156,"seoDescription":1157,"seoTitle":1158,"stem":1159,"tags":1160,"updated":6,"__hash__":1167},"blog\u002Fblog\u002Fwordpress-headless-campo-galeria-acf.md","Um campo de galeria múltipla no WordPress sem ACF PRO",null,"Larissa Santos",{"type":9,"value":10,"toc":1115},"minimark",[11,29,32,35,40,59,62,66,74,77,121,124,127,131,134,139,145,243,254,260,280,286,296,345,352,358,388,403,410,414,417,569,587,590,597,601,608,612,615,663,667,670,856,866,973,985,991,994,1000,1003,1007,1010,1013,1016,1019,1022,1026,1029,1032,1035,1038,1040,1044,1050,1056,1062,1068,1070,1074,1111],[12,13,14,15,22,23,28],"p",{},"Nos dois artigos anteriores dessa série contei a ",[16,17,21],"a",{"href":18,"rel":19},"https:\u002F\u002Fwww.larisantos.com.br\u002Fblog\u002Fnuxt-wordpress-headless-nuxt",[20],"nofollow","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 ",[16,24,27],{"href":25,"rel":26},"https:\u002F\u002Fwww.larisantos.com.br\u002Fblog\u002Fwordpress-headless-mu-plugins",[20],"onde as customizações desse WordPress precisavam morar"," para não sumirem numa troca de tema.",[12,30,31],{},"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.",[33,34],"hr",{},[36,37,39],"h2",{"id":38},"dez-campos-numerados","Dez campos numerados",[12,41,42,43,47,48,51,52,47,55,58],{},"Galeria de imagem e vídeo eram campos ACF numerados, a solução que eu mesma tinha dado antes: ",[44,45,46],"code",{},"imagem_1"," até ",[44,49,50],{},"imagem_10"," na home e nos posts, ",[44,53,54],{},"video_1",[44,56,57],{},"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.",[12,60,61],{},"O que se quer é óbvio: um campo só, seleção múltipla, quantidade livre, reordenação arrastando.",[36,63,65],{"id":64},"por-que-não-comprei-o-campo-pronto","Por que não comprei o campo pronto",[12,67,68,69,73],{},"O ACF tem exatamente esse campo, chamado Gallery. Ele é exclusivo do ",[70,71,72],"strong",{},"ACF PRO",", que é licença paga anual, e o projeto usa a versão free.",[12,75,76],{},"As opções reais eram três:",[78,79,80,93],"table",{},[81,82,83],"thead",{},[84,85,86,90],"tr",{},[87,88,89],"th",{},"Caminho",[87,91,92],{},"Custo",[94,95,96,105,113],"tbody",{},[84,97,98,102],{},[99,100,101],"td",{},"Comprar ACF PRO",[99,103,104],{},"licença anual, resolve em dois cliques",[84,106,107,110],{},[99,108,109],{},"Instalar outro plugin de campos (Meta Box, Carbon Fields)",[99,111,112],{},"grátis, mas mais um plugin para manter, e o conteúdo passa a viver em dois sistemas diferentes",[84,114,115,118],{},[99,116,117],{},"Escrever o campo",[99,119,120],{},"código próprio para manter, zero dependência nova",[12,122,123],{},"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.",[12,125,126],{},"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.",[36,128,130],{"id":129},"a-anatomia-do-campo","A anatomia do campo",[12,132,133],{},"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.",[135,136,138],"h3",{"id":137},"a-lógica-em-php","A lógica, em PHP",[12,140,141,144],{},[70,142,143],{},"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:",[146,147,152],"pre",{"className":148,"code":149,"language":150,"meta":151,"style":151},"language-php shiki shiki-themes material-theme-lighter github-dark github-dark","'galeria' => [\n    'meta' => 'teciklar_galeria',\n    'library' => 'image',\n    'titles' => [\n        'page' => 'Imagens da Esfera',\n        'post' => 'Galeria do post',\n    ],\n],\n'videos' => [\n    'meta' => 'teciklar_videos',\n    'library' => 'video',\n    'titles' => [\n        'post' => 'Vídeos do post',\n    ],\n],\n","php","",[44,153,154,162,168,174,180,186,192,198,204,210,216,222,227,233,238],{"__ignoreMap":151},[155,156,159],"span",{"class":157,"line":158},"line",1,[155,160,161],{},"'galeria' => [\n",[155,163,165],{"class":157,"line":164},2,[155,166,167],{},"    'meta' => 'teciklar_galeria',\n",[155,169,171],{"class":157,"line":170},3,[155,172,173],{},"    'library' => 'image',\n",[155,175,177],{"class":157,"line":176},4,[155,178,179],{},"    'titles' => [\n",[155,181,183],{"class":157,"line":182},5,[155,184,185],{},"        'page' => 'Imagens da Esfera',\n",[155,187,189],{"class":157,"line":188},6,[155,190,191],{},"        'post' => 'Galeria do post',\n",[155,193,195],{"class":157,"line":194},7,[155,196,197],{},"    ],\n",[155,199,201],{"class":157,"line":200},8,[155,202,203],{},"],\n",[155,205,207],{"class":157,"line":206},9,[155,208,209],{},"'videos' => [\n",[155,211,213],{"class":157,"line":212},10,[155,214,215],{},"    'meta' => 'teciklar_videos',\n",[155,217,219],{"class":157,"line":218},11,[155,220,221],{},"    'library' => 'video',\n",[155,223,225],{"class":157,"line":224},12,[155,226,179],{},[155,228,230],{"class":157,"line":229},13,[155,231,232],{},"        'post' => 'Vídeos do post',\n",[155,234,236],{"class":157,"line":235},14,[155,237,197],{},[155,239,241],{"class":157,"line":240},15,[155,242,203],{},[12,244,245,246,249,250,253],{},"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 ",[44,247,248],{},"titles"," faz dois trabalhos ao mesmo tempo: define o rótulo e define onde o campo existe. O campo de vídeo só tem ",[44,251,252],{},"post",", então ele simplesmente não aparece em páginas. Um campo novo no futuro é uma entrada nova nesse array.",[12,255,256,259],{},[70,257,258],{},"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:",[146,261,263],{"className":148,"code":262,"language":150,"meta":151,"style":151},"if ($post->post_type === 'page' && (int) $post->ID !== TECIKLAR_HOME_PAGE_ID) {\n    return [];\n}\n",[44,264,265,270,275],{"__ignoreMap":151},[155,266,267],{"class":157,"line":158},[155,268,269],{},"if ($post->post_type === 'page' && (int) $post->ID !== TECIKLAR_HOME_PAGE_ID) {\n",[155,271,272],{"class":157,"line":164},[155,273,274],{},"    return [];\n",[155,276,277],{"class":157,"line":170},[155,278,279],{},"}\n",[12,281,282,285],{},[70,283,284],{},"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.",[12,287,288,291,292,295],{},[70,289,290],{},"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 ",[44,293,294],{},"intval",":",[146,297,299],{"className":148,"code":298,"language":150,"meta":151,"style":151},"$raw = sanitize_text_field($submitted[$key]);\n$ids = array_values(array_filter(array_map('intval', explode(',', $raw))));\n\nif (empty($ids)) {\n    delete_post_meta($post_id, $field['meta']);\n    continue;\n}\n\nupdate_post_meta($post_id, $field['meta'], $ids);\n",[44,300,301,306,311,317,322,327,332,336,340],{"__ignoreMap":151},[155,302,303],{"class":157,"line":158},[155,304,305],{},"$raw = sanitize_text_field($submitted[$key]);\n",[155,307,308],{"class":157,"line":164},[155,309,310],{},"$ids = array_values(array_filter(array_map('intval', explode(',', $raw))));\n",[155,312,313],{"class":157,"line":170},[155,314,316],{"emptyLinePlaceholder":315},true,"\n",[155,318,319],{"class":157,"line":176},[155,320,321],{},"if (empty($ids)) {\n",[155,323,324],{"class":157,"line":182},[155,325,326],{},"    delete_post_meta($post_id, $field['meta']);\n",[155,328,329],{"class":157,"line":188},[155,330,331],{},"    continue;\n",[155,333,334],{"class":157,"line":194},[155,335,279],{},[155,337,338],{"class":157,"line":200},[155,339,316],{"emptyLinePlaceholder":315},[155,341,342],{"class":157,"line":206},[155,343,344],{},"update_post_meta($post_id, $field['meta'], $ids);\n",[12,346,347,348,351],{},"Repare que lista vazia ",[70,349,350],{},"apaga"," o meta em vez de gravar um array vazio. Deixar lixo no banco é o tipo de coisa que depois aparece como bug fantasma.",[12,353,354,357],{},[70,355,356],{},"A exposição na API"," entra como campo registrado, com schema declarado, e não como um apêndice improvisado:",[146,359,361],{"className":148,"code":360,"language":150,"meta":151,"style":151},"$items[] = [\n    'src' => wp_get_attachment_url($id),\n    'alt' => (string) get_post_meta($id, '_wp_attachment_image_alt', true),\n    'mime' => (string) get_post_mime_type($id),\n];\n",[44,362,363,368,373,378,383],{"__ignoreMap":151},[155,364,365],{"class":157,"line":158},[155,366,367],{},"$items[] = [\n",[155,369,370],{"class":157,"line":164},[155,371,372],{},"    'src' => wp_get_attachment_url($id),\n",[155,374,375],{"class":157,"line":170},[155,376,377],{},"    'alt' => (string) get_post_meta($id, '_wp_attachment_image_alt', true),\n",[155,379,380],{"class":157,"line":176},[155,381,382],{},"    'mime' => (string) get_post_mime_type($id),\n",[155,384,385],{"class":157,"line":182},[155,386,387],{},"];\n",[12,389,390,391,394,395,398,399,402],{},"São três decisões embutidas em três linhas. O ",[44,392,393],{},"src"," vai como URL pronta, porque o front só usa o endereço do arquivo. O ",[44,396,397],{},"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 ",[44,400,401],{},"mime"," entra porque é barato e deixa a resposta autodescritiva: quem consome sabe o que recebeu sem inferir pela extensão do arquivo.",[12,404,405,406,409],{},"O campo funciona com o parâmetro ",[44,407,408],{},"_fields"," da API, que é como se pede uma resposta enxuta. Guarde esse detalhe, ele volta mais adiante.",[135,411,413],{"id":412},"o-comportamento-no-editor","O comportamento no editor",[12,415,416],{},"O WordPress já tem a janela de biblioteca de mídia pronta e ela aceita seleção múltipla. O JavaScript aqui é basicamente cola:",[146,418,422],{"className":419,"code":420,"language":421,"meta":151,"style":151},"language-js shiki shiki-themes material-theme-lighter github-dark github-dark","frame = wp.media({\n  title: $add.data('frame-title'),\n  button: { text: teciklarMedia.button },\n  library: { type: $add.data('library') },\n  multiple: 'add'\n})\n","js",[44,423,424,451,485,511,545,561],{"__ignoreMap":151},[155,425,426,430,434,437,441,445,448],{"class":157,"line":158},[155,427,429],{"class":428},"sMo7A","frame ",[155,431,433],{"class":432},"sFfmW","=",[155,435,436],{"class":428}," wp",[155,438,440],{"class":439},"sG-J9",".",[155,442,444],{"class":443},"sK_r7","media",[155,446,447],{"class":428},"(",[155,449,450],{"class":439},"{\n",[155,452,453,457,459,462,464,467,469,473,477,479,482],{"class":157,"line":164},[155,454,456],{"class":455},"sdv8B","  title",[155,458,295],{"class":439},[155,460,461],{"class":428}," $add",[155,463,440],{"class":439},[155,465,466],{"class":443},"data",[155,468,447],{"class":428},[155,470,472],{"class":471},"sF_wb","'",[155,474,476],{"class":475},"s0vBq","frame-title",[155,478,472],{"class":471},[155,480,481],{"class":428},")",[155,483,484],{"class":439},",\n",[155,486,487,490,492,495,498,500,503,505,508],{"class":157,"line":170},[155,488,489],{"class":455},"  button",[155,491,295],{"class":439},[155,493,494],{"class":439}," {",[155,496,497],{"class":455}," text",[155,499,295],{"class":439},[155,501,502],{"class":428}," teciklarMedia",[155,504,440],{"class":439},[155,506,507],{"class":428},"button ",[155,509,510],{"class":439},"},\n",[155,512,513,516,518,520,523,525,527,529,531,533,535,538,540,543],{"class":157,"line":176},[155,514,515],{"class":455},"  library",[155,517,295],{"class":439},[155,519,494],{"class":439},[155,521,522],{"class":455}," type",[155,524,295],{"class":439},[155,526,461],{"class":428},[155,528,440],{"class":439},[155,530,466],{"class":443},[155,532,447],{"class":428},[155,534,472],{"class":471},[155,536,537],{"class":475},"library",[155,539,472],{"class":471},[155,541,542],{"class":428},") ",[155,544,510],{"class":439},[155,546,547,550,552,555,558],{"class":157,"line":182},[155,548,549],{"class":455},"  multiple",[155,551,295],{"class":439},[155,553,554],{"class":471}," '",[155,556,557],{"class":475},"add",[155,559,560],{"class":471},"'\n",[155,562,563,566],{"class":157,"line":188},[155,564,565],{"class":439},"}",[155,567,568],{"class":428},")\n",[12,570,571,572,575,576,579,580,582,583,586],{},"Duas coisas nesse trecho fazem diferença de uso. O ",[44,573,574],{},"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 é ",[44,577,578],{},"multiple: 'add'",": com ",[44,581,557],{},", cada clique numa miniatura acrescenta à seleção, enquanto com ",[44,584,585],{},"true"," o clique troca a seleção anterior, a menos que o editor segure Shift ou Ctrl\u002FCmd, o que é uma forma silenciosa de fazer o editor perder trabalho.",[12,588,589],{},"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.",[12,591,592,593,596],{},"Um detalhe pequeno melhorou muito o campo de vídeo: a miniatura é um elemento ",[44,594,595],{},"\u003Cvideo 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.",[135,598,600],{"id":599},"as-miniaturas","As miniaturas",[12,602,603,604,607],{},"Poucas dezenas de linhas de CSS: grade flexível, miniaturas quadradas com ",[44,605,606],{},"object-fit: cover",", cursor de mover para sinalizar que arrasta, e um botão redondo de remover no canto de cada item.",[36,609,611],{"id":610},"como-ficou-para-quem-publica","Como ficou para quem publica",[12,613,614],{},"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.",[78,616,617,630],{},[81,618,619],{},[84,620,621,624,627],{},[87,622,623],{},"Onde",[87,625,626],{},"Caixa",[87,628,629],{},"Tipo",[94,631,632,643,653],{},[84,633,634,637,640],{},[99,635,636],{},"Home",[99,638,639],{},"Imagens da Esfera",[99,641,642],{},"imagens",[84,644,645,648,651],{},[99,646,647],{},"Post",[99,649,650],{},"Galeria do post",[99,652,642],{},[84,654,655,657,660],{},[99,656,647],{},[99,658,659],{},"Vídeos do post",[99,661,662],{},"vídeos",[36,664,666],{"id":665},"o-lado-do-nuxt","O lado do Nuxt",[12,668,669],{},"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:",[146,671,675],{"className":672,"code":673,"language":674,"meta":151,"style":151},"language-json shiki shiki-themes material-theme-lighter github-dark github-dark","{\n  \"acf\": { \"...\": \"...\" },\n  \"galeria\": [\n    { \"src\": \"https:\u002F\u002F...\u002Ffoto.jpg\", \"alt\": \"\", \"mime\": \"image\u002Fjpeg\" }\n  ],\n  \"videos\": [{ \"src\": \"https:\u002F\u002F...\u002Fclipe.mp4\", \"alt\": \"\", \"mime\": \"video\u002Fmp4\" }]\n}\n","json",[44,676,677,681,718,732,786,791,852],{"__ignoreMap":151},[155,678,679],{"class":157,"line":158},[155,680,450],{"class":439},[155,682,683,687,691,694,696,698,701,705,707,709,711,713,715],{"class":157,"line":164},[155,684,686],{"class":685},"swu5b","  \"",[155,688,690],{"class":689},"sod2m","acf",[155,692,693],{"class":685},"\"",[155,695,295],{"class":439},[155,697,494],{"class":439},[155,699,700],{"class":685}," \"",[155,702,704],{"class":703},"s3afY","...",[155,706,693],{"class":685},[155,708,295],{"class":439},[155,710,700],{"class":471},[155,712,704],{"class":475},[155,714,693],{"class":471},[155,716,717],{"class":439}," },\n",[155,719,720,722,725,727,729],{"class":157,"line":170},[155,721,686],{"class":685},[155,723,724],{"class":689},"galeria",[155,726,693],{"class":685},[155,728,295],{"class":439},[155,730,731],{"class":439}," [\n",[155,733,734,737,739,741,743,745,747,750,752,755,757,759,761,763,766,768,770,772,774,776,778,781,783],{"class":157,"line":176},[155,735,736],{"class":439},"    {",[155,738,700],{"class":685},[155,740,393],{"class":703},[155,742,693],{"class":685},[155,744,295],{"class":439},[155,746,700],{"class":471},[155,748,749],{"class":475},"https:\u002F\u002F...\u002Ffoto.jpg",[155,751,693],{"class":471},[155,753,754],{"class":439},",",[155,756,700],{"class":685},[155,758,397],{"class":703},[155,760,693],{"class":685},[155,762,295],{"class":439},[155,764,765],{"class":471}," \"\"",[155,767,754],{"class":439},[155,769,700],{"class":685},[155,771,401],{"class":703},[155,773,693],{"class":685},[155,775,295],{"class":439},[155,777,700],{"class":471},[155,779,780],{"class":475},"image\u002Fjpeg",[155,782,693],{"class":471},[155,784,785],{"class":439}," }\n",[155,787,788],{"class":157,"line":182},[155,789,790],{"class":439},"  ],\n",[155,792,793,795,798,800,802,805,807,809,811,813,815,818,820,822,824,826,828,830,832,834,836,838,840,842,844,847,849],{"class":157,"line":188},[155,794,686],{"class":685},[155,796,797],{"class":689},"videos",[155,799,693],{"class":685},[155,801,295],{"class":439},[155,803,804],{"class":439}," [{",[155,806,700],{"class":685},[155,808,393],{"class":703},[155,810,693],{"class":685},[155,812,295],{"class":439},[155,814,700],{"class":471},[155,816,817],{"class":475},"https:\u002F\u002F...\u002Fclipe.mp4",[155,819,693],{"class":471},[155,821,754],{"class":439},[155,823,700],{"class":685},[155,825,397],{"class":703},[155,827,693],{"class":685},[155,829,295],{"class":439},[155,831,765],{"class":471},[155,833,754],{"class":439},[155,835,700],{"class":685},[155,837,401],{"class":703},[155,839,693],{"class":685},[155,841,295],{"class":439},[155,843,700],{"class":471},[155,845,846],{"class":475},"video\u002Fmp4",[155,848,693],{"class":471},[155,850,851],{"class":439}," }]\n",[155,853,854],{"class":157,"line":194},[155,855,279],{"class":439},[12,857,858,859,861,862,865],{},"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 ",[44,860,46],{},", ",[44,863,864],{},"imagem_2"," e assim por diante:",[146,867,869],{"className":419,"code":868,"language":421,"meta":151,"style":151},"Object.entries(campos)\n  .sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }))\n  .map(([, value]) => value)\n",[44,870,871,884,951],{"__ignoreMap":151},[155,872,873,876,878,881],{"class":157,"line":158},[155,874,875],{"class":428},"Object",[155,877,440],{"class":439},[155,879,880],{"class":443},"entries",[155,882,883],{"class":428},"(campos)\n",[155,885,886,889,892,894,897,900,903,906,909,912,916,919,921,924,927,929,932,934,936,939,941,945,948],{"class":157,"line":164},[155,887,888],{"class":439},"  .",[155,890,891],{"class":443},"sort",[155,893,447],{"class":428},[155,895,896],{"class":439},"([",[155,898,16],{"class":899},"sk1zL",[155,901,902],{"class":439},"],",[155,904,905],{"class":439}," [",[155,907,908],{"class":899},"b",[155,910,911],{"class":439},"])",[155,913,915],{"class":914},"sFsEu"," =>",[155,917,918],{"class":428}," a",[155,920,440],{"class":439},[155,922,923],{"class":443},"localeCompare",[155,925,926],{"class":428},"(b",[155,928,754],{"class":439},[155,930,931],{"class":685}," undefined",[155,933,754],{"class":439},[155,935,494],{"class":439},[155,937,938],{"class":455}," numeric",[155,940,295],{"class":439},[155,942,944],{"class":943},"sMrrN"," true",[155,946,947],{"class":439}," }",[155,949,950],{"class":428},"))\n",[155,952,953,955,958,960,963,966,968,970],{"class":157,"line":170},[155,954,888],{"class":439},[155,956,957],{"class":443},"map",[155,959,447],{"class":428},[155,961,962],{"class":439},"([,",[155,964,965],{"class":899}," value",[155,967,911],{"class":439},[155,969,915],{"class":914},[155,971,972],{"class":428}," value)\n",[12,974,975,976,978,979,981,982,984],{},"Esse ",[44,977,891],{}," com comparação numérica existia só para ",[44,980,50],{}," não vir logo depois de ",[44,983,46],{},", 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.",[135,986,988,989],{"id":987},"a-armadilha-do-_fields","A armadilha do ",[44,990,408],{},[12,992,993],{},"Essa merece destaque porque é a falha mais provável de acontecer no futuro e a mais difícil de perceber.",[12,995,996,997,999],{},"A REST API do WordPress aceita um parâmetro ",[44,998,408],{}," 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.",[12,1001,1002],{},"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.",[36,1004,1006],{"id":1005},"migrar-com-o-site-no-ar","Migrar com o site no ar",[12,1008,1009],{},"Os campos antigos ainda tinham conteúdo e o site estava publicado. Trocar tudo de uma vez significaria um intervalo com galerias vazias.",[12,1011,1012],{},"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.",[12,1014,1015],{},"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.",[12,1017,1018],{},"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.",[12,1020,1021],{},"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.",[135,1023,1025],{"id":1024},"na-mão-ou-por-script","Na mão ou por script",[12,1027,1028],{},"Considerei escrever um script de migração e descartei, porque eram cinco posts.",[12,1030,1031],{},"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.",[12,1033,1034],{},"Se fossem cinquenta posts, a conta seria o contrário.",[12,1036,1037],{},"Depois de tudo migrado e verificado, os campos antigos saíram do ACF e o fallback saiu do código.",[33,1039],{},[36,1041,1043],{"id":1042},"conclusões","Conclusões",[12,1045,1046,1049],{},[70,1047,1048],{},"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.",[12,1051,1052,1055],{},[70,1053,1054],{},"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.",[12,1057,1058,1061],{},[70,1059,1060],{},"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.",[12,1063,1064,1067],{},[70,1065,1066],{},"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.",[33,1069],{},[36,1071,1073],{"id":1072},"leituras-relacionadas","Leituras relacionadas",[1075,1076,1077,1084,1090,1097,1104],"ul",{},[1078,1079,1080],"li",{},[16,1081,1083],{"href":18,"rel":1082},[20],"WordPress Headless com Nuxt: quando faz sentido e como estruturar",[1078,1085,1086],{},[16,1087,1089],{"href":25,"rel":1088},[20],"WordPress Headless: mu-plugins ou tema filho? onde ficam as customizações",[1078,1091,1092],{},[16,1093,1096],{"href":1094,"rel":1095},"https:\u002F\u002Fcodex.wordpress.org\u002FJavascript_Reference\u002Fwp.media",[20],"Javascript Reference: wp.media (WordPress Codex)",[1078,1098,1099],{},[16,1100,1103],{"href":1101,"rel":1102},"https:\u002F\u002Fdeveloper.wordpress.org\u002Freference\u002Ffunctions\u002Fregister_rest_field\u002F",[20],"register_rest_field (WordPress Developer Resources)",[1078,1105,1106],{},[16,1107,1110],{"href":1108,"rel":1109},"https:\u002F\u002Fdeveloper.wordpress.org\u002Fplugins\u002Fmetadata\u002Fcustom-meta-boxes\u002F",[20],"Custom Meta Boxes (WordPress Plugin Handbook)",[1112,1113,1114],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMo7A, html code.shiki .sMo7A{--shiki-light:#90A4AE;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sFfmW, html code.shiki .sFfmW{--shiki-light:#39ADB5;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sG-J9, html code.shiki .sG-J9{--shiki-light:#39ADB5;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sK_r7, html code.shiki .sK_r7{--shiki-light:#6182B8;--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .sdv8B, html code.shiki .sdv8B{--shiki-light:#E53935;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sF_wb, html code.shiki .sF_wb{--shiki-light:#39ADB5;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .s0vBq, html code.shiki .s0vBq{--shiki-light:#91B859;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .swu5b, html code.shiki .swu5b{--shiki-light:#39ADB5;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sod2m, html code.shiki .sod2m{--shiki-light:#9C3EDA;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s3afY, html code.shiki .s3afY{--shiki-light:#E2931D;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sk1zL, html code.shiki .sk1zL{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#FFAB70;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .sFsEu, html code.shiki .sFsEu{--shiki-light:#9C3EDA;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sMrrN, html code.shiki .sMrrN{--shiki-light:#FF5370;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}",{"title":151,"searchDepth":164,"depth":164,"links":1116},[1117,1118,1119,1124,1125,1129,1132,1133],{"id":38,"depth":164,"text":39},{"id":64,"depth":164,"text":65},{"id":129,"depth":164,"text":130,"children":1120},[1121,1122,1123],{"id":137,"depth":170,"text":138},{"id":412,"depth":170,"text":413},{"id":599,"depth":170,"text":600},{"id":610,"depth":164,"text":611},{"id":665,"depth":164,"text":666,"children":1126},[1127],{"id":987,"depth":170,"text":1128},"A armadilha do _fields",{"id":1005,"depth":164,"text":1006,"children":1130},[1131],{"id":1024,"depth":170,"text":1025},{"id":1042,"depth":164,"text":1043},{"id":1072,"depth":164,"text":1073},"2026-09-12T10:20:00-03:00","Como substituí dez campos ACF numerados por um campo único de seleção múltipla com reordenação por arrastar, escrito à mão em PHP e JavaScript, e como migrei o conteúdo dos posts com o site no ar.","md",[1138,1141,1144,1147,1150],{"question":1139,"answer":1140},"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.",{"question":1142,"answer":1143},"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.",{"question":1145,"answer":1146},"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.",{"question":1148,"answer":1149},"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.",{"question":1151,"answer":1152},"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.","\u002Fimages\u002Fblog\u002Fwordpress-headless-campo-galeria-acf.jpg",{},"\u002Fblog\u002Fwordpress-headless-campo-galeria-acf",{"title":5,"description":1135},"Como escrever um campo de galeria múltipla no WordPress sem comprar o ACF PRO, expor na REST API e migrar o conteúdo sem derrubar o site.","Galeria múltipla no WordPress sem ACF PRO","blog\u002Fwordpress-headless-campo-galeria-acf",[1161,1162,1163,150,690,1164,1165,1166],"wordpress","headless","nuxt","rest-api","mu-plugins","migracao","qXwfpoT9E0MfENz8FkfqBLDMVZ2cJAzCOGcvB7M1-Nc",1790345154971]