@polymorphism-tech/morph-spec 7.5.0 → 7.7.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/README.md +2 -2
- package/bin/morph-spec.js +18 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +12 -4
- package/framework/MORPH.md +18 -10
- package/framework/agents.json +13 -5
- package/framework/commands/morph-preflight.md +10 -5
- package/framework/hooks/README.md +8 -10
- package/framework/hooks/claude-code/core/gate-guard.js +36 -16
- package/framework/hooks/claude-code/core/protect-spec-files.js +20 -5
- package/framework/hooks/claude-code/core/tasks-json-guard.js +81 -0
- package/framework/hooks/shared/tasks-validator.js +68 -0
- package/framework/mcp/registry.json +5 -48
- package/framework/rubrics/backend.json +3 -3
- package/framework/rubrics/default.json +3 -3
- package/framework/rubrics/frontend.json +3 -3
- package/framework/rules/infrastructure-standards.md +4 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +22 -2
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +84 -5
- package/framework/skills/core/morph-apply/SKILL.md +53 -40
- package/framework/skills/core/morph-brainstorming/SKILL.md +7 -12
- package/framework/skills/core/morph-eval/SKILL.md +10 -8
- package/framework/skills/core/morph-implement/SKILL.md +27 -21
- package/framework/skills/core/morph-plan/SKILL.md +12 -21
- package/framework/skills/core/morph-proposal/SKILL.md +3 -8
- package/framework/skills/core/morph-review/SKILL.md +30 -14
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +80 -8
- package/framework/standards/ai-agents/durable-workflows-hitl.md +191 -0
- package/framework/standards/ai-agents/rag-custom-pgvector.md +246 -243
- package/framework/standards/ai-agents/setup.md +3 -0
- package/framework/standards/ai-agents/vector-data-extensions.md +170 -0
- package/framework/standards/ai-agents/workflows.md +190 -187
- package/framework/standards/infrastructure/docker/{easypanel-deploy.md → coolify-deploy.md} +49 -20
- package/framework/standards/integration/mcp/mcp-tools.md +38 -43
- package/framework/templates/README.md +7 -7
- package/framework/templates/REGISTRY.json +14 -14
- package/framework/templates/context/CONTEXT.md +3 -3
- package/framework/templates/docs/proposal.md +2 -2
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +1 -1
- package/framework/templates/infrastructure/docker/coolify.template.json +49 -0
- package/framework/templates/infrastructure/github/README.md +6 -6
- package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +33 -0
- package/package.json +1 -1
- package/src/commands/project/doctor.js +36 -1
- package/src/commands/state/approve.js +29 -4
- package/src/commands/state/score.js +88 -0
- package/src/commands/tasks/dag.js +68 -0
- package/src/dashboard/scanner/rubrics-scanner.js +1 -1
- package/src/lib/generators/recap-generator.js +1 -1
- package/src/lib/tasks/dispatch-planner.js +201 -0
- package/src/lib/validators/score-aggregator.js +3 -3
- package/src/scripts/setup-infra.js +42 -0
- package/src/utils/agents-installer.js +142 -10
- package/src/utils/hooks-installer.js +13 -5
- package/framework/rubrics/eval-rubric.json +0 -66
- package/framework/templates/infrastructure/docker/easypanel.template.json +0 -46
- package/framework/templates/infrastructure/github/workflows/deploy-easypanel.yml.hbs +0 -31
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* tasks-json-guard.js — PostToolUse hook (validate-and-warn, never block)
|
|
4
|
+
*
|
|
5
|
+
* Event: PostToolUse | Matcher: Write|Edit
|
|
6
|
+
*
|
|
7
|
+
* After any Write/Edit that touches a tasks.json under .morph/features/,
|
|
8
|
+
* re-reads the file from disk and validates the schema (status enum, effort
|
|
9
|
+
* enum). Invalid → injects a warning so the LLM fixes the file immediately;
|
|
10
|
+
* valid → logs one activity line and stays silent.
|
|
11
|
+
*
|
|
12
|
+
* The production failure this guards: a leaf sub-agent wrote
|
|
13
|
+
* status:"completed" (not in the enum — the schema accepts "done") into
|
|
14
|
+
* tasks.json and nothing flagged it. The blocking pre-write validation for
|
|
15
|
+
* post-Gate-2 edits lives in protect-spec-files.js (PreToolUse); this
|
|
16
|
+
* PostToolUse pass also covers pre-gate authoring (morph-plan) and anything
|
|
17
|
+
* that landed on disk regardless.
|
|
18
|
+
*
|
|
19
|
+
* Fail-open: exits 0 on any error.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFileSync } from 'fs';
|
|
23
|
+
import { isAbsolute, join } from 'path';
|
|
24
|
+
import { stateExists } from '../../shared/state-reader.js';
|
|
25
|
+
import { injectContext, pass } from '../../shared/hook-response.js';
|
|
26
|
+
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
27
|
+
import { getFilePath } from '../../shared/payload-utils.js';
|
|
28
|
+
import { isFeaturePath, getBasename } from '../../shared/phase-utils.js';
|
|
29
|
+
import { VALID_TASK_STATUS, validateTasksDoc } from '../../shared/tasks-validator.js';
|
|
30
|
+
|
|
31
|
+
function readStdin() {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(readFileSync(0, 'utf8') || '{}');
|
|
34
|
+
} catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function projectDir() {
|
|
40
|
+
return process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const cwd = projectDir();
|
|
45
|
+
if (!stateExists(cwd)) pass();
|
|
46
|
+
|
|
47
|
+
const payload = readStdin();
|
|
48
|
+
const filePath = getFilePath(payload);
|
|
49
|
+
if (!filePath) pass();
|
|
50
|
+
if (!isFeaturePath(filePath)) pass();
|
|
51
|
+
if (getBasename(filePath) !== 'tasks.json') pass();
|
|
52
|
+
|
|
53
|
+
const absPath = isAbsolute(filePath) ? filePath : join(cwd, filePath);
|
|
54
|
+
|
|
55
|
+
let doc;
|
|
56
|
+
try {
|
|
57
|
+
doc = JSON.parse(readFileSync(absPath, 'utf-8'));
|
|
58
|
+
} catch {
|
|
59
|
+
logHookActivity('tasks-json-guard', 'PostToolUse', 'invalid', cwd);
|
|
60
|
+
injectContext(
|
|
61
|
+
'⚠️ [morph-spec] tasks.json INVÁLIDO — o arquivo gravado não parseia como JSON. ' +
|
|
62
|
+
'Corrija o arquivo antes de continuar.'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { valid, errors } = validateTasksDoc(doc);
|
|
67
|
+
if (!valid) {
|
|
68
|
+
logHookActivity('tasks-json-guard', 'PostToolUse', 'invalid', cwd);
|
|
69
|
+
injectContext(
|
|
70
|
+
'⚠️ [morph-spec] tasks.json com schema inválido após o write:\n' +
|
|
71
|
+
errors.map(e => ` • ${e}`).join('\n') +
|
|
72
|
+
`\nStatus válidos: ${VALID_TASK_STATUS.join(', ')}. Corrija antes de marcar a task como done.`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
logHookActivity('tasks-json-guard', 'PostToolUse', 'ok', cwd);
|
|
77
|
+
pass();
|
|
78
|
+
} catch {
|
|
79
|
+
// Fail-open
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tasks-validator.js — minimal hook-side schema validation for tasks.json.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors VALID_STATUS / VALID_EFFORT from src/lib/tasks/tasks-json-parser.js.
|
|
5
|
+
* Hooks are installed standalone into target projects (.morph/framework/hooks/),
|
|
6
|
+
* so they cannot import from src/ — the enum lives here, duplicated on purpose
|
|
7
|
+
* and covered by tests on both sides.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const VALID_TASK_STATUS = ['pending', 'in_progress', 'done', 'skipped', 'blocked'];
|
|
11
|
+
export const VALID_TASK_EFFORT = ['S', 'M', 'L', 'XL'];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validate the parts of a tasks.json document that hooks care about:
|
|
15
|
+
* a tasks[] array whose entries carry a valid status (and effort, when set).
|
|
16
|
+
* The production failure this guards: a leaf agent writing status "completed"
|
|
17
|
+
* (not in the enum) without any validation.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} doc - parsed tasks.json
|
|
20
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
21
|
+
*/
|
|
22
|
+
export function validateTasksDoc(doc) {
|
|
23
|
+
if (!doc || typeof doc !== 'object' || !Array.isArray(doc.tasks)) {
|
|
24
|
+
return { valid: false, errors: ['tasks.json must be an object with a tasks[] array'] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const errors = [];
|
|
28
|
+
for (const task of doc.tasks) {
|
|
29
|
+
const id = task?.id ?? '?';
|
|
30
|
+
if (!VALID_TASK_STATUS.includes(task?.status)) {
|
|
31
|
+
errors.push(`task '${id}': invalid status '${task?.status}' (valid: ${VALID_TASK_STATUS.join(', ')})`);
|
|
32
|
+
}
|
|
33
|
+
if (task?.effort !== undefined && !VALID_TASK_EFFORT.includes(task.effort)) {
|
|
34
|
+
errors.push(`task '${id}': invalid effort '${task.effort}' (valid: ${VALID_TASK_EFFORT.join(', ')})`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { valid: errors.length === 0, errors };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Planned-vs-real outputs divergence: tasks whose `outputs` changed relative
|
|
42
|
+
* to the previous document but whose `notes` is still empty. The divergence
|
|
43
|
+
* itself is legitimate (outputs is a mutable progress field) — shipping it
|
|
44
|
+
* without an explanatory note is what gets flagged.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} oldDoc - parsed previous tasks.json
|
|
47
|
+
* @param {object} newDoc - parsed proposed tasks.json
|
|
48
|
+
* @returns {string[]} human-readable violation messages (empty = OK)
|
|
49
|
+
*/
|
|
50
|
+
export function outputsDivergenceViolations(oldDoc, newDoc) {
|
|
51
|
+
const violations = [];
|
|
52
|
+
const oldById = new Map((oldDoc?.tasks ?? []).map(t => [t.id, t]));
|
|
53
|
+
|
|
54
|
+
for (const task of newDoc?.tasks ?? []) {
|
|
55
|
+
const old = oldById.get(task.id);
|
|
56
|
+
if (!old) continue;
|
|
57
|
+
const outputsChanged =
|
|
58
|
+
JSON.stringify(old.outputs ?? []) !== JSON.stringify(task.outputs ?? []);
|
|
59
|
+
const hasNote = typeof task.notes === 'string' && task.notes.trim().length > 0;
|
|
60
|
+
if (outputsChanged && !hasNote) {
|
|
61
|
+
violations.push(
|
|
62
|
+
`task '${task.id}': outputs diverged from the plan but 'notes' is empty — ` +
|
|
63
|
+
`record why in 'notes' in the same edit`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return violations;
|
|
68
|
+
}
|
|
@@ -58,50 +58,6 @@
|
|
|
58
58
|
"setupGuide": null
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
|
-
"github": {
|
|
62
|
-
"phases": ["tasks", "implement"],
|
|
63
|
-
"tools": ["get_repo", "search_code", "create_issue", "create_pull_request", "search_issues"],
|
|
64
|
-
"detection": "mcpServers includes github",
|
|
65
|
-
"fallback": "Bash `gh` CLI commands",
|
|
66
|
-
"usage": "Repo metadata, code search, issue creation, PR management",
|
|
67
|
-
"healthCheck": {
|
|
68
|
-
"toolPattern": "github",
|
|
69
|
-
"testCall": "Call any read-only GitHub tool (e.g., search_repositories or get_repo) to verify connectivity",
|
|
70
|
-
"testParams": {},
|
|
71
|
-
"successIndicator": "Returns repository data or search results without auth errors",
|
|
72
|
-
"failureHint": "GitHub MCP not responding. If using Docker, ensure Docker Desktop is running. Verify GITHUB_PERSONAL_ACCESS_TOKEN env var is set. Ensure the token has not expired and has the required scopes (repo, read:org)."
|
|
73
|
-
},
|
|
74
|
-
"install": {
|
|
75
|
-
"autoInstall": false,
|
|
76
|
-
"transport": "stdio",
|
|
77
|
-
"config": {
|
|
78
|
-
"type": "stdio",
|
|
79
|
-
"command": "docker",
|
|
80
|
-
"args": ["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"],
|
|
81
|
-
"env": {
|
|
82
|
-
"GITHUB_PERSONAL_ACCESS_TOKEN": ""
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
"credentials": [
|
|
86
|
-
{
|
|
87
|
-
"name": "GitHub Personal Access Token",
|
|
88
|
-
"envVar": "GITHUB_PERSONAL_ACCESS_TOKEN",
|
|
89
|
-
"secret": true,
|
|
90
|
-
"helpUrl": "https://github.com/settings/tokens"
|
|
91
|
-
}
|
|
92
|
-
],
|
|
93
|
-
"prerequisites": [
|
|
94
|
-
{
|
|
95
|
-
"name": "Docker",
|
|
96
|
-
"checkCommand": "docker --version",
|
|
97
|
-
"installUrl": "https://docs.docker.com/get-docker/"
|
|
98
|
-
}
|
|
99
|
-
],
|
|
100
|
-
"stackRelevance": ["*"],
|
|
101
|
-
"warnings": [],
|
|
102
|
-
"setupGuide": "Official GitHub MCP Server (github/github-mcp-server). Requires Docker."
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
61
|
"vercel": {
|
|
106
62
|
"phases": ["implement"],
|
|
107
63
|
"tools": ["search_docs", "list_projects", "get_deployments", "get_deployment_logs", "manage_env_vars"],
|
|
@@ -234,7 +190,8 @@
|
|
|
234
190
|
{
|
|
235
191
|
"name": "Gemini API Key",
|
|
236
192
|
"envVar": "GEMINI_API_KEY",
|
|
237
|
-
"
|
|
193
|
+
"secret": true,
|
|
194
|
+
"helpUrl": "https://aistudio.google.com/apikey"
|
|
238
195
|
}
|
|
239
196
|
],
|
|
240
197
|
"prerequisites": ["Google AI Studio API key"],
|
|
@@ -247,8 +204,8 @@
|
|
|
247
204
|
"phaseMatrix": {
|
|
248
205
|
"proposal": ["context7"],
|
|
249
206
|
"uiux": ["playwright", "context7", "stitch", "gemini-image"],
|
|
250
|
-
"design": ["context7"
|
|
251
|
-
"tasks": ["context7"
|
|
252
|
-
"implement": ["context7", "playwright", "
|
|
207
|
+
"design": ["context7"],
|
|
208
|
+
"tasks": ["context7"],
|
|
209
|
+
"implement": ["context7", "playwright", "vercel", "gemini-image"]
|
|
253
210
|
}
|
|
254
211
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "backend",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Backend-focused rubric. Higher weight on architecture, security, and package health.",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Backend-focused rubric. Higher weight on architecture, security, and package health. Deterministic-validator rubric — approve threshold aligned to the single bar 9 from MORPH.md §0.",
|
|
5
5
|
"categories": {
|
|
6
6
|
"functionality": {
|
|
7
7
|
"weight": 0.25,
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"thresholds": {
|
|
38
|
-
"approve":
|
|
38
|
+
"approve": 9.0,
|
|
39
39
|
"retry": 6.0
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "default",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Default rubric for general-purpose projects. Balanced weights across all categories.",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Default rubric for general-purpose projects. Balanced weights across all categories. Deterministic-validator rubric — approve threshold aligned to the single bar 9 from MORPH.md §0 (the LLM eval rubrics live in framework/evals/rubrics/*.md).",
|
|
5
5
|
"categories": {
|
|
6
6
|
"functionality": {
|
|
7
7
|
"weight": 0.30,
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
32
|
"thresholds": {
|
|
33
|
-
"approve":
|
|
33
|
+
"approve": 9.0,
|
|
34
34
|
"retry": 6.0
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "frontend",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Frontend-focused rubric. Higher weight on design system compliance and UI quality.",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Frontend-focused rubric. Higher weight on design system compliance and UI quality. Deterministic-validator rubric — approve threshold aligned to the single bar 9 from MORPH.md §0.",
|
|
5
5
|
"categories": {
|
|
6
6
|
"functionality": {
|
|
7
7
|
"weight": 0.25,
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
32
|
"thresholds": {
|
|
33
|
-
"approve":
|
|
33
|
+
"approve": 9.0,
|
|
34
34
|
"retry": 6.0
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -3,14 +3,14 @@ paths:
|
|
|
3
3
|
- "**/Dockerfile*"
|
|
4
4
|
- "**/docker-compose*"
|
|
5
5
|
- "**/.github/**"
|
|
6
|
-
- "**/
|
|
6
|
+
- "**/coolify*"
|
|
7
7
|
stacks:
|
|
8
8
|
- docker
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# Infrastructure Standards
|
|
12
12
|
|
|
13
|
-
@.morph/framework/standards/infrastructure/docker/
|
|
13
|
+
@.morph/framework/standards/infrastructure/docker/coolify-deploy.md
|
|
14
14
|
|
|
15
15
|
## Infrastructure Templates
|
|
16
16
|
|
|
@@ -19,9 +19,9 @@ stacks:
|
|
|
19
19
|
| .NET API Dockerfile | `.morph/framework/templates/infrastructure/docker/dockerfile-api.dockerfile` | Multi-stage .NET |
|
|
20
20
|
| Next.js Dockerfile | `.morph/framework/templates/infrastructure/docker/dockerfile-web.dockerfile` | Multi-stage Next.js |
|
|
21
21
|
| Docker Compose | `.morph/framework/templates/infrastructure/docker/docker-compose.template.yml` | Local dev env |
|
|
22
|
-
|
|
|
22
|
+
| Coolify Config | `.morph/framework/templates/infrastructure/docker/coolify.template.json` | Deployment config |
|
|
23
23
|
| .NET Build CI | `.morph/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs` | CI workflow |
|
|
24
24
|
| Docker Push CI | `.morph/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs` | Build & push |
|
|
25
|
-
| Deploy
|
|
25
|
+
| Deploy Coolify | `.morph/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs` | CD via SSH |
|
|
26
26
|
|
|
27
27
|
Read the template and adapt to your project structure.
|
|
@@ -4,7 +4,7 @@ description: Guia a criação de uma landing page nova ponta-a-ponta a partir do
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar um novo projeto de LP com o template Umbraco genérico
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "7.
|
|
7
|
+
cliVersion: "7.7.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# create-umbraco-lp
|
|
@@ -82,17 +82,31 @@ UMBRACO_ADMIN_PASSWORD=<gerada>
|
|
|
82
82
|
UMBRACO_GLOBAL_ID=<uuid-gerado>
|
|
83
83
|
ALLOWED_ORIGINS=http://localhost:3000
|
|
84
84
|
REVALIDATE_SECRET=<gerado>
|
|
85
|
+
FRONTEND_URL=http://localhost:3000
|
|
85
86
|
LEADS_INTEGRATION=<integração-escolhida>
|
|
86
87
|
```
|
|
87
88
|
|
|
89
|
+
`FRONTEND_URL` (junto com `REVALIDATE_SECRET`) alimenta o `RevalidateOnPublishHandler` do
|
|
90
|
+
backend: a cada publish no Umbraco, ele chama `/api/revalidate` do frontend sozinho, em
|
|
91
|
+
código — não depende de configurar nada em Settings > Webhooks no backoffice. **Em
|
|
92
|
+
produção, atualize `FRONTEND_URL` para o domínio público real do frontend.**
|
|
93
|
+
|
|
88
94
|
E gera `backend/appsettings.Development.json` com o bloco Unattended install do Umbraco (usando o email/senha computados pelo script). Isso significa que um projeto recém-scaffoldado **auto-instala no `dotnet run`** — sem necessidade de rodar o instalador UI manualmente.
|
|
89
95
|
|
|
90
96
|
E escreve `frontend/.env.local` com:
|
|
91
97
|
```
|
|
92
98
|
NEXT_PUBLIC_UMBRACO_URL=https://localhost:44324
|
|
99
|
+
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
|
93
100
|
REVALIDATE_SECRET=<mesmo-secret>
|
|
94
101
|
```
|
|
95
102
|
|
|
103
|
+
`NEXT_PUBLIC_SITE_URL` é usada por `robots.ts`/`sitemap.ts`. **Em produção, precisa ser
|
|
104
|
+
setada como Build Variable no Coolify** — o `Dockerfile` do template
|
|
105
|
+
declara `ARG NEXT_PUBLIC_SITE_URL` (e `ARG NEXT_PUBLIC_UMBRACO_URL`) justamente para isso;
|
|
106
|
+
sem marcar como Build Variable, a env var não chega ao `next build` e o `robots.txt`/
|
|
107
|
+
`sitemap.xml` publicam o domínio placeholder do template (`minha-landing-page.com.br`) em
|
|
108
|
+
produção — já aconteceu num projeto real.
|
|
109
|
+
|
|
96
110
|
> **Nota de overwrite:** se `.env` já existe, `new-lp.ps1` prompta (Read-Host) para sobrescrever — portanto, uma execução não-interativa em um diretório que já tem `.env` vai bloquear na prompt. Execute em um scaffold fresco, ou responda à prompt interativamente.
|
|
97
111
|
|
|
98
112
|
> **Nota importante:** `ALLOWED_ORIGINS` é gravado com `http://localhost:3000` pelo script independentemente do que foi informado no passo 1. Se o usuário especificou um domínio de produção, a forma de aplicá-lo depende do modo de execução:
|
|
@@ -229,9 +243,15 @@ Antes de fazer deploy ou entregar o projeto ao cliente:
|
|
|
229
243
|
- [ ] Delivery API responde com o node `lpHome` populado: `/umbraco/delivery/api/v2/content?contentType=lpHome&take=1`.
|
|
230
244
|
- [ ] `.env` contém `ALLOWED_ORIGINS` com o domínio de produção correto (separado por vírgula se múltiplos).
|
|
231
245
|
- [ ] `UMBRACO_GLOBAL_ID` é único para este projeto (gerado pelo script — não reutilizar de outro projeto).
|
|
232
|
-
- [ ] SEO: preencher
|
|
246
|
+
- [ ] SEO: preencher no backoffice, aba SEO do node `lpHome` — Page Title, Meta Title, Meta Description, **Social Image** (1200x630, sem ela não há `og:image` e o link não gera preview no WhatsApp/redes), Canonical Url (só a URL, sem tag HTML).
|
|
247
|
+
- [ ] Favicon: preencher o campo **Favicon** no backoffice (aba Configuration) — `app/icon.tsx`/`apple-icon.tsx` já leem esse campo dinamicamente; sem ele, cai no favicon estático genérico do template.
|
|
248
|
+
- [ ] `NEXT_PUBLIC_SITE_URL` **e** `NEXT_PUBLIC_UMBRACO_URL` setadas como **Build Variable** no provedor (não só runtime) — conferir `robots.txt`/`sitemap.xml` em produção com o domínio real após o primeiro deploy.
|
|
249
|
+
- [ ] `FRONTEND_URL` + `REVALIDATE_SECRET` setadas no backend em produção (revalidação automática pós-publish via `RevalidateOnPublishHandler`) — testar: publicar algo no backoffice e conferir no log do backend a linha `RevalidateOnPublish: cache do frontend invalidado após publish.`
|
|
250
|
+
- [ ] No Index: confirmar que está **desligado** no backoffice antes de ir pra produção (o campo é opt-in pra esconder do Google, não deveria vir ligado por padrão).
|
|
233
251
|
- [ ] Variáveis sensíveis (`UMBRACO_ADMIN_PASSWORD`, `REVALIDATE_SECRET`, tokens de integração) não estão commitadas no repositório (`.env` deve estar no `.gitignore`).
|
|
234
252
|
- [ ] Em produção com Docker Compose: `docker-compose up --build` e confirmar que backend e frontend sobem com as variáveis do `.env`.
|
|
253
|
+
- [ ] `browserslist` presente em `frontend/package.json` — já vem do template; só confirmar que não foi removido/sobrescrito durante a customização.
|
|
254
|
+
- [ ] Se o cliente vai subir fotos de câmera/celular pelo Media Picker (galeria, hero, etc.): confirmar que `ResizeLargeMediaUploadsHandler` está registrado em `backend/Program.cs` (já vem do template) — sem ele, upload cru de 10+ MB pode virar 500 intermitente sob concorrência no otimizador do Next. Setar `MEDIA_BACKFILL_SECRET` em produção se for preciso reprocessar mídia já publicada (ver seção "Mídia" do README do template).
|
|
235
255
|
|
|
236
256
|
> **Nota Docker — `LEADS_INTEGRATION` e `SEED_DEMO_CONTENT`:** o `docker-compose.yml` **não** injeta essas duas variáveis no container do backend. Mesmo que estejam no `.env`, o seed sempre inicializa `leadIntegration` como `"none"` no boot Docker. Isso é esperado: o editor deve configurar a integração manualmente no backoffice após o primeiro boot. Se precisar seeding automático da integração via Docker, adicione a variável na seção `environment` do serviço `backend` em `docker-compose.yml` manualmente.
|
|
237
257
|
|
|
@@ -4,7 +4,7 @@ description: Migra uma landing page existente (React/Vite, Next.js estático ou
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber instrução "use /migrate-lp-to-umbraco {projeto}" do usuário, ou quando o usuário descrever necessidade de migrar uma LP para Umbraco
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "7.
|
|
7
|
+
cliVersion: "7.7.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# migrate-lp-to-umbraco
|
|
@@ -100,6 +100,16 @@ O objetivo desta etapa não é "entender" a LP, é **capturar tudo o que é nece
|
|
|
100
100
|
- Registrar o `sha256` de cada asset (vira a base da paridade de checksum no passo 6).
|
|
101
101
|
- **Não reencodar nem otimizar** nenhum asset — qualquer reencode quebra a paridade.
|
|
102
102
|
|
|
103
|
+
**SEO/GEO da origem — capturar para semear, não perder no lift:** a paridade visual não
|
|
104
|
+
cobre isso sozinha — um lift perfeito que esquece o SEO da origem entrega uma LP idêntica
|
|
105
|
+
visualmente mas invisível pra buscadores/IA no dia 1. Capturar da origem:
|
|
106
|
+
- `<title>`, `<meta name="description">`, `<link rel="canonical">`.
|
|
107
|
+
- Open Graph: `og:title`, `og:description`, `og:image` (baixar a imagem, idealmente 1200x630).
|
|
108
|
+
- JSON-LD (`<script type="application/ld+json">`) — schema completo, se existir.
|
|
109
|
+
- Favicon (todos os tamanhos/formatos disponíveis) e `apple-touch-icon`.
|
|
110
|
+
- `robots.txt` e `sitemap.xml` da origem — confirmar se há regras além do default (bots de IA bloqueados? outras URLs?).
|
|
111
|
+
- `llms.txt` da origem, se existir.
|
|
112
|
+
|
|
103
113
|
**Formulário de captação** — campos presentes, validação no frontend, endpoint atual, integração de CRM/leads.
|
|
104
114
|
|
|
105
115
|
**Origem apenas por URL (fallback):** se não houver código-fonte, scrapear o HTML renderizado, extrair o CSS computado dos elementos e baixar todos os assets referenciados (incluindo fontes). O lift parte do markup renderizado + CSS computado.
|
|
@@ -268,7 +278,22 @@ sectionNode.SetValue("videoFundo", mediaJson);
|
|
|
268
278
|
|
|
269
279
|
Coloque os assets de seed em `backend/seed-assets/` (imagens, vídeos, ícones da LP de origem).
|
|
270
280
|
|
|
271
|
-
**
|
|
281
|
+
**Semear também o SEO/GEO capturado no passo 1** — não deixar em branco pra "o editor preencher depois" (isso já causou um site indo pro ar sem `og:image`, com favicon genérico e apontando pro domínio errado no sitemap num projeto real):
|
|
282
|
+
|
|
283
|
+
```csharp
|
|
284
|
+
content.SetValue("pageTitleSeo", "<título da origem>");
|
|
285
|
+
content.SetValue("titleSeo", "<og:title da origem>");
|
|
286
|
+
content.SetValue("descriptionSeo", "<meta description da origem>");
|
|
287
|
+
content.SetValue("canonicalUrlSeo", "<URL canônica de produção>"); // só a URL, sem tag HTML
|
|
288
|
+
content.SetValue("noIndexSeo", false); // é Mandatory=false a partir do fix de 2026-07-02 — não deixar true
|
|
289
|
+
content.SetValue("jsonLDSchemaSeo", OrigemJsonLd); // JSON-LD capturado da origem, se existir
|
|
290
|
+
// imageSeo (og:image) e favicon: importar como media (mesmo padrão de CreateOrGetMedia)
|
|
291
|
+
// e setar via MediaPicker3, igual ao padrão de mídia acima.
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`imageSeo` (1200x630) e `favicon` alimentam `app/icon.tsx`/`apple-icon.tsx` e o `<meta property="og:image">` automaticamente — não precisam de nenhum código extra no frontend, só o seed correto.
|
|
295
|
+
|
|
296
|
+
**O que vai no SeedContentHandler:** textos de inauguração, mídia local (arquivos em `seed-assets/`), ordem das seções, configuração inicial, **e o SEO/GEO capturado no passo 1**. O conteúdo fica editável no backoffice após o primeiro boot — o seed é apenas o estado inicial.
|
|
272
297
|
|
|
273
298
|
**O que NÃO vai no SeedContentHandler:** tokens de integração (GHL API Key, webhook URL). Esses ficam no nó `leadsConfig` que o operador preenche diretamente no backoffice após o boot (ver passo 5).
|
|
274
299
|
|
|
@@ -298,12 +323,58 @@ Copiar as variáveis de design da origem para `frontend/src/app/globals.css` no
|
|
|
298
323
|
}
|
|
299
324
|
```
|
|
300
325
|
|
|
301
|
-
|
|
326
|
+
**Fontes — auto-hospedar, nunca `<link>` externo pro Google Fonts:** um `<link rel="stylesheet"
|
|
327
|
+
href="https://fonts.googleapis.com/css2?...">` no `<head>` é render-blocking (round-trip
|
|
328
|
+
externo antes de qualquer paint) e, combinado com `font-display: swap`, causa CLS quando a
|
|
329
|
+
fonte troca depois do primeiro paint — foi 1.680ms de render-blocking estimado e 0.221 de CLS
|
|
330
|
+
medidos num caso real (Barone_LP). **Não usar `next/font/google`** aqui (diferente do passo 4
|
|
331
|
+
de `create-umbraco-lp`, que pode usar): o nome de família que ele gera é escopado/diferente do
|
|
332
|
+
literal da origem, e o gate de paridade (passo 6b.2) compara `font-family` computado
|
|
333
|
+
byte-a-byte contra a origem — `next/font` quebra esse check.
|
|
334
|
+
|
|
335
|
+
O fix correto, que preserva a paridade:
|
|
336
|
+
1. Buscar a CSS real do Google pra descobrir as URLs `.woff2` (`curl` na mesma URL que iria no
|
|
337
|
+
`<link>`) — geralmente **um arquivo por família+estilo já cobre todos os pesos estáticos**
|
|
338
|
+
usados (Google costuma reaproveitar o mesmo arquivo pra weight 400/500/600/700 de uma fonte
|
|
339
|
+
não-variável). Baixar só o subset `latin` (`unicode-range: U+0000-00FF, ...`) — cobre toda
|
|
340
|
+
acentuação do português (á, ã, ç, é... estão em U+00C0-00FF).
|
|
341
|
+
2. Salvar os `.woff2` em `frontend/public/fonts/`.
|
|
342
|
+
3. Declarar `@font-face` em `globals.css` (ou no arquivo de tema) com o **mesmo `font-family`
|
|
343
|
+
literal da origem** (`'Cormorant'`, `'Jost'`, etc. — não o nome gerado por uma lib) e
|
|
344
|
+
`font-display: optional` (não `swap`) — `optional` só troca pro custom font se ele chegar
|
|
345
|
+
quase instantaneamente; senão fica no fallback pra aquela visita, evitando o reflow visível
|
|
346
|
+
que gerou o CLS.
|
|
347
|
+
4. `<link rel="preload" href="/fonts/arquivo.woff2" as="font" type="font/woff2" crossOrigin="anonymous">`
|
|
348
|
+
no `<head>` do `layout.tsx`, sem nenhum `<link>` pro domínio do Google.
|
|
349
|
+
|
|
350
|
+
Verificar ao final: `getComputedStyle(el).fontFamily` no elemento de heading do hero deve
|
|
351
|
+
retornar **a mesma string literal que a origem** (ex.: `"Cormorant, Georgia, serif"`) — só a
|
|
352
|
+
origem do arquivo mudou (de `fonts.gstatic.com` pra same-origin), não o nome da família.
|
|
302
353
|
|
|
303
354
|
#### 4d. Componentes frontend dos módulos novos
|
|
304
355
|
|
|
305
356
|
Para cada módulo novo, criar o componente em `frontend/src/components/modules/NomeSection.tsx` **a partir do componente da origem copiado verbatim** — colar o JSX/markup/classes/CSS da origem e trocar só os literais de conteúdo por `data.*`:
|
|
306
357
|
|
|
358
|
+
> **Exceção deliberada ao verbatim — imagens de mídia do Umbraco:** o componente `Picture`
|
|
359
|
+
> lifted da origem provavelmente emite um `<img>` cru. Pra qualquer `src` que vier do Media
|
|
360
|
+
> Picker (URL do backend, editável no backoffice — diferente de um asset estático em
|
|
361
|
+
> `/public`), usar `next/image` (`fill`, com o container já dimensionado) desde o primeiro
|
|
362
|
+
> lift, não um `<img>` cru. Isso **não** quebra o gate de paridade (passo 6b.3 só faz checksum
|
|
363
|
+
> de assets em `/public`, nunca de URLs de mídia) — pelo contrário, sem isso qualquer foto que
|
|
364
|
+
> o cliente trocar depois no backoffice chega crua (fotos de câmera/celular fácil 10+ MB) e
|
|
365
|
+
> vira lento ou 500 sob concorrência (ver seção "Mídia" no README do template).
|
|
366
|
+
>
|
|
367
|
+
> **Pitfall a checar nesse lift:** `next/image fill` aplica `position/height/width` **inline**
|
|
368
|
+
> no `<img>` renderizado — isso vence qualquer regra de classe (`.minha-grade img { height:
|
|
369
|
+
> 116%; margin-top: -8% }`, por exemplo) que a origem use pra efeitos de oversize/parallax/zoom
|
|
370
|
+
> em cima de um `<img>` cru, mesmo que a regra continue no CSS lifted verbatim. Se a origem tem
|
|
371
|
+
> algum truque desses (imagem deliberadamente maior que o container + margin negativo, comum
|
|
372
|
+
> em seções com parallax scroll-scrub), a troca pra `fill` abre um vão visível onde a "sobra"
|
|
373
|
+
> da imagem deveria estar. Fix: `!important` na propriedade de classe que está sendo vencida
|
|
374
|
+
> (normalmente só `height`) — é uma das poucas justificativas legítimas pra `!important`, já
|
|
375
|
+
> que é o único jeito de vencer um inline style que você não controla diretamente. Checar isso
|
|
376
|
+
> na hora do lift, não esperar aparecer como bug visual depois.
|
|
377
|
+
|
|
307
378
|
```tsx
|
|
308
379
|
// Padrão: componente com 'use client', recebe { data: SectionProps },
|
|
309
380
|
// faz cast para o tipo tipado, e lê props via data.property.
|
|
@@ -365,6 +436,12 @@ Objetivo: **provar** que a LP migrada é idêntica à origem. Com o lift verbati
|
|
|
365
436
|
|
|
366
437
|
**Hierarquia do gate:** os checks **determinísticos** (6b) são o **bloqueio**. O **screenshot diff** (6c) é **corroboração secundária** — nunca o bloqueio único, porque font hinting/antialiasing diferem entre ambientes mesmo com CSS byte-idêntico.
|
|
367
438
|
|
|
439
|
+
> Os dois pontos do passo 4c (fontes auto-hospedadas) e 4d (`next/image fill` pra mídia do
|
|
440
|
+
> Umbraco) não são drift de paridade — são a paridade **certa** pedindo um detalhe extra de
|
|
441
|
+
> implementação. O checksum de assets (6b.3) só cobre `/public`; o estilo computado (6b.2)
|
|
442
|
+
> continua batendo porque `font-family` fica literal. Nenhum dos dois deveria fazer o gate
|
|
443
|
+
> falhar quando bem aplicados.
|
|
444
|
+
|
|
368
445
|
#### 6a. Boot + checklist manual (sanidade)
|
|
369
446
|
|
|
370
447
|
```bash
|
|
@@ -403,7 +480,9 @@ A migração é "exata" quando **todos** são verdade:
|
|
|
403
480
|
- **6c screenshot diff** abaixo da tolerância em todos os breakpoints (mobile/tablet/desktop).
|
|
404
481
|
- Todas as seções renderizam com conteúdo real (não placeholders) e na ordem da origem.
|
|
405
482
|
- Formulário envia lead com sucesso para a integração configurada (testar com dado real em staging; `--demo-seed` popula a home com módulos de demonstração via `SEED_DEMO_CONTENT=true`, não envia lead de teste).
|
|
406
|
-
- Backoffice: editar um campo de texto, publicar — a mudança reflete na página (
|
|
483
|
+
- Backoffice: editar um campo de texto, publicar — a mudança reflete na página **em produção, sem precisar de deploy manual** (o `RevalidateOnPublishHandler` do backend chama `/api/revalidate` sozinho; só funciona com `FRONTEND_URL`/`REVALIDATE_SECRET` setados) e **sem** alterar o markup/estilo renderizado.
|
|
484
|
+
- SEO/GEO da origem migrado, não perdido: `curl` a home de produção e conferir `<title>`, `<meta description>`, `og:image`, canonical e JSON-LD batendo com o que foi capturado no passo 1 — e que o favicon do browser não é o ícone genérico do template.
|
|
485
|
+
- `robots.txt`/`sitemap.xml` de produção apontam pro domínio real (não o placeholder `minha-landing-page.com.br` do template) — só acontece se `NEXT_PUBLIC_SITE_URL` foi setada como Build Variable no provedor.
|
|
407
486
|
- Nenhuma chave de API ou token aparece em arquivo versionado.
|
|
408
487
|
|
|
409
488
|
## Anti-padrões
|
|
@@ -415,7 +494,7 @@ A migração é "exata" quando **todos** são verdade:
|
|
|
415
494
|
- Não saltear o smoke-boot (passo 3) — problemas de ambiente devem ser detectados antes de portar conteúdo.
|
|
416
495
|
- Não reutilizar `customHtmlSection` para tudo — módulos dedicados permitem edição granular no backoffice e ISR por tag.
|
|
417
496
|
- **Não reinterpretar layout/CSS nem "limpar" o markup** — portar verbatim; o módulo Umbraco só embrulha o mesmo markup da origem. Rebuild = drift.
|
|
418
|
-
- **Não reencodar nem otimizar assets** na migração — quebra a paridade de checksum (6b) e altera o render.
|
|
497
|
+
- **Não reencodar nem otimizar assets da origem** (o que vai pra `/public`/`seed-assets`) na migração — quebra a paridade de checksum (6b) e altera o render. Isso **não** se aplica a imagens que vêm do Media Picker do Umbraco (ver passo 4d) — essas não são asset da origem, não entram no checksum, e otimizá-las via `next/image` é o comportamento correto, não uma violação de paridade.
|
|
419
498
|
- **Não assumir o tema do template** — portar o Tailwind/`@theme` (e as fontes) da origem; a mesma classe utilitária renderiza diferente entre versões/temas.
|
|
420
499
|
- **Não tratar o screenshot diff como gate único** — o bloqueio são os checks determinísticos (6b); o diff visual é secundário.
|
|
421
500
|
|