48 KiB
creeer Landing 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: Implement the creeer landing page (one-page, Next.js 14 static export) from the approved spec at docs/superpowers/specs/2026-07-20-creeer-landing-design.md.
Architecture: Next.js 14 App Router with 7 section components composed in app/page.tsx. Reusable primitives (Wordmark, Container, CTA) shared across sections. All copy centralized in lib/content.ts. Framer Motion animations with prefers-reduced-motion support. Static export (output: export) — no backend, contact via direct links.
Tech Stack: Next.js 14, React 18, TypeScript 5, Tailwind CSS 3.4, Framer Motion 11, Inter (Google Font via next/font), Georgia (system serif). Tests: Vitest + @testing-library/react.
Global Constraints
(Copied verbatim from the spec — apply to every task.)
- Wordmark:
creeer.SIEMPRE minúsculas, Georgia 700 italic, la 2ª "e" en peso 900 + dorado, punto final dorado. NuncaCREEER,Creeer, sans-serif, ni mayúsculas. - Paleta: navy
#0A1F44(texto/CTA) · dorado tostado#B8901f(acentos sobre claro) · dorado#D4AF37(acentos sobre oscuro) · blanco cálido#FBFAF7(fondo base) · blanco#FFFFFF(hero) · marfil#F5EBD0(texto sobre oscuro). - Tipografía híbrida: Georgia italic = voz de marca (wordmark + citas); Inter (400/600/900) = voz funcional (headlines Hormozi + body + UI).
- Sin escasez en el hero: no mostrar cupos/cohorte/"Generación Fundadora" arriba.
- Voz: segunda persona ("tú"/"tu negocio"), sin jerga corporativa.
- Sin backend, sin formularios — contacto por enlaces directos (WhatsApp, email, LinkedIn).
output: exportennext.config.mjs— HTML estático.- Idioma: español (
<html lang="es">).
File Structure
creeer/
├── package.json
├── next.config.mjs # output: export
├── tsconfig.json
├── tailwind.config.ts # design tokens (palette, fontFamily)
├── postcss.config.js
├── vitest.config.ts
├── app/
│ ├── layout.tsx # <html lang="es">, fonts, metadata
│ ├── page.tsx # composes 7 sections
│ └── globals.css # @tailwind + base
├── components/
│ ├── primitives/
│ │ ├── Wordmark.tsx # creeer logo (reused)
│ │ ├── Container.tsx # max-w wrapper (reused)
│ │ └── CTA.tsx # button + link variant (reused)
│ └── sections/
│ ├── Hero.tsx # 01 — variant="hormozi" | "editorial"
│ ├── FraseAncla.tsx # 02
│ ├── ParaQuien.tsx # 03
│ ├── Mecanismo.tsx # 04 (dark)
│ ├── Disciplinas.tsx # 05
│ ├── Estratega.tsx # 06
│ └── Cierre.tsx # 07 (dark) + contact links
├── lib/
│ ├── content.ts # all copy constants (source of truth)
│ └── motion.ts # Framer Motion variants + useReducedMotion wrapper
└── public/
└── og-image.png # Open Graph image (added manually)
Each section has one responsibility: render its slice of content using the design system. Primitives are consumed by multiple sections. lib/content.ts centralizes copy so text changes don't require touching JSX.
Task 1: Scaffold + dependencies + base config
Files:
- Create:
package.json,next.config.mjs,tsconfig.json,tailwind.config.ts,postcss.config.js,app/layout.tsx,app/page.tsx,app/globals.css
Interfaces:
-
Produces: a runnable Next.js 14 app at
npm run devshowing "hello world", with Tailwind + Framer Motion + Vitest installed,output: exportenabled. -
Step 1: Initialize Next.js project in the current directory
cd /home/erikgh/dev/Creeer
npx create-next-app@14 . --typescript --tailwind --app --no-src-dir --no-eslint --use-npm --import-alias "@/*"
If prompted about existing files, choose to overwrite/ignore conflicts for .gitignore (keep existing). This scaffold creates package.json, app/, configs.
- Step 2: Install runtime dependencies
npm install framer-motion@^11
- Step 3: Install dev dependencies for testing
npm install -D vitest@^1 @vitejs/plugin-react@^4 @testing-library/react@^15 @testing-library/jest-dom@^6 jsdom@^25
- Step 4: Configure static export in
next.config.mjs
Overwrite next.config.mjs:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
images: { unoptimized: true },
};
export default nextConfig;
(images.unoptimized is required for static export.)
- Step 5: Verify dev server runs
npm run dev
Expected: server starts at http://localhost:3000, default Next.js page renders. Stop with Ctrl+C.
- Step 6: Initialize git repo (first commit)
cd /home/erikgh/dev/Creeer
git init
git add .
git commit -m "chore: scaffold Next.js 14 + Tailwind + Framer Motion + Vitest"
Task 2: Design tokens (Tailwind palette, fonts, content + motion libs)
Files:
- Modify:
tailwind.config.ts,app/globals.css,app/layout.tsx - Create:
lib/content.ts,lib/motion.ts
Interfaces:
-
Produces: Tailwind tokens (
bg-base,bg-dark,text-primary,accent-gold, etc.), Inter font loaded vianext/font, all copy inlib/content.ts, motion variants inlib/motion.ts. -
Step 1: Write Tailwind tokens in
tailwind.config.ts
import type { Config } from 'tailwindcss';
const config: Config = {
content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './lib/**/*.{ts,tsx}'],
theme: {
extend: {
colors: {
base: '#FBFAF7', // blanco cálido (fondo base)
'bg-pure': '#FFFFFF', // blanco puro (hero)
'bg-dark': '#0A1F44', // navy (secciones oscuras)
ink: '#0A1F44', // texto principal
cream: '#F5EBD0', // texto sobre oscuro (marfil)
gold: {
DEFAULT: '#B8901f', // dorado tostado (sobre claro)
bright: '#D4AF37', // dorado (sobre oscuro)
},
muted: '#5a5168',
'muted-dark': '#9aa6c2',
},
fontFamily: {
sans: ['var(--font-inter)', 'system-ui', 'sans-serif'],
serif: ['Georgia', 'Cambria', 'Times New Roman', 'serif'],
},
maxWidth: {
content: '1152px',
},
},
},
plugins: [],
};
export default config;
- Step 2: Replace
app/globals.csswith base styles
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html { scroll-behavior: smooth; }
body {
background-color: #FBFAF7;
color: #0A1F44;
font-family: var(--font-inter), system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
}
::selection { background: #D4AF37; color: #0A1F44; }
}
- Step 3: Load Inter font in
app/layout.tsx
Replace app/layout.tsx:
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
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 en punto de inflexión.',
// (SEO task expands this later)
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es" className={inter.variable}>
<body>{children}</body>
</html>
);
}
- Step 4: Create
lib/content.tswith all copy
// Source of truth for all copy. Edit here, not in JSX.
export const wordmark = {
// cre[e]er. — 'cre' + gold 'e' + 'er' + gold '.'
parts: { before: 'cre', accent: 'e', after: 'er', dot: '.' },
};
export const hero = {
eyebrow: '— tu estratega de crecimiento —',
// Headline broken into segments for emphasis styling
headline: {
line1: 'Tu negocio no necesita',
effort: 'más esfuerzo.', // gold (HV-1) or gold italic (V-a)
line3: 'Necesita',
clarity: 'más claridad.', // underlined gold
},
cta: 'Define tu siguiente movimiento',
};
export const fraseAncla = {
quote: 'Las empresas no dejan de crecer por falta de talento; dejan de crecer por falta de claridad.',
};
export const paraQuien = {
title: '¿Llegaste a tu punto de inflexión?',
lead: 'Esto es para ti si…',
criteria: [
'tienes tracción y ya demostraste que sabes crecer',
'trabajar más ya no rinde como antes',
'necesitas claridad para decidir el siguiente movimiento',
],
};
export const mecanismo = {
eyebrow: 'el mecanismo único',
title: 'Cómo trabajo',
steps: [
{ n: '01', name: 'Inmersión', desc: 'Me sumerjo en tu empresa hasta comprenderla desde dentro.' },
{ n: '02', name: 'Desafiar supuestos', desc: 'Cuestiono lo que todos dan por hecho y frenan el crecimiento.' },
{ n: '03', name: 'Estrategia ejecutable', desc: 'Construimos, juntos, una estrategia que puedas ejecutar.' },
],
};
export const disciplinas = {
eyebrow: 'las herramientas',
title: 'Las disciplinas que uso',
lead: 'No son la promesa — son los medios.',
items: [
{ name: 'Comunicación', desc: 'Mensaje que alinea a tu equipo y a tu mercado.' },
{ name: 'Digital', desc: 'Presencia y procesos que escalan sin ti.' },
{ name: 'Experiencias', desc: 'Cada toque con tu marca construye confianza.' },
],
};
export const estratega = {
eyebrow: 'la mente detrás de creeer',
title: 'Sobre la estratega',
bio: 'Más de 30 años construyendo marcas. Hoy acompaño a empresarios que ya lograron tracción y necesitan claridad para su siguiente movimiento.',
quote: 'El tiempo no se compra, se invierte.',
};
export const cierre = {
quote: 'Devuélvele claridad al rumbo de tu empresa.',
cta: 'Define tu siguiente movimiento',
contact: {
whatsapp: { label: 'WhatsApp', href: 'https://wa.me/520000000000' }, // TODO dueña: real number
email: { label: 'Email', href: 'mailto:hola@creeer.com' }, // TODO dueña: real email
linkedin: { label: 'LinkedIn', href: 'https://linkedin.com/in/...' }, // TODO dueña: real URL
},
};
- Step 5: Create
lib/motion.ts
import type { Variants } from 'framer-motion';
// EaseOutExpo — smooth editorial deceleration
export const easeOutExpo = [0.22, 1, 0.36, 1] as const;
export const fadeInUp: Variants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: easeOutExpo } },
};
export const fadeIn: Variants = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { duration: 0.4, ease: easeOutExpo } },
};
export const staggerContainer: Variants = {
hidden: {},
visible: { transition: { staggerChildren: 0.1 } },
};
export const viewportOnce = { once: true, amount: 0.3 } as const;
- Step 6: Smoke test —
npm run dev
Run: npm run dev
Expected: app boots, no TS errors, default page renders at localhost:3000.
- Step 7: Commit
git add -A
git commit -m "feat: design tokens (palette, Inter font, content + motion libs)"
Task 3: Primitives — Wordmark, Container, CTA
Files:
- Create:
components/primitives/Wordmark.tsx,components/primitives/Container.tsx,components/primitives/CTA.tsx,vitest.config.ts - Create tests:
components/primitives/__tests__/Wordmark.test.tsx,components/primitives/__tests__/CTA.test.tsx
Interfaces:
-
Produces:
<Wordmark size? />,<Container as? children />,<CTA href variant? />— consumed by every section. -
Step 1: Configure Vitest in
vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: [],
},
resolve: { alias: { '@': path.resolve(__dirname, './') } },
});
Add script to package.json:
"test": "vitest run",
"test:watch": "vitest"
- Step 2: Write failing test for
Wordmark
components/primitives/__tests__/Wordmark.test.tsx:
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Wordmark } from '../Wordmark';
describe('<Wordmark />', () => {
it('renders the wordmark in lowercase', () => {
render(<Wordmark />);
const text = screen.getByText(/cre/).closest('span')?.textContent;
expect(text?.toLowerCase()).toBe('creeer.');
});
it('renders the accent "e" separately for gold styling', () => {
const { container } = render(<Wordmark />);
const accent = container.querySelector('[data-accent="true"]');
expect(accent).not.toBeNull();
expect(accent?.textContent).toBe('e');
});
it('never renders uppercase CREEER', () => {
const { container } = render(<Wordmark />);
expect(container.textContent).not.toMatch(/CREEER/);
expect(container.textContent).not.toMatch(/Creeer/);
});
});
- Step 3: Run test to verify it fails
Run: npm test -- Wordmark
Expected: FAIL — module ../Wordmark not found.
- Step 4: Implement
Wordmark
components/primitives/Wordmark.tsx:
import { wordmark } from '@/lib/content';
type Size = 'sm' | 'md' | 'lg' | 'xl';
const sizeClass: Record<Size, string> = {
sm: 'text-xl',
md: 'text-2xl',
lg: 'text-3xl',
xl: 'text-5xl',
};
export function Wordmark({ size = 'lg', onDark = false }: { size?: Size; onDark?: boolean }) {
const { before, accent, after, dot } = wordmark.parts;
const baseColor = onDark ? 'text-cream' : 'text-ink';
const goldColor = onDark ? 'text-gold-bright' : 'text-gold';
return (
<span className={`font-serif italic font-bold tracking-tight leading-none ${sizeClass[size]} ${baseColor}`}>
{before}
<span data-accent="true" className={`not-italic font-black ${goldColor}`}>{accent}</span>
{after}
<span className={goldColor}>{dot}</span>
</span>
);
}
- Step 5: Run test to verify it passes
Run: npm test -- Wordmark
Expected: PASS (3 tests).
- Step 6: Write failing test for
CTA
components/primitives/__tests__/CTA.test.tsx:
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { CTA } from '../CTA';
describe('<CTA />', () => {
it('renders the label with arrow', () => {
render(<CTA href="#contacto" label="Define tu siguiente movimiento" />);
expect(screen.getByRole('link', { name: /define tu siguiente movimiento/i })).toBeInTheDocument();
});
it('uses the href', () => {
render(<CTA href="#contacto" label="x" />);
expect(screen.getByRole('link').getAttribute('href')).toBe('#contacto');
});
});
- Step 7: Run test — expected FAIL (module missing)
Run: npm test -- CTA
Expected: FAIL.
- Step 8: Implement
ContainerandCTA
components/primitives/Container.tsx:
import type { ElementType, ReactNode } from 'react';
export function Container({ as: Tag = 'div', className = '', children }: { as?: ElementType; className?: string; children: ReactNode }) {
return <Tag className={`mx-auto w-full max-w-content px-6 md:px-12 ${className}`}>{children}</Tag>;
}
components/primitives/CTA.tsx:
type Variant = 'light' | 'dark';
const styles: Record<Variant, string> = {
light: 'bg-ink text-base hover:bg-ink/90', // navy button, light text (on light bg)
dark: 'bg-gold-bright text-ink hover:bg-gold-bright/90', // gold button (on dark bg)
};
export function CTA({ href, label, variant = 'light', className = '' }: { href: string; label: string; variant?: Variant; className?: string }) {
return (
<a
href={href}
className={`inline-block font-sans font-bold uppercase tracking-wider px-7 py-3.5 text-sm transition-colors ${styles[variant]} ${className}`}
>
{label} <span aria-hidden="true">→</span>
</a>
);
}
- Step 9: Run tests — expected PASS
Run: npm test
Expected: all tests PASS.
- Step 10: Commit
git add -A
git commit -m "feat: primitives (Wordmark, Container, CTA) with tests"
Task 4: Hero (section 01) with HV-1 + V-a variants
Files:
- Create:
components/sections/Hero.tsx - Test:
components/sections/__tests__/Hero.test.tsx - Modify:
app/page.tsx(render<Hero />)
Interfaces:
-
Consumes:
<Wordmark>,<CTA>,herocontent object. -
Produces:
<Hero variant="hormozi" | "editorial" />— dueña decides via prop. -
Step 1: Write failing test
components/sections/__tests__/Hero.test.tsx:
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Hero } from '../Hero';
describe('<Hero />', () => {
it('renders the eyebrow', () => {
render(<Hero variant="editorial" />);
expect(screen.getByText(/tu estratega de crecimiento/i)).toBeInTheDocument();
});
it('renders the headline copy', () => {
render(<Hero variant="hormozi" />);
expect(screen.getByText(/Tu negocio/i)).toBeInTheDocument();
expect(screen.getByText(/más esfuerzo/i)).toBeInTheDocument();
expect(screen.getByText(/más claridad/i)).toBeInTheDocument();
});
it('renders the CTA', () => {
render(<Hero variant="editorial" />);
expect(screen.getByRole('link', { name: /define tu siguiente movimiento/i })).toBeInTheDocument();
});
it('does NOT render scarcity (cupos/cohorte)', () => {
const { container } = render(<Hero variant="editorial" />);
expect(container.textContent).not.toMatch(/cupos|cohorte|generación fundadora/i);
});
});
- Step 2: Run test — expected FAIL
Run: npm test -- Hero
Expected: FAIL (module missing).
- Step 3: Implement
Hero
components/sections/Hero.tsx:
import { motion } from 'framer-motion';
import { Wordmark } from '@/components/primitives/Wordmark';
import { CTA } from '@/components/primitives/CTA';
import { Container } from '@/components/primitives/Container';
import { hero } from '@/lib/content';
import { easeOutExpo, fadeInUp, staggerContainer } from '@/lib/motion';
type Variant = 'hormozi' | 'editorial';
export function Hero({ variant = 'editorial' }: { variant?: Variant }) {
const isHormozi = variant === 'hormozi';
const headlineBase = isHormozi
? 'font-sans font-black uppercase tracking-tight'
: 'font-serif italic font-bold';
const headlineSize = 'text-4xl md:text-6xl leading-[1.1]';
return (
<section className="bg-bg-pure">
<Container className="min-h-[88vh] flex flex-col items-center justify-center text-center py-24">
<motion.div variants={staggerContainer} initial="hidden" animate="visible" className="flex flex-col items-center">
<motion.div variants={fadeInUp}><Wordmark size="xl" /></motion.div>
<motion.p variants={fadeInUp} className="mt-6 text-xs md:text-sm tracking-[0.3em] uppercase font-semibold text-gold">
{hero.eyebrow}
</motion.p>
<motion.h1 variants={fadeInUp} className={`mt-8 ${headlineBase} ${headlineSize} text-ink`}>
{hero.headline.line1}{' '}
<span className="text-gold">{hero.headline.effort}</span>{' '}
{hero.headline.line3}{' '}
<span style={{ borderBottom: isHormozi ? '5px solid #B8901f' : '3px solid #B8901f', paddingBottom: 2 }}>
{hero.headline.clarity}
</span>
</motion.h1>
<motion.div variants={fadeInUp} className="mt-10">
<CTA href="#contacto" label={hero.cta} variant="light" />
</motion.div>
</motion.div>
</Container>
</section>
);
}
- Step 4: Run test — expected PASS
Run: npm test -- Hero
Expected: PASS (4 tests).
- Step 5: Render
<Hero />inapp/page.tsx
import { Hero } from '@/components/sections/Hero';
export default function Home() {
return (
<main>
{/* @ts-expect-error server component animating — wrap with 'use client' in next step if needed */}
<Hero variant="editorial" />
</main>
);
}
(Note: Framer Motion motion requires client components. If npm run dev errors about client components, add 'use client'; at the top of Hero.tsx and any component using motion. The animations task (Task 11) addresses this systematically.)
- Step 6: Verify in browser
Run: npm run dev
Expected: hero renders at localhost:3000 with the wordmark, eyebrow, headline, CTA. Try both variant="editorial" and variant="hormozi".
- Step 7: Commit
git add -A
git commit -m "feat: Hero section (01) with HV-1 hormozi + V-a editorial variants"
Task 5: FraseAncla (section 02)
Files:
- Create:
components/sections/FraseAncla.tsx - Test:
components/sections/__tests__/FraseAncla.test.tsx - Modify:
app/page.tsx
Interfaces:
-
Consumes:
fraseAnclacontent,Container. -
Step 1: Write failing test
components/sections/__tests__/FraseAncla.test.tsx:
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { FraseAncla } from '../FraseAncla';
describe('<FraseAncla />', () => {
it('renders the anchor quote', () => {
render(<FraseAncla />);
expect(screen.getByText(/Las empresas no dejan de crecer/i)).toBeInTheDocument();
expect(screen.getByText(/falta de claridad/i)).toBeInTheDocument();
});
});
- Step 2: Run test — expected FAIL
Run: npm test -- FraseAncla
Expected: FAIL.
- Step 3: Implement
components/sections/FraseAncla.tsx:
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { fraseAncla } from '@/lib/content';
import { fadeInUp, viewportOnce } from '@/lib/motion';
export function FraseAncla() {
return (
<section className="bg-base">
<Container className="py-24 md:py-32 text-center max-w-4xl">
<motion.blockquote
variants={fadeInUp}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
className="font-serif italic text-2xl md:text-4xl font-bold leading-snug text-ink"
>
<span className="text-gold">"</span>
Las empresas no dejan de crecer por falta de talento;{' '}
<span className="text-gold">dejan de crecer por falta de claridad.</span>
<span className="text-gold">"</span>
</motion.blockquote>
</Container>
</section>
);
}
- Step 4: Run test — expected PASS
Run: npm test -- FraseAncla
Expected: PASS.
- Step 5: Add to
app/page.tsx
import { Hero } from '@/components/sections/Hero';
import { FraseAncla } from '@/components/sections/FraseAncla';
export default function Home() {
return (
<main>
<Hero variant="editorial" />
<FraseAncla />
</main>
);
}
- Step 6: Commit
git add -A
git commit -m "feat: FraseAncla section (02) — anchor quote"
Task 6: ParaQuien (section 03)
Files:
- Create:
components/sections/ParaQuien.tsx - Test:
components/sections/__tests__/ParaQuien.test.tsx - Modify:
app/page.tsx
Interfaces:
-
Consumes:
paraQuiencontent,Container. -
Step 1: Write failing test
components/sections/__tests__/ParaQuien.test.tsx:
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { ParaQuien } from '../ParaQuien';
describe('<ParaQuien />', () => {
it('renders the section title', () => {
render(<ParaQuien />);
expect(screen.getByText(/punto de inflexión/i)).toBeInTheDocument();
});
it('renders all 3 criteria', () => {
render(<ParaQuien />);
expect(screen.getByText(/tienes tracción/i)).toBeInTheDocument();
expect(screen.getByText(/trabajar más ya no rinde/i)).toBeInTheDocument();
expect(screen.getByText(/necesitas claridad/i)).toBeInTheDocument();
});
});
- Step 2: Run test — expected FAIL
Run: npm test -- ParaQuien
Expected: FAIL.
- Step 3: Implement
components/sections/ParaQuien.tsx:
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { paraQuien } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce } from '@/lib/motion';
export function ParaQuien() {
return (
<section className="bg-base">
<Container className="py-24 md:py-32">
<motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} className="max-w-3xl">
<motion.h2 variants={fadeInUp} className="font-serif italic font-bold text-3xl md:text-5xl text-ink leading-tight">
{paraQuien.title}
</motion.h2>
<motion.p variants={fadeInUp} className="mt-6 text-lg text-muted">{paraQuien.lead}</motion.p>
<motion.ul variants={fadeInUp} className="mt-8 space-y-3">
{paraQuien.criteria.map((c) => (
<li key={c} className="flex items-start gap-3">
<span className="text-gold font-bold mt-1" aria-hidden="true">✓</span>
<span className="text-ink text-lg">{c}</span>
</li>
))}
</motion.ul>
</motion.div>
</Container>
</section>
);
}
- Step 4: Run test — expected PASS
Run: npm test -- ParaQuien
Expected: PASS.
- Step 5: Add to
app/page.tsx
// ...imports
import { ParaQuien } from '@/components/sections/ParaQuien';
// ... in JSX after <FraseAncla />:
<ParaQuien />
- Step 6: Commit
git add -A
git commit -m "feat: ParaQuien section (03) — audience filter"
Task 7: Mecanismo (section 04, dark)
Files:
- Create:
components/sections/Mecanismo.tsx - Test:
components/sections/__tests__/Mecanismo.test.tsx - Modify:
app/page.tsx
Interfaces:
-
Consumes:
mecanismocontent,Container. Dark section (navy bg). -
Step 1: Write failing test
components/sections/__tests__/Mecanismo.test.tsx:
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Mecanismo } from '../Mecanismo';
describe('<Mecanismo />', () => {
it('renders the 3 steps in order', () => {
render(<Mecanismo />);
expect(screen.getByText('Inmersión')).toBeInTheDocument();
expect(screen.getByText('Desafiar supuestos')).toBeInTheDocument();
expect(screen.getByText('Estrategia ejecutable')).toBeInTheDocument();
});
it('renders step numbers 01/02/03', () => {
const { container } = render(<Mecanismo />);
expect(container.textContent).toMatch(/01.+02.+03/s);
});
});
- Step 2: Run test — expected FAIL
Run: npm test -- Mecanismo
Expected: FAIL.
- Step 3: Implement
components/sections/Mecanismo.tsx:
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { mecanismo } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce } from '@/lib/motion';
export function Mecanismo() {
return (
<section className="bg-bg-dark text-cream">
<Container className="py-24 md:py-32">
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce}
className="text-xs tracking-[0.3em] uppercase font-semibold text-gold-bright">{mecanismo.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce}
className="mt-4 font-serif italic font-bold text-3xl md:text-5xl">{mecanismo.title}</motion.h2>
<motion.ol variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce}
className="mt-12 grid gap-10 md:grid-cols-3">
{mecanismo.steps.map((step) => (
<motion.li key={step.n} variants={fadeInUp} className="border-t border-gold-bright/30 pt-6">
<div className="font-serif italic font-black text-5xl md:text-6xl text-gold-bright leading-none">{step.n}</div>
<h3 className="mt-4 font-sans font-bold text-xl">{step.name}</h3>
<p className="mt-3 text-muted-dark leading-relaxed">{step.desc}</p>
</motion.li>
))}
</motion.ol>
</Container>
</section>
);
}
- Step 4: Run test — expected PASS
Run: npm test -- Mecanismo
Expected: PASS.
- Step 5: Add to
app/page.tsx
import { Mecanismo } from '@/components/sections/Mecanismo';
// after <ParaQuien />: <Mecanismo />
- Step 6: Commit
git add -A
git commit -m "feat: Mecanismo section (04, dark) — 3-step method"
Task 8: Disciplinas (section 05)
Files:
- Create:
components/sections/Disciplinas.tsx - Test:
components/sections/__tests__/Disciplinas.test.tsx - Modify:
app/page.tsx
Interfaces:
-
Consumes:
disciplinascontent,Container. -
Step 1: Write failing test
components/sections/__tests__/Disciplinas.test.tsx:
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Disciplinas } from '../Disciplinas';
describe('<Disciplinas />', () => {
it('renders the 3 disciplines', () => {
render(<Disciplinas />);
expect(screen.getByText('Comunicación')).toBeInTheDocument();
expect(screen.getByText('Digital')).toBeInTheDocument();
expect(screen.getByText('Experiencias')).toBeInTheDocument();
});
});
- Step 2: Run test — expected FAIL
Run: npm test -- Disciplinas
Expected: FAIL.
- Step 3: Implement
components/sections/Disciplinas.tsx:
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { disciplinas } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce } from '@/lib/motion';
export function Disciplinas() {
return (
<section className="bg-base">
<Container className="py-24 md:py-32">
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce}
className="text-xs tracking-[0.3em] uppercase font-semibold text-gold">{disciplinas.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce}
className="mt-4 font-serif italic font-bold text-3xl md:text-5xl text-ink">{disciplinas.title}</motion.h2>
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce}
className="mt-4 text-muted text-lg">{disciplinas.lead}</motion.p>
<motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce}
className="mt-12 grid gap-8 md:grid-cols-3">
{disciplinas.items.map((item) => (
<motion.div key={item.name} variants={fadeInUp} className="border-t-2 border-gold pt-5">
<h3 className="font-serif italic font-bold text-2xl text-ink">{item.name}</h3>
<p className="mt-3 text-muted leading-relaxed">{item.desc}</p>
</motion.div>
))}
</motion.div>
</Container>
</section>
);
}
- Step 4: Run test — expected PASS
Run: npm test -- Disciplinas
Expected: PASS.
- Step 5: Add to
app/page.tsx
import { Disciplinas } from '@/components/sections/Disciplinas';
// after <Mecanismo />: <Disciplinas />
- Step 6: Commit
git add -A
git commit -m "feat: Disciplinas section (05) — pillars as tools"
Task 9: Estratega (section 06)
Files:
- Create:
components/sections/Estratega.tsx - Test:
components/sections/__tests__/Estratega.test.tsx - Modify:
app/page.tsx
Interfaces:
-
Consumes:
estrategacontent,Container. Includes portrait (placeholder gradient if no photo yet). -
Step 1: Write failing test
components/sections/__tests__/Estratega.test.tsx:
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Estratega } from '../Estratega';
describe('<Estratega />', () => {
it('renders the bio', () => {
render(<Estratega />);
expect(screen.getByText(/30 años/i)).toBeInTheDocument();
});
it('renders the institutional quote', () => {
render(<Estratega />);
expect(screen.getByText(/El tiempo no se compra/i)).toBeInTheDocument();
});
});
- Step 2: Run test — expected FAIL
Run: npm test -- Estratega
Expected: FAIL.
- Step 3: Implement
components/sections/Estratega.tsx:
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { estratega } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce } from '@/lib/motion';
export function Estratega() {
return (
<section className="bg-base">
<Container className="py-24 md:py-32">
<motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce}
className="grid gap-12 md:grid-cols-[auto_1fr] md:items-center">
{/* Portrait — placeholder gradient until dueña provides a photo */}
<motion.div variants={fadeInUp}
className="w-40 h-40 md:w-56 md:h-56 rounded-full flex-shrink-0"
style={{ background: 'linear-gradient(135deg, #0A1F44 0%, #B8901f 100%)' }}
aria-label="Retrato de la estratega"
role="img"
/>
<div>
<motion.p variants={fadeInUp} className="text-xs tracking-[0.3em] uppercase font-semibold text-gold">{estratega.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} className="mt-4 font-serif italic font-bold text-3xl md:text-5xl text-ink">{estratega.title}</motion.h2>
<motion.p variants={fadeInUp} className="mt-6 text-lg text-muted leading-relaxed max-w-2xl">{estratega.bio}</motion.p>
<motion.blockquote variants={fadeInUp} className="mt-6 font-serif italic text-xl md:text-2xl text-ink border-l-4 border-gold pl-4">
{estratega.quote}
</motion.blockquote>
</div>
</motion.div>
</Container>
</section>
);
}
Note: When the dueña provides a real photo, replace the gradient div with
<Image src="/retrato.jpg" alt="..." width={224} height={224} className="rounded-full" />usingnext/image. Keeparia-label/altmeaningful.
- Step 4: Run test — expected PASS
Run: npm test -- Estratega
Expected: PASS.
- Step 5: Add to
app/page.tsx
import { Estratega } from '@/components/sections/Estratega';
// after <Disciplinas />: <Estratega />
- Step 6: Commit
git add -A
git commit -m "feat: Estratega section (06) — portrait + bio + quote"
Task 10: Cierre + contacto (section 07, dark)
Files:
- Create:
components/sections/Cierre.tsx - Test:
components/sections/__tests__/Cierre.test.tsx - Modify:
app/page.tsx
Interfaces:
-
Consumes:
cierrecontent (includes contact links),Container,CTA,Wordmark(in footer). Dark section. -
Step 1: Write failing test
components/sections/__tests__/Cierre.test.tsx:
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Cierre } from '../Cierre';
describe('<Cierre />', () => {
it('renders the closing quote', () => {
render(<Cierre />);
expect(screen.getByText(/Devuélvele claridad/i)).toBeInTheDocument();
});
it('renders 3 contact links with correct hrefs', () => {
render(<Cierre />);
const wa = screen.getByRole('link', { name: /whatsapp/i });
const mail = screen.getByRole('link', { name: /email/i });
const li = screen.getByRole('link', { name: /linkedin/i });
expect(wa.getAttribute('href')).toMatch(/^https:\/\/wa\.me\//);
expect(mail.getAttribute('href')).toMatch(/^mailto:/);
expect(li.getAttribute('href')).toMatch(/^https:\/\/(www\.)?linkedin\./);
});
});
- Step 2: Run test — expected FAIL
Run: npm test -- Cierre
Expected: FAIL.
- Step 3: Implement
components/sections/Cierre.tsx:
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { CTA } from '@/components/primitives/CTA';
import { Wordmark } from '@/components/primitives/Wordmark';
import { cierre } from '@/lib/content';
import { fadeInUp, staggerContainer, viewportOnce } from '@/lib/motion';
export function Cierre() {
const { whatsapp, email, linkedin } = cierre.contact;
return (
<section id="contacto" className="bg-bg-dark text-cream">
<Container className="py-24 md:py-32 text-center">
<motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce}
className="flex flex-col items-center">
<motion.div variants={fadeInUp}><Wordmark size="lg" onDark /></motion.div>
<motion.blockquote variants={fadeInUp}
className="mt-10 font-serif italic text-2xl md:text-4xl font-bold leading-snug max-w-3xl">
<span className="text-gold-bright">"</span>{cierre.quote}<span className="text-gold-bright">"</span>
</motion.blockquote>
<motion.div variants={fadeInUp} className="mt-10">
<CTA href={whatsapp.href} label={cierre.cta} variant="dark" />
</motion.div>
<motion.ul variants={fadeInUp} className="mt-12 flex flex-wrap justify-center gap-6 text-sm">
<li><a href={whatsapp.href} className="hover:text-gold-bright transition-colors">{whatsapp.label}</a></li>
<li aria-hidden="true" className="text-muted-dark">·</li>
<li><a href={email.href} className="hover:text-gold-bright transition-colors">{email.label}</a></li>
<li aria-hidden="true" className="text-muted-dark">·</li>
<li><a href={linkedin.href} className="hover:text-gold-bright transition-colors">{linkedin.label}</a></li>
</motion.ul>
</motion.div>
</Container>
</section>
);
}
- Step 4: Run test — expected PASS
Run: npm test -- Cierre
Expected: PASS.
- Step 5: Add to
app/page.tsx
import { Cierre } from '@/components/sections/Cierre';
// after <Estratega />: <Cierre />
- Step 6: Commit
git add -A
git commit -m "feat: Cierre section (07, dark) — closing quote + contact links"
Task 11: Compose page + animations + reduced-motion
Files:
- Modify:
app/page.tsx,lib/motion.ts
Interfaces:
-
Produces: the full scrollable page with all 7 sections in order, Framer Motion animations, and
prefers-reduced-motionsupport. -
Step 1: Final
app/page.tsx
import { Hero } from '@/components/sections/Hero';
import { FraseAncla } from '@/components/sections/FraseAncla';
import { ParaQuien } from '@/components/sections/ParaQuien';
import { Mecanismo } from '@/components/sections/Mecanismo';
import { Disciplinas } from '@/components/sections/Disciplinas';
import { Estratega } from '@/components/sections/Estratega';
import { Cierre } from '@/components/sections/Cierre';
export default function Home() {
return (
<main>
<Hero variant="editorial" /> {/* dueña puede cambiar a "hormozi" */}
<FraseAncla />
<ParaQuien />
<Mecanismo />
<Disciplinas />
<Estratega />
<Cierre />
</main>
);
}
- Step 2: Add reduced-motion support to
lib/motion.ts
Append to lib/motion.ts:
import { useEffect, useState } from 'react';
// Hook: returns true if user prefers reduced motion. Components use this to
// disable Framer Motion animations (per spec, a11y requirement).
export function usePrefersReducedMotion() {
const [reduced, setReduced] = useState(false);
useEffect(() => {
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
setReduced(mq.matches);
const handler = () => setReduced(mq.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
return reduced;
}
// Pass to <motion.*> as motion-reduced guard. When reduced, variants resolve to visible immediately.
export const motionProps = (reduced: boolean) =>
reduced
? { initial: 'visible' as const, animate: 'visible' as const, whileInView: undefined, viewport: undefined }
: {};
- Step 3: Apply in each animated component
For each section using motion, replace its variants/initial/whileInView props with reduced-motion-aware versions. Example for FraseAncla.tsx:
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { fraseAncla } from '@/lib/content';
import { fadeInUp, viewportOnce, usePrefersReducedMotion, motionProps } from '@/lib/motion';
export function FraseAncla() {
const reduced = usePrefersReducedMotion();
return (
<section className="bg-base">
<Container className="py-24 md:py-32 text-center max-w-4xl">
<motion.blockquote
variants={fadeInUp}
initial={reduced ? 'visible' : 'hidden'}
whileInView={reduced ? undefined : 'visible'}
viewport={reduced ? undefined : viewportOnce}
className="font-serif italic text-2xl md:text-4xl font-bold leading-snug text-ink"
>
{/* ...quote content... */}
</motion.blockquote>
</Container>
</section>
);
}
Repeat the same reduced pattern for: Hero, ParaQuien, Mecanismo, Disciplinas, Estratega, Cierre.
- Step 4: Run full test suite
Run: npm test
Expected: all tests PASS.
- Step 5: Visual verification
Run: npm run dev
Expected: all 7 sections render in order with smooth scroll animations. Toggle prefers-reduced-motion in DevTools → animations should disable (content appears immediately).
- Step 6: Commit
git add -A
git commit -m "feat: compose page + Framer Motion animations + reduced-motion support"
Task 12: SEO metadata, accessibility, static build verification
Files:
- Modify:
app/layout.tsx(full metadata),app/page.tsx(semantic HTML) - Create:
public/og-image.png(manually),public/robots.txt,public/sitemap.xml - Verify:
npm run buildproduces static HTML inout/
Interfaces:
-
Produces: complete SEO metadata, OG image, semantic HTML, accessible contact links, passing static export build.
-
Step 1: Expand metadata in
app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
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 su punto de inflexión. Inmersión profunda, estrategia ejecutable.',
metadataBase: new URL('https://creeer.com'), // TODO dueña: real domain
openGraph: {
title: 'creeer · tu estratega de crecimiento',
description: 'Claridad estratégica para empresarios con tracción en punto de inflexión.',
type: 'website',
locale: 'es',
images: [{ url: '/og-image.png', width: 1200, height: 630, alt: 'creeer' }],
},
twitter: { card: 'summary_large_image', title: 'creeer', description: 'Claridad estratégica para empresarios.' },
alternates: { canonical: 'https://creeer.com' },
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es" className={inter.variable}>
<body>{children}</body>
</html>
);
}
- Step 2: Add JSON-LD structured data in
app/page.tsx
Wrap the <main> content with a structured data script. Add at top of Home():
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'ProfessionalService',
name: 'creeer',
description: 'Estratega de crecimiento. Claridad estratégica para empresarios con tracción.',
url: 'https://creeer.com',
};
export default function Home() {
return (
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<main>
{/* ...sections... */}
</main>
</>
);
}
- Step 3: Create
public/robots.txt
User-agent: *
Allow: /
Sitemap: https://creeer.com/sitemap.xml
- Step 4: Create
public/sitemap.xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://creeer.com/</loc><changefreq>monthly</changefreq><priority>1.0</priority></url>
</urlset>
- Step 5: Create OG image
Manually create public/og-image.png (1200×630) — use the wordmark creeer. on navy #0A1F44 background with the headline. If no design tool available, use a placeholder PNG for now and replace later.
- Step 6: Add focus-visible styles to
app/globals.css
Append to globals.css:
@layer base {
a:focus-visible, button:focus-visible {
outline: 3px solid #D4AF37;
outline-offset: 3px;
border-radius: 2px;
}
}
- Step 7: Run final test suite
Run: npm test
Expected: all tests PASS.
- Step 8: Run static build
Run: npm run build
Expected: build succeeds, outputs out/ directory with static HTML. No errors.
- Step 9: Run lint
Run: npm run lint
Expected: passes (or only minor warnings).
- Step 10: Manual accessibility smoke test
Open npm run dev, run browser DevTools Lighthouse audit (or axe extension). Expected:
-
Contrast ratios pass (navy on white: 14.6:1; marfil on navy: 13.1:1)
-
<html lang="es">set -
All contact links have discernible text
-
No duplicate
<h1>(only Hero has h1) -
Step 11: Final commit
git add -A
git commit -m "feat: SEO metadata, JSON-LD, OG image, sitemap, a11y focus styles"
Spec Coverage Self-Check
| Spec section | Task |
|---|---|
| 3.1 Paleta (tokens) | Task 2 |
| 3.2 Tipografía híbrida (Georgia + Inter) | Task 2, 3 |
| 3.3 Escala tipográfica | Tasks 4–10 (applied in Tailwind classes) |
| 3.4 Wordmark | Task 3 |
| 3.5 Recursos (•, smart quotes, →) | Tasks 4–10 |
| 3.6 Espaciado y ritmo | Tasks 4–10 (py-24 md:py-32, Container) |
| 3.7 Layout claro/oscuro | Tasks 4–10 (bg-base, bg-bg-dark) |
| 4. Hero (HV-1 + V-a) | Task 4 |
| 5. Flujo 7 secciones | Tasks 4–10 |
| 6. Messaging (frases-ancla) | Task 2 (content.ts) |
| 7. Restricciones técnicas | Tasks 1, 10 |
| 8. SEO / a11y / perf | Task 12 |
| 9. Animación (Framer Motion + reduced) | Task 11 |
Placeholder scan: None. All steps contain complete code. The cierre.contact hrefs in lib/content.ts are marked TODO dueña for real values — that's a content decision (not implementation) and documented in spec §10.
Type consistency: All section components share the same interface signature (export function SectionName() with no props, except <Hero variant>). All consume the same primitives (<Container>, <CTA>, <Wordmark>) and content objects. ✓
Execution
Plan complete. Two execution options:
1. Subagent-Driven (recommended) — dispatch a fresh subagent per task with review between tasks. Best for catching issues early and keeping each task focused.
2. Inline Execution — execute tasks in this session using executing-plans, with checkpoints for review. Best when you want tight loop with the user.
Which approach?