# creeer Landing Page 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:** Build the one-page `creeer` landing site (7 sections) as a statically-exported Next.js 14 App Router site, matching the approved design spec exactly.
**Architecture:** A single `app/page.tsx` assembles seven independent section components (`components/sections/*.tsx`), each wrapped in a shared `MotionSection` scroll-reveal wrapper that respects `prefers-reduced-motion`. A shared `Wordmark` component renders the brand mark everywhere it appears. No backend, no client-side state beyond motion — content is static JSX.
**Tech Stack:** Next.js 14 (App Router) + TypeScript + Tailwind CSS 3 + Framer Motion 11, `output: export`. Tests: Vitest + React Testing Library + jsdom.
## Global Constraints
- Idioma: español, `lang="es"` en ``.
- Wordmark SIEMPRE minúsculas: `creeer` — nunca `CREEER` ni `Creeer`.
- Paleta exacta (no aproximar): `bg-base #FBFAF7`, `bg-pure #FFFFFF`, `bg-dark #0A1F44`, `text-primary #0A1F44`, `text-on-dark #F5EBD0`, `accent-gold #B8901f`, `accent-gold-bright #D4AF37`, `text-muted #5a5168`, `text-muted-dark #9aa6c2`. NO amarillo eléctrico tipo Hormozi.
- Sin backend, sin formulario — contacto solo por enlaces directos (WhatsApp/email/LinkedIn).
- `next.config.js` debe tener `output: 'export'` **y** `images.unoptimized: true` (la optimización de imágenes de Next no funciona en export estático).
- Container max-width: `1152px`.
- Voz: segunda persona ("tú"/"tu negocio"), sin jerga corporativa.
- Toda animación Framer Motion debe desactivarse con `useReducedMotion()`.
- Contraste WCAG 2.2 AA: `accent-gold` (#B8901f) solo para texto grande/decorativo, nunca body text sobre fondo claro.
- Un único `
` en toda la página (vive en el Hero).
- Datos de contacto reales (WhatsApp/email/LinkedIn), foto de la dueña y elección final HV-1 vs V-a están **pendientes de la dueña** (ver spec §10). Este plan usa placeholders explícitos marcados como tales — no bloquean el build.
---
### Task 1: Next.js scaffold, Tailwind tokens, fonts, root layout
**Files:**
- Create: `package.json`
- Create: `tsconfig.json`
- Create: `next.config.js`
- Create: `postcss.config.js`
- Create: `tailwind.config.ts`
- Create: `.eslintrc.json`
- Create: `vitest.config.ts`
- Create: `vitest.setup.ts`
- Create: `test/mocks/next-font-google.ts`
- Create: `app/layout.tsx`
- Create: `app/layout.test.tsx`
- Create: `app/globals.css`
- Create: `app/page.tsx` (placeholder, replaced in Task 11)
- Create: `components/MotionProvider.tsx`
**Interfaces:**
- Produces: Tailwind color tokens (`bg-base`, `bg-pure`, `bg-dark`, `text-primary`, `text-on-dark`, `accent-gold`, `accent-gold-bright`, `text-muted`, `text-muted-dark`), `font-serif` (Georgia), `font-sans` (Inter via `--font-inter`), `max-w-container` (1152px). All later tasks consume these.
- Produces: working `npm test` (Vitest) and `npm run build` (static export) pipelines.
- Produces: `MotionProvider({ children: ReactNode })` at `components/MotionProvider.tsx`, wrapping the app in `` per spec §8.3 (bundle-size). Consumed by `app/layout.tsx` here, and required context for Tasks 3 and 4, which must use framer-motion's `m.*` components (not `motion.*`) so the `strict` flag catches accidental full-bundle imports.
- [ ] **Step 1: Write `package.json`**
```json
{
"name": "creeer",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"framer-motion": "^11.3.19",
"next": "^14.2.5",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
"@types/node": "^20.14.14",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-config-next": "^14.2.5",
"jsdom": "^24.1.1",
"postcss": "^8.4.40",
"tailwindcss": "^3.4.7",
"typescript": "^5.5.4",
"vitest": "^2.0.5"
}
}
```
- [ ] **Step 2: Write `tsconfig.json`**
```json
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
```
- [ ] **Step 3: Write `next.config.js`**
```js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
images: {
unoptimized: true,
},
};
module.exports = nextConfig;
```
- [ ] **Step 4: Write `postcss.config.js`**
```js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
```
- [ ] **Step 5: Write `tailwind.config.ts`**
```ts
import type { Config } from 'tailwindcss';
const config: Config = {
content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
theme: {
extend: {
colors: {
'bg-base': '#FBFAF7',
'bg-pure': '#FFFFFF',
'bg-dark': '#0A1F44',
'text-primary': '#0A1F44',
'text-on-dark': '#F5EBD0',
'accent-gold': '#B8901f',
'accent-gold-bright': '#D4AF37',
'text-muted': '#5a5168',
'text-muted-dark': '#9aa6c2',
},
fontFamily: {
serif: ['Georgia', 'serif'],
sans: ['var(--font-inter)', 'system-ui', 'sans-serif'],
},
maxWidth: {
container: '1152px',
},
},
},
plugins: [],
};
export default config;
```
- [ ] **Step 6: Write `.eslintrc.json`**
```json
{
"extends": "next/core-web-vitals"
}
```
- [ ] **Step 7: Write `test/mocks/next-font-google.ts`**
Vitest doesn't run through Next's compiler, so `next/font/google` can't resolve its font manifest. Alias it to this mock so components importing it stay testable.
```ts
export function Inter() {
return {
className: 'font-inter-mock',
variable: '--font-inter',
};
}
```
- [ ] **Step 8: Write `vitest.config.ts`**
```ts
import path from 'path';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
globals: true,
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
'next/font/google': path.resolve(
__dirname,
'test/mocks/next-font-google.ts'
),
},
},
});
```
- [ ] **Step 9: Write `vitest.setup.ts`**
```ts
import '@testing-library/jest-dom/vitest';
class MockIntersectionObserver {
observe() {}
unobserve() {}
disconnect() {}
}
// @ts-expect-error jsdom doesn't implement IntersectionObserver
global.IntersectionObserver = MockIntersectionObserver;
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
}),
});
```
- [ ] **Step 10: Write `components/MotionProvider.tsx`**
Wraps the app in Framer Motion's `LazyMotion` so every animated component can import the lightweight `m.*` primitives instead of the full `motion.*` bundle (spec §8.3: "usar `LazyMotion` + `domAnimation` para reducir bundle"). `strict` makes framer-motion throw if a `motion.*` component is ever used underneath by mistake.
```tsx
'use client';
import type { ReactNode } from 'react';
import { LazyMotion, domAnimation } from 'framer-motion';
export function MotionProvider({ children }: { children: ReactNode }) {
return (
{children}
);
}
```
- [ ] **Step 11: Write `app/globals.css`**
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
color-scheme: light;
}
*:focus-visible {
outline: 3px solid #b8901f;
outline-offset: 2px;
}
```
- [ ] **Step 12: Write `app/layout.tsx`**
```tsx
import type { Metadata } from 'next';
import type { ReactNode } from 'react';
import { Inter } from 'next/font/google';
import { MotionProvider } from '@/components/MotionProvider';
import './globals.css';
const inter = Inter({
subsets: ['latin'],
weight: ['400', '600', '700', '900'],
variable: '--font-inter',
display: 'swap',
});
export const metadata: Metadata = {
title: 'creeer · tu estratega de crecimiento',
description:
'Claridad estratégica para empresarios con tracción que llegaron a un punto de inflexión. Define tu siguiente movimiento.',
};
export default function RootLayout({ children }: { children: ReactNode }) {
return (
{children}
);
}
```
- [ ] **Step 13: Write the failing test `app/layout.test.tsx`**
```tsx
import { describe, expect, it } from 'vitest';
import { render } from '@testing-library/react';
import RootLayout, { metadata } from './layout';
describe('RootLayout', () => {
it('sets the approved SEO title and description', () => {
expect(metadata.title).toBe('creeer · tu estratega de crecimiento');
expect(metadata.description).toContain('Claridad estratégica');
});
it('declares Spanish as the document language', () => {
const { container } = render(
contenido
);
expect(container.querySelector('html')).toHaveAttribute('lang', 'es');
});
});
```
- [ ] **Step 14: Write the placeholder `app/page.tsx`**
Temporary stub — replaced entirely in Task 11 once all sections exist.
```tsx
export default function Home() {
return (
creeer
);
}
```
- [ ] **Step 15: Install dependencies**
Run: `npm install`
Expected: installs without errors, creates `node_modules/` and `package-lock.json`.
- [ ] **Step 16: Run the test to verify it passes**
Run: `npm test -- app/layout.test.tsx`
Expected: PASS, 2 tests passing.
- [ ] **Step 17: Verify the static export build works**
Run: `npm run build`
Expected: build succeeds, output includes a line for route `/`, and `out/index.html` exists afterward.
- [ ] **Step 18: Commit**
```bash
git add package.json package-lock.json tsconfig.json next.config.js postcss.config.js tailwind.config.ts .eslintrc.json vitest.config.ts vitest.setup.ts test/ app/ components/MotionProvider.tsx
git commit -m "chore: scaffold Next.js app with Tailwind tokens, fonts, and test runner"
```
---
### Task 2: Wordmark component
**Files:**
- Create: `components/Wordmark.tsx`
- Create: `components/Wordmark.test.tsx`
**Interfaces:**
- Consumes: Tailwind tokens from Task 1 (`text-primary`, `text-on-dark`, `accent-gold`, `font-serif`).
- Produces: `Wordmark({ size?: 'base' | 'lg', onDark?: boolean })` at `components/Wordmark.tsx`. Consumed by Task 4 (Hero).
- [ ] **Step 1: Write the failing tests**
```tsx
import { describe, expect, it } from 'vitest';
import { render } from '@testing-library/react';
import { Wordmark } from './Wordmark';
describe('Wordmark', () => {
it('renders the middle "e" in gold with heavier weight than the rest', () => {
const { container } = render();
const goldE = container.querySelector('.font-black.text-accent-gold');
expect(goldE).not.toBeNull();
expect(goldE?.textContent).toBe('e');
});
it('renders the trailing period in gold', () => {
const { container } = render();
const spans = container.querySelectorAll('span.text-accent-gold');
const period = Array.from(spans).find((el) => el.textContent === '.');
expect(period).not.toBeUndefined();
});
it('never renders the wordmark in uppercase', () => {
const { container } = render();
expect(container.textContent?.toLowerCase()).toBe(container.textContent);
});
it('switches to the on-dark ink color when onDark is true', () => {
const { container } = render();
expect(container.firstElementChild?.className).toContain(
'text-text-on-dark'
);
});
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `npm test -- components/Wordmark.test.tsx`
Expected: FAIL with "Cannot find module './Wordmark'".
- [ ] **Step 3: Write `components/Wordmark.tsx`**
```tsx
export function Wordmark({
size = 'base',
onDark = false,
}: {
size?: 'base' | 'lg';
onDark?: boolean;
}) {
const inkColor = onDark ? 'text-text-on-dark' : 'text-text-primary';
const sizeClass = size === 'lg' ? 'text-4xl md:text-5xl' : 'text-2xl';
return (
cre
e
er
.
);
}
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `npm test -- components/Wordmark.test.tsx`
Expected: PASS, 4 tests passing.
- [ ] **Step 5: Commit**
```bash
git add components/Wordmark.tsx components/Wordmark.test.tsx
git commit -m "feat: add Wordmark component with gold middle-e brand rule"
```
---
### Task 3: MotionSection scroll-reveal wrapper
**Files:**
- Create: `components/MotionSection.tsx`
- Create: `components/MotionSection.test.tsx`
**Interfaces:**
- Consumes: `framer-motion`'s `m` and `useReducedMotion` (the `m.*` lightweight primitives, not `motion.*` — see `MotionProvider` in Task 1, which enforces this via `strict`).
- Produces: `MotionSection({ children: ReactNode, className?: string })` at `components/MotionSection.tsx`, rendering a ``. Consumed by Tasks 5–10 (all sections except Hero, which has its own staggered entrance).
- [ ] **Step 1: Write the failing tests**
```tsx
import { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render } from '@testing-library/react';
import { LazyMotion, domAnimation } from 'framer-motion';
import { MotionSection } from './MotionSection';
function mockReducedMotion(matches: boolean) {
vi.spyOn(window, 'matchMedia').mockImplementation((query: string) => ({
matches,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
}));
}
function renderWithMotion(ui: React.ReactElement) {
return render(
{ui}
);
}
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
describe('MotionSection', () => {
it('renders a plain section with no inline animation styles when reduced motion is preferred', () => {
mockReducedMotion(true);
const { container } = renderWithMotion(
contenido
);
const section = container.querySelector('section');
expect(section).not.toBeNull();
expect(section?.getAttribute('style')).toBeNull();
});
it('applies an initial opacity of 0 before entering the viewport when motion is allowed', () => {
mockReducedMotion(false);
const { container } = renderWithMotion(
contenido
);
expect(container.querySelector('section')?.getAttribute('style')).toContain(
'opacity: 0'
);
});
it('always renders its children regardless of motion preference', () => {
mockReducedMotion(true);
const { getByText } = renderWithMotion(
contenido visible
);
expect(getByText('contenido visible')).toBeInTheDocument();
});
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `npm test -- components/MotionSection.test.tsx`
Expected: FAIL with "Cannot find module './MotionSection'".
- [ ] **Step 3: Write `components/MotionSection.tsx`**
```tsx
'use client';
import type { ReactNode } from 'react';
import { m, useReducedMotion } from 'framer-motion';
const EASE_OUT_EXPO: [number, number, number, number] = [0.22, 1, 0.36, 1];
export function MotionSection({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
const shouldReduceMotion = useReducedMotion();
if (shouldReduceMotion) {
return ;
}
return (
{children}
);
}
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `npm test -- components/MotionSection.test.tsx`
Expected: PASS, 3 tests passing.
Note: this component renders `m.section` without a `LazyMotion` ancestor in production only through `app/layout.tsx`'s `MotionProvider` (Task 1) — outside of a real render tree, `m` components need that provider to have their animation features loaded, which is why the test above wraps every render in its own ``.
- [ ] **Step 5: Commit**
```bash
git add components/MotionSection.tsx components/MotionSection.test.tsx
git commit -m "feat: add MotionSection scroll-reveal wrapper with reduced-motion support"
```
---
### Task 4: Hero section (01)
**Files:**
- Create: `components/sections/Hero.tsx`
- Create: `components/sections/Hero.test.tsx`
**Interfaces:**
- Consumes: `Wordmark` (Task 2).
- Produces: `Hero({ variant?: 'hormozi' | 'editorial' })` at `components/sections/Hero.tsx`, default `'editorial'`. Renders the page's single `` and the CTA linking to `#contacto`. Consumed by Task 11.
**Note on the default:** the spec leaves HV-1 vs V-a as the dueña's decision (§4, §10). `variant` defaults to `'editorial'` (V-a) because it's the closer match to the master tone "seguridad + cercanía" — swap the default or the call site in Task 11 the moment she decides.
- [ ] **Step 1: Write the failing tests**
```tsx
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Hero } from './Hero';
describe('Hero', () => {
it('renders the editorial variant in Georgia italic by default', () => {
render();
const headline = screen.getByRole('heading', { level: 1 });
expect(headline.className).toContain('font-serif');
expect(headline.className).toContain('italic');
});
it('renders the hormozi variant in uppercase sans when requested', () => {
render();
const headline = screen.getByRole('heading', { level: 1 });
expect(headline.className).toContain('uppercase');
expect(headline.className).not.toContain('font-serif');
});
it('renders exactly one h1 with the approved headline copy', () => {
render();
const headings = screen.getAllByRole('heading', { level: 1 });
expect(headings).toHaveLength(1);
expect(headings[0].textContent).toContain('más esfuerzo');
expect(headings[0].textContent).toContain('más claridad');
});
it('renders the CTA linking to the contact section', () => {
render();
const cta = screen.getByRole('link', {
name: /Define tu siguiente movimiento/,
});
expect(cta).toHaveAttribute('href', '#contacto');
});
it('never shows scarcity language in the hero', () => {
render();
const text = screen.getByRole('heading', { level: 1 }).textContent ?? '';
expect(text).not.toMatch(/cupo|cohorte|generaci[oó]n fundadora/i);
});
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `npm test -- components/sections/Hero.test.tsx`
Expected: FAIL with "Cannot find module './Hero'".
- [ ] **Step 3: Write `components/sections/Hero.tsx`**
```tsx
'use client';
import { m, useReducedMotion } from 'framer-motion';
import { Wordmark } from '@/components/Wordmark';
const EASE_OUT_EXPO: [number, number, number, number] = [0.22, 1, 0.36, 1];
const STAGGER_DELAYS = [0, 0.1, 0.2, 0.3];
type HeroVariant = 'hormozi' | 'editorial';
function HeroHeadline({ variant }: { variant: HeroVariant }) {
if (variant === 'hormozi') {
return (
Tu negocio no necesita más esfuerzo.
Necesita{' '}
más claridad
.
);
}
return (
Tu negocio no necesita más esfuerzo.
Necesita{' '}
más claridad
.
);
}
export function Hero({ variant = 'editorial' }: { variant?: HeroVariant }) {
const shouldReduceMotion = useReducedMotion();
const item = (delay: number) =>
shouldReduceMotion
? {}
: {
initial: { opacity: 0, y: 16 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.5, delay, ease: EASE_OUT_EXPO },
};
return (
— tu estratega de crecimiento —
Define tu siguiente movimiento →
);
}
```
Note: Hero's tests only assert `className`/text/`href`, none of which depend on the animation feature bundle being loaded, so `Hero.test.tsx` renders the component directly with no `LazyMotion` wrapper needed (unlike `MotionSection.test.tsx`).
- [ ] **Step 4: Run tests to verify they pass**
Run: `npm test -- components/sections/Hero.test.tsx`
Expected: PASS, 5 tests passing.
- [ ] **Step 5: Commit**
```bash
git add components/sections/Hero.tsx components/sections/Hero.test.tsx
git commit -m "feat: add Hero section with hormozi/editorial variant switch"
```
---
### Task 5: AnchorQuote section (02)
**Files:**
- Create: `components/sections/AnchorQuote.tsx`
- Create: `components/sections/AnchorQuote.test.tsx`
**Interfaces:**
- Consumes: `MotionSection` (Task 3).
- Produces: `AnchorQuote()` at `components/sections/AnchorQuote.tsx`. Consumed by Task 11.
- [ ] **Step 1: Write the failing tests**
```tsx
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import { AnchorQuote } from './AnchorQuote';
describe('AnchorQuote', () => {
it('renders the approved anchor quote verbatim', () => {
render();
expect(
screen.getByText(/no dejan de crecer por falta de claridad/)
).toBeInTheDocument();
});
it('does not introduce a second h1 on the page', () => {
render();
expect(screen.queryAllByRole('heading', { level: 1 })).toHaveLength(0);
});
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `npm test -- components/sections/AnchorQuote.test.tsx`
Expected: FAIL with "Cannot find module './AnchorQuote'".
- [ ] **Step 3: Write `components/sections/AnchorQuote.tsx`**
```tsx
import { MotionSection } from '@/components/MotionSection';
export function AnchorQuote() {
return (
“Las empresas no dejan de crecer por falta de talento; dejan de
crecer por falta de claridad.”
);
}
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `npm test -- components/sections/AnchorQuote.test.tsx`
Expected: PASS, 2 tests passing.
- [ ] **Step 5: Commit**
```bash
git add components/sections/AnchorQuote.tsx components/sections/AnchorQuote.test.tsx
git commit -m "feat: add AnchorQuote section"
```
---
### Task 6: ForWhom section (03)
**Files:**
- Create: `components/sections/ForWhom.tsx`
- Create: `components/sections/ForWhom.test.tsx`
**Interfaces:**
- Consumes: `MotionSection` (Task 3).
- Produces: `ForWhom()` at `components/sections/ForWhom.tsx`. Consumed by Task 11.
- [ ] **Step 1: Write the failing tests**
```tsx
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import { ForWhom } from './ForWhom';
describe('ForWhom', () => {
it('renders exactly the three approved qualifying chips in order', () => {
render();
const items = screen.getAllByRole('listitem');
expect(items.map((item) => item.textContent)).toEqual([
'✓ tienes tracción',
'✓ más esfuerzo no rinde',
'✓ necesitas claridad',
]);
});
it('uses an h2, never a second h1', () => {
render();
expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument();
expect(screen.queryAllByRole('heading', { level: 1 })).toHaveLength(0);
});
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `npm test -- components/sections/ForWhom.test.tsx`
Expected: FAIL with "Cannot find module './ForWhom'".
- [ ] **Step 3: Write `components/sections/ForWhom.tsx`**
```tsx
import { MotionSection } from '@/components/MotionSection';
const CHIPS = ['tienes tracción', 'más esfuerzo no rinde', 'necesitas claridad'];
export function ForWhom() {
return (
¿Llegaste a tu punto de inflexión?
Esto es para ti si...
{CHIPS.map((chip) => (
-
✓ {chip}
))}
);
}
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `npm test -- components/sections/ForWhom.test.tsx`
Expected: PASS, 2 tests passing.
- [ ] **Step 5: Commit**
```bash
git add components/sections/ForWhom.tsx components/sections/ForWhom.test.tsx
git commit -m "feat: add ForWhom qualifying section"
```
---
### Task 7: HowIWork section (04, dark)
**Files:**
- Create: `components/sections/HowIWork.tsx`
- Create: `components/sections/HowIWork.test.tsx`
**Interfaces:**
- Consumes: `MotionSection` (Task 3).
- Produces: `HowIWork()` at `components/sections/HowIWork.tsx`. Consumed by Task 11.
- [ ] **Step 1: Write the failing tests**
```tsx
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import { HowIWork } from './HowIWork';
describe('HowIWork', () => {
it('renders the three mechanism steps in order, numbered, with no icons', () => {
render();
const items = screen.getAllByRole('listitem');
expect(items.map((item) => item.textContent)).toEqual([
'01Inmersión',
'02Desafiar supuestos',
'03Estrategia ejecutable',
]);
expect(screen.queryAllByRole('img')).toHaveLength(0);
});
it('uses the dark section background', () => {
const { container } = render();
expect(container.querySelector('section')?.className).toContain(
'bg-bg-dark'
);
});
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `npm test -- components/sections/HowIWork.test.tsx`
Expected: FAIL with "Cannot find module './HowIWork'".
- [ ] **Step 3: Write `components/sections/HowIWork.tsx`**
```tsx
import { MotionSection } from '@/components/MotionSection';
const STEPS = [
{ number: '01', title: 'Inmersión' },
{ number: '02', title: 'Desafiar supuestos' },
{ number: '03', title: 'Estrategia ejecutable' },
];
export function HowIWork() {
return (
Cómo trabajo
{STEPS.map((step) => (
-
{step.number}
{step.title}
))}
);
}
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `npm test -- components/sections/HowIWork.test.tsx`
Expected: PASS, 2 tests passing.
- [ ] **Step 5: Commit**
```bash
git add components/sections/HowIWork.tsx components/sections/HowIWork.test.tsx
git commit -m "feat: add HowIWork dark mechanism section"
```
---
### Task 8: Disciplines section (05)
**Files:**
- Create: `components/sections/Disciplines.tsx`
- Create: `components/sections/Disciplines.test.tsx`
**Interfaces:**
- Consumes: `MotionSection` (Task 3).
- Produces: `Disciplines()` at `components/sections/Disciplines.tsx`. Consumed by Task 11.
- [ ] **Step 1: Write the failing tests**
```tsx
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Disciplines } from './Disciplines';
describe('Disciplines', () => {
it('renders the three disciplines as tools, not as the core promise', () => {
render();
expect(screen.getByText('Comunicación')).toBeInTheDocument();
expect(screen.getByText('Digital')).toBeInTheDocument();
expect(screen.getByText('Experiencias')).toBeInTheDocument();
});
it('gives each card a gold top border', () => {
const { container } = render();
expect(container.querySelectorAll('.border-t-4')).toHaveLength(3);
});
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `npm test -- components/sections/Disciplines.test.tsx`
Expected: FAIL with "Cannot find module './Disciplines'".
- [ ] **Step 3: Write `components/sections/Disciplines.tsx`**
```tsx
import { MotionSection } from '@/components/MotionSection';
const DISCIPLINES = ['Comunicación', 'Digital', 'Experiencias'];
export function Disciplines() {
return (
Las herramientas
{DISCIPLINES.map((discipline) => (
))}
);
}
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `npm test -- components/sections/Disciplines.test.tsx`
Expected: PASS, 2 tests passing.
- [ ] **Step 5: Commit**
```bash
git add components/sections/Disciplines.tsx components/sections/Disciplines.test.tsx
git commit -m "feat: add Disciplines section"
```
---
### Task 9: About section (06)
**Files:**
- Create: `components/sections/About.tsx`
- Create: `components/sections/About.test.tsx`
**Interfaces:**
- Consumes: `MotionSection` (Task 3).
- Produces: `About()` at `components/sections/About.tsx`. Consumed by Task 11.
**Note:** no professional photo exists yet (spec §5, §10 — pending). Renders a navy/gold gradient placeholder labeled for screen readers; swap the `div` for a Next `` the moment a photo is provided (`images.unoptimized: true` from Task 1 already makes this drop-in safe under static export).
- [ ] **Step 1: Write the failing tests**
```tsx
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import { About } from './About';
describe('About', () => {
it('labels the portrait placeholder for screen readers until a real photo is provided', () => {
render();
expect(
screen.getByRole('img', { name: /Retrato de la estratega/ })
).toBeInTheDocument();
});
it('renders the first-person bio and the institutional quote', () => {
render();
expect(screen.getByText(/Hoy ayudo a empresarios/)).toBeInTheDocument();
expect(
screen.getByText(/El tiempo no se compra, se invierte/)
).toBeInTheDocument();
});
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `npm test -- components/sections/About.test.tsx`
Expected: FAIL with "Cannot find module './About'".
- [ ] **Step 3: Write `components/sections/About.tsx`**
```tsx
import { MotionSection } from '@/components/MotionSection';
export function About() {
return (
La mente detrás de creeer
+30 años construyendo marcas. Hoy ayudo a empresarios con
tracción a encontrar la claridad estratégica que los lleva a su
siguiente movimiento.
“El tiempo no se compra, se invierte.”
);
}
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `npm test -- components/sections/About.test.tsx`
Expected: PASS, 2 tests passing.
- [ ] **Step 5: Commit**
```bash
git add components/sections/About.tsx components/sections/About.test.tsx
git commit -m "feat: add About section with portrait placeholder"
```
---
### Task 10: Closing section (07, dark) + contact links
**Files:**
- Create: `components/sections/Closing.tsx`
- Create: `components/sections/Closing.test.tsx`
**Interfaces:**
- Consumes: `MotionSection` (Task 3).
- Produces: `Closing()` at `components/sections/Closing.tsx`, rendering the `id="contacto"` anchor target that Hero's CTA (Task 4) scrolls to. Consumed by Task 11.
**Note:** WhatsApp number, email, and LinkedIn URL are placeholders pending the dueña's real contact details (spec §10). Replace the three values in `CONTACT_LINKS` the moment she provides them — no other code changes needed.
- [ ] **Step 1: Write the failing tests**
```tsx
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Closing } from './Closing';
describe('Closing', () => {
it('exposes the three contact channels with accessible labels', () => {
render();
expect(
screen.getByRole('link', { name: 'Escribir por WhatsApp' })
).toHaveAttribute('href', 'https://wa.me/521234567890');
expect(
screen.getByRole('link', { name: 'Escribir por correo electrónico' })
).toHaveAttribute('href', 'mailto:hola@creeer.com');
expect(
screen.getByRole('link', { name: 'Ver perfil de LinkedIn' })
).toHaveAttribute('href', 'https://www.linkedin.com/in/creeer');
});
it('points the closing CTA at the primary contact channel', () => {
render();
const cta = screen.getByRole('link', {
name: 'Define tu siguiente movimiento →',
});
expect(cta).toHaveAttribute('href', 'https://wa.me/521234567890');
});
it('provides the #contacto anchor target for the hero CTA to scroll to', () => {
const { container } = render();
expect(container.querySelector('#contacto')).not.toBeNull();
});
it('uses the dark section background', () => {
const { container } = render();
expect(container.querySelector('section')?.className).toContain(
'bg-bg-dark'
);
});
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `npm test -- components/sections/Closing.test.tsx`
Expected: FAIL with "Cannot find module './Closing'".
- [ ] **Step 3: Write `components/sections/Closing.tsx`**
```tsx
import { MotionSection } from '@/components/MotionSection';
const CONTACT_LINKS = [
{
label: 'WhatsApp',
href: 'https://wa.me/521234567890',
ariaLabel: 'Escribir por WhatsApp',
},
{
label: 'Email',
href: 'mailto:hola@creeer.com',
ariaLabel: 'Escribir por correo electrónico',
},
{
label: 'LinkedIn',
href: 'https://www.linkedin.com/in/creeer',
ariaLabel: 'Ver perfil de LinkedIn',
},
];
export function Closing() {
return (
);
}
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `npm test -- components/sections/Closing.test.tsx`
Expected: PASS, 4 tests passing.
- [ ] **Step 5: Commit**
```bash
git add components/sections/Closing.tsx components/sections/Closing.test.tsx
git commit -m "feat: add Closing section with contact links"
```
---
### Task 11: Assemble the page, structured data, and verify the build
**Files:**
- Modify: `app/page.tsx` (replace the Task 1 placeholder)
- Create: `app/page.test.tsx`
**Interfaces:**
- Consumes: `Hero` (Task 4), `AnchorQuote` (Task 5), `ForWhom` (Task 6), `HowIWork` (Task 7), `Disciplines` (Task 8), `About` (Task 9), `Closing` (Task 10).
- [ ] **Step 1: Write the failing tests**
```tsx
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import Home from './page';
describe('Home page', () => {
it('renders exactly one h1 across the whole page', () => {
render();
expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1);
});
it('renders the seven sections in spec order', () => {
const { container } = render();
expect(container.querySelectorAll('section')).toHaveLength(7);
});
it('repeats the primary CTA copy at the top and the bottom of the page', () => {
render();
expect(
screen.getAllByText('Define tu siguiente movimiento →')
).toHaveLength(2);
});
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `npm test -- app/page.test.tsx`
Expected: FAIL — the placeholder page only renders one `` and zero `` elements, so the section-count and CTA-repetition assertions fail.
- [ ] **Step 3: Replace `app/page.tsx`**
```tsx
import { Hero } from '@/components/sections/Hero';
import { AnchorQuote } from '@/components/sections/AnchorQuote';
import { ForWhom } from '@/components/sections/ForWhom';
import { HowIWork } from '@/components/sections/HowIWork';
import { Disciplines } from '@/components/sections/Disciplines';
import { About } from '@/components/sections/About';
import { Closing } from '@/components/sections/Closing';
const STRUCTURED_DATA = {
'@context': 'https://schema.org',
'@type': 'ProfessionalService',
name: 'creeer',
description:
'Claridad estratégica para empresarios con tracción que llegaron a un punto de inflexión.',
url: 'https://creeer.com',
};
export default function Home() {
return (
);
}
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `npm test`
Expected: PASS, all test files green (Task 1 through Task 11).
- [ ] **Step 5: Verify lint passes**
Run: `npm run lint`
Expected: "No ESLint warnings or errors".
- [ ] **Step 6: Verify the static export build**
Run: `npm run build`
Expected: build succeeds; `out/index.html` exists and contains the wordmark text `creeer`, the seven `` block.
- [ ] **Step 7: Manual visual check**
Run: `npm run dev`, open `http://localhost:3000`, and confirm against the spec:
- Scroll through all 7 sections in order; sections 04 and 07 are navy with gold accents, the rest are light.
- The hero entrance animates in (wordmark → eyebrow → headline → CTA).
- Toggle `variant="hormozi"` in `app/page.tsx` locally to eyeball the other hero option for the dueña's later review, then revert to `"editorial"` before committing.
- In OS/browser settings enable "reduce motion" and reload — all animations should disappear with content still visible.
Stop the dev server (Ctrl+C) when done.
- [ ] **Step 8: Commit**
```bash
git add app/page.tsx app/page.test.tsx
git commit -m "feat: assemble the seven-section landing page with structured data"
```
---
## After this plan
- Hand `hero-finalists.md` back to the dueña for the HV-1 vs V-a decision — flipping it is a one-line `variant` change in `app/page.tsx`.
- Once she provides a photo, replace the placeholder `div` in `About.tsx` with `next/image`.
- Once she provides real contact info, update the three entries in `Closing.tsx`'s `CONTACT_LINKS`.
- Consider adding an Open Graph image once final copy/photo are set (spec §8.1).