Pular para conteúdo

0015 — O kit de UI não tem slot de head

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-16 · Decidido em: 2026-07-16

Contexto

A 0.21.0 deu ao layout.html um irmão do fragment head: o headComExtras(title, extras), para a tela que precisa de markup próprio dentro do <head> — tipicamente o <noscript><meta http-equiv="refresh"> de fallback sem JS, que fora do <head> é inválido. O irmão nasceu com nome distinto de propósito (o Thymeleaf não faz overload por aridade — lição da 0.19.0), e o changelog afirmou: "as telas que já chamam head('Título') seguem intactas".

A afirmação era falsa. O headComExtras era um segundo elemento <head> no mesmo documento, e um documento HTML tem um só: o parser funde o segundo no primeiro, e o head('Título') passou a arrastar o ${extras} do irmão. Toda view do kit quebrou com Error resolving fragment: "${extras}" (template: "layout" - line 53) — inclusive as que não sabiam que o slot existia.

Números que decidiram o desfecho: 0 views nos apps usavam o slot, 126 chamavam head(). Uma feature sem nenhum uso derrubou o kit inteiro. O caso que a motivou existe (bi-transporte-xls tem o <noscript><meta refresh> numa tela de admin), mas aquele app tem layout próprio e ainda não adotou o kit — a demanda era hipotética.

Reproduzido com Thymeleaf 3.1.5 antes de decidir, e três hipóteses caíram:

  • não é colisão de nome (a regra A do checa-views já cobria isso): renomear o irmão mantendo o <head> continua quebrado;
  • não é "head dentro de body": dois <head> irmãos, ambos fora do <body>, quebram igual. Basta serem dois;
  • a cura óbvia é uma armadilha: trocar o <head> do irmão por um <div> deixa os dois fragments verdes e emite <div> onde devia ir <head> — parse limpo, HTML inválido.

Duas curas funcionam de verdade (verificadas verdes): dois arquivos com um <head> cada, e a variante que ainda extrai os metas para um fragment comum, para não ter dois donos do mesmo markup (§1.9).

Decisão

O kit tem um head(title) e ponto — sem variante com slot. As duas curas que funcionam foram rejeitadas: ambas pagam complexidade permanente (arquivo extra, três fragments de head) por uma feature com zero uso. YAGNI. Isto reverte a decisão de desenho da 0.21.0.

Tela que precise de markup próprio no <head> escreve o <head> à mão nela e traz o caso ao central. O slot volta a ser desenhado quando existir tela real precisando dele — com o caso na mão, não por antecipação.

Regra G no checa-views.py: <html>/<head>/<body> declarado duas vezes no mesmo arquivo reprova. Vale mesmo com nomes distintos e mesmo fora do <body> — o que quebra é o parser fundir elemento. Só declaração real conta: o cabeçalho do kit documenta o uso com <head th:replace=...> dentro de um comentário, e os três apps Micronaut reais têm isso — por isso a regra lê o parser, nunca regex.

O central não renderiza o kit. O gate honesto para "o kit renderiza" é JVM, e o central é Python/Node; montar um projeto Java aqui foi avaliado e recusado. Consequência aceita e declarada: defeito de render no kit segue chegando ao app — quem o pega é o CI do primeiro app que adotar o kit e tiver teste de render. O que o central garante é o shape, e que as duas classes já vividas não voltam.

Consequências

  • O checa-views reprova o layout.html real da 0.21.0 na linha do segundo <head>, e passa nos 46 views reais dos três apps Micronaut.
  • A fixture LAYOUT_OK do testa-checa-views era o layout quebrado da 0.21.0 e se dizia conformante: o teste do checker atestava contra um defeito. Corrigida — é a "fixture inventada" da §6 dentro de casa, e o motivo de a suíte não ter pego nada.
  • Ninguém precisa migrar: nenhum app usava o slot. Quem copiou o kit 0.21.0 ou 0.22.x re-deriva com /xadm-docs e volta a renderizar.
  • A classe "markup que parseia e mesmo assim está errado" (o <div>) continua sem gate no central. Está escrita na receita e na docstring do check, para ninguém confiar no verde além do que ele afirma.