@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.
- package/CLAUDE.md +6 -1
- package/README.md +13 -2
- package/bin/morph-spec.js +48 -0
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -0
- package/framework/MORPH.md +2 -1
- package/framework/agents.json +20 -9
- package/framework/commands/create-umbraco-lp.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +18 -0
- package/framework/evals/rubrics/code-quality.md +17 -0
- package/framework/evals/rubrics/condensed.md +3 -1
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/phase-utils.js +2 -0
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/mcp/registry.json +97 -11
- package/framework/rubrics/backend.json +2 -2
- package/framework/rubrics/default.json +2 -2
- package/framework/rubrics/frontend.json +2 -2
- package/framework/schemas/tasks.schema.json +25 -0
- package/framework/skills/core/create-umbraco-lp/SKILL.md +13 -2
- package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +598 -0
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +92 -7
- package/framework/skills/core/morph-apply/SKILL.md +12 -3
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +12 -2
- package/framework/skills/core/morph-plan/SKILL.md +45 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +11 -3
- package/framework/skills/core/morph-stack-scan/SKILL.md +13 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/PROVENANCE.json +41 -0
- package/framework/standards/STANDARDS.json +218 -7
- package/framework/standards/ai-agents/agent-session.md +160 -160
- package/framework/standards/ai-agents/batching.md +193 -193
- package/framework/standards/ai-agents/context-providers.md +188 -188
- package/framework/standards/ai-agents/mcp-server.md +250 -250
- package/framework/standards/ai-agents/mcp-tools.md +197 -197
- package/framework/standards/ai-agents/middleware-patterns.md +207 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
- package/framework/standards/ai-agents/modalities-vision.md +201 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
- package/framework/standards/ai-agents/production.md +231 -231
- package/framework/standards/ai-agents/prompts-in-db.md +214 -214
- package/framework/standards/ai-agents/providers/model-registry.md +158 -158
- package/framework/standards/ai-agents/service-tier-flex.md +135 -135
- package/framework/standards/ai-agents/structured-output.md +249 -249
- package/framework/standards/ai-agents/sweet-spot.md +109 -109
- package/framework/standards/frontend/design-system/ai-image-generation.md +43 -0
- package/framework/standards/frontend/design-system/ai-video-generation.md +113 -0
- package/framework/standards/frontend/design-system/premium-finish.md +160 -0
- package/framework/standards/frontend/gsap/gsap-core.md +1 -1
- package/framework/standards/frontend/gsap/gsap-plugins.md +1 -1
- package/framework/standards/frontend/gsap/gsap-scrolltrigger.md +3 -1
- package/framework/standards/frontend/nextjs/motion-patterns.md +3 -1
- package/framework/standards/frontend/scroll-driven/frame-scrub.md +287 -0
- package/framework/standards/frontend/scroll-driven/scroll-components.md +256 -0
- package/framework/standards/frontend/scroll-driven/smooth-scroll.md +148 -0
- package/framework/standards/frontend/seo/meta-schema-audit.md +134 -0
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/context/ARCHITECTURE.md +20 -0
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/dashboard/dashboard.js +53 -53
- package/src/commands/graph/affected.js +89 -0
- package/src/commands/graph/build.js +94 -0
- package/src/commands/graph/explain.js +96 -0
- package/src/commands/graph/status.js +93 -0
- package/src/commands/index.js +13 -13
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +11 -0
- package/src/core/paths/output-schema.js +7 -0
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/HookInspector.jsx +418 -418
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/detectors/design-system-detector.js +188 -188
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/generators/architecture-scanner.js +45 -4
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/graph/god-nodes.js +75 -0
- package/src/lib/graph/graph-health.js +61 -0
- package/src/lib/graph/graph-loader.js +86 -0
- package/src/lib/graph/graphify-runtime.js +100 -0
- package/src/lib/graph/graphifyignore.js +86 -0
- package/src/lib/graph/sanitize.js +280 -0
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/tasks-json-parser.js +26 -0
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/reuse/index.js +10 -0
- package/src/lib/validators/reuse/reuse-validator.js +324 -0
- package/src/lib/validators/reuse/symbol-scanner.js +144 -0
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/validation-runner.js +40 -1
- package/src/scripts/setup-infra.js +60 -1
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +40 -0
- 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.
|