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.
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
- Edição: administração do Cockpit v2 (PHP self-hosted, base de dados SQLite).
- Renderização: templates PHP do lado do servidor que buscam JSON à API REST do Cockpit a cada pedido, com uma cache curta de edge/disco.
- Build step: nenhum para conteúdo. O único "build" é minificar CSS e JS — e até isso é opcional e corre localmente, não no servidor.
- Deploy:
./deploy.sh— minificar, espelhar por FTP para a OVH, purgar a Cloudflare. Sem runner de CI. - i18n: traduções por campo guardadas no Cockpit (
title,title_pt,title_es), servidas via prefixos de URL (/pt/,/es/).
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:
- Minificar. Regeneramos
assets/css/main.min.csscom o rcssmin eassets/js/*.min.jscom 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. - Espelho por FTP. O
lftpespelha o diretório de trabalho para/www/na OVH, apenas aditivo (sem--delete), excluindostorage/, a instalação docockpit/, segredos,.git/enode_modules. Os ficheiros PHP que escreveu são os ficheiros PHP que correm. Não há transpilação pelo meio para depurar. - Purga da Cloudflare. Uma chamada à API
purge_everythinglimpa 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:
- Não há servidor de base de dados para operar. Sem pools de ligações, sem processo separado para monitorizar, sem credenciais para rodar, sem fatura de Postgres gerido. A base de dados é um ficheiro que se faz backup copiando.
- É rápido em leituras. Um journal de estúdio ou um site de marketing é esmagadoramente tráfego de leitura sobre um conjunto de dados medido em megabytes. Ler do disco local com SQLite é mais rápido do que um round-trip de rede a um servidor de base de dados.
- Backups são triviais. Snapshot do ficheiro. Restauro substituindo-o. Versione-o se quiser.
Onde o SQLite é a ferramenta errada — sejamos honestos:
- Concorrência de escrita. O SQLite serializa escritas com um lock ao nível da base de dados. Para um punhado de editores isto é irrelevante. Para uma app com muitos utilizadores a escrever em simultâneo, é uma parede — recorra a Postgres ou MySQL (o Cockpit suporta-os).
- Escalabilidade horizontal. Um ficheiro no disco de um servidor não escala para uma frota de servidores de aplicação a partilhar uma base de dados. Se precisa de vários nós de escrita, o SQLite acabou.
- Vive no mesmo host. Em alojamento partilhado, a disponibilidade do seu CMS está atada a esse host. Mitigamos com a Cloudflare à frente e backups disciplinados, mas é um acoplamento real.
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:
- O conteúdo muda muito mais vezes do que o código, e os editores não devem esperar por um build para publicar.
- O site tem forma de conteúdo-e-marketing: páginas, posts, portfólio, coleções estruturadas — não uma web app com estado.
- Quer um deploy que uma única pessoa entende por completo, sem lock-in de plataforma e sem fatura de minutos de build.
- Está em alojamento partilhado barato com um CDN de edge, e prefere não operar um servidor de base de dados nem um runner de build.
- A manutenibilidade a longo prazo importa mais do que perseguir a framework do ano. PHP de 2024 vai correr em 2034.
Recorra antes a Next.js ou Astro quando:
- Está a construir uma aplicação — autenticação, dashboards, tempo real, muito estado no cliente. PHP renderizado no servidor sem framework de JS é a ferramenta errada para interatividade rica.
- A equipa já vive em React/TypeScript e o modelo de componentes, a segurança de tipos e o ecossistema são ganhos líquidos de produtividade para essa equipa.
- Precisa mesmo de HTML estático pré-renderizado no edge absoluto, com zero dependência da origem — um site de documentação, uma página de lançamento à espera de um pico de tráfego onde não tolera nenhuma origem no caminho.
- Quer a DX de uma camada de conteúdo tipada, pipelines de otimização de imagem e MDX já incluídos, e está contente por assumir a maquinaria de build/deploy que vem com isso.
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:
- Sem segurança de tipos na fronteira da API. Um campo do Cockpit renomeado não dá erro em tempo de compilação — vai renderizar vazio. Apanhamos isto com fallbacks de null-coalescing e um smoke check após o deploy, não com um type checker.
- Renderizar a cada pedido precisa de cache. Bater na API do CMS a cada pedido sem cache seria desperdício; a Cloudflare e uma cache curta na origem fazem o trabalho que um SSG faz em tempo de build. Está a trocar uma cache de build por uma cache de edge.
- O SQLite ata a disponibilidade de escrita do CMS a um host. Bom para uma equipa editorial pequena; mau para um produto multi-utilizador com muita escrita.
- A renderização é sua. Nenhuma framework lhe dá routing, i18n ou tratamento de imagem de graça — escrevemo-los nós (umas centenas de linhas de PHP deliberado). Isso é uma feature se valoriza transparência, um custo se queria tudo incluído.
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.