8 MIN LEITURA · Pedro Thomaz

Um CMS Headless Sem Build Step: PHP + Cockpit, Renderizado no Servidor

Dá para ter um CMS headless sem build step de JS. Eis como juntamos o Cockpit v2 a PHP renderizado no servidor, deploys por FTP e zero pipeline de npm.

Um CMS Headless Sem Build Step: PHP + Cockpit, Renderizado no Servidor

Dá perfeitamente para ter um CMS headless sem build step de JavaScript. Junta-se uma API de conteúdos como o Cockpit v2 a PHP renderizado no servidor, e a experiência de edição mantém-se desacoplada e moderna enquanto o front end é entregue como HTML puro — sem bundler, sem node_modules no servidor, sem rebuild a cada alteração de conteúdo. É esta a stack que serve o site que está a ler, e abaixo fica exatamente como está montada, onde ganha e onde seria a escolha errada.

O que "CMS headless sem build step" significa de facto

Um CMS headless guarda conteúdo e expõe-no através de uma API, sem opinião sobre como o renderiza. O Cockpit CMS v2 é um CMS headless self-hosted em PHP: definem-se coleções (por exemplo "journal", "portfolio", "team") como modelos JSON, editam-se entradas na sua UI de administração, e leem-se de volta por uma API REST. A "cabeça" — aquilo que transforma conteúdo em páginas — é inteiramente sua.

"Sem build step" é a parte em que as pessoas se enganam. Não significa nenhuma ferramenta. Significa nenhuma compilação entre o seu código-fonte e aquilo que o servidor corre. Não há bundler a transformar .jsx ou .ts em artefactos a publicar, não há framework a exigir npm run build antes de uma única página existir, não há diretório dist/ que seja a verdadeira fonte de verdade. Os nossos ficheiros PHP são o que se publica. O browser recebe HTML renderizado no servidor logo no primeiro byte.

Em concreto, o front end aqui é PHP 8.3 em alojamento partilhado da OVH, atrás da Cloudflare. Um pedido chega ao router.php, que despacha para uma página como journal-post.php, que chama a API do Cockpit, recebe JSON e devolve HTML. É esta a pipeline inteira. Não há React, não há hidratação, não há router do lado do cliente.

A versão curta

Porquê um CMS headless em PHP em vez de um SSG

A resposta-padrão dos anos 2020 para "front end de CMS headless" é um gerador de sites estáticos — Next.js, Astro, Gatsby — que puxa da API em tempo de build e emite ficheiros estáticos. É uma arquitetura genuinamente boa. Recorremos a ela em muitos projetos. Mas tem um imposto que nenhum tutorial menciona.

Um SSG acopla a publicação a um build. Corrija-se uma gralha no CMS e a página não muda até correr um build e fazer novo deploy. Para esconder essa latência montam-se webhooks, regeneração estática incremental (ISR), uma fila de builds, uma plataforma de alojamento que corre os builds, e toda uma história de invalidação de cache para quando o ISR fica desatualizado. Nada disto é difícil isoladamente. Tudo isto são peças móveis que podem partir às 23h de uma sexta-feira, e a maior parte existe para disfarçar o facto de se ter transformado uma edição de conteúdo num deployment de software.

O PHP renderizado no servidor colapsa tudo isso. Conteúdo é dados, buscados em tempo de pedido. Um editor grava no Cockpit, a alteração está no ar no pedido seguinte (ou na expiração seguinte da cache — segundos, não um build). Não há webhook, não há minuto de build, não há tarefa de regeneração. O "deploy" só acontece quando o código muda, o que num site de marketing ou num journal de estúdio é drasticamente menos frequente do que a mudança de conteúdo.

É esta a tese central: para sites movidos a conteúdo, onde os editores publicam mais vezes do que os programadores fazem deploy, desacoplar o conteúdo do build vale mais do que o desempenho bruto de ficheiros estáticos pré-renderizados — sobretudo quando, de qualquer forma, há um CDN de edge a fazer o trabalho pesado de cache.

A história do deploy: FTP, minificar, purgar

Como não há artefacto de build, o deploy é quase insultuosamente simples. A coisa toda é um único script:

./deploy.sh   # minificar → espelhar por FTP → purgar Cloudflare

Três passos:

  1. Minificar. Regeneramos assets/css/main.min.css com o rcssmin e assets/js/*.min.js com o terser — mas só se a fonte for mais recente que o output. Esta é a única "compilação" no sistema, é opcional e corre na máquina do programador. O servidor nunca vê uma toolchain.
  2. Espelho por FTP. O lftp espelha o diretório de trabalho para /www/ na OVH, apenas aditivo (sem --delete), excluindo storage/, a instalação do cockpit/, segredos, .git/ e node_modules. Os ficheiros PHP que escreveu são os ficheiros PHP que correm. Não há transpilação pelo meio para depurar.
  3. Purga da Cloudflare. Uma chamada à API purge_everything limpa a cache de edge para que o novo código fique no ar globalmente em segundos.

Corremos ./deploy.sh --dry-run primeiro sempre que a lista de exclusões muda, para o lftp imprimir o que iria enviar antes de mover seja o que for. Sem plataforma de CI/CD, sem minutos de build, sem pipeline em YAML. Este script é o CI/CD. Um júnior consegue lê-lo de cima a baixo em dois minutos e perceber todo o caminho do portátil até produção — o que, em si, é uma feature de fiabilidade.

SQLite como base de dados do CMS: os trade-offs honestos

O Cockpit v2 usa por omissão SQLite, um único ficheiro em disco, e mantemo-lo assim. Isto surpreende as pessoas, por isso sejamos precisos sobre quando é a escolha certa e quando não é.

Onde o SQLite ganha para um CMS headless:

Onde o SQLite é a ferramenta errada — sejamos honestos:

A regra que usamos: se o conteúdo é editado por uma equipa pequena e lido pelo mundo, o SQLite não é um compromisso — é a escolha correta, aborrecida e durável. Se a base de dados é a aplicação (conteúdo gerado por utilizadores, muito fan-out de escrita, escala multi-tenant), não é.

i18n por campo, não por build

Este site é trilingue — inglês, português (pt-PT) e espanhol — e o modelo de i18n é onde a abordagem sem build realmente compensa. A tradução vive por campo, dentro do CMS. O Cockpit guarda as variantes localizadas ao lado do campo base: uma entrada de journal tem title, title_pt, title_es e o mesmo para excerpt e body. Um editor traduz um post no mesmo ecrã de administração, sem código pelo meio.

O front end resolve o idioma a partir do prefixo do URL. /pt/journal/... serve português, /es/... serve espanhol, os caminhos sem prefixo servem inglês. O template PHP escolhe o campo certo em tempo de renderização — em traços largos:

$lang  = current_locale();          // 'en' | 'pt' | 'es'
$title = $entry['title_' . $lang] ?? $entry['title'];

O ganho: adicionar uma tradução nunca dispara um rebuild. Grava-se o campo no Cockpit, a página localizada está no ar no pedido seguinte. Com um SSG, três idiomas podem significar o triplo da superfície de build e uma matriz de rotas por idioma regenerada a cada alteração. Aqui é uma busca de campo extra em tempo de pedido. (Aprendemos da maneira difícil a não redirecionar automaticamente os visitantes para /pt/ ou /es/ com base no navigator.language — isso sequestra o URL que as pessoas de facto pediram. O idioma é uma escolha, não um palpite.)

Quando esta stack ganha ao Next.js / Astro — e quando não

Escolha PHP renderizado no servidor + CMS headless quando:

Recorra antes a Next.js ou Astro quando:

Não há vencedor universal. O erro é tratar "CMS headless" como sinónimo de "SSG de JavaScript." Headless só significa que o conteúdo está desacoplado da renderização. PHP a renderizar esse conteúdo a cada pedido é tão "headless" quanto um build de Next.js a consumir a mesma API — apenas move o trabalho de tempo de build para tempo de pedido, e num site de conteúdo atrás de um CDN essa é frequentemente a melhor troca.

Os limites, ditos sem rodeios

Não fingimos que isto não tem arestas:

Também construímos a versão com framework de JavaScript — é a resposta certa para muito trabalho de cliente. Mas para um site movido a conteúdo, mantido por uma equipa pequena, um CMS headless sem build step é mais rápido de pôr no ar, mais barato de manter e dramaticamente mais sereno de operar. O melhor build step é, muitas vezes, aquele que não acrescentou.