@polymorphism-tech/morph-spec 8.28.0 → 8.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/CLAUDE.md +6 -1
  2. package/README.md +13 -2
  3. package/bin/morph-spec.js +48 -0
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +2 -0
  9. package/framework/MORPH.md +2 -1
  10. package/framework/agents.json +20 -9
  11. package/framework/commands/create-umbraco-lp.md +1 -1
  12. package/framework/commands/morph-status.md +1 -1
  13. package/framework/docs/CLI.md +18 -0
  14. package/framework/evals/rubrics/code-quality.md +17 -0
  15. package/framework/evals/rubrics/condensed.md +3 -1
  16. package/framework/hooks/claude-code/statusline.sh +11 -11
  17. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  18. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  19. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  20. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  21. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  22. package/framework/hooks/shared/phase-utils.js +2 -0
  23. package/framework/hooks/shared/stdin-reader.js +26 -26
  24. package/framework/mcp/registry.json +97 -11
  25. package/framework/rubrics/backend.json +2 -2
  26. package/framework/rubrics/default.json +2 -2
  27. package/framework/rubrics/frontend.json +2 -2
  28. package/framework/schemas/tasks.schema.json +25 -0
  29. package/framework/skills/core/create-umbraco-lp/SKILL.md +13 -2
  30. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +598 -0
  31. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +92 -7
  32. package/framework/skills/core/morph-apply/SKILL.md +12 -3
  33. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  34. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  35. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  36. package/framework/skills/core/morph-implement/SKILL.md +12 -2
  37. package/framework/skills/core/morph-plan/SKILL.md +45 -3
  38. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  39. package/framework/skills/core/morph-review/SKILL.md +11 -3
  40. package/framework/skills/core/morph-stack-scan/SKILL.md +13 -1
  41. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  42. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  43. package/framework/standards/PROVENANCE.json +41 -0
  44. package/framework/standards/STANDARDS.json +218 -7
  45. package/framework/standards/ai-agents/agent-session.md +160 -160
  46. package/framework/standards/ai-agents/batching.md +193 -193
  47. package/framework/standards/ai-agents/context-providers.md +188 -188
  48. package/framework/standards/ai-agents/mcp-server.md +250 -250
  49. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  50. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  51. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  52. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  53. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  54. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  55. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  56. package/framework/standards/ai-agents/production.md +231 -231
  57. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  58. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  59. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  60. package/framework/standards/ai-agents/structured-output.md +249 -249
  61. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  62. package/framework/standards/frontend/design-system/ai-image-generation.md +43 -0
  63. package/framework/standards/frontend/design-system/ai-video-generation.md +113 -0
  64. package/framework/standards/frontend/design-system/premium-finish.md +160 -0
  65. package/framework/standards/frontend/gsap/gsap-core.md +1 -1
  66. package/framework/standards/frontend/gsap/gsap-plugins.md +1 -1
  67. package/framework/standards/frontend/gsap/gsap-scrolltrigger.md +3 -1
  68. package/framework/standards/frontend/nextjs/motion-patterns.md +3 -1
  69. package/framework/standards/frontend/scroll-driven/frame-scrub.md +287 -0
  70. package/framework/standards/frontend/scroll-driven/scroll-components.md +256 -0
  71. package/framework/standards/frontend/scroll-driven/smooth-scroll.md +148 -0
  72. package/framework/standards/frontend/seo/meta-schema-audit.md +134 -0
  73. package/framework/templates/.idea/morph-templates.xml +92 -0
  74. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  75. package/framework/templates/IDE-SNIPPETS.md +262 -262
  76. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  77. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  78. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  79. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  80. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  81. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  82. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  83. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  84. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  85. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  86. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  87. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  88. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  89. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  90. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  91. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  92. package/framework/templates/code/typescript/contracts.ts +167 -167
  93. package/framework/templates/context/ARCHITECTURE.md +20 -0
  94. package/framework/templates/docs/clarifications.md +253 -253
  95. package/framework/templates/docs/schema-analysis.md +119 -119
  96. package/framework/templates/docs/ui-components.md +52 -52
  97. package/framework/templates/docs/ui-design-system.md +76 -76
  98. package/framework/templates/docs/ui-flows.md +167 -167
  99. package/framework/templates/docs/ui-mockups.md +98 -98
  100. package/framework/templates/feature/decisions.md +122 -122
  101. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  102. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  103. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  104. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  105. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  106. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  107. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  108. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  109. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  110. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  111. package/framework/templates/infrastructure/github/README.md +56 -56
  112. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  113. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  114. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  115. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  116. package/framework/templates/integrations/asaas-client.cs +387 -387
  117. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  118. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  119. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  120. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  121. package/framework/templates/project/validate.js +120 -120
  122. package/framework/templates/state.template.json +17 -17
  123. package/framework/templates/ui/design-system.css +226 -226
  124. package/package.json +1 -1
  125. package/src/commands/dashboard/dashboard.js +53 -53
  126. package/src/commands/graph/affected.js +89 -0
  127. package/src/commands/graph/build.js +94 -0
  128. package/src/commands/graph/explain.js +96 -0
  129. package/src/commands/graph/status.js +93 -0
  130. package/src/commands/index.js +13 -13
  131. package/src/commands/project/checkpoint.js +209 -209
  132. package/src/commands/project/doctor.js +11 -0
  133. package/src/core/paths/output-schema.js +7 -0
  134. package/src/core/state/phase-state-machine.js +197 -197
  135. package/src/core/templates/template-renderer.js +477 -477
  136. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  137. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  138. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  139. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  140. package/src/dashboard/app/main.jsx +13 -13
  141. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  142. package/src/dashboard/app/pages/HookInspector.jsx +418 -418
  143. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  144. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  145. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  146. package/src/dashboard/app/pages/Standards.jsx +332 -332
  147. package/src/dashboard/app/styles/index.css +47 -47
  148. package/src/dashboard/index.html +12 -12
  149. package/src/dashboard/package.json +24 -24
  150. package/src/dashboard/scanner/phases-scanner.js +15 -15
  151. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  152. package/src/dashboard/scanner/rules-scanner.js +37 -37
  153. package/src/dashboard/scanner/skills-scanner.js +43 -43
  154. package/src/dashboard/scanner/squads-scanner.js +13 -13
  155. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  156. package/src/dashboard/vite.config.js +24 -24
  157. package/src/lib/detectors/design-system-detector.js +188 -188
  158. package/src/lib/detectors/index.js +217 -217
  159. package/src/lib/generators/architecture-scanner.js +45 -4
  160. package/src/lib/generators/design-system-generator.js +508 -508
  161. package/src/lib/generators/settings-generator.js +209 -209
  162. package/src/lib/graph/god-nodes.js +75 -0
  163. package/src/lib/graph/graph-health.js +61 -0
  164. package/src/lib/graph/graph-loader.js +86 -0
  165. package/src/lib/graph/graphify-runtime.js +100 -0
  166. package/src/lib/graph/graphifyignore.js +86 -0
  167. package/src/lib/graph/sanitize.js +280 -0
  168. package/src/lib/hooks/hook-executor.js +167 -167
  169. package/src/lib/installers/mcp-installer.js +719 -719
  170. package/src/lib/tasks/task-classifier.js +30 -30
  171. package/src/lib/tasks/task-parser.js +209 -209
  172. package/src/lib/tasks/tasks-json-parser.js +26 -0
  173. package/src/lib/validators/checkpoint-persistence.js +39 -39
  174. package/src/lib/validators/css/css-validator.js +340 -340
  175. package/src/lib/validators/reuse/index.js +10 -0
  176. package/src/lib/validators/reuse/reuse-validator.js +324 -0
  177. package/src/lib/validators/reuse/symbol-scanner.js +144 -0
  178. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  179. package/src/lib/validators/shared/issue-counter.js +14 -14
  180. package/src/lib/validators/validation-runner.js +40 -1
  181. package/src/scripts/setup-infra.js +60 -1
  182. package/src/utils/agent-id-mapper.js +11 -11
  183. package/src/utils/claude-settings-manager.js +300 -300
  184. package/src/utils/file-copier.js +40 -0
  185. package/src/utils/skills-installer.js +187 -187
@@ -0,0 +1,598 @@
1
+ # Umbraco 17 / .NET 10 + Next.js: Padrões e Gotchas do Sistema de Leads (Frente 4)
2
+
3
+ > **Origem:** documentado a partir do código real em `MORPH_Umbraco_LP_Template` (template canônico) e validado contra `Gardim_LP_Umbraco` (implementação de referência).
4
+ > **Natureza:** referência consultável para devs adaptando o template para novos clientes — não é um skill executável. A lógica vive no template repo; este doc é o mapa dos padrões e armadilhas.
5
+ > **Convenção de citação:** cada padrão cita o **arquivo real** no `MORPH_Umbraco_LP_Template`. Símbolos (método/classe/alias) são âncoras estáveis; números de linha, quando aparecem, são auxiliares e podem deslizar entre versões.
6
+
7
+ ---
8
+
9
+ ## Sumário de Seções
10
+
11
+ 1. [Controller público no Umbraco v14+/v17](#1-controller-público-no-umbraco-v14v17)
12
+ 2. [DI via IComposer + estratégia multi-sink](#2-di-via-icomposer--estratégia-multi-sink)
13
+ 3. [Leitura de conteúdo publicado server-side](#3-leitura-de-conteúdo-publicado-server-side)
14
+ 4. [Segurança da Delivery API — DisallowedContentTypeAliases](#4-segurança-da-delivery-api--disallowedcontenttypealiases)
15
+ 5. [uSync como config-as-code](#5-usync-como-config-as-code)
16
+ 6. [GOTCHA: Dropdown Flexível armazena JSON array](#6-gotcha-dropdown-flexível-armazena-json-array)
17
+ 7. [Seeding de conteúdo programático e idempotente](#7-seeding-de-conteúdo-programático-e-idempotente)
18
+ 8. [Implicit usings: o que é grátis e o que falta](#8-implicit-usings-o-que-é-grátis-e-o-que-falta)
19
+ 9. [Frontend thin-proxy (Next.js route handler)](#9-frontend-thin-proxy-nextjs-route-handler)
20
+ 10. [Endpoint Ping — validação sem vazar segredos](#10-endpoint-ping--validação-sem-vazar-segredos)
21
+ 11. [Padrão de sink: skip silencioso em dev/staging](#11-padrão-de-sink-skip-silencioso-em-devstaging)
22
+ 12. [Build do Next.js exige backend acessível](#12-build-do-nextjs-exige-backend-acessível)
23
+
24
+ ---
25
+
26
+ ## 1. Controller público no Umbraco v14+/v17
27
+
28
+ ### O que / por quê
29
+
30
+ `UmbracoApiController` (base de controllers públicos no Umbraco 13 e anteriores) foi removido no v14. O controller de leads é um `ControllerBase` puro do ASP.NET Core, com roteamento por atributo (`[ApiController]` + `[Route]`) — sem nenhuma herança de tipo Umbraco.
31
+
32
+ ### Como
33
+
34
+ ```csharp
35
+ // backend/Controllers/LeadController.cs
36
+ [ApiController]
37
+ [Route("/api/leads")]
38
+ public sealed class LeadController : ControllerBase
39
+ {
40
+ // nenhuma herança Umbraco — só DI de serviços Umbraco no construtor
41
+ }
42
+ ```
43
+
44
+ O construtor recebe dependências Umbraco por DI (`IUmbracoContextFactory`, `IDocumentNavigationQueryService`) ao lado de serviços ASP.NET padrão (`IHostEnvironment`, `ILogger<T>`) e a coleção de sinks (`IEnumerable<ILeadSink>`).
45
+
46
+ **Nota de mecanismo (verificado vs. inferido):** o pipeline em `backend/Program.cs` usa `.AddBackOffice().AddWebsite().AddDeliveryApi().AddComposers()` e, no middleware, `WithEndpoints(u => { u.UseBackOfficeEndpoints(); u.UseWebsiteEndpoints(); })`. Não há `MapControllers()` explícito; o controller é descoberto pelo pipeline MVC que o Umbraco habilita. O que é certo afirmar: `.AddComposers()` é o que registra o `IComposer` (ver seção 2) — ele **não** é "o registrador do controller". Evite afirmar o mecanismo exato de descoberta do controller além disso.
47
+
48
+ **Arquivos:** `backend/Controllers/LeadController.cs` (atributos e construtor); `backend/Program.cs` (pipeline).
49
+
50
+ ---
51
+
52
+ ## 2. DI via IComposer + estratégia multi-sink
53
+
54
+ ### O que / por quê
55
+
56
+ O ponto de extensão para registrar serviços no DI do Umbraco é `IComposer` (namespace `Umbraco.Cms.Core.Composing`). Ele recebe um `IUmbracoBuilder` que expõe `builder.Services` — o `IServiceCollection` padrão do ASP.NET Core.
57
+
58
+ Para despachar o lead à integração certa (GHL, prospectPRO ou webhook genérico) sem um `switch` no controller, o template usa **strategy pattern via `IEnumerable<ILeadSink>`**: cada implementação é registrada como singleton do mesmo serviço `ILeadSink`, e o controller seleciona pelo `Key`.
59
+
60
+ ### Como
61
+
62
+ ```csharp
63
+ // backend/Lead/ILeadSink.cs
64
+ public interface ILeadSink
65
+ {
66
+ string Key { get; } // "ghl" | "prospectpro" | "webhook"
67
+ Task<LeadSendResult> SendAsync(LeadInput input, LeadsConfig cfg, CancellationToken ct);
68
+ }
69
+
70
+ // backend/Lead/LeadComposer.cs
71
+ public sealed class LeadComposer : IComposer
72
+ {
73
+ public void Compose(IUmbracoBuilder builder)
74
+ {
75
+ builder.Services.AddSingleton<ILeadSink, GhlLeadSink>();
76
+ builder.Services.AddSingleton<ILeadSink, ProspectProLeadSink>();
77
+ builder.Services.AddSingleton<ILeadSink, WebhookLeadSink>();
78
+ }
79
+ }
80
+
81
+ // backend/Controllers/LeadController.cs (dispatch, dentro de Submit)
82
+ var sink = _sinks.FirstOrDefault(s => s.Key == cfg.Integration);
83
+ ```
84
+
85
+ O ASP.NET Core DI injeta `IEnumerable<ILeadSink>` automaticamente quando há múltiplas implementações do mesmo serviço. Adicionar uma quarta integração = nova classe `ILeadSink` + uma linha no `Compose`. O `Key` de cada sink é uma propriedade fixa (`"ghl"`, `"prospectpro"`, `"webhook"`) que deve casar com os valores do dropdown `leadIntegration` (seção 5/6).
86
+
87
+ **Arquivos:** `backend/Lead/LeadComposer.cs`; `backend/Lead/ILeadSink.cs`; `backend/Controllers/LeadController.cs` (construtor `IEnumerable<ILeadSink> sinks` + dispatch em `Submit`); sinks em `backend/Lead/GhlLeadSink.cs`, `backend/Lead/ProspectProLeadSink.cs`, `backend/Lead/WebhookLeadSink.cs`.
88
+
89
+ ---
90
+
91
+ ## 3. Leitura de conteúdo publicado server-side
92
+
93
+ ### O que / por quê
94
+
95
+ O controller precisa ler o node `leadsConfig` (que contém tokens de integração) do cache publicado — sem passar pela Delivery API pública, que exclui esse tipo (seção 4). O padrão usado em `ReadConfig()`:
96
+
97
+ 1. Abrir um contexto Umbraco com `using var cref = _umbracoContextFactory.EnsureUmbracoContext();` (a forma exata usada no código).
98
+ 2. Pegar o cache de conteúdo via `cref.UmbracoContext.Content`.
99
+ 3. Navegar pelas raízes com `IDocumentNavigationQueryService.TryGetRootKeys(out IEnumerable<Guid> rootKeys)`.
100
+ 4. Resolver cada raiz com `content.GetById(k)`, filtrar nulos com `.WhereNotNull()` e selecionar pelo `ContentType.Alias`.
101
+ 5. Ler valores tipados via `node.Value<T>(alias)` (extension de `Umbraco.Extensions`).
102
+
103
+ ### Como
104
+
105
+ ```csharp
106
+ // backend/Controllers/LeadController.cs — método ReadConfig()
107
+ private LeadsConfig? ReadConfig()
108
+ {
109
+ using var cref = _umbracoContextFactory.EnsureUmbracoContext();
110
+ IPublishedContentCache? content = cref.UmbracoContext.Content;
111
+ if (content is null || !_navigationQueryService.TryGetRootKeys(out IEnumerable<Guid> rootKeys))
112
+ return null;
113
+
114
+ IPublishedContent? node = rootKeys
115
+ .Select(k => content.GetById(k))
116
+ .WhereNotNull()
117
+ .FirstOrDefault(c => c.ContentType.Alias == ConfigAlias); // ConfigAlias = "leadsConfig"
118
+
119
+ if (node is null) return null;
120
+
121
+ return new LeadsConfig
122
+ {
123
+ Integration = (node.Value<string>("leadIntegration") ?? "none").Trim(),
124
+ NodeKey = node.Key,
125
+ GhlToken = (node.Value<string>("ghlApiToken") ?? string.Empty).Trim(),
126
+ // ... demais campos GHL / prospectPRO / webhook
127
+ };
128
+ }
129
+ ```
130
+
131
+ **Nota de tipo (`UmbracoContextReference`):** use `using var cref = _umbracoContextFactory.EnsureUmbracoContext();` — a forma usada no código — e acesse `cref.UmbracoContext.Content`. O `var` infere o tipo correto sem você precisar nomeá-lo; é a forma idiomática e a que está no template.
132
+
133
+ **Usings que o código importa explicitamente** (não cobertos pelos implicit usings — ver seção 8):
134
+ ```csharp
135
+ using Umbraco.Cms.Core.Models.PublishedContent; // IPublishedContent
136
+ using Umbraco.Cms.Core.PublishedCache; // IPublishedContentCache
137
+ using Umbraco.Cms.Core.Services.Navigation; // IDocumentNavigationQueryService
138
+ using Umbraco.Cms.Core.Web; // IUmbracoContextFactory
139
+ using Umbraco.Extensions; // node.Value<T>() / .WhereNotNull()
140
+ ```
141
+
142
+ **Arquivos:** `backend/Controllers/LeadController.cs` (método `ReadConfig` + bloco de usings no topo).
143
+
144
+ ---
145
+
146
+ ## 4. Segurança da Delivery API — DisallowedContentTypeAliases
147
+
148
+ ### O que / por quê
149
+
150
+ A Delivery API do Umbraco exclui por **content type inteiro**, não por propriedade. A chave `DisallowedContentTypeAliases` remove um tipo da superfície pública. O template usa isso para esconder `leadsConfig` (que guarda tokens) da API pública, lendo-o só server-side pelo cache publicado (seção 3).
151
+
152
+ ### Como
153
+
154
+ ```jsonc
155
+ // backend/appsettings.json — caminho COMPLETO da chave: Umbraco:CMS:DeliveryApi
156
+ {
157
+ "Umbraco": {
158
+ "CMS": {
159
+ "DeliveryApi": {
160
+ "Enabled": true,
161
+ "PublicAccess": true,
162
+ "RichTextOutputAsMarkdown": false,
163
+ "ApiKey": "",
164
+ "DisallowedContentTypeAliases": [ "leadsConfig" ]
165
+ }
166
+ }
167
+ }
168
+ }
169
+ ```
170
+
171
+ > Atenção ao aninhamento: a chave fica sob `Umbraco:CMS:DeliveryApi`, não em um bloco `DeliveryApi` de topo. Mesma regra para `uSync` (seção 5), que **é** de topo — não confunda os dois.
172
+
173
+ O `ContentType` `leadsConfig` é declarado com `AllowAtRoot=True` e descrição explícita de que não é exposto na Delivery API (ver o `.config`). O controller lê o node normalmente pelo published cache (seção 3); o frontend nunca recebe o node nem os tokens.
174
+
175
+ **Arquivos:** `backend/appsettings.json` (`Umbraco:CMS:DeliveryApi:DisallowedContentTypeAliases`); `backend/uSync/v17/ContentTypes/leadsConfig.config` (a `<Description>` documenta a intenção: "NÃO é exposto na Delivery API pública — tokens ficam seguros e são lidos só server-side pelo LeadController").
176
+
177
+ ---
178
+
179
+ ## 5. uSync como config-as-code
180
+
181
+ ### O que / por quê
182
+
183
+ uSync sincroniza DataTypes, ContentTypes, MediaTypes e Languages via arquivos versionados (`.config`, formato XML) em `backend/uSync/v17/`. `ImportAtStartup: "All"` reimporta o schema a cada boot — idempotente e replicável em ambientes limpos.
184
+
185
+ ### Como
186
+
187
+ ```jsonc
188
+ // backend/appsettings.json — bloco uSync é de TOPO (não sob Umbraco:CMS)
189
+ {
190
+ "uSync": {
191
+ "Sets": {
192
+ "Default": {
193
+ "DisabledHandlers": [ "MemberTypeHandler", "MacroHandler" ]
194
+ }
195
+ },
196
+ "Settings": {
197
+ "UIEnabledGroups": "Settings",
198
+ "ExportOnSave": "Settings",
199
+ "ImportAtStartup": "All",
200
+ "ExportAtStartup": "None",
201
+ "BackgroundStartup": true
202
+ }
203
+ }
204
+ }
205
+ ```
206
+
207
+ > O template **já desabilita** `MemberTypeHandler` e `MacroHandler` (em `Sets:Default:DisabledHandlers`) — handlers sem uso numa LP simples, evitando ruído no import. Não é um "pode-se desabilitar"; está feito.
208
+
209
+ **Formato do DataType dropdown** (`backend/uSync/v17/DataTypes/LeadIntegrationDropdown.config`):
210
+ ```xml
211
+ <?xml version="1.0" encoding="utf-8"?>
212
+ <DataType Key="a3000001-0000-0000-0000-000000000001" Alias="Lead Integration" Level="1">
213
+ <Info>
214
+ <Name>Lead Integration</Name>
215
+ <EditorAlias>Umbraco.DropDown.Flexible</EditorAlias>
216
+ <DatabaseType>Nvarchar</DatabaseType>
217
+ </Info>
218
+ <Config><![CDATA[{
219
+ "Items": [
220
+ {"id": 1, "value": "none"},
221
+ {"id": 2, "value": "ghl"},
222
+ {"id": 3, "value": "prospectpro"},
223
+ {"id": 4, "value": "webhook"}
224
+ ],
225
+ "Multiple": false
226
+ }]]></Config>
227
+ </DataType>
228
+ ```
229
+
230
+ **Regras do formato:**
231
+ - `Items` é array de `{id: number, value: string}` — **sem campo `label` separado** nesta versão do editor; o `value` é o que aparece e o que é salvo.
232
+ - `Multiple: false` → single-select (mas ainda persiste como JSON array — ver seção 6).
233
+ - O `Config` vai dentro de `<![CDATA[ ... ]]>` e é **JSON**, não XML.
234
+ - `Key` (GUID) e `Alias` são únicos no repositório. Use GUIDs fixos para que o import seja idempotente e replicável entre ambientes.
235
+
236
+ **Arquivos:** `backend/appsettings.json` (blocos `uSync:Sets` e `uSync:Settings`); `backend/uSync/v17/DataTypes/LeadIntegrationDropdown.config`; `backend/uSync/v17/ContentTypes/leadsConfig.config`.
237
+
238
+ ---
239
+
240
+ ## 6. GOTCHA: Dropdown Flexível armazena JSON array
241
+
242
+ ### O que / por quê
243
+
244
+ `Umbraco.DropDown.Flexible` persiste o valor como **JSON array**, mesmo com `Multiple: false`. Ou seja, `"ghl"` deve ser gravado como `["ghl"]`.
245
+
246
+ O template já faz assim: o `SeedContentHandler`, ao gravar `leadIntegration`, **serializa explicitamente como array de um elemento** em vez da string crua. Isso não é preferência de estilo, é o formato que o property value converter exige na leitura. Verificado em campo: com a string crua gravada, o `FlexibleDropdownPropertyValueConverter` lança `JsonException: 'a' is an invalid start of a value` na primeira leitura, e o documento inteiro passa a devolver 500 na Delivery API.
247
+
248
+ ### Fix (escrita)
249
+
250
+ ```csharp
251
+ // backend/Infrastructure/SeedContentHandler.cs — EnsureLeadsConfigNode()
252
+ var leadIntegration = Environment.GetEnvironmentVariable("LEADS_INTEGRATION") ?? "none";
253
+ node.SetValue("leadIntegration", System.Text.Json.JsonSerializer.Serialize(new[] { leadIntegration }));
254
+ // Grava no banco: ["none"] (ou ["ghl"] etc.) — formato JSON array que o converter espera.
255
+ ```
256
+
257
+ ### Leitura
258
+
259
+ ```csharp
260
+ // backend/Controllers/LeadController.cs — ReadConfig()
261
+ Integration = (node.Value<string>("leadIntegration") ?? "none").Trim();
262
+ // Com Multiple:false, Value<string>() devolve a string desembrulhada ("ghl"), não o array.
263
+ ```
264
+
265
+ **Regra mnemônica:** escreva com `JsonSerializer.Serialize(new[]{ value })`, leia com `Value<string>()`. O Umbraco faz o wrap/unwrap — desde que o valor escrito esteja no formato JSON array.
266
+
267
+ ### O caso que mais escapa: dropdown dentro de Block List
268
+
269
+ A regra acima é fácil de lembrar quando existe um `SetValue` para escrever. Dentro de um Block List não existe: o bloco é montado como **JSON literal** no seed, e aí a tentação é escrever o valor como string, que é o que qualquer outro campo do bloco usa.
270
+
271
+ ```jsonc
272
+ // errado — o backoffice aceita, a leitura estoura
273
+ "beat1CtaAcao": "ancora",
274
+
275
+ // certo — array JSON, igual ao que SetValue produziria
276
+ "beat1CtaAcao": ["ancora"],
277
+ ```
278
+
279
+ **O estrago não fica no campo.** O converter roda ao montar o elemento do bloco, então a exceção sobe pelo `BlockListPropertyValueConverter` e **derruba o documento inteiro**: a Delivery API devolve **500 na home**, não um campo vazio. E como o `next build` faz SSG consultando a Delivery API, o build do frontend morre junto, reportando apenas `npm run build ... exit code 1`. O sintoma que chega ao dev ("o projeto não sobe do zero", "o build quebrou no Docker") está a três camadas da linha errada.
280
+
281
+ Isso só aparece com **banco novo**. Um ambiente que já rodava continua funcionando, porque o valor foi gravado antes ou corrigido por uma edição no backoffice, que sempre grava no formato certo. Ou seja: passa despercebido até o primeiro `--reset-db`, o primeiro deploy limpo ou o primeiro colega clonando o projeto.
282
+
283
+ ### Diagnóstico
284
+
285
+ O `JsonException` informa o **primeiro caractere** do valor ofensor, e isso basta para achá-lo:
286
+
287
+ 1. Ler o stack no log (`umbraco/Logs/*.json`, campo `@x`): anotar o converter que falhou e o caractere citado (`'a' is an invalid start of a value`).
288
+ 2. Isolar o documento: `GET /umbraco/delivery/api/v2/content?filter=contentType:<alias>`, um tipo por vez, até achar o que devolve 500.
289
+ 3. Listar o que exige array: `grep -rn "<Type>Umbraco.DropDown.Flexible</Type>" backend/uSync/v17/ContentTypes/` e conferir como cada propriedade é seedada.
290
+ 4. Confirmar por A/B: corrigir uma e re-seedar com banco limpo. Se o caractere do erro **mudar**, aquela foi resolvida e existe outra; se o 500 sumir, acabou.
291
+
292
+ **Arquivos:** `backend/Infrastructure/SeedContentHandler.cs` (`EnsureLeadsConfigNode`, a chamada `SetValue("leadIntegration", JsonSerializer.Serialize(new[]{ leadIntegration }))`); `backend/Controllers/LeadController.cs` (`ReadConfig`, leitura de `leadIntegration` com `Value<string>()`).
293
+
294
+ ---
295
+
296
+ ## 7. Seeding de conteúdo programático e idempotente
297
+
298
+ ### O que / por quê
299
+
300
+ O `SeedContentHandler` implementa `INotificationAsyncHandler<uSyncImportCompletedNotification>` — dispara depois que o uSync terminou de importar o schema (DataTypes + ContentTypes), garantindo que os tipos existem antes de criar nodes. É registrado em `Program.cs`.
301
+
302
+ Idempotência: cada `Ensure...` guarda contra duplicata com filtro **por content type alias** sobre as root content — não uma query genérica sem filtro (que poderia pegar o node errado).
303
+
304
+ ```csharp
305
+ // backend/Infrastructure/SeedContentHandler.cs
306
+
307
+ // guarda da home (HandleAsync) — filtra por "empreendimentoHome"
308
+ var existing = _contentService.GetRootContent()
309
+ .FirstOrDefault(c => c.ContentType.Alias == "empreendimentoHome");
310
+
311
+ // guarda do config (EnsureLeadsConfigNode) — filtra por "leadsConfig"
312
+ var existing = _contentService.GetRootContent()
313
+ .FirstOrDefault(c => c.ContentType.Alias == "leadsConfig");
314
+ ```
315
+
316
+ > Note que são **duas guardas distintas, por alias diferente**: `empreendimentoHome` para a home, `leadsConfig` para o node de config. Filtrar por alias evita pegar o primeiro root node por acaso.
317
+
318
+ ### Registro e publicação
319
+
320
+ ```csharp
321
+ // backend/Program.cs — no pipeline do CreateUmbracoBuilder
322
+ .AddNotificationAsyncHandler<uSyncImportCompletedNotification,
323
+ MORPH.Backend.Infrastructure.SeedContentHandler>()
324
+
325
+ // backend/Infrastructure/SeedContentHandler.cs — publicação (home e config)
326
+ _contentService.Publish(content, Array.Empty<string>());
327
+ // Array.Empty<string>() é a forma usada no código: publicação invariant (sem culturas).
328
+ ```
329
+
330
+ **Variáveis de ambiente** para personalizar sem alterar código:
331
+ ```csharp
332
+ var projectName = Environment.GetEnvironmentVariable("PROJECT_NAME") ?? "Meu Empreendimento";
333
+ var leadIntegration = Environment.GetEnvironmentVariable("LEADS_INTEGRATION") ?? "none";
334
+ var isSeedDemo = string.Equals(Environment.GetEnvironmentVariable("SEED_DEMO_CONTENT"), "true",
335
+ StringComparison.OrdinalIgnoreCase); // popula módulos demo
336
+ ```
337
+
338
+ **Tokens e segredos NÃO são semeados** — ficam em branco para o editor preencher no backoffice. Isso é intencional (comentário explícito no código: "Template genérico: NÃO semear tokens/ids — ficam em branco pro editor preencher no backoffice").
339
+
340
+ **Arquivos:** `backend/Infrastructure/SeedContentHandler.cs` (handler inteiro: `HandleAsync`, `EnsureLeadsConfigNode`, `PopulateDemoModules`); `backend/Program.cs` (`AddNotificationAsyncHandler<...>`).
341
+
342
+ ---
343
+
344
+ ## 8. Implicit usings: o que é grátis e o que falta
345
+
346
+ ### O que / por quê
347
+
348
+ O projeto liga `<ImplicitUsings>enable</ImplicitUsings>` (`backend/MORPH.Backend.csproj`) sobre o `Microsoft.NET.Sdk.Web`. Isso gera um conjunto de `global using` automático. **Não confie na memória** sobre o que está incluído — o arquivo gerado é a fonte da verdade: `backend/obj/Debug/net10.0/MORPH.Backend.GlobalUsings.g.cs`.
349
+
350
+ ### O que JÁ é implicit (não precisa importar)
351
+
352
+ Verbatim do `GlobalUsings.g.cs` gerado:
353
+
354
+ | Namespace implicit | Cobre |
355
+ |---|---|
356
+ | `Microsoft.Extensions.Logging` | `ILogger<T>`, `ILoggerFactory` |
357
+ | `Microsoft.Extensions.Hosting` | `IHostEnvironment`, `.IsDevelopment()` |
358
+ | `Microsoft.Extensions.DependencyInjection` | `IServiceCollection`, `AddSingleton<>` |
359
+ | `Microsoft.Extensions.Configuration` | `IConfiguration` |
360
+ | `Umbraco.Cms.Core.DependencyInjection` | `IUmbracoBuilder` |
361
+ | `Umbraco.Extensions` | `node.Value<T>()`, `.WhereNotNull()` |
362
+ | `System.Linq`, `System.Net.Http`, `System.Threading.Tasks`, etc. | LINQ, HttpClient, Task |
363
+
364
+ > Prova empírica: `backend/Lead/GhlLeadSink.cs` usa `ILogger<GhlLeadSink>?` **sem** importar `Microsoft.Extensions.Logging` e compila — porque o namespace é implicit. (Os arquivos `WebhookLeadSink.cs`/`ProspectProLeadSink.cs` repetem o `using Microsoft.Extensions.Logging;` por hábito; é redundante, não obrigatório.)
365
+
366
+ ### O que NÃO é implicit (precisa importar)
367
+
368
+ Os namespaces **Umbraco** abaixo não estão no conjunto implicit e geram `CS0246` se faltarem. São exatamente os que `LeadController.cs` / `LeadComposer.cs` importam à mão:
369
+
370
+ | Tipo | Using necessário |
371
+ |---|---|
372
+ | `IPublishedContent` | `using Umbraco.Cms.Core.Models.PublishedContent;` |
373
+ | `IPublishedContentCache` | `using Umbraco.Cms.Core.PublishedCache;` |
374
+ | `IDocumentNavigationQueryService` | `using Umbraco.Cms.Core.Services.Navigation;` |
375
+ | `IUmbracoContextFactory` | `using Umbraco.Cms.Core.Web;` |
376
+ | `IComposer` | `using Umbraco.Cms.Core.Composing;` |
377
+
378
+ > Atenção ao par: `IUmbracoBuilder` é implicit (`Umbraco.Cms.Core.DependencyInjection`), mas `IComposer` **não** é (`Umbraco.Cms.Core.Composing`). Ao escrever um novo `IComposer`, você precisa de ambos os usings (um implicit, um explícito), como em `LeadComposer.cs`.
379
+
380
+ **Dica de diagnóstico:** antes de adicionar usings por tentativa-e-erro, abra `backend/obj/Debug/net10.0/MORPH.Backend.GlobalUsings.g.cs` e confira o que já está lá. É o jeito determinístico de saber.
381
+
382
+ **Arquivos:** `backend/MORPH.Backend.csproj` (`<ImplicitUsings>enable</ImplicitUsings>`); `backend/obj/Debug/net10.0/MORPH.Backend.GlobalUsings.g.cs` (lista gerada — fonte da verdade); `backend/Controllers/LeadController.cs` e `backend/Lead/LeadComposer.cs` (usings explícitos Umbraco); `backend/Lead/GhlLeadSink.cs` (prova de que `Microsoft.Extensions.Logging` é implicit).
383
+
384
+ ---
385
+
386
+ ## 9. Frontend thin-proxy (Next.js route handler)
387
+
388
+ ### O que / por quê
389
+
390
+ A LP Next.js **não** implementa lógica de integração. O route handler só faz proxy do POST do formulário para o backend Umbraco. Motivos:
391
+ - Tokens de integração ficam no backend Umbraco (server-side, ocultos da Delivery API — seção 4).
392
+ - Evita CORS browser → Umbraco: o form posta same-origin para `/api/leads` (Next.js), que repassa para o Umbraco.
393
+
394
+ ### Como
395
+
396
+ ```typescript
397
+ // frontend/src/app/api/leads/route.ts
398
+ const BACKEND = (
399
+ process.env.UMBRACO_INTERNAL_URL || // container-to-container (ex.: http://backend:19223)
400
+ process.env.NEXT_PUBLIC_UMBRACO_URL || // fallback
401
+ 'http://localhost:19223'
402
+ ).replace(/\/+$/, '')
403
+
404
+ export async function POST(request: NextRequest): Promise<NextResponse> {
405
+ const raw = await request.text()
406
+ try {
407
+ const res = await fetch(`${BACKEND}/api/leads`, {
408
+ method: 'POST',
409
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
410
+ body: raw || '{}',
411
+ cache: 'no-store',
412
+ })
413
+ const text = await res.text()
414
+ return new NextResponse(text || '{"ok":false}', {
415
+ status: res.status,
416
+ headers: { 'Content-Type': 'application/json' },
417
+ })
418
+ } catch (err) {
419
+ console.error('[leads proxy] backend inalcançável:', err instanceof Error ? err.message : err)
420
+ return NextResponse.json({ ok: false, error: 'backend_unreachable' }, { status: 502 })
421
+ }
422
+ }
423
+ ```
424
+
425
+ **Variáveis de ambiente:**
426
+ - `UMBRACO_INTERNAL_URL` — URL interna container-to-container (não exposta ao browser). Preferida.
427
+ - `NEXT_PUBLIC_UMBRACO_URL` — URL pública do Umbraco (também usada pelo `umbraco-client.ts` para a Delivery API).
428
+
429
+ O proxy repassa o body cru (`raw`) sem reinterpretar. Honeypot (`hp`), validação (nome/telefone/consent) e dispatch acontecem **no controller Umbraco**, não no proxy. O controller trata honeypot devolvendo `200 { ok: true }` sem processar quando `hp` não está vazio.
430
+
431
+ **Arquivos:** `frontend/src/app/api/leads/route.ts` (handler inteiro).
432
+
433
+ ---
434
+
435
+ ## 10. Endpoint Ping — validação sem vazar segredos
436
+
437
+ ### O que / por quê
438
+
439
+ `GET /api/leads` (método `Ping`) devolve só `{ ok, service }` em produção e diagnóstico ampliado em Development. Permite validar a config em CI/deploy sem nunca expor tokens ou URLs de integração.
440
+
441
+ ### Como
442
+
443
+ ```csharp
444
+ // backend/Controllers/LeadController.cs — Ping()
445
+ [HttpGet]
446
+ public IActionResult Ping()
447
+ {
448
+ if (!_env.IsDevelopment())
449
+ return Ok(new { ok = true, service = "leads" });
450
+
451
+ var cfg = ReadConfig();
452
+ bool ready = cfg?.Integration switch
453
+ {
454
+ "ghl" => !string.IsNullOrWhiteSpace(cfg.GhlToken),
455
+ "prospectpro" => !string.IsNullOrWhiteSpace(cfg.PpApiUrl) && !string.IsNullOrWhiteSpace(cfg.PpApiToken),
456
+ "webhook" => !string.IsNullOrWhiteSpace(cfg.WhUrl),
457
+ _ => false,
458
+ };
459
+ return Ok(new
460
+ {
461
+ ok = true,
462
+ service = "leads", // presente nas DUAS respostas (prod e dev)
463
+ dev = true,
464
+ integration = cfg?.Integration, // "ghl" | "prospectpro" | "webhook" | "none"
465
+ configFound = cfg is not null,
466
+ ready, // bool derivado: a integração está completa?
467
+ nodeId = cfg?.NodeKey, // GUID do node (não é segredo)
468
+ });
469
+ }
470
+ ```
471
+
472
+ `ready` é boolean derivado da presença dos campos — **nunca** o token em si. `nodeId` é o GUID interno (`LeadsConfig.NodeKey`), não sensível. O campo `service = "leads"` aparece em ambos os ramos (prod e dev).
473
+
474
+ **Arquivos:** `backend/Controllers/LeadController.cs` (método `Ping`).
475
+
476
+ ---
477
+
478
+ ## 11. Padrão de sink: skip silencioso em dev/staging
479
+
480
+ ### O que / por quê
481
+
482
+ Todo sink faz **early return** quando a config essencial falta, devolvendo `new LeadSendResult(true, true)` — `Ok: true, Skipped: true`. Assim a LP funciona (form dá sucesso visual) mesmo sem integração configurada — útil em dev, staging ou antes de o cliente preencher os tokens no backoffice.
483
+
484
+ O controller traduz `Skipped: true` em `200 { ok: true, skipped: true }` — sem erro no form.
485
+
486
+ ### Como
487
+
488
+ ```csharp
489
+ // LeadSendResult: backend/Lead/LeadModels.cs
490
+ public sealed record LeadSendResult(bool Ok, bool Skipped, string? Id = null, string? Error = null);
491
+
492
+ // backend/Lead/GhlLeadSink.cs — SendAsync()
493
+ if (string.IsNullOrWhiteSpace(cfg.GhlToken))
494
+ return new LeadSendResult(true, true); // skip silencioso
495
+
496
+ // backend/Lead/WebhookLeadSink.cs — SendAsync()
497
+ if (string.IsNullOrWhiteSpace(cfg.WhUrl))
498
+ return new LeadSendResult(true, true);
499
+
500
+ // backend/Lead/ProspectProLeadSink.cs — SendAsync()
501
+ if (string.IsNullOrWhiteSpace(cfg.PpApiUrl) || string.IsNullOrWhiteSpace(cfg.PpApiToken))
502
+ return new LeadSendResult(true, true);
503
+ ```
504
+
505
+ ```csharp
506
+ // backend/Controllers/LeadController.cs — Submit() (dispatch)
507
+ var sink = _sinks.FirstOrDefault(s => s.Key == cfg.Integration);
508
+ if (sink is null)
509
+ return Ok(new { ok = true, skipped = true }); // "none" ou integração desconhecida
510
+
511
+ var r = await sink.SendAsync(body, cfg, ct);
512
+ if (r.Skipped) return Ok(new { ok = true, skipped = true });
513
+ return r.Ok
514
+ ? Ok(new { ok = true, id = r.Id })
515
+ : StatusCode(502, new { ok = false, error = r.Error ?? "send_failed" });
516
+ ```
517
+
518
+ > Há ainda um terceiro caminho de skip: se `ReadConfig()` devolve `null` (node `leadsConfig` ausente), o controller loga warning e devolve `200 { ok: true, skipped: true }` — não bloqueia a LP.
519
+
520
+ **Arquivos:** `backend/Lead/GhlLeadSink.cs`, `backend/Lead/WebhookLeadSink.cs`, `backend/Lead/ProspectProLeadSink.cs` (cada `SendAsync` com o early return); `backend/Lead/LeadModels.cs` (`LeadSendResult`); `backend/Controllers/LeadController.cs` (`Submit`, dispatch + guarda de config nula).
521
+
522
+ ---
523
+
524
+ ## 12. Build do Next.js exige backend acessível
525
+
526
+ ### O que / por quê
527
+
528
+ `next build` chama `getHome()` (RSC) para renderizar as páginas. Se o Umbraco não estiver acessível durante o build de produção, ele **lança erro proposital** com instrução clara. Para CI sem backend, há a flag `E2E_FAKE_DATA=true` que usa a fixture.
529
+
530
+ ### Como
531
+
532
+ ```typescript
533
+ // frontend/src/lib/umbraco-client.ts
534
+ const UMBRACO_URL = process.env.NEXT_PUBLIC_UMBRACO_URL ?? 'http://localhost:5001'
535
+ const API_BASE = `${UMBRACO_URL}/umbraco/delivery/api/v2`
536
+ const USE_FAKE_DATA = process.env.E2E_FAKE_DATA === 'true'
537
+
538
+ export async function getHome(): Promise<EmpreendimentoHome | null> {
539
+ if (USE_FAKE_DATA) return FAKE_HOME
540
+ try {
541
+ const data = await fetchApi<{ items: EmpreendimentoHome[] }>(
542
+ '/content?contentType=empreendimentoHome&take=1'
543
+ )
544
+ return data.items?.[0] ?? null
545
+ } catch (e) {
546
+ const message = `[umbraco-client] getHome failed at ${API_BASE}: ${...}`
547
+ if (process.env.NEXT_PHASE === 'phase-production-build') {
548
+ throw new Error(`${message}\n\nO Umbraco precisa estar acessível durante \`next build\`. Defina E2E_FAKE_DATA=true para usar fixture, ou suba o backend antes de buildar.`)
549
+ }
550
+ console.error(message)
551
+ return null // fora de build: degrada gracioso, página renderiza vazia
552
+ }
553
+ }
554
+ ```
555
+
556
+ **Opções em CI:**
557
+ 1. `E2E_FAKE_DATA=true` → usa `FAKE_HOME` de `frontend/src/lib/fake-data.ts`; não requer backend.
558
+ 2. Subir o backend Umbraco antes do `next build` → build real de produção.
559
+
560
+ > Comportamento por fase: **só** em `NEXT_PHASE === 'phase-production-build'` o erro é lançado. Em runtime (request normal) a falha degrada para `null` e a página renderiza vazia em vez de quebrar.
561
+
562
+ **Arquivos:** `frontend/src/lib/umbraco-client.ts` (`getHome`, `fetchApi`, `getMediaUrl`); `frontend/src/lib/fake-data.ts` (`FAKE_HOME`).
563
+
564
+ ---
565
+
566
+ ## Apêndice: fluxo do lead (end-to-end)
567
+
568
+ ```
569
+ Browser form
570
+ │ POST /api/leads (JSON: nome, telefone, email, tipo_projeto, mensagem,
571
+ │ consent, hp, utm_source, utm_medium, utm_campaign)
572
+ ▼
573
+ Next.js /api/leads (frontend/src/app/api/leads/route.ts)
574
+ │ proxy same-origin do body cru → UMBRACO_INTERNAL_URL/api/leads
575
+ ▼
576
+ LeadController.Submit (backend/Controllers/LeadController.cs)
577
+ │ 1. honeypot: hp preenchido → 200 { ok:true } sem processar
578
+ │ 2. validação: nome, telefone, consent (faltou → 422)
579
+ │ 3. ReadConfig() → node leadsConfig do published cache
580
+ │ (oculto da Delivery API; seção 3+4)
581
+ │ config nula → 200 { ok:true, skipped:true }
582
+ │ 4. dispatch: _sinks.FirstOrDefault(s => s.Key == cfg.Integration)
583
+ ▼
584
+ GhlLeadSink | ProspectProLeadSink | WebhookLeadSink (backend/Lead/*)
585
+ │ skip silencioso se token/URL ausente → { ok:true, skipped:true }
586
+ ▼
587
+ API externa (GHL upsert / prospectPRO / webhook genérico)
588
+ ```
589
+
590
+ ---
591
+
592
+ ## Onde este doc vive no morph-spec-framework
593
+
594
+ **Localização:** `framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md`, instalado junto com a skill em `.claude/skills/create-umbraco-lp/references/`. É referência estática carregada sob demanda, não skill executável.
595
+
596
+ Justificativa: o conteúdo é consumido por humanos (devs adaptando o template para novos clientes) e potencialmente por skills futuras (ex.: um `morph-framework-infra` ou skill de setup Umbraco). Não contém lógica de execução — é mapa de padrões. Pode ser linkado do README do `MORPH_Umbraco_LP_Template`.
597
+
598
+ Se o framework ganhar um skill de infraestrutura, as seções 6 (dropdown gotcha) e 3 (leitura server-side de config oculta) merecem virar notas de alerta no prompt do skill — são as armadilhas que mais custam tempo.