---
name: migrate-lp-to-umbraco
description: Migra uma landing page existente (React/Vite, Next.js estático ou HTML) para o template Umbraco-headless MORPH_Umbraco_LP_Template entregando a LP EXATAMENTE igual à origem (mesmo CSS, imagens, fontes e comportamento) + Umbraco por cima — obtenção automática do template, inventário de seções, lift-and-rewire verbatim de cada seção para módulo editável, conexão do formulário ao backend de leads e gate de paridade determinístico. Repo-agnóstica: roda em qualquer lugar, sem template nem origem pré-baixados.
version: core
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
user-invokable: true
cliVersion: "8.37.1"
stacks:
  - umbraco
---

# migrate-lp-to-umbraco

## Objetivo

Conduzir a migração de uma landing page de origem arbitrária para o stack Umbraco 17 / .NET 10 + Next.js do template `MORPH_Umbraco_LP_Template`, entregando uma LP **visual e funcionalmente idêntica à origem** (mesmo CSS, imagens, fontes, animações e comportamento) com o Umbraco embrulhando o conteúdo por cima — projeto funcional e idempotentemente seedável onde todo conteúdo editorial (textos, imagens, vídeos) fica editável no backoffice, e as credenciais de integração de leads nunca saem do backoffice.

**Princípio central — lift-and-rewire, não rebuild.** A paridade exata é garantida **por construção**: copia-se o markup/CSS/classes/assets de cada seção da origem **verbatim** para o componente do módulo, trocando apenas os literais de conteúdo (texto, `src` de mídia) por leituras do backoffice. O render é idêntico porque o markup é o mesmo; o gate de paridade (passo 6) é uma **confirmação** de que o lift foi limpo, não uma ferramenta para descobrir e remendar drift. Reescrever cada seção do zero ("rebuild-then-compare") nunca converge para "exatamente igual" — não fazer isso.

O processo é reproduzível para qualquer LP (React/Vite, Next.js estático ou HTML). A skill é **repo-agnóstica**: não depende de nenhum repo de exemplo nem do template estarem pré-baixados — ela obtém o template sozinha (passo 0).

## Quando invocar

- Usuário pede "use /migrate-lp-to-umbraco {projeto}" ou equivalente natural ("migrar LP para Umbraco", "portar site para Umbraco headless", "rebuild no template Umbraco").
- Projeto de origem já existe (brownfield) — LP em React/Vite/Next.js/HTML.

## Argumentos

- `{projeto}` — nome do projeto em kebab-case (ex.: `reserva-das-flores`, `lp-imobiliaria`). Se ausente, perguntar via `AskUserQuestion`.

## Pré-condições

- **Acesso ao template** — `git` + (preferencialmente) `gh` autenticado com acesso ao repo **privado** `polymorphism-tech/MORPH_Umbraco_LP_Template`. O template **não** precisa estar pré-baixado: o passo 0 o obtém. (Para clone via `git` direto, é preciso credencial HTTPS ou chave SSH com acesso à org.)
- **Origem acessível** — em uma das duas formas (a skill suporta ambas):
  - **(a) código-fonte local** da LP (React/Vite, Next.js ou HTML) — caminho de **fidelidade máxima**, pois o CSS, o markup e os assets exatos estão disponíveis para o lift verbatim.
  - **(b) apenas a URL** da LP em produção — fallback: scrapear o HTML renderizado, o CSS computado e baixar todos os assets. O mesmo gate de paridade (passo 6) se aplica.
- **Docker** disponível para boot local (`docker compose`), ou .NET 10 SDK + Node para boot sem container.
- **Se a origem é scroll-driven** (canvas frame-scrub, pin, parallax, smooth scroll), leia antes os standards `frontend/scroll-driven/frame-scrub.md`, `smooth-scroll.md` e `scroll-components.md`. Eles carregam o conhecimento que o lift precisa preservar; o passo 4e diz onde os assets dessa animação vivem no destino.

## Passo 0 — Obter o template (sempre)

A skill **sempre** garante uma cópia local do template antes de qualquer outro passo. Roda em qualquer diretório — não assume nada pré-baixado.

**Cache de referência (read-only).** Manter uma cópia canônica do template num cache estável por máquina, para leitura dos arquivos de referência (sinks, `SeedContentHandler`, `moduleRegistry`, e2e) e como base do scaffold:

- bash: `TPL="${MORPH_TEMPLATE_CACHE:-$HOME/.morph/templates}/MORPH_Umbraco_LP_Template"`
- PowerShell: `$Tpl = "$env:USERPROFILE\.morph\templates\MORPH_Umbraco_LP_Template"`

**Rotina check-local → clone → pull:**

```bash
# Bash — obtém ou atualiza o cache do template
TPL="${MORPH_TEMPLATE_CACHE:-$HOME/.morph/templates}/MORPH_Umbraco_LP_Template"
if [ -d "$TPL/.git" ]; then
  git -C "$TPL" pull --ff-only || echo "offline — usando cache existente"
else
  mkdir -p "$(dirname "$TPL")"
  gh repo clone polymorphism-tech/MORPH_Umbraco_LP_Template "$TPL" \
    || git clone https://github.com/polymorphism-tech/MORPH_Umbraco_LP_Template.git "$TPL" \
    || git clone git@github.com:polymorphism-tech/MORPH_Umbraco_LP_Template.git "$TPL"
fi
```

```powershell
# PowerShell — obtém ou atualiza o cache do template
$Tpl = "$env:USERPROFILE\.morph\templates\MORPH_Umbraco_LP_Template"
if (Test-Path "$Tpl\.git") {
  git -C "$Tpl" pull --ff-only
} else {
  New-Item -ItemType Directory -Force (Split-Path $Tpl) | Out-Null
  gh repo clone polymorphism-tech/MORPH_Umbraco_LP_Template "$Tpl"
  if (-not (Test-Path "$Tpl\.git")) { git clone https://github.com/polymorphism-tech/MORPH_Umbraco_LP_Template.git "$Tpl" }
  if (-not (Test-Path "$Tpl\.git")) { git clone git@github.com:polymorphism-tech/MORPH_Umbraco_LP_Template.git "$Tpl" }
}
```

Notas:
- Branch padrão do template é **`master`** — **não** fazer `git checkout` de outra branch (o template já está generalizado: doctype raiz `lpHome`, scripts `new-lp`, sinks de lead, e2e).
- `gh repo clone` é o caminho primário por ser repo **privado** (usa o token já autenticado). HTTPS/SSH são fallback.
- O cache é **somente leitura**: nunca rodar `new-lp` nem editar dentro dele — o scaffold (passo 3) trabalha sobre uma **cópia fresca**.

## Fluxo

### 1. Analisar a origem — capturar PARA UM LIFT VERBATIM

O objetivo desta etapa não é "entender" a LP, é **capturar tudo o que é necessário para reproduzi-la byte-a-byte**. Cada item abaixo alimenta o lift do passo 4.

**Inventário de seções** — para cada bloco visual da página, documentar:
- Nome e ordem na página.
- **Markup e classes exatos**: a árvore DOM e os nomes de classe de cada seção, como estão (não normalizar nem "limpar").
- Conteúdo textual: títulos, subtítulos, corpo, CTAs, links.
- Mídia: imagens, vídeos, posters — registrar o caminho/URL de cada um para baixar abaixo.
- **Comportamento interativo**: animação de scroll/scroll-expand, carrossel, marquee, autoplay de vídeo, hovers, mapa, máscara/validação de formulário — documentar cada um com detalhe suficiente para reproduzir idêntico.
- Dados estruturados aninhados (ex.: lista de produtos com nome+imagem+descrição, lista de depoimentos).
- **Breakpoints responsivos** e o que muda em cada um (mobile/tablet/desktop).

**Tema e CSS — copiar VERBATIM, não reinterpretar:**
- Capturar o CSS da origem como está: bloco `@theme`/tokens (variáveis `--color-*`, `--font-*`), classes custom, `@layer`, `@keyframes`, media queries. Os valores serão **portados sem alteração** no passo 4 — não arredondar cores nem aproximar espaçamentos.
- **Config do Tailwind**: portar a config/tema da origem. Tailwind v4 é CSS-based (`@theme` em `globals.css`). ⚠️ A **mesma classe utilitária renderiza diferente** se a versão ou o tema do Tailwind divergir — portar o tema da origem, **nunca** assumir o do template.

**Assets — baixar byte-a-byte e registrar checksum:**
- Baixar/copiar **todos** os assets: imagens, vídeos, posters, ícones e **fontes** (arquivos `.woff2`/`.ttf` + famílias e pesos exatos). Web-fonts são parte da fidelidade — mesma família, mesmo peso, mesmo arquivo.
- Registrar o `sha256` de cada asset (vira a base da paridade de checksum no passo 6).
- **Não reencodar nem otimizar** nenhum asset — qualquer reencode quebra a paridade.

**SEO/GEO da origem — capturar para semear, não perder no lift:** a paridade visual não
cobre isso sozinha — um lift perfeito que esquece o SEO da origem entrega uma LP idêntica
visualmente mas invisível pra buscadores/IA no dia 1. Capturar da origem:
- `<title>`, `<meta name="description">`, `<link rel="canonical">`.
- Open Graph: `og:title`, `og:description`, `og:image` (baixar a imagem, idealmente 1200x630).
- JSON-LD (`<script type="application/ld+json">`) — schema completo, se existir.
- Favicon (todos os tamanhos/formatos disponíveis) e `apple-touch-icon`.
- `robots.txt` e `sitemap.xml` da origem — confirmar se há regras além do default (bots de IA bloqueados? outras URLs?).
- `llms.txt` da origem, se existir.

**Formulário de captação** — campos presentes, validação no frontend, endpoint atual, integração de CRM/leads.

**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.

**Exemplo ilustrativo (LP fictícia):**
- Origem: `src/App.tsx` (React/Vite) — componentes `Navbar`, `Hero`, `HorizontalScroll`, `Manifesto`, `Galeria`, `Testimonial`, `CTA`, `Footer`.
- Tema extraído do bloco `@theme` em `src/index.css`: `--color-surface`, `--color-primary`, `--color-tertiary`, etc.; tipografia (ex.) Cormorant Garamond + Jost (baixar os arquivos de fonte).
- Formulário: campos nome, e-mail, telefone + POST para servidor com integração de leads.

Produzir `docs/origem-inventario.md` no repositório destino com esta análise (incluindo a tabela de assets com `sha256`).

### 2. Mapear origem para doctypes/módulos do template

Comparar cada seção inventariada com os módulos disponíveis no template:

| Módulo (alias uSync) | Propósito |
|---|---|
| `heroSection` | Hero genérico com título, subtítulo, imagem/vídeo de fundo e CTA |
| `diferencialSection` / `diferencialItem` | Lista de diferenciais/features com ícone e descrição |
| `testimonialSection` / `testimonialItem` | Depoimentos com nome, cargo e texto |
| `galeriaSection` | Galeria de imagens |
| `videoSection` | Bloco com vídeo embutido |
| `equipeSection` / `equipeMembro` | Equipe com foto, nome e cargo |
| `certificacoesSection` / `certificacaoItem` | Selos e certificações |
| `localizacaoSection` | Mapa ou endereço |
| `contatoSection` | Seção de formulário de captação |
| `customHtmlSection` | Bloco HTML livre (fallback para conteúdo sem módulo dedicado) |

Para cada seção da origem, determinar (na ótica do lift verbatim — fidelidade primeiro):
1. **Reuso direto de módulo do template**: **somente** quando o markup e o CSS renderizados do módulo do template já são idênticos aos da origem. Se houver qualquer diferença visual, **não** reusar — isso introduz drift. Na dúvida, prefira lift verbatim.
2. **Módulo do template com ajuste de campos**: o módulo serve mas precisa de propriedades extras — adicionar no uSync `.config` do módulo e no componente frontend. O markup permanece o da origem.
3. **Módulo novo via lift verbatim (caso padrão)**: a seção vira um novo doctype + componente que **reproduz o markup/CSS/classes da origem verbatim** (ver passo 4). Criar o doctype + componente + registro no `moduleRegistry.ts`. Nomear o alias em camelCase com sufixo `Section` (ex.: `manifestoSection`, `galeriaSection`).

> Regra prática: a decisão default é **lift verbatim** (opção 3). Reusar um módulo do template (opções 1/2) só quando a paridade visual é garantida — caso contrário a LP migrada deixa de ser "exatamente igual".

Documentar decisões e justificativas em `docs/mapeamento-modulos.md`.

**Exemplo ilustrativo — módulos lifted da origem:**
- `heroSection` (lift): Hero com efeito scroll-expand + vídeo de fundo expansível — markup e animação copiados verbatim da origem.
- `brandStorySection`: Bloco editorial de história da marca com imagem e texto em colunas.
- `galeriaSection` / `galeriaItem`: Grade de itens com imagem, nome e descrição.
- `processoSection` / `processoStep`: Processo em etapas (passo a passo numerado).
- `marqueeSection`: Faixa de texto em loop (palavras-chave da marca).
- `statsSection`: Contador de métricas (anos, projetos, etc.).
- Módulos reutilizados diretamente (só porque o render bate com a origem): `testimonialSection`, `contatoSection`.

### 3. Scaffold via new-lp

Criar o projeto destino a partir de uma **cópia fresca** do template (nunca dentro do cache do passo 0) e rodar o bootstrap nessa cópia:

```bash
# Bash (Linux/macOS/WSL) — cópia fresca do cache para o diretório do projeto
TPL="${MORPH_TEMPLATE_CACHE:-$HOME/.morph/templates}/MORPH_Umbraco_LP_Template"
cp -r "$TPL" ./{projeto} && cd ./{projeto}
rm -rf .git && git init   # desacopla do histórico do template
# --lead-integration: none | ghl | prospectpro | webhook
# --reset-db: primeiro boot limpo de um scaffold novo
./scripts/new-lp.sh "Nome do Projeto" admin@projeto.com \
  --lead-integration ghl \
  --reset-db
```

```powershell
# PowerShell (Windows) — cópia fresca do cache para o diretório do projeto
$Tpl = "$env:USERPROFILE\.morph\templates\MORPH_Umbraco_LP_Template"
Copy-Item $Tpl ".\{projeto}" -Recurse; Set-Location ".\{projeto}"
Remove-Item .git -Recurse -Force; git init   # desacopla do histórico do template
# new-lp.ps1 usa parâmetros NOMEADOS (não posicionais/--flag):
.\scripts\new-lp.ps1 -ProjectName "Nome do Projeto" -AdminEmail admin@projeto.com `
  -LeadIntegration ghl -ResetDb
```

> Alternativa: em vez de copiar do cache, fazer um clone fresco direto no diretório do projeto (`gh repo clone polymorphism-tech/MORPH_Umbraco_LP_Template {projeto}`) e então `rm -rf .git && git init`.

O script:
- Gera `.env` com `UMBRACO_ADMIN_EMAIL`, `UMBRACO_ADMIN_PASSWORD` (senha aleatória), `UMBRACO_GLOBAL_ID`, `REVALIDATE_SECRET`, `PROJECT_NAME` e `LEAD_INTEGRATION`.
- Integração de lead selecionada em `--lead-integration` (`none`/`ghl`/`prospectpro`/`webhook`). Em modo não-interativo sem a flag, assume `none`.

Nunca commitar `.env`. O cache do passo 0 permanece intacto (read-only) — todo o trabalho acontece nesta cópia.

**Smoke-boot antes de qualquer customização** — boot limpo do template para confirmar que o ambiente funciona. Suba **na ordem**, backend primeiro, e só depois construa o frontend:

```bash
docker compose up -d backend
# espere a Delivery API, não o backoffice (ver abaixo)
until [ "$(curl -s -o /dev/null -w '%{http_code}' \
  'http://localhost:5001/umbraco/delivery/api/v2/content?take=1')" = "200" ]; do sleep 5; done
docker compose up --build -d frontend
```

Backend sobe em `http://localhost:5001`, frontend em `http://localhost:3000`. Acessar `http://localhost:3000` e confirmar que a página carrega sem erro 500. Isso elimina problemas de ambiente antes de introduzir customizações; falha precoce economiza horas.

**Atenção (gotcha Build do Next.js):** o Dockerfile do frontend executa `next build` na imagem, e nesse momento o Next busca conteúdo via Delivery API para o SSG. Se a API não responder **conteúdo**, o build falha, e a mensagem que chega é só `npm run build ... exit code 1`, sem dizer que o problema é o backend. Por isso `docker compose up --build` sozinho, do zero, não é um comando confiável: ele constrói o frontend junto com o backend.

> **O critério de prontidão é a Delivery API responder 200, nunca o backoffice.** Um healthcheck
> que bate em `/umbraco` fica **healthy** enquanto `/umbraco/delivery/api/v2/content` ainda devolve
> **500**: o backoffice sobe, o uSync importa, o seed grava, e a API só quebra quando alguém pede o
> conteúdo. `depends_on: condition: service_healthy` apontando para o healthcheck errado dá uma
> falsa sensação de ordem resolvida e o build do frontend morre mesmo assim. Aponte o healthcheck
> do backend para a Delivery API.
>
> **E se ela devolver 500, não é questão de esperar mais.** Um 500 persistente na Delivery API com
> banco recém-seedado é quase sempre **dado seedado no formato errado**, não lentidão de boot.
> Aumentar timeout não resolve; vá para a seção "Gotchas de campo" e diagnostique pelo log do
> Umbraco antes de seguir.

### 4. Lift verbatim — portar markup, mídia e tema

**Esta é a etapa que garante a paridade exata.** Para cada seção, o trabalho é um **lift-and-rewire**, não um rebuild:

1. **Copiar o componente da origem verbatim** para `frontend/src/components/modules/NomeSection.tsx` — mesmo JSX/HTML, mesmas classes, mesmo CSS, mesma estrutura. Não "limpar", não refatorar, não trocar libs de animação.
2. **Trocar apenas os literais de conteúdo** (texto, `src` de imagem/vídeo, labels) por leituras do `data` prop do Umbraco. Tudo o que é layout/estilo/markup permanece igual.
3. **Seedar** esses valores via `SeedContentHandler` para baterem com a origem (passo 4b) → o primeiro render é idêntico **por construção**; a editabilidade no backoffice fica por cima sem mudar o markup renderizado.
4. **Copiar os assets sem reencode** (passo 4b) e **portar o CSS/tema verbatim** (passo 4c).

Os passos 4a-4d abaixo detalham cada parte. A regra que atravessa todos: **se mudou o markup ou o CSS renderizado, não é mais "exatamente igual"**.

#### 4a. Módulos novos — uSync ContentType

Para cada módulo novo identificado no passo 2, criar o arquivo `.config` em `backend/uSync/v17/ContentTypes/`. Espelhar a estrutura dos módulos existentes (ex.: `heroSection.config`):

```xml
<!-- Exemplo: atelierSection.config -->
<?xml version="1.0" encoding="utf-8"?>
<ContentType Key="{novo-guid}" Alias="atelierSection" Level="2">
  <Info>
    <Name>Atelier Section</Name>
    <Icon>icon-layers</Icon>
    <Thumbnail>folder.png</Thumbnail>
    <Description></Description>
    <AllowAtRoot>False</AllowAtRoot>
    <IsListView>False</IsListView>
    <Variations>Nothing</Variations>
    <IsElement>true</IsElement>
    <Folder>Sections</Folder>
    <Compositions />
    <DefaultTemplate></DefaultTemplate>
    <AllowedTemplates />
  </Info>
  <Structure />
  <GenericProperties>
    <GenericProperty>
      <Key>{guid-prop}</Key>
      <Name>Titulo</Name>
      <Alias>titulo</Alias>
      <Definition>0cc0eba1-9960-42c9-bf9b-60e150b429ae</Definition>
      <Type>Umbraco.TextBox</Type>
      <Mandatory>true</Mandatory>
      <Description><![CDATA[]]></Description>
      <SortOrder>0</SortOrder>
      <Tab Alias="content/content">Content</Tab>
      <Variations>Nothing</Variations>
    </GenericProperty>
    <!-- propriedades adicionais -->
  </GenericProperties>
</ContentType>
```

Cada módulo carrega suas próprias propriedades diretamente — os módulos do template não usam compositions compartilhadas para campos de seção (o `<Compositions />` é vazio nos módulos existentes; as compositions em `lpHome.config` são para SEO, cookie e analytics — composições transversais da home, não dos módulos filhos).

Para que o novo módulo apareça como opção de bloco na home, registrá-lo no DataType de Block List (`modules` em `lpHome.config`). O `<Definition>649d20e8-e4d5-42a0-9f27-28a537f9ebda</Definition>` aponta para esse DataType — adicionar o novo `ContentTypeKey` no XML do DataType em `backend/uSync/v17/DataTypes/` (arquivo correspondente) e no `SeedContentHandler` para o bloco ser incluído no seed programático.

#### 4b. Seed de conteúdo — SeedContentHandler

O `SeedContentHandler` (`backend/Infrastructure/SeedContentHandler.cs`) roda na notificação `uSyncImportCompletedNotification` — após o Umbraco importar todos os schemas — e cria a home com conteúdo inicial se ainda não existir. É **idempotente**: verifica se já há um nó raiz do tipo `lpHome` (ou alias equivalente) antes de criar.

Padrão de seed de uma seção com mídia:

```csharp
// 1. Importar asset para a Media Library (idempotente por nome de arquivo)
var mediaItem = CreateOrGetMedia("hero-video.mp4", mediaFolder, "umbracoMediaVideo");
// mediaItem.Key é um Guid estável entre boots

// 2. Serializar referência como MediaPicker3 JSON
var mediaJson = JsonSerializer.Serialize(new[] {
    new { key = Guid.NewGuid(), mediaKey = mediaItem.Key }
});

// 3. Setar na propriedade do nó de conteúdo
sectionNode.SetValue("videoFundo", mediaJson);
```

Coloque os assets de seed em `backend/seed-assets/` (imagens, vídeos, ícones da LP de origem).

**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):

```csharp
content.SetValue("pageTitleSeo", "<título da origem>");
content.SetValue("titleSeo", "<og:title da origem>");
content.SetValue("descriptionSeo", "<meta description da origem>");
content.SetValue("canonicalUrlSeo", "<URL canônica de produção>"); // só a URL, sem tag HTML
content.SetValue("noIndexSeo", false); // é Mandatory=false a partir do fix de 2026-07-02 — não deixar true
content.SetValue("jsonLDSchemaSeo", OrigemJsonLd); // JSON-LD capturado da origem, se existir
// imageSeo (og:image) e favicon: importar como media (mesmo padrão de CreateOrGetMedia)
// e setar via MediaPicker3, igual ao padrão de mídia acima.
```

`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.

**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.

**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).

#### 4c. Tema — portabilidade do index.css

Copiar as variáveis de design da origem para `frontend/src/app/globals.css` no bloco `@theme`:

```css
/* Antes (origem — index.css/CSS da LP) */
@theme {
  --color-surface: #F5F0EB;
  --color-primary: #8B1A1A;
  --color-primary-hover: #A82020;
  --color-tertiary: #1A1A1A;
  --font-display: 'Cormorant Garamond', serif;
  --font-body: 'Jost', sans-serif;
}

/* Depois (destino — frontend/src/app/globals.css) */
@theme {
  --color-surface: #F5F0EB;   /* portado diretamente */
  --color-primary: #8B1A1A;
  --color-primary-hover: #A82020;
  --color-tertiary: #1A1A1A;
  --font-display: 'Cormorant Garamond', serif;
  --font-body: 'Jost', sans-serif;
}
```

**Fontes — auto-hospedar, nunca `<link>` externo pro Google Fonts:** um `<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?...">` no `<head>` é render-blocking (round-trip
externo antes de qualquer paint) e, combinado com `font-display: swap`, causa CLS quando a
fonte troca depois do primeiro paint — foi 1.680ms de render-blocking estimado e 0.221 de CLS
medidos num caso real (Barone_LP). **Não usar `next/font/google`** aqui (diferente do passo 4
de `create-umbraco-lp`, que pode usar): o nome de família que ele gera é escopado/diferente do
literal da origem, e o gate de paridade (passo 6b.2) compara `font-family` computado
byte-a-byte contra a origem — `next/font` quebra esse check.

O fix correto, que preserva a paridade:
1. Buscar a CSS real do Google pra descobrir as URLs `.woff2` (`curl` na mesma URL que iria no
   `<link>`) — geralmente **um arquivo por família+estilo já cobre todos os pesos estáticos**
   usados (Google costuma reaproveitar o mesmo arquivo pra weight 400/500/600/700 de uma fonte
   não-variável). Baixar só o subset `latin` (`unicode-range: U+0000-00FF, ...`) — cobre toda
   acentuação do português (á, ã, ç, é... estão em U+00C0-00FF).
2. Salvar os `.woff2` em `frontend/public/fonts/`.
3. Declarar `@font-face` em `globals.css` (ou no arquivo de tema) com o **mesmo `font-family`
   literal da origem** (`'Cormorant'`, `'Jost'`, etc. — não o nome gerado por uma lib) e
   `font-display: optional` (não `swap`) — `optional` só troca pro custom font se ele chegar
   quase instantaneamente; senão fica no fallback pra aquela visita, evitando o reflow visível
   que gerou o CLS.
4. `<link rel="preload" href="/fonts/arquivo.woff2" as="font" type="font/woff2" crossOrigin="anonymous">`
   no `<head>` do `layout.tsx`, sem nenhum `<link>` pro domínio do Google.

Verificar ao final: `getComputedStyle(el).fontFamily` no elemento de heading do hero deve
retornar **a mesma string literal que a origem** (ex.: `"Cormorant, Georgia, serif"`) — só a
origem do arquivo mudou (de `fonts.gstatic.com` pra same-origin), não o nome da família.

#### 4d. Componentes frontend dos módulos novos

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.*`:

> **Exceção deliberada ao verbatim — imagens de mídia do Umbraco:** o componente `Picture`
> lifted da origem provavelmente emite um `<img>` cru. Pra qualquer `src` que vier do Media
> Picker (URL do backend, editável no backoffice — diferente de um asset estático em
> `/public`), usar `next/image` (`fill`, com o container já dimensionado) desde o primeiro
> lift, não um `<img>` cru. Isso **não** quebra o gate de paridade (passo 6b.3 só faz checksum
> de assets em `/public`, nunca de URLs de mídia) — pelo contrário, sem isso qualquer foto que
> o cliente trocar depois no backoffice chega crua (fotos de câmera/celular fácil 10+ MB) e
> vira lento ou 500 sob concorrência (ver seção "Mídia" no README do template).
>
> **Pitfall a checar nesse lift:** `next/image fill` aplica `position/height/width` **inline**
> no `<img>` renderizado — isso vence qualquer regra de classe (`.minha-grade img { height:
> 116%; margin-top: -8% }`, por exemplo) que a origem use pra efeitos de oversize/parallax/zoom
> em cima de um `<img>` cru, mesmo que a regra continue no CSS lifted verbatim. Se a origem tem
> algum truque desses (imagem deliberadamente maior que o container + margin negativo, comum
> em seções com parallax scroll-scrub), a troca pra `fill` abre um vão visível onde a "sobra"
> da imagem deveria estar. Fix: `!important` na propriedade de classe que está sendo vencida
> (normalmente só `height`) — é uma das poucas justificativas legítimas pra `!important`, já
> que é o único jeito de vencer um inline style que você não controla diretamente. Checar isso
> na hora do lift, não esperar aparecer como bug visual depois.

```tsx
// Padrão: componente com 'use client', recebe { data: SectionProps },
// faz cast para o tipo tipado, e lê props via data.property.
// O corpo do return é o MARKUP DA ORIGEM VERBATIM — mesmas tags, mesmas classes,
// mesma animação. Só os literais (texto, src) viram leituras de `d`.
'use client'
import type { SectionProps, NomeSectionProps } from '@/lib/types'

type Props = { data: SectionProps }

export default function NomeSection({ data }: Props) {
  const d = data as NomeSectionProps
  // return ( <markup da origem, idêntico, com {d.titulo} no lugar do texto fixo> )
}
```

Declarar o tipo correspondente em `frontend/src/lib/types.ts` com os aliases de propriedade do uSync.

Registrar o módulo em `frontend/src/components/modules/moduleRegistry.ts`:

```ts
nomeSection: () => import('./NomeSection'),
```

#### 4e. Sequências de frames e assets de animação (LP scroll-driven)

Se a origem tem hero (ou seção) com **canvas frame-scrub** (o scroll avançando um vídeo quadro a quadro), a animação atravessa a migração intacta, mas só se os assets forem para o balde certo. O modelo abaixo é o que roda em produção; foi decidido uma vez e não deve ser reaberto por engano.

**Dois baldes, e a fronteira não é opinião:**

| O quê | Onde | Editável no backoffice |
|---|---|---|
| Sequência de frames (`hero-frames/`, `oficio-frames/`), fontes, monograma da marca, ícones posicionais | `frontend/public/` | não, é asset estrutural |
| Poster do scrub, fotos editoriais, logo, imagem de OG | `backend/seed-assets/` → Media Library → MediaPicker3 | sim |

**Por que os frames não vão para a Media Library.** Três razões, em ordem de gravidade:

1. O processamento on-the-fly do ImageSharp (`?format=webp`) exige URL assinada por HMAC, e no Umbraco 17 o HMAC não é relaxável por configuração. Uma sequência servida de `/media/` perde o webp e não tem como recuperá-lo no frontend headless.
2. São 145 arquivos por sequência. Como requests assinadas ao backend, é um custo por visitante que não existe servindo de `/public`.
3. O `SeedContentHandler` só enxerga `backend/seed-assets/` e o banco. Deixando os frames em `frontend/public/`, eles ficam fora do universo do seed: sobrevivem a re-seed, a re-import do uSync e a um `--reset-db` completo, sem nenhum tratamento especial.

**O poster é o que fica editável.** É ele que aparece no mobile e sob `prefers-reduced-motion`, então é o único da dupla que o cliente tem motivo para trocar. Frames fixos, poster no MediaPicker.

**Cache-bust é obrigatório e manual.** `frontend/public/` não recebe hash de build (o Dockerfile copia o diretório verbatim e o Next só versiona `.next/static`), então reextrair frames mantendo os mesmos nomes serve os bytes antigos de qualquer cache intermediário. O `framePath` carrega um `?v=N` que sobe a cada reextração:

```ts
const framePath = (i: number) => `/hero-frames/frame_${String(i).padStart(3, '0')}.jpg?v=2`
```

**No lift, não trocar a lib de animação.** O template não traz `gsap` nem `lenis`; se a origem usa, adicione ao `frontend/package.json` do destino. Portar o registry de motion e o runtime de scroll como componentes `'use client'`, com o guard de SSR no registro do ScrollTrigger (ele toca `document`) e o runtime montado uma vez na page. Detalhe completo em `frontend/scroll-driven/smooth-scroll.md` e `frontend/scroll-driven/frame-scrub.md` no acervo de standards.

### 5. Conectar o formulário ao backend de leads

O template usa uma arquitetura de **multi-sink**: o backend seleciona automaticamente a integração correta pelo valor de `leadIntegration` configurado no nó `leadsConfig` do backoffice.

**Sinks disponíveis no template:**

| `Key` do sink | Classe | Integração |
|---|---|---|
| `ghl` | `GhlLeadSink` | GoHighLevel CRM |
| `prospectpro` | `ProspectProLeadSink` | ProspectPRO |
| `webhook` | `WebhookLeadSink` | Webhook HTTP genérico |

O `LeadController` (`backend/Controllers/LeadController.cs`) recebe `IEnumerable<ILeadSink>` via DI e despacha para `sinks.FirstOrDefault(s => s.Key == cfg.Integration)`. Adicionar uma nova integração = implementar `ILeadSink` + uma linha no `LeadComposer.cs`.

**Configuração pós-boot (nunca em código):**

1. Acessar o backoffice em `http://localhost:5001/umbraco`.
2. Navegar até o nó **Leads Config** (criado automaticamente pelo `SeedContentHandler`).
3. Preencher:
   - `Lead Integration`: selecionar `ghl` / `prospectpro` / `webhook`.
   - `API Key` / `Webhook URL` / campos específicos da integração.
4. Publicar o nó.

O token nunca aparece no código-fonte nem em variáveis de ambiente — lido do cache publicado pelo `ReadConfig()` do controller via `IUmbracoContextFactory`.

**Frontend:** o componente `ContactSection` (`frontend/src/components/modules/ContactSection.tsx`) faz POST para `/api/leads` (rota do Next.js que faz thin-proxy para `{NEXT_PUBLIC_UMBRACO_URL}/api/leads`). Nenhuma credencial no frontend.

**Campos mínimos esperados pelo LeadController:** `name`, `email`, `phone`. Campos extras são aceitos no payload como propriedades livres e repassados ao sink.

**Normalização de telefone:** aplicar máscara no frontend antes do POST para garantir o formato `+55XXXXXXXXXXX` — o GHL rejeita telefones sem DDI. O template tem o helper `LeadHelpers.NormalizePhoneBR()` no backend como fallback (idempotente: preserva o `+55` se já presente), mas normalizar no frontend evita ambiguidade.

### 6. Gate de paridade — confirmar que ficou EXATAMENTE igual

Objetivo: **provar** que a LP migrada é idêntica à origem. Com o lift verbatim (passo 4), os checks determinísticos abaixo passam **por construção** — se algum falha, é sinal de que o lift introduziu drift naquela seção; voltar e corrigir o lift, não "ajustar até parecer".

**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.

> Os dois pontos do passo 4c (fontes auto-hospedadas) e 4d (`next/image fill` pra mídia do
> Umbraco) não são drift de paridade — são a paridade **certa** pedindo um detalhe extra de
> implementação. O checksum de assets (6b.3) só cobre `/public`; o estilo computado (6b.2)
> continua batendo porque `font-family` fica literal. Nenhum dos dois deveria fazer o gate
> falhar quando bem aplicados.

#### 6a. Boot + checklist manual (sanidade)

Mesma ordem do passo 3: backend primeiro, Delivery API respondendo 200, e só então o frontend.

```bash
docker compose up -d backend
until [ "$(curl -s -o /dev/null -w '%{http_code}' 'http://localhost:5001/umbraco/delivery/api/v2/content?take=1')" = "200" ]; do sleep 5; done
docker compose up --build -d frontend
```

Percorrer a página em `http://localhost:3000` e conferir, lado a lado com a origem: todas as seções na ordem certa, textos/mídias corretos, formulário visível, navegação e footer funcionando. Isso é só uma sanidade rápida antes dos checks automáticos.

#### 6b. Checks determinísticos (o bloqueio)

A skill **scaffolda** o harness de paridade no projeto destino (o template **não** o traz): criar `frontend/e2e/parity.spec.ts` e `frontend/compare-sections.mjs`. O harness compara `http://localhost:3000` (destino) contra a origem (URL de produção ou um build local da origem) e **falha** se qualquer dimensão divergir:

1. **Estrutura DOM + nomes de classe** por seção — `compare-sections.mjs` extrai o esqueleto de tags+classes de cada `<section>`/`<nav>`/`<footer>` dos dois lados (agrupado por chave semântica/texto âncora) e aponta diferenças. Adaptar o array `KEYS` (`[nome, matchFn]`) aos textos âncora das seções do projeto.
2. **Estilo computado** em elementos amostrados — para cada seção, comparar `getComputedStyle` de elementos-chave (cor, `font-family`, `font-size`, `font-weight`, paddings/margins, `border-radius`) origem × destino. Divergência = falha.
3. **Checksum de assets** — o conjunto de `sha256` de todas as imagens/vídeos/fontes servidas pelo destino deve igualar o conjunto capturado da origem no passo 1. Asset reencode/otimizado quebra aqui (proposital).
4. **Comportamento por interação** — uma asserção Playwright por comportamento capturado no passo 1 (scroll-expand, **canvas frame-scrub**, carrossel, marquee, autoplay de vídeo, hover, máscara/validação de telefone, submit do formulário com `/api/leads` mockado retornando `{"ok":true}`).

> **Scrub e qualquer efeito dirigido por scroll se verificam COM MOVIMENTO.** Um screenshot
> estático não distingue um scrub vivo de um canvas quebrado: o canvas pode estar 1x1 desenhando
> um pixel esticado, ou ignorando o scroll por completo, e a foto continua plausível. Dirija o
> scroll e compare o que foi desenhado em posições diferentes:
>
> ```js
> const lenis = window.__lenis
> const h = document.querySelector('header.hero-scrub').getBoundingClientRect().height
> for (const p of [0, .15, .3, .45, .6, .8, 1]) {
>   lenis.scrollTo(Math.round((h - innerHeight) * p), { immediate: true })
>   await new Promise(r => setTimeout(r, 350))   // deixa o scrub:0.4 assentar
>   // capture aqui: screenshot, ou uma assinatura de pixels via ctx.getImageData
> }
> ```
>
> A espera não é opcional: com `scrub: 0.4` o desenho não salta, e sem ela você fotografa o frame
> anterior. **Critério de aprovação:** as capturas diferem entre si. Todas iguais significa scrub
> morto, mesmo que a página pareça correta. Confirmar também que os frames respondem 200 com o
> `?v=N` em uso e que os beats de texto trocam ao longo do percurso.

```bash
cd frontend
npx playwright test            # roda parity.spec.ts + smoke (home.spec.ts do template)
node compare-sections.mjs http://localhost:3000/ https://origem-em-producao.com.br/
```

#### 6c. Screenshot diff (corroboração secundária)

Captura full-page origem × destino em múltiplos viewports (mobile/tablet/desktop). Para não gerar ruído:
- **desabilitar animações** (`prefers-reduced-motion` / CSS de teste), **aguardar as web-fonts** (`document.fonts.ready`), **congelar vídeo/carrossel** num frame fixo;
- usar **tolerância ciente de antialiasing** (ex.: `maxDiffPixelRatio` pequeno, threshold de cor).

Um diff acima da tolerância é um **alerta para investigar** (provável seção com lift incompleto), não uma reprovação automática se os checks de 6b passam.

#### 6d. Critérios de aceite

A migração é "exata" quando **todos** são verdade:
- **6b determinístico passa**: DOM+classes, estilo computado, checksum de assets e comportamento batem com a origem em todas as seções.
- **6c screenshot diff** abaixo da tolerância em todos os breakpoints (mobile/tablet/desktop).
- Todas as seções renderizam com conteúdo real (não placeholders) e na ordem da origem.
- 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).
- 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.
- 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.
- `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.
- Nenhuma chave de API ou token aparece em arquivo versionado.

## Gotchas de campo

Furos que já custaram build quebrado ou deploy revertido em migração real. Nenhum deles aparece como erro óbvio no caminho feliz.

**Property editor guarda JSON, não escalar. Seedar string crua derruba a Delivery API inteira.** `Umbraco.DropDown.Flexible` persiste **array JSON mesmo com `Multiple: false`**. Dentro do JSON de um Block List a tentação é escrever `"ctaAcao": "ancora"` como qualquer outro campo do bloco; o correto é `["ancora"]`. O erro não fica no campo: a exceção sobe pelo `BlockListPropertyValueConverter` e **derruba o documento inteiro** (500 na home), o que mata o `next build` com SSG e chega até você como `npm run build ... exit code 1`. Só aparece com banco novo, então passa despercebido até o primeiro `--reset-db` ou deploy limpo.

Regra, diagnóstico pela letra citada no `JsonException` e o procedimento de A/B estão em `references/umbraco-lp-patterns.md` (seção 6) da skill `create-umbraco-lp`, instalada em `.claude/skills/create-umbraco-lp/`. Leia essa seção antes de seedar qualquer dropdown.

**MediaPicker3 sempre volta array, mesmo em single-pick.** Na Delivery API, todo campo de mídia chega como array (`poster`, `logo`, `favicon`, `imageSeo`, `modalLogo`), embutido sem `expand`. Acessar `campo.url` direto funciona enquanto o campo está vazio e passa a estourar no instante em que um editor o preenche. Pior: em `generateMetadata` o Next trata a exceção como fatal e derruba o build inteiro, então o sintoma é um deploy que quebra sozinho depois de alguém publicar no backoffice, sem nenhuma mudança de código. Tipar como `UmbracoImage[]` e indexar em `[0]`, sempre com fallback para o asset estático de `/public`.

**Label de Block List aninhado usa UFM.** O backoffice do Umbraco 17 (Bellissima) renderiza o label do bloco com Umbraco Flavored Markdown, `{=alias}`, e não com o `{{ alias }}` do backoffice AngularJS antigo. Com a sintaxe velha o header do bloco mostra a chave literal para o editor. É só display do backoffice, não afeta o render da LP, e por isso passa despercebido em toda validação automática.

**Reverse proxy e bind do Next.** Atrás de Traefik ou Coolify, o backend precisa de `UseForwardedHeaders` (X-Forwarded-Proto/For), senão o OpenIddict do backoffice não vê HTTPS e o login em `/umbraco` falha com `ID2083: This server only accepts HTTPS requests`. E o runner do frontend precisa de `HOSTNAME=0.0.0.0`: sem isso o `server.js` do Next standalone faz bind no id do container, o healthcheck via `localhost:3000` nunca passa, o container nunca fica healthy e o deploy dá rollback sem erro de aplicação nenhum.

**Antes de escrever qualquer um destes, confira se o template já resolveu.** Correções são portadas do projeto de volta para o `MORPH_Umbraco_LP_Template`, então parte desta lista pode já estar no scaffold que o passo 0 baixou. Ensinar de novo uma correção existente cria divergência.

## Anti-padrões

- Não commitar `.env` nem nenhum arquivo com tokens ou senhas.
- Não colocar credenciais de integração em variáveis de ambiente do container — elas pertencem ao nó `leadsConfig` no backoffice.
- Não criar módulos novos com alias que conflite com os do template (verificar `lpHome.config` e os `.config` existentes antes de nomear).
- Não usar `Guid.NewGuid()` em `SeedContentHandler` para o `Key` de um `mediaKey` que deve ser estável entre boots — calcular o GUID deterministicamente a partir do nome do arquivo ou recuperar o existente com `GetByKey`.
- Não saltear o smoke-boot (passo 3) — problemas de ambiente devem ser detectados antes de portar conteúdo.
- Não reutilizar `customHtmlSection` para tudo — módulos dedicados permitem edição granular no backoffice e ISR por tag.
- **Não reinterpretar layout/CSS nem "limpar" o markup** — portar verbatim; o módulo Umbraco só embrulha o mesmo markup da origem. Rebuild = drift.
- **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.
- **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.
- **Não tratar o screenshot diff como gate único** — o bloqueio são os checks determinísticos (6b); o diff visual é secundário.

## Output esperado

- `docs/origem-inventario.md` — inventário de seções, conteúdo, mídia e identidade visual da LP de origem.
- `docs/mapeamento-modulos.md` — tabela de mapeamento seção → doctype/módulo com decisão (reuso direto / ajuste / lift verbatim).
- `backend/uSync/v17/ContentTypes/*.config` — novos doctypes para os módulos lifted da origem.
- `backend/Infrastructure/SeedContentHandler.cs` — seed idempotente com textos e mídia iniciais.
- `backend/seed-assets/` — assets de seed (imagens, vídeos copiados da origem).
- `frontend/src/components/modules/*.tsx` — componentes dos módulos novos.
- `frontend/src/components/modules/moduleRegistry.ts` — atualizado com novos aliases.
- `frontend/src/lib/types.ts` — tipos TypeScript para as props dos novos módulos.
- `frontend/src/app/globals.css` — tema portado verbatim da origem (paleta + tipografia + fontes).
- `frontend/e2e/home.spec.ts` — smoke test adaptado para o projeto.
- `frontend/e2e/parity.spec.ts` — **gerado pela skill**: gate determinístico (DOM+classes, estilo computado, checksum de assets, comportamento) + screenshot diff.
- `frontend/compare-sections.mjs` — **gerado pela skill**: comparação de esqueleto estrutural origem × destino.
- `.env` gerado pelo `new-lp.sh` / `new-lp.ps1` — **não versionado** (`.gitignore`).

## Referências

Repo do template (privado, branch `master`): `polymorphism-tech/MORPH_Umbraco_LP_Template` — obtido automaticamente no passo 0.

- `MORPH_Umbraco_LP_Template/scripts/new-lp.sh` e `new-lp.ps1` — scaffold de nova LP
- `MORPH_Umbraco_LP_Template/backend/Lead/LeadComposer.cs` — registro dos sinks no DI
- `MORPH_Umbraco_LP_Template/backend/Lead/ILeadSink.cs` — interface de integração de leads
- `MORPH_Umbraco_LP_Template/backend/Controllers/LeadController.cs` — controller público + `ReadConfig()` + dispatch
- `MORPH_Umbraco_LP_Template/backend/Lead/GhlLeadSink.cs`, `ProspectProLeadSink.cs`, `WebhookLeadSink.cs` — sinks concretos
- `MORPH_Umbraco_LP_Template/backend/Infrastructure/SeedContentHandler.cs` — seed idempotente com mídia (MediaPicker3)
- `MORPH_Umbraco_LP_Template/backend/uSync/v17/ContentTypes/lpHome.config` — doctype raiz da home (Block List `modules`)
- `MORPH_Umbraco_LP_Template/frontend/src/components/modules/moduleRegistry.ts` — registro dos módulos
- `MORPH_Umbraco_LP_Template/frontend/src/app/globals.css` — bloco `@theme` para portar o tema da origem
- `MORPH_Umbraco_LP_Template/frontend/e2e/home.spec.ts` + `playwright.config.ts` — base de e2e (estender com `parity.spec.ts`)
- `MORPH_Umbraco_LP_Template/docker-compose.yml` — orquestração backend + frontend com variáveis de ambiente
- `.claude/skills/create-umbraco-lp/references/umbraco-lp-patterns.md` — padrões e gotchas do template (controller público, multi-sink, leitura server-side, segurança da Delivery API, uSync, **dropdown como JSON array (seção 6)**, seed idempotente, build do Next.js exigindo backend). Instalado junto com a skill irmã; carregue sob demanda.
