Files
Creeer-Web/docs/superpowers/plans/2026-07-21-creeer-landing-additions.md
T

463 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# creeer Landing Additions — Header + Cómo empezar + Footer — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a minimal sticky Header, a new "Cómo empezar" on-ramp section (section 07), and a slim Footer to the existing 7-section `creeer` landing page.
**Architecture:** Three additive pieces reusing the existing primitives (`Wordmark`, `CTA`, `Container`), tokens, the `motion` + `motionProps(reduced)` pattern, and the copy-in-`lib/content.ts` convention. Header and Footer are static server components wrapping `<main>`; `ComoEmpezar` is a client section following the exact pattern of `Mecanismo.tsx`/`Disciplinas.tsx`. Cierre becomes section 08. No backend, no forms — the site stays a static export.
**Tech Stack:** Next.js 14 (App Router, `output: export`), React 18, TypeScript, Tailwind CSS 3.4, Framer Motion 11. Tests: Vitest + @testing-library/react.
## Global Constraints
(Copied from the spec `docs/superpowers/specs/2026-07-21-creeer-landing-additions-design.md` — apply to every task.)
- **Wordmark** always via the `Wordmark` primitive — never re-implemented, never uppercased.
- **Paleta (tokens):** `ink #0A1F44` · `gold #B8901f` (accents on LIGHT) · `gold-bright #D4AF37` (accents on DARK) · `base #FBFAF7` (light bg, class `bg-base`) · dark bg class `bg-bg-dark` · `cream` (text on dark) · `muted`/`muted-dark`.
- **Tipografía:** Georgia italic (`font-serif italic`) = wordmark + section titles; Inter = body/UI.
- **Sin backend, sin formularios** — contacto solo por enlaces directos (WhatsApp/mailto/LinkedIn).
- **Un único `<h1>`** en toda la página — vive en el Hero. New pieces use `<h2>`/`<h3>` or no heading.
- **Todo el copy** vive en `lib/content.ts` (nuevas claves `header`, `comoEmpezar`, `footer`).
- **Voz** en segunda persona, español.
- **Reduced motion:** every Framer Motion element gets `{...motionProps(reduced)}` after its `initial`/`whileInView`/`viewport` props (`reduced = usePrefersReducedMotion()`). Header/Footer are static (no motion).
- **`tsconfig.json` has no `target`** → avoid ES2018+ regex flags (`s`/dotAll) or `tsc --noEmit` fails.
## File Structure
```
components/
├── layout/
│ ├── Header.tsx # Task 1 — sticky bar (server component)
│ ├── Footer.tsx # Task 3 — slim footer strip (server component)
│ └── __tests__/
│ ├── Header.test.tsx # Task 1
│ └── Footer.test.tsx # Task 3
└── sections/
├── ComoEmpezar.tsx # Task 2 — section 07 (client, motion pattern)
└── __tests__/
└── ComoEmpezar.test.tsx # Task 2
lib/content.ts # Tasks 1/2/3 append `header` / `comoEmpezar` / `footer`
app/page.tsx # Tasks 1/2/3 wire the pieces; Task 4 anchor offset
components/sections/Cierre.tsx # Task 4 — add scroll-mt to the #contacto anchor
```
Each component task also wires itself into `app/page.tsx` (matching the repo's established incremental-compose pattern), so the page stays working after every task.
---
### Task 1: Header (minimal sticky)
**Files:**
- Create: `components/layout/Header.tsx`
- Create: `components/layout/__tests__/Header.test.tsx`
- Modify: `lib/content.ts` (append `header`)
- Modify: `app/page.tsx` (place `<Header />` before `<main>`)
**Interfaces:**
- Consumes: `Wordmark` (`size`, `onDark`), `CTA` (`href`, `label`, `variant`), `Container`, and `header.cta` from content.
- Produces: `Header()` at `components/layout/Header.tsx` — no props. Consumed by `app/page.tsx`.
- [ ] **Step 1: Append the `header` copy to `lib/content.ts`**
Add at the end of `lib/content.ts`:
```ts
export const header = {
cta: 'Hablemos', // short nav CTA → #contacto (the full CTA lives in Hero/Cierre)
};
```
- [ ] **Step 2: Write the failing test `components/layout/__tests__/Header.test.tsx`**
```tsx
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Header } from '../Header';
describe('<Header />', () => {
it('renders the wordmark', () => {
const { container } = render(<Header />);
expect(container.textContent).toContain('creeer');
});
it('renders a persistent CTA linking to the contact section', () => {
render(<Header />);
const cta = screen.getByRole('link', { name: /Hablemos/i });
expect(cta).toHaveAttribute('href', '#contacto');
});
});
```
- [ ] **Step 3: Run the test to verify it fails**
Run: `npm test -- Header`
Expected: FAIL with "Cannot find module '../Header'".
- [ ] **Step 4: Write `components/layout/Header.tsx`**
```tsx
import { Wordmark } from '@/components/primitives/Wordmark';
import { CTA } from '@/components/primitives/CTA';
import { Container } from '@/components/primitives/Container';
import { header } from '@/lib/content';
export function Header() {
return (
<header className="sticky top-0 z-50 border-b border-ink/10 bg-base/80 backdrop-blur">
<Container className="flex items-center justify-between py-3">
<a href="#top" aria-label="creeer — inicio">
<Wordmark size="md" />
</a>
<nav aria-label="Principal">
<CTA href="#contacto" label={header.cta} variant="light" />
</nav>
</Container>
</header>
);
}
```
Notes: server component (no `'use client'`, no motion — `position: sticky` is pure CSS). `#top` is the special top-of-document fragment (no element with that id is required). The `CTA` primitive appends the `→`, so `header.cta` holds just the word.
- [ ] **Step 5: Run the test to verify it passes**
Run: `npm test -- Header`
Expected: PASS, 2 tests.
- [ ] **Step 6: Wire `<Header />` into `app/page.tsx`**
Add the import after the existing section imports (top of the file):
```tsx
import { Header } from '@/components/layout/Header';
```
Then insert `<Header />` between the JSON-LD `<script>` and `<main>`:
```tsx
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<Header />
<main>
```
- [ ] **Step 7: Typecheck and run the full suite**
Run: `npx tsc --noEmit && npm test`
Expected: tsc clean; all tests pass.
- [ ] **Step 8: Commit**
```bash
git add components/layout/Header.tsx components/layout/__tests__/Header.test.tsx lib/content.ts app/page.tsx
git commit -m "feat: sticky Header with persistent contact CTA"
```
---
### Task 2: "Cómo empezar" section (07)
**Files:**
- Create: `components/sections/ComoEmpezar.tsx`
- Create: `components/sections/__tests__/ComoEmpezar.test.tsx`
- Modify: `lib/content.ts` (append `comoEmpezar`)
- Modify: `app/page.tsx` (place `<ComoEmpezar />` before `<Cierre />`)
**Interfaces:**
- Consumes: `Container`, `CTA`, `comoEmpezar` + `cierre.contact.whatsapp.href` from content, and `fadeInUp`/`staggerContainer`/`viewportOnce`/`usePrefersReducedMotion`/`motionProps` from `lib/motion`.
- Produces: `ComoEmpezar()` at `components/sections/ComoEmpezar.tsx` — no props. Consumed by `app/page.tsx`.
- [ ] **Step 1: Append the `comoEmpezar` copy to `lib/content.ts`**
Add at the end of `lib/content.ts`:
```ts
export const comoEmpezar = {
eyebrow: 'cómo empezar',
title: 'Tu siguiente movimiento empieza con una conversación.',
steps: [
{ n: '01', name: 'Escríbeme', desc: 'Cuéntame dónde está tu negocio hoy.' },
{ n: '02', name: 'Sesión de diagnóstico', desc: 'Una conversación para ubicar tu punto de inflexión.' },
{ n: '03', name: 'Estrategia ejecutable', desc: 'Sales con claridad y tu siguiente movimiento definido.' },
],
cta: 'Agenda tu sesión de diagnóstico',
whatsappText: 'Hola, quiero agendar una sesión de diagnóstico',
};
```
- [ ] **Step 2: Write the failing test `components/sections/__tests__/ComoEmpezar.test.tsx`**
```tsx
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { ComoEmpezar } from '../ComoEmpezar';
describe('<ComoEmpezar />', () => {
it('renders the on-ramp steps', () => {
render(<ComoEmpezar />);
expect(screen.getByText('Escríbeme')).toBeInTheDocument();
expect(screen.getByText('Sesión de diagnóstico')).toBeInTheDocument();
});
it('links the CTA to WhatsApp with a prefilled message', () => {
render(<ComoEmpezar />);
const cta = screen.getByRole('link', { name: /Agenda tu sesión de diagnóstico/i });
const href = cta.getAttribute('href') ?? '';
expect(href).toMatch(/^https:\/\/wa\.me\//);
expect(href).toContain('text=');
});
});
```
- [ ] **Step 3: Run the test to verify it fails**
Run: `npm test -- ComoEmpezar`
Expected: FAIL with "Cannot find module '../ComoEmpezar'".
- [ ] **Step 4: Write `components/sections/ComoEmpezar.tsx`**
```tsx
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { CTA } from '@/components/primitives/CTA';
import { comoEmpezar, cierre } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
export function ComoEmpezar() {
const reduced = usePrefersReducedMotion();
const waHref = `${cierre.contact.whatsapp.href}?text=${encodeURIComponent(comoEmpezar.whatsappText)}`;
return (
<section className="bg-base">
<Container className="py-24 md:py-32">
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="text-xs tracking-[0.3em] uppercase font-semibold text-gold">{comoEmpezar.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-4 font-serif italic font-bold text-3xl md:text-5xl text-ink">{comoEmpezar.title}</motion.h2>
<motion.ol variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-12 grid gap-10 md:grid-cols-3">
{comoEmpezar.steps.map((step) => (
<motion.li key={step.n} variants={fadeInUp} className="border-t border-gold/30 pt-6">
<div className="font-serif italic font-black text-5xl md:text-6xl text-gold leading-none">{step.n}</div>
<h3 className="mt-4 font-sans font-bold text-xl text-ink">{step.name}</h3>
<p className="mt-3 text-muted leading-relaxed">{step.desc}</p>
</motion.li>
))}
</motion.ol>
<motion.div variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="mt-12">
<CTA href={waHref} label={comoEmpezar.cta} variant="light" />
</motion.div>
</Container>
</section>
);
}
```
This mirrors `Mecanismo.tsx` exactly, but in the light-section palette (`bg-base`, accents in `gold` not `gold-bright`, `text-muted` for descriptions, `text-ink` for headings).
- [ ] **Step 5: Run the test to verify it passes**
Run: `npm test -- ComoEmpezar`
Expected: PASS, 2 tests.
- [ ] **Step 6: Wire `<ComoEmpezar />` into `app/page.tsx`**
Add the import with the other section imports:
```tsx
import { ComoEmpezar } from '@/components/sections/ComoEmpezar';
```
Insert `<ComoEmpezar />` between `<Estratega />` and `<Cierre />`:
```tsx
<Estratega />
<ComoEmpezar />
<Cierre />
```
- [ ] **Step 7: Typecheck and run the full suite**
Run: `npx tsc --noEmit && npm test`
Expected: tsc clean; all tests pass.
- [ ] **Step 8: Commit**
```bash
git add components/sections/ComoEmpezar.tsx components/sections/__tests__/ComoEmpezar.test.tsx lib/content.ts app/page.tsx
git commit -m "feat: Cómo empezar section (07) — 3-step on-ramp to WhatsApp"
```
---
### Task 3: Footer (slim strip)
**Files:**
- Create: `components/layout/Footer.tsx`
- Create: `components/layout/__tests__/Footer.test.tsx`
- Modify: `lib/content.ts` (append `footer`)
- Modify: `app/page.tsx` (place `<Footer />` after `</main>`)
**Interfaces:**
- Consumes: `Wordmark` (`size`, `onDark`), `Container`, and `footer.tagline`/`footer.legal` from content.
- Produces: `Footer()` at `components/layout/Footer.tsx` — no props. Consumed by `app/page.tsx`.
- [ ] **Step 1: Append the `footer` copy to `lib/content.ts`**
Add at the end of `lib/content.ts`:
```ts
export const footer = {
tagline: 'tu estratega de crecimiento',
legal: '© 2026 creeer',
};
```
- [ ] **Step 2: Write the failing test `components/layout/__tests__/Footer.test.tsx`**
```tsx
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Footer } from '../Footer';
describe('<Footer />', () => {
it('renders the tagline and the legal line', () => {
render(<Footer />);
expect(screen.getByText('tu estratega de crecimiento')).toBeInTheDocument();
expect(screen.getByText(/© 2026 creeer/)).toBeInTheDocument();
});
it('does not introduce a second h1', () => {
render(<Footer />);
expect(screen.queryByRole('heading', { level: 1 })).toBeNull();
});
});
```
- [ ] **Step 3: Run the test to verify it fails**
Run: `npm test -- Footer`
Expected: FAIL with "Cannot find module '../Footer'".
- [ ] **Step 4: Write `components/layout/Footer.tsx`**
```tsx
import { Wordmark } from '@/components/primitives/Wordmark';
import { Container } from '@/components/primitives/Container';
import { footer } from '@/lib/content';
export function Footer() {
return (
<footer className="bg-bg-dark text-cream border-t border-cream/10">
<Container className="flex flex-col items-center gap-3 py-10 text-center md:flex-row md:justify-between md:text-left">
<Wordmark size="sm" onDark />
<p className="text-sm text-muted-dark">{footer.tagline}</p>
<p className="text-xs text-muted-dark">{footer.legal}</p>
</Container>
</footer>
);
}
```
Slim dark strip (`bg-bg-dark text-cream`) with a subtle top divider. It deliberately does NOT repeat the contact links — `Cierre` already owns them, and a second dark block of contacts would be redundant.
- [ ] **Step 5: Run the test to verify it passes**
Run: `npm test -- Footer`
Expected: PASS, 2 tests.
- [ ] **Step 6: Wire `<Footer />` into `app/page.tsx`**
Add the import near the `Header` import:
```tsx
import { Footer } from '@/components/layout/Footer';
```
Insert `<Footer />` after the closing `</main>` and before the fragment close:
```tsx
</main>
<Footer />
</>
```
- [ ] **Step 7: Typecheck and run the full suite**
Run: `npx tsc --noEmit && npm test`
Expected: tsc clean; all tests pass.
- [ ] **Step 8: Commit**
```bash
git add components/layout/Footer.tsx components/layout/__tests__/Footer.test.tsx lib/content.ts app/page.tsx
git commit -m "feat: slim Footer strip with wordmark, tagline and legal line"
```
---
### Task 4: Sticky-header anchor offset + static build verification
**Files:**
- Modify: `components/sections/Cierre.tsx` (add `scroll-mt` to the `#contacto` anchor target)
**Interfaces:**
- Consumes: the composed page from Tasks 1–3.
- Produces: a verified static export; the `#contacto` anchor target that clears the sticky header.
- [ ] **Step 1: Locate the `#contacto` anchor in `components/sections/Cierre.tsx`**
Run: `grep -n 'id="contacto"' components/sections/Cierre.tsx`
Expected: one match — the element carrying `id="contacto"` (the closing section's outer element).
- [ ] **Step 2: Add `scroll-mt-24` to that element's className**
On the element that has `id="contacto"`, add `scroll-mt-24` to its `className` so the sticky header (~60px) never overlaps the contact anchor when the Header/Hero CTAs jump to it. Example — if the element reads:
```tsx
<section id="contacto" className="bg-bg-dark text-cream">
```
change it to:
```tsx
<section id="contacto" className="bg-bg-dark text-cream scroll-mt-24">
```
(Only add the one utility class; do not change anything else in `Cierre.tsx`.)
- [ ] **Step 3: Run the full suite and typecheck**
Run: `npx tsc --noEmit && npm test`
Expected: tsc clean; all tests pass (Cierre's existing tests still green — the class addition changes nothing they assert).
- [ ] **Step 4: Run lint**
Run: `npx next lint`
Expected: "No ESLint warnings or errors".
- [ ] **Step 5: Run the static build and verify the output**
Run: `npm run build`
Expected: build succeeds. Then verify the composed page:
(The exported HTML is minified onto few lines, so count occurrences with `grep -o … | wc -l`, not `grep -c` which counts lines.)
Run: `grep -o '<section' out/index.html | wc -l` → expect `8` (Hero, FraseAncla, ParaQuien, Mecanismo, Disciplinas, Estratega, ComoEmpezar, Cierre — one `<section>` each).
Run: `grep -o '<h1' out/index.html | wc -l` → expect `1` (single h1, still the Hero).
Run: `grep -o '<header' out/index.html | wc -l` → expect `1`.
Run: `grep -o '<footer' out/index.html | wc -l` → expect `1`.
Run: `grep -o 'wa.me' out/index.html | wc -l` → expect at least `2` (Cierre's WhatsApp link + ComoEmpezar's prefilled link).
- [ ] **Step 6: Commit**
```bash
git add components/sections/Cierre.tsx
git commit -m "fix: scroll-margin on #contacto so the sticky header never overlaps it"
```
---
## After this plan
- The header/footer/on-ramp are complete. Remaining follow-ups (own branches): testimonials/social proof, results/case studies, FAQ, real OG image + branded favicon, real portrait, and the dueña's real contact details (the `wa.me/520000000000` placeholder in `lib/content.ts` still applies to both Cierre and the new ComoEmpezar CTA).
- Merge `feat/header-comoempezar-footer` into `main` once reviewed.