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

18 KiB
Raw Blame History

creeer Landing Additions — Header + Cómo empezar + Footer — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add a minimal sticky Header, a new "Cómo empezar" on-ramp section (section 07), and a slim Footer to the existing 7-section creeer landing page.

Architecture: Three additive pieces reusing the existing primitives (Wordmark, CTA, Container), tokens, the motion + motionProps(reduced) pattern, and the copy-in-lib/content.ts convention. Header and Footer are static server components wrapping <main>; ComoEmpezar is a client section following the exact pattern of Mecanismo.tsx/Disciplinas.tsx. Cierre becomes section 08. No backend, no forms — the site stays a static export.

Tech Stack: Next.js 14 (App Router, output: export), React 18, TypeScript, Tailwind CSS 3.4, Framer Motion 11. Tests: Vitest + @testing-library/react.

Global Constraints

(Copied from the spec docs/superpowers/specs/2026-07-21-creeer-landing-additions-design.md — apply to every task.)

  • Wordmark always via the Wordmark primitive — never re-implemented, never uppercased.
  • Paleta (tokens): ink #0A1F44 · gold #B8901f (accents on LIGHT) · gold-bright #D4AF37 (accents on DARK) · base #FBFAF7 (light bg, class bg-base) · dark bg class bg-bg-dark · cream (text on dark) · muted/muted-dark.
  • Tipografía: Georgia italic (font-serif italic) = wordmark + section titles; Inter = body/UI.
  • Sin backend, sin formularios — contacto solo por enlaces directos (WhatsApp/mailto/LinkedIn).
  • Un único <h1> en toda la página — vive en el Hero. New pieces use <h2>/<h3> or no heading.
  • Todo el copy vive en lib/content.ts (nuevas claves header, comoEmpezar, footer).
  • Voz en segunda persona, español.
  • Reduced motion: every Framer Motion element gets {...motionProps(reduced)} after its initial/whileInView/viewport props (reduced = usePrefersReducedMotion()). Header/Footer are static (no motion).
  • tsconfig.json has no target → avoid ES2018+ regex flags (s/dotAll) or tsc --noEmit fails.

File Structure

components/
├── layout/
│   ├── Header.tsx                     # Task 1 — sticky bar (server component)
│   ├── Footer.tsx                     # Task 3 — slim footer strip (server component)
│   └── __tests__/
│       ├── Header.test.tsx            # Task 1
│       └── Footer.test.tsx            # Task 3
└── sections/
    ├── ComoEmpezar.tsx                # Task 2 — section 07 (client, motion pattern)
    └── __tests__/
        └── ComoEmpezar.test.tsx       # Task 2
lib/content.ts                         # Tasks 1/2/3 append `header` / `comoEmpezar` / `footer`
app/page.tsx                           # Tasks 1/2/3 wire the pieces; Task 4 anchor offset
components/sections/Cierre.tsx         # Task 4 — add scroll-mt to the #contacto anchor

Each component task also wires itself into app/page.tsx (matching the repo's established incremental-compose pattern), so the page stays working after every task.


Task 1: Header (minimal sticky)

Files:

  • Create: components/layout/Header.tsx
  • Create: components/layout/__tests__/Header.test.tsx
  • Modify: lib/content.ts (append header)
  • Modify: app/page.tsx (place <Header /> before <main>)

Interfaces:

  • Consumes: Wordmark (size, onDark), CTA (href, label, variant), Container, and header.cta from content.

  • Produces: Header() at components/layout/Header.tsx — no props. Consumed by app/page.tsx.

  • Step 1: Append the header copy to lib/content.ts

Add at the end of lib/content.ts:

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 /> into app/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 (append comoEmpezar)
  • Modify: app/page.tsx (place <ComoEmpezar /> before <Cierre />)

Interfaces:

  • Consumes: Container, CTA, comoEmpezar + cierre.contact.whatsapp.href from content, and fadeInUp/staggerContainer/viewportOnce/usePrefersReducedMotion/motionProps from lib/motion.

  • Produces: ComoEmpezar() at components/sections/ComoEmpezar.tsx — no props. Consumed by app/page.tsx.

  • Step 1: Append the comoEmpezar copy to lib/content.ts

Add at the end of lib/content.ts:

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 /> into app/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"

Files:

  • Create: components/layout/Footer.tsx
  • Create: components/layout/__tests__/Footer.test.tsx
  • Modify: lib/content.ts (append footer)
  • Modify: app/page.tsx (place <Footer /> after </main>)

Interfaces:

  • Consumes: Wordmark (size, onDark), Container, and footer.tagline/footer.legal from content.

  • Produces: Footer() at components/layout/Footer.tsx — no props. Consumed by app/page.tsx.

  • Step 1: Append the footer copy to lib/content.ts

Add at the end of lib/content.ts:

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 /> into app/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 (add scroll-mt to the #contacto anchor target)

Interfaces:

  • Consumes: the composed page from Tasks 1–3.

  • Produces: a verified static export; the #contacto anchor target that clears the sticky header.

  • Step 1: Locate the #contacto anchor in components/sections/Cierre.tsx

Run: grep -n 'id="contacto"' components/sections/Cierre.tsx Expected: one match — the element carrying id="contacto" (the closing section's outer element).

  • Step 2: Add scroll-mt-24 to that element's className

On the element that has id="contacto", add scroll-mt-24 to its className so the sticky header (~60px) never overlaps the contact anchor when the Header/Hero CTAs jump to it. Example — if the element reads:

<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/520000000000 placeholder in lib/content.ts still applies to both Cierre and the new ComoEmpezar CTA).
  • Merge feat/header-comoempezar-footer into main once reviewed.