docs(plans): registra o follow-up do JSON-LD da petição

Débito da S48 (#1443): a página de petição serializa o JSON-LD sem
escapar '<', enquanto o artigo já escapa. A Issue #1447 fecha o escopo
com um helper único em src/utilities/seo.ts usado nas duas páginas.
This commit is contained in:
Francisco Solla 2026-10-22 00:22:15 -03:00
parent 5786bc57dc
commit 5be50b182c

View file

@ -0,0 +1,56 @@
# Impl: S48-FOLLOWUP — escape de `<` no JSON-LD da página de petição
Status: aprovado
Atualizado em: 2026-10-22
Issue: S48-FOLLOWUP-JSONLD
Intenção: débito registrado na entrega da Issue #1443 (`docs/plans/abaixo-assinado-pl-5594-2026-impl.md`, D6)
Appetite: ~1h eng — correção pontual de paridade de segurança, sem mudança de shape
## Leitura do débito
- **Outcome:** o JSON-LD das páginas públicas nunca é quebrado por um valor de conteúdo que contenha `</script>`; a página de petição passa a ter a mesma proteção que a página de artigo.
- **O que NÃO negociar:** o shape do JSON-LD (`Article`), a URL canônica e o HTML renderizado não mudam; conteúdo continua admin-only; sem schema/migration.
- **O que reavaliar:** o artigo repete a serialização inline — a correção deve extrair o dono do concern em vez de criar uma segunda cópia.
## Abordagem recomendada
**Opções:** A) repetir `JSON.stringify(jsonLd).replace(/</g, '\\u003c')` inline na petição; B) extrair `serializeJsonLd(value)` em `src/utilities/seo.ts` e usar nas duas páginas; C) sanitizador/escaping de HTML completo.
**Recomendação:** B — `src/utilities/seo.ts` já é o dono do SEO; uma função pura (string JSON com `<` escapado para `\u003c`) elimina a divergência entre artigo e petição e é unit-testável. Aplicar nas duas páginas (`[type]/[category]/[slug]/page.tsx` e `abaixo-assinado/[id]/page.tsx`).
**Rejeitadas:** A porque twina a lógica e deixa a próxima página pública repetir o erro; C porque `JSON.stringify` já cuida de aspas/backslashes e o vetor real é o fechamento de `</script>` — um sanitizador amplo seria cerimônia sem volatilidade.
### Componentes / mudanças
- **`serializeJsonLd`** (`src/utilities/seo.ts`): `JSON.stringify(value).replace(/</g, '\\u003c')`, com comentário do vetor `</script>`.
- **Duas páginas públicas**: trocar o `JSON.stringify` inline pelo helper; nenhuma mudança de shape.
- **Teste**: unit do helper (`</script>` vira `\u003c/script>`, strings normais intactas, JSON continua parseável) e pin de que as duas páginas usam o helper (source-level, como o pin da copy da petição).
- **Migration:** nenhuma.
- **UI:** Impeccable A — sem UI nova; nenhum trigger do `designer`.
## Fases verificáveis
1. **Helper + adoção** — extrair e usar nas duas páginas.
2. **Testes** — unit do helper + pin das páginas; `pnpm test:unit`.
3. **Gates** — `pnpm gate:fast`; entrega com `pnpm push`.
## Rabbit holes / Não escopo
- Não criar sanitizador genérico, não escapar `>`/`&` por reflexo, não mudar o `@type`/campos do JSON-LD.
- Não tocar em canonical/OG/metadata; não criar rota/endpoint.
## Riscos e mitigação
- **Quebrar o JSON-LD** (escape errado): o unit test faz `JSON.parse` do resultado e compara o objeto; a página continua renderizando o script.
- **Divergência futura**: o helper único + pin source-level impede uma terceira cópia inline.
## Aceite de engenharia
- [ ] `serializeJsonLd` é o único caminho de serialização JSON-LD nas páginas públicas.
- [ ] `</script>` em título/subtítulo não fecha o script; JSON permanece válido.
- [ ] Sem schema/migration/URL nova; unit verde e `pnpm gate:fast` limpo.
- [ ] Invariantes AGENTS/engineering-standards preservados (sem PII em log, access inalterado).
## Self-score decision-quality
5/5 — decisão cara (onde mora a serialização) tem rejeitadas; appetite de 1h; rabbit holes nomeados; depth check reusa o dono `src/utilities/seo.ts` em vez de twinar; o outcome original da S48 não é reescrito.