18 KiB
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
Wordmarkprimitive — never re-implemented, never uppercased. - Paleta (tokens):
ink #0A1F44·gold #B8901f(accents on LIGHT) ·gold-bright #D4AF37(accents on DARK) ·base #FBFAF7(light bg, classbg-base) · dark bg classbg-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 clavesheader,comoEmpezar,footer). - Voz en segunda persona, español.
- Reduced motion: every Framer Motion element gets
{...motionProps(reduced)}after itsinitial/whileInView/viewportprops (reduced = usePrefersReducedMotion()). Header/Footer are static (no motion). tsconfig.jsonhas notarget→ avoid ES2018+ regex flags (s/dotAll) ortsc --noEmitfails.
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(appendheader) - Modify:
app/page.tsx(place<Header />before<main>)
Interfaces:
-
Consumes:
Wordmark(size,onDark),CTA(href,label,variant),Container, andheader.ctafrom content. -
Produces:
Header()atcomponents/layout/Header.tsx— no props. Consumed byapp/page.tsx. -
Step 1: Append the
headercopy tolib/content.ts
Add at the end of lib/content.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
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
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 />intoapp/page.tsx
Add the import after the existing section imports (top of the file):
import { Header } from '@/components/layout/Header';
Then insert <Header /> between the JSON-LD <script> and <main>:
<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
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(appendcomoEmpezar) - Modify:
app/page.tsx(place<ComoEmpezar />before<Cierre />)
Interfaces:
-
Consumes:
Container,CTA,comoEmpezar+cierre.contact.whatsapp.hreffrom content, andfadeInUp/staggerContainer/viewportOnce/usePrefersReducedMotion/motionPropsfromlib/motion. -
Produces:
ComoEmpezar()atcomponents/sections/ComoEmpezar.tsx— no props. Consumed byapp/page.tsx. -
Step 1: Append the
comoEmpezarcopy tolib/content.ts
Add at the end of lib/content.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
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
'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 />intoapp/page.tsx
Add the import with the other section imports:
import { ComoEmpezar } from '@/components/sections/ComoEmpezar';
Insert <ComoEmpezar /> between <Estratega /> and <Cierre />:
<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
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(appendfooter) - Modify:
app/page.tsx(place<Footer />after</main>)
Interfaces:
-
Consumes:
Wordmark(size,onDark),Container, andfooter.tagline/footer.legalfrom content. -
Produces:
Footer()atcomponents/layout/Footer.tsx— no props. Consumed byapp/page.tsx. -
Step 1: Append the
footercopy tolib/content.ts
Add at the end of lib/content.ts:
export const footer = {
tagline: 'tu estratega de crecimiento',
legal: '© 2026 creeer',
};
- Step 2: Write the failing test
components/layout/__tests__/Footer.test.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
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 />intoapp/page.tsx
Add the import near the Header import:
import { Footer } from '@/components/layout/Footer';
Insert <Footer /> after the closing </main> and before the fragment close:
</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
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(addscroll-mtto the#contactoanchor target)
Interfaces:
-
Consumes: the composed page from Tasks 1–3.
-
Produces: a verified static export; the
#contactoanchor target that clears the sticky header. -
Step 1: Locate the
#contactoanchor incomponents/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-24to 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:
<section id="contacto" className="bg-bg-dark text-cream">
change it to:
<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
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/520000000000placeholder inlib/content.tsstill applies to both Cierre and the new ComoEmpezar CTA). - Merge
feat/header-comoempezar-footerintomainonce reviewed.