diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a14c70..28faff1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,10 +5,22 @@ on: push: branches: - main + workflow_dispatch: permissions: contents: read +env: + NODE_ENV: test + NEXT_PUBLIC_RECAPTCHA_SITE_KEY_V2: test-site-key + RECAPTCHA_SECRET_KEY_V2: test-secret-key + OPENAI_API_KEY: test-openai-key + BREVO_SMTP_PASSWORD: test-brevo-password + BREVO_USER: test-brevo-user + BREVO_TO: test-to@example.com + BREVO_FROM: test-from@example.com + SITE_URL: https://www.michaelzick.com + jobs: validate: runs-on: ubuntu-latest @@ -18,17 +30,24 @@ jobs: uses: actions/checkout@v6 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.x + check-latest: true cache: npm - name: Install dependencies run: npm ci + - name: Agent brief sync + run: npm run agent-briefs:check + - name: Lint run: npm run lint + - name: Typecheck + run: npm run typecheck + - name: Test run: npm test diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..df77927 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,91 @@ +name: Security Checks + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + secrets: + name: Secret Scan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Run Gitleaks + run: docker run --rm -v "$PWD:/repo" ghcr.io/gitleaks/gitleaks:latest detect --source /repo --redact --verbose + + dependencies: + name: Dependency Audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: 24.x + check-latest: true + cache: npm + + - run: npm ci + - run: npm audit --audit-level=moderate + + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + permissions: + contents: read + steps: + - name: Review dependency changes + env: + GH_TOKEN: ${{ github.token }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2026-03-10" \ + "/repos/${GITHUB_REPOSITORY}/dependency-graph/compare/${BASE_SHA}...${HEAD_SHA}" \ + > dependency-review.json + + jq -r ' + [ + .[] + | select(.change_type == "added" or .change_type == "changed") + | . as $dependency + | (.vulnerabilities // [])[] + | select((.severity | ascii_downcase) as $severity | ["moderate", "high", "critical"] | index($severity)) + | "\($dependency.manifest): \($dependency.name)@\($dependency.version) introduces \(.severity) \(.advisory_ghsa_id): \(.advisory_summary) (\(.advisory_url))" + ] + | .[] + ' dependency-review.json > dependency-review-findings.txt + + if [ -s dependency-review-findings.txt ]; then + cat dependency-review-findings.txt + exit 1 + fi + + echo "No moderate or higher vulnerable dependency changes detected." + + codeql: + name: CodeQL Scan + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@v6 + + - uses: github/codeql-action/init@v4 + with: + languages: javascript-typescript + + - uses: github/codeql-action/autobuild@v4 + + - uses: github/codeql-action/analyze@v4 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..eb7e259 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,174 @@ +# Michael Zick — Agent Orientation (Codex / AGENTS.md) + +This document is the canonical project brief for AI coding agents. Read it at the start of every session instead of re-exploring the repo. Keep it current: see [Maintaining this file](#maintaining-this-file). + +Sibling files [CLAUDE.md](CLAUDE.md) (Claude Code) and [GEMINI.md](GEMINI.md) (Gemini CLI) mirror this content for other harnesses. Update all three together when code structure changes. + +--- + +## 1. Project overview + +**michaelzick.com** is Michael Zick's coaching website and conversion platform for Nice Guy Recovery / Reality Alignment coaching. It combines static marketing pages, a blog, lead-capture forms, an AI-assisted questionnaire, analytics instrumentation, and Nice Guy University promotion flows. + +Primary flows: +- Visitors learn about Michael's coaching model through the home, about, testimonials, contact, and Nice Guy University pages. +- Visitors submit a multi-step questionnaire and receive an OpenAI-generated coaching analysis. +- Contact and NGU coupon forms validate Invisible reCAPTCHA v2, rate-limit submissions, and send Brevo SMTP email notifications. +- Blog readers browse JSON-backed posts with category/tag filters and structured data for SEO. +- CTA and navigation interactions are tracked through a shared analytics wrapper for GA4 and Amplitude. + +## 2. Tech stack + +- **Framework:** Next.js 16 App Router with React 19 and TypeScript; the package is currently pinned to the patched 16.3 canary line until the same security fixes land in a stable release. +- **Styling:** Tailwind CSS 3, global styles in `app/globals.css`, image assets in `public/img/`. +- **Server routes:** Next route handlers under `app/api/*`, using Node runtime where email/OpenAI APIs are needed. +- **AI and email:** OpenAI Node SDK for questionnaire analysis; Nodemailer with Brevo SMTP for notifications. +- **Bot protection:** Classic Invisible reCAPTCHA v2 via `NEXT_PUBLIC_RECAPTCHA_SITE_KEY_V2` and `RECAPTCHA_SECRET_KEY_V2`. +- **Analytics:** GA4 and Amplitude scripts in `components/SiteAnalyticsScripts.tsx`; tracked events in `lib/analytics.ts`. +- **Testing:** Node's built-in test runner for compiled unit tests, TypeScript test build via `tsconfig.test.json`, and Playwright for E2E/mobile UI checks. +- **Tooling:** npm with `package-lock.json`, Node 24 LTS, ESLint flat config via `eslint-config-next/core-web-vitals`. + +## 3. Repository layout + +``` +michaelzick.com/ +├── app/ # Next App Router pages, layout, metadata, API routes, sitemap/robots +├── components/ # React UI components, navigation, sections, blog, questionnaire, hooks +├── content/blog/ # JSON-backed blog content fixtures and production posts +├── lib/ # Analytics, blog utilities, structured data, server validation/helpers +├── public/ # Favicons, manifest, generated sitemap, static image assets +├── scripts/ # Node scripts such as sitemap generation +├── skills/ # Repo-local agent skills +├── tests/ # Node unit tests and Playwright E2E tests +├── types/ # Ambient type declarations +├── .github/workflows/ # CI and security automation +├── package.json # npm scripts and dependency list +└── package-lock.json # npm lockfile; keep npm workflow +``` + +## 4. Application structure + +### 4.1 App Router pages + +- `app/layout.tsx` defines global metadata, JSON-LD, analytics scripts, nav, NGU promo, footer, and Open Sans. +- `app/page.tsx` renders the home page through `components/HomePageContent.tsx`. +- `app/about/page.tsx`, `app/testimonials/page.tsx`, and `app/contact/page.tsx` are static marketing/conversion pages. +- `app/contact/ContactContent.tsx` provides the contact page client experience. +- `app/questionnaire/page.tsx` renders the questionnaire flow. +- `app/nice-guy-university/page.tsx` renders the NGU promotional page and outbound CTAs. +- `app/blog/page.tsx` and `app/blog/[slug]/page.tsx` render blog index/detail pages with structured data. +- `app/sitemap.ts` and `app/robots.ts` expose Next-generated SEO metadata routes. + +### 4.2 API routes + +- `app/api/analyze/route.ts` accepts questionnaire submissions, applies honeypot/rate limiting/length checks, calls OpenAI (`gpt-5-mini` with fallback to `gpt-4o-mini`), and optionally emails the result via Brevo SMTP. +- `app/api/contact/route.ts` validates contact submissions, enforces per-IP rate limits, verifies Invisible reCAPTCHA v2, and sends contact email via Brevo. +- `app/api/ngu-coupon/route.ts` validates NGU coupon signups, verifies reCAPTCHA, sends the visitor coupon email, and sends the internal notification email. +- Shared server helpers live in `lib/server/`: contact and NGU normalization/validation/email builders, OpenAI client construction, and in-memory rate limiting. + +### 4.3 Components and client behavior + +- `components/navigation/` contains desktop/mobile navigation primitives used by `components/NavBar.tsx`. +- `components/sections/` contains major home-page content bands; keep visual changes consistent with the existing premium coaching brand. +- `components/questionnaire/` contains the questionnaire steps, fields, form, and analysis rendering. +- `components/blog/` contains blog filters, cards, hero, breadcrumbs, similar posts, and scroll-to-top behavior. +- `components/ContactForm.tsx` and `components/NguCouponSignupForm.tsx` load and execute Invisible reCAPTCHA v2 before posting to API routes. +- `components/TrackedLink.tsx` and `components/TrackedCtaLink.tsx` centralize CTA/link tracking. +- `components/hooks/` contains UI hooks for scroll tracking, fade-in behavior, and title visibility. + +### 4.4 Content, SEO, and analytics + +- Blog source content lives in `content/blog/posts.json`; `lib/blog.ts` normalizes slugs, excerpts, filters, dates, and similar posts. +- Site-wide brand/SEO constants live in `lib/site.ts`. +- Structured data helpers live in `lib/site-structured-data.ts` and `lib/blog-structured-data.ts`. +- `scripts/generate-sitemap.js` writes `public/sitemap.xml`; use `SITE_URL` to override the production base URL. +- Analytics scripts are hardcoded in `components/SiteAnalyticsScripts.tsx`; event dispatch lives in `lib/analytics.ts`. + +## 5. Environment + +No committed `.env.example` currently exists. Environment variables used by the app: + +- `OPENAI_API_KEY` — required for questionnaire analysis. +- `BREVO_SMTP_PASSWORD`, `BREVO_USER`, `BREVO_TO`, `BREVO_FROM` — required for contact and NGU email routes; optional for questionnaire result notification. +- `NEXT_PUBLIC_RECAPTCHA_SITE_KEY_V2` — public Invisible reCAPTCHA v2 site key used by browser forms. +- `RECAPTCHA_SECRET_KEY_V2` — server-side Invisible reCAPTCHA v2 secret used with Google `siteverify`. +- `SITE_URL` — optional sitemap generation override; defaults to `https://www.michaelzick.com`. +- `PORT` — used by `npm start` and Playwright web server startup. +- `CI` and `PLAYWRIGHT_SKIP_BUILD` — influence Playwright server reuse/build behavior. + +Do not commit `.env`, API keys, SMTP credentials, reCAPTCHA secrets, Vercel secrets, or production form exports. + +## 6. Commands + +```bash +npm run dev # Next dev server +npm run build # Production Next build +npm start # Start built Next app on $PORT +npm run lint # ESLint / Next core web vitals +npm run typecheck # tsc --noEmit +npm test # Compile test TS, run node --test, clean .test-dist +npm run test:e2e # Playwright E2E/mobile UI tests +npm run sitemap # Regenerate public/sitemap.xml +npm run agent-briefs:sync # Regenerate CLAUDE.md and GEMINI.md from AGENTS.md +npm run agent-briefs:check # Fail if CLAUDE.md or GEMINI.md drift from AGENTS.md +npm run check # Agent brief check + lint + typecheck + unit tests + build +``` + +CI runs the brief sync check, lint, typecheck, unit tests, production build, and Playwright Chromium E2E tests on the latest Node 24 patch. The security workflow runs Gitleaks, `npm audit --audit-level=moderate`, pull request dependency review via GitHub's Dependency Review API, and CodeQL. + +## 7. Conventions and coding standards + +- **Coding standards:** use `skills/coding-standards/SKILL.md` before implementation, refactors, API route work, UI state changes, security-sensitive code, analytics changes, and tests. +- **Package manager:** use npm and keep `package-lock.json`; do not introduce pnpm/yarn lockfiles. +- **Node version:** use Node 24, matching `.nvmrc`, `package.json#engines`, and GitHub Actions. +- **TypeScript:** the project is not yet strict (`strict: false`); keep new code strongly typed and avoid spreading `any` further. +- **Next boundaries:** keep browser-only code behind client components/hooks and server-only APIs in route handlers or `lib/server/*`. +- **API routes:** validate untrusted request bodies before use, enforce rate limits on public write routes, avoid logging secrets or full sensitive submissions, and return stable JSON errors. +- **Forms:** contact and NGU submissions must keep Invisible reCAPTCHA v2 verification and accessible failure states. +- **Analytics:** send events through `lib/analytics.ts` or tracked link components so GA4 and Amplitude payloads stay aligned. +- **SEO:** update metadata, structured data, sitemap generation, and canonical URLs when adding durable public pages or blog behavior. +- **Styling:** use Tailwind utility patterns already present in nearby components; keep pages responsive and verify mobile layouts when touching nav, hero, forms, CTAs, or promotional modals. +- **Testing:** unit-test pure helpers in `tests/*.test.ts`; use Playwright for routed UI behavior, mobile layout, reCAPTCHA flow mocks, and conversion-critical interactions. +- **Completion gate:** before marking meaningful work done, run `npm run lint`, `npm run typecheck`, and relevant tests. For PR-ready changes, run `npm run check`; add E2E when UI behavior changed. + +## 8. Key files map + +| Path | What lives here | +|---|---| +| [app/layout.tsx](app/layout.tsx) | Root metadata, scripts, global shell, nav, NGU promo, footer | +| [app/page.tsx](app/page.tsx) | Home route wrapper | +| [components/HomePageContent.tsx](components/HomePageContent.tsx) | Home page composition | +| [components/NavBar.tsx](components/NavBar.tsx) | Site navigation shell | +| [components/NguPromo.tsx](components/NguPromo.tsx) | Timed NGU promotion modal/banner behavior | +| [components/ContactForm.tsx](components/ContactForm.tsx) | Contact form client UX and reCAPTCHA execution | +| [components/NguCouponSignupForm.tsx](components/NguCouponSignupForm.tsx) | NGU coupon signup client UX | +| [components/questionnaire/QuestionnaireForm.tsx](components/questionnaire/QuestionnaireForm.tsx) | Questionnaire form state and submission | +| [components/questionnaire/steps.ts](components/questionnaire/steps.ts) | Questionnaire question definitions | +| [app/api/analyze/route.ts](app/api/analyze/route.ts) | OpenAI questionnaire analysis route | +| [app/api/contact/route.ts](app/api/contact/route.ts) | Contact email + reCAPTCHA route | +| [app/api/ngu-coupon/route.ts](app/api/ngu-coupon/route.ts) | NGU coupon email + reCAPTCHA route | +| [lib/server/contact.ts](lib/server/contact.ts) | Contact normalization, validation, config, email text | +| [lib/server/ngu-coupon.ts](lib/server/ngu-coupon.ts) | NGU coupon normalization, validation, config, email text | +| [lib/server/rate-limit.ts](lib/server/rate-limit.ts) | In-memory rate limiting helpers | +| [lib/blog.ts](lib/blog.ts) | Blog post normalization and filters | +| [lib/site.ts](lib/site.ts) | Site and brand constants | +| [lib/analytics.ts](lib/analytics.ts) | GA4/Amplitude event helpers | +| [scripts/generate-sitemap.js](scripts/generate-sitemap.js) | Static sitemap generation | +| [playwright.config.ts](playwright.config.ts) | Playwright web server and reporter configuration | +| [skills/coding-standards/SKILL.md](skills/coding-standards/SKILL.md) | Repo-local production coding standards | +| [skills/sync-agent-briefs/SKILL.md](skills/sync-agent-briefs/SKILL.md) | Workflow for syncing agent orientation files | + +--- + +## Maintaining this file + +**Whenever durable project facts change, update `AGENTS.md` in the same change.** Examples that require an update: + +- Adding, removing, renaming, or re-homing a route, API route, component group, content source, test category, or script. +- Changing form submission behavior, analytics conventions, SEO/structured-data behavior, reCAPTCHA/email/OpenAI integrations, or security posture. +- Changing root `package.json` scripts, CI/security workflows, Node version, environment variables, or completion workflow. +- Changing a file listed in [Key files map](#8-key-files-map), or adding something that belongs in it. + +Treat `AGENTS.md` as the canonical source for mirrored harness briefs. After updating it, run `npm run agent-briefs:sync` and `npm run agent-briefs:check` so [CLAUDE.md](CLAUDE.md) and [GEMINI.md](GEMINI.md) stay aligned. + +Do **not** use this file for ephemeral notes, in-flight TODOs, debugging logs, or session state. It is a map, not a journal. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ceb8041 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,174 @@ +# Michael Zick — Agent Orientation (Claude Code) + +This document is the canonical project brief for AI coding agents. Read it at the start of every session instead of re-exploring the repo. Keep it current: see [Maintaining this file](#maintaining-this-file). + +Sibling files [AGENTS.md](AGENTS.md) (Codex) and [GEMINI.md](GEMINI.md) (Gemini CLI) mirror this content for other harnesses. Update all three together when code structure changes. + +--- + +## 1. Project overview + +**michaelzick.com** is Michael Zick's coaching website and conversion platform for Nice Guy Recovery / Reality Alignment coaching. It combines static marketing pages, a blog, lead-capture forms, an AI-assisted questionnaire, analytics instrumentation, and Nice Guy University promotion flows. + +Primary flows: +- Visitors learn about Michael's coaching model through the home, about, testimonials, contact, and Nice Guy University pages. +- Visitors submit a multi-step questionnaire and receive an OpenAI-generated coaching analysis. +- Contact and NGU coupon forms validate Invisible reCAPTCHA v2, rate-limit submissions, and send Brevo SMTP email notifications. +- Blog readers browse JSON-backed posts with category/tag filters and structured data for SEO. +- CTA and navigation interactions are tracked through a shared analytics wrapper for GA4 and Amplitude. + +## 2. Tech stack + +- **Framework:** Next.js 16 App Router with React 19 and TypeScript; the package is currently pinned to the patched 16.3 canary line until the same security fixes land in a stable release. +- **Styling:** Tailwind CSS 3, global styles in `app/globals.css`, image assets in `public/img/`. +- **Server routes:** Next route handlers under `app/api/*`, using Node runtime where email/OpenAI APIs are needed. +- **AI and email:** OpenAI Node SDK for questionnaire analysis; Nodemailer with Brevo SMTP for notifications. +- **Bot protection:** Classic Invisible reCAPTCHA v2 via `NEXT_PUBLIC_RECAPTCHA_SITE_KEY_V2` and `RECAPTCHA_SECRET_KEY_V2`. +- **Analytics:** GA4 and Amplitude scripts in `components/SiteAnalyticsScripts.tsx`; tracked events in `lib/analytics.ts`. +- **Testing:** Node's built-in test runner for compiled unit tests, TypeScript test build via `tsconfig.test.json`, and Playwright for E2E/mobile UI checks. +- **Tooling:** npm with `package-lock.json`, Node 24 LTS, ESLint flat config via `eslint-config-next/core-web-vitals`. + +## 3. Repository layout + +``` +michaelzick.com/ +├── app/ # Next App Router pages, layout, metadata, API routes, sitemap/robots +├── components/ # React UI components, navigation, sections, blog, questionnaire, hooks +├── content/blog/ # JSON-backed blog content fixtures and production posts +├── lib/ # Analytics, blog utilities, structured data, server validation/helpers +├── public/ # Favicons, manifest, generated sitemap, static image assets +├── scripts/ # Node scripts such as sitemap generation +├── skills/ # Repo-local agent skills +├── tests/ # Node unit tests and Playwright E2E tests +├── types/ # Ambient type declarations +├── .github/workflows/ # CI and security automation +├── package.json # npm scripts and dependency list +└── package-lock.json # npm lockfile; keep npm workflow +``` + +## 4. Application structure + +### 4.1 App Router pages + +- `app/layout.tsx` defines global metadata, JSON-LD, analytics scripts, nav, NGU promo, footer, and Open Sans. +- `app/page.tsx` renders the home page through `components/HomePageContent.tsx`. +- `app/about/page.tsx`, `app/testimonials/page.tsx`, and `app/contact/page.tsx` are static marketing/conversion pages. +- `app/contact/ContactContent.tsx` provides the contact page client experience. +- `app/questionnaire/page.tsx` renders the questionnaire flow. +- `app/nice-guy-university/page.tsx` renders the NGU promotional page and outbound CTAs. +- `app/blog/page.tsx` and `app/blog/[slug]/page.tsx` render blog index/detail pages with structured data. +- `app/sitemap.ts` and `app/robots.ts` expose Next-generated SEO metadata routes. + +### 4.2 API routes + +- `app/api/analyze/route.ts` accepts questionnaire submissions, applies honeypot/rate limiting/length checks, calls OpenAI (`gpt-5-mini` with fallback to `gpt-4o-mini`), and optionally emails the result via Brevo SMTP. +- `app/api/contact/route.ts` validates contact submissions, enforces per-IP rate limits, verifies Invisible reCAPTCHA v2, and sends contact email via Brevo. +- `app/api/ngu-coupon/route.ts` validates NGU coupon signups, verifies reCAPTCHA, sends the visitor coupon email, and sends the internal notification email. +- Shared server helpers live in `lib/server/`: contact and NGU normalization/validation/email builders, OpenAI client construction, and in-memory rate limiting. + +### 4.3 Components and client behavior + +- `components/navigation/` contains desktop/mobile navigation primitives used by `components/NavBar.tsx`. +- `components/sections/` contains major home-page content bands; keep visual changes consistent with the existing premium coaching brand. +- `components/questionnaire/` contains the questionnaire steps, fields, form, and analysis rendering. +- `components/blog/` contains blog filters, cards, hero, breadcrumbs, similar posts, and scroll-to-top behavior. +- `components/ContactForm.tsx` and `components/NguCouponSignupForm.tsx` load and execute Invisible reCAPTCHA v2 before posting to API routes. +- `components/TrackedLink.tsx` and `components/TrackedCtaLink.tsx` centralize CTA/link tracking. +- `components/hooks/` contains UI hooks for scroll tracking, fade-in behavior, and title visibility. + +### 4.4 Content, SEO, and analytics + +- Blog source content lives in `content/blog/posts.json`; `lib/blog.ts` normalizes slugs, excerpts, filters, dates, and similar posts. +- Site-wide brand/SEO constants live in `lib/site.ts`. +- Structured data helpers live in `lib/site-structured-data.ts` and `lib/blog-structured-data.ts`. +- `scripts/generate-sitemap.js` writes `public/sitemap.xml`; use `SITE_URL` to override the production base URL. +- Analytics scripts are hardcoded in `components/SiteAnalyticsScripts.tsx`; event dispatch lives in `lib/analytics.ts`. + +## 5. Environment + +No committed `.env.example` currently exists. Environment variables used by the app: + +- `OPENAI_API_KEY` — required for questionnaire analysis. +- `BREVO_SMTP_PASSWORD`, `BREVO_USER`, `BREVO_TO`, `BREVO_FROM` — required for contact and NGU email routes; optional for questionnaire result notification. +- `NEXT_PUBLIC_RECAPTCHA_SITE_KEY_V2` — public Invisible reCAPTCHA v2 site key used by browser forms. +- `RECAPTCHA_SECRET_KEY_V2` — server-side Invisible reCAPTCHA v2 secret used with Google `siteverify`. +- `SITE_URL` — optional sitemap generation override; defaults to `https://www.michaelzick.com`. +- `PORT` — used by `npm start` and Playwright web server startup. +- `CI` and `PLAYWRIGHT_SKIP_BUILD` — influence Playwright server reuse/build behavior. + +Do not commit `.env`, API keys, SMTP credentials, reCAPTCHA secrets, Vercel secrets, or production form exports. + +## 6. Commands + +```bash +npm run dev # Next dev server +npm run build # Production Next build +npm start # Start built Next app on $PORT +npm run lint # ESLint / Next core web vitals +npm run typecheck # tsc --noEmit +npm test # Compile test TS, run node --test, clean .test-dist +npm run test:e2e # Playwright E2E/mobile UI tests +npm run sitemap # Regenerate public/sitemap.xml +npm run agent-briefs:sync # Regenerate CLAUDE.md and GEMINI.md from AGENTS.md +npm run agent-briefs:check # Fail if CLAUDE.md or GEMINI.md drift from AGENTS.md +npm run check # Agent brief check + lint + typecheck + unit tests + build +``` + +CI runs the brief sync check, lint, typecheck, unit tests, production build, and Playwright Chromium E2E tests on the latest Node 24 patch. The security workflow runs Gitleaks, `npm audit --audit-level=moderate`, pull request dependency review via GitHub's Dependency Review API, and CodeQL. + +## 7. Conventions and coding standards + +- **Coding standards:** use `skills/coding-standards/SKILL.md` before implementation, refactors, API route work, UI state changes, security-sensitive code, analytics changes, and tests. +- **Package manager:** use npm and keep `package-lock.json`; do not introduce pnpm/yarn lockfiles. +- **Node version:** use Node 24, matching `.nvmrc`, `package.json#engines`, and GitHub Actions. +- **TypeScript:** the project is not yet strict (`strict: false`); keep new code strongly typed and avoid spreading `any` further. +- **Next boundaries:** keep browser-only code behind client components/hooks and server-only APIs in route handlers or `lib/server/*`. +- **API routes:** validate untrusted request bodies before use, enforce rate limits on public write routes, avoid logging secrets or full sensitive submissions, and return stable JSON errors. +- **Forms:** contact and NGU submissions must keep Invisible reCAPTCHA v2 verification and accessible failure states. +- **Analytics:** send events through `lib/analytics.ts` or tracked link components so GA4 and Amplitude payloads stay aligned. +- **SEO:** update metadata, structured data, sitemap generation, and canonical URLs when adding durable public pages or blog behavior. +- **Styling:** use Tailwind utility patterns already present in nearby components; keep pages responsive and verify mobile layouts when touching nav, hero, forms, CTAs, or promotional modals. +- **Testing:** unit-test pure helpers in `tests/*.test.ts`; use Playwright for routed UI behavior, mobile layout, reCAPTCHA flow mocks, and conversion-critical interactions. +- **Completion gate:** before marking meaningful work done, run `npm run lint`, `npm run typecheck`, and relevant tests. For PR-ready changes, run `npm run check`; add E2E when UI behavior changed. + +## 8. Key files map + +| Path | What lives here | +|---|---| +| [app/layout.tsx](app/layout.tsx) | Root metadata, scripts, global shell, nav, NGU promo, footer | +| [app/page.tsx](app/page.tsx) | Home route wrapper | +| [components/HomePageContent.tsx](components/HomePageContent.tsx) | Home page composition | +| [components/NavBar.tsx](components/NavBar.tsx) | Site navigation shell | +| [components/NguPromo.tsx](components/NguPromo.tsx) | Timed NGU promotion modal/banner behavior | +| [components/ContactForm.tsx](components/ContactForm.tsx) | Contact form client UX and reCAPTCHA execution | +| [components/NguCouponSignupForm.tsx](components/NguCouponSignupForm.tsx) | NGU coupon signup client UX | +| [components/questionnaire/QuestionnaireForm.tsx](components/questionnaire/QuestionnaireForm.tsx) | Questionnaire form state and submission | +| [components/questionnaire/steps.ts](components/questionnaire/steps.ts) | Questionnaire question definitions | +| [app/api/analyze/route.ts](app/api/analyze/route.ts) | OpenAI questionnaire analysis route | +| [app/api/contact/route.ts](app/api/contact/route.ts) | Contact email + reCAPTCHA route | +| [app/api/ngu-coupon/route.ts](app/api/ngu-coupon/route.ts) | NGU coupon email + reCAPTCHA route | +| [lib/server/contact.ts](lib/server/contact.ts) | Contact normalization, validation, config, email text | +| [lib/server/ngu-coupon.ts](lib/server/ngu-coupon.ts) | NGU coupon normalization, validation, config, email text | +| [lib/server/rate-limit.ts](lib/server/rate-limit.ts) | In-memory rate limiting helpers | +| [lib/blog.ts](lib/blog.ts) | Blog post normalization and filters | +| [lib/site.ts](lib/site.ts) | Site and brand constants | +| [lib/analytics.ts](lib/analytics.ts) | GA4/Amplitude event helpers | +| [scripts/generate-sitemap.js](scripts/generate-sitemap.js) | Static sitemap generation | +| [playwright.config.ts](playwright.config.ts) | Playwright web server and reporter configuration | +| [skills/coding-standards/SKILL.md](skills/coding-standards/SKILL.md) | Repo-local production coding standards | +| [skills/sync-agent-briefs/SKILL.md](skills/sync-agent-briefs/SKILL.md) | Workflow for syncing agent orientation files | + +--- + +## Maintaining this file + +**Whenever durable project facts change, update `AGENTS.md` in the same change.** Examples that require an update: + +- Adding, removing, renaming, or re-homing a route, API route, component group, content source, test category, or script. +- Changing form submission behavior, analytics conventions, SEO/structured-data behavior, reCAPTCHA/email/OpenAI integrations, or security posture. +- Changing root `package.json` scripts, CI/security workflows, Node version, environment variables, or completion workflow. +- Changing a file listed in [Key files map](#8-key-files-map), or adding something that belongs in it. + +Treat `AGENTS.md` as the canonical source for mirrored harness briefs. After updating it, run `npm run agent-briefs:sync` and `npm run agent-briefs:check` so [CLAUDE.md](CLAUDE.md) and [GEMINI.md](GEMINI.md) stay aligned. + +Do **not** use this file for ephemeral notes, in-flight TODOs, debugging logs, or session state. It is a map, not a journal. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..e825869 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,174 @@ +# Michael Zick — Agent Orientation (Gemini CLI) + +This document is the canonical project brief for AI coding agents. Read it at the start of every session instead of re-exploring the repo. Keep it current: see [Maintaining this file](#maintaining-this-file). + +Sibling files [CLAUDE.md](CLAUDE.md) (Claude Code) and [AGENTS.md](AGENTS.md) (Codex) mirror this content for other harnesses. Update all three together when code structure changes. + +--- + +## 1. Project overview + +**michaelzick.com** is Michael Zick's coaching website and conversion platform for Nice Guy Recovery / Reality Alignment coaching. It combines static marketing pages, a blog, lead-capture forms, an AI-assisted questionnaire, analytics instrumentation, and Nice Guy University promotion flows. + +Primary flows: +- Visitors learn about Michael's coaching model through the home, about, testimonials, contact, and Nice Guy University pages. +- Visitors submit a multi-step questionnaire and receive an OpenAI-generated coaching analysis. +- Contact and NGU coupon forms validate Invisible reCAPTCHA v2, rate-limit submissions, and send Brevo SMTP email notifications. +- Blog readers browse JSON-backed posts with category/tag filters and structured data for SEO. +- CTA and navigation interactions are tracked through a shared analytics wrapper for GA4 and Amplitude. + +## 2. Tech stack + +- **Framework:** Next.js 16 App Router with React 19 and TypeScript; the package is currently pinned to the patched 16.3 canary line until the same security fixes land in a stable release. +- **Styling:** Tailwind CSS 3, global styles in `app/globals.css`, image assets in `public/img/`. +- **Server routes:** Next route handlers under `app/api/*`, using Node runtime where email/OpenAI APIs are needed. +- **AI and email:** OpenAI Node SDK for questionnaire analysis; Nodemailer with Brevo SMTP for notifications. +- **Bot protection:** Classic Invisible reCAPTCHA v2 via `NEXT_PUBLIC_RECAPTCHA_SITE_KEY_V2` and `RECAPTCHA_SECRET_KEY_V2`. +- **Analytics:** GA4 and Amplitude scripts in `components/SiteAnalyticsScripts.tsx`; tracked events in `lib/analytics.ts`. +- **Testing:** Node's built-in test runner for compiled unit tests, TypeScript test build via `tsconfig.test.json`, and Playwright for E2E/mobile UI checks. +- **Tooling:** npm with `package-lock.json`, Node 24 LTS, ESLint flat config via `eslint-config-next/core-web-vitals`. + +## 3. Repository layout + +``` +michaelzick.com/ +├── app/ # Next App Router pages, layout, metadata, API routes, sitemap/robots +├── components/ # React UI components, navigation, sections, blog, questionnaire, hooks +├── content/blog/ # JSON-backed blog content fixtures and production posts +├── lib/ # Analytics, blog utilities, structured data, server validation/helpers +├── public/ # Favicons, manifest, generated sitemap, static image assets +├── scripts/ # Node scripts such as sitemap generation +├── skills/ # Repo-local agent skills +├── tests/ # Node unit tests and Playwright E2E tests +├── types/ # Ambient type declarations +├── .github/workflows/ # CI and security automation +├── package.json # npm scripts and dependency list +└── package-lock.json # npm lockfile; keep npm workflow +``` + +## 4. Application structure + +### 4.1 App Router pages + +- `app/layout.tsx` defines global metadata, JSON-LD, analytics scripts, nav, NGU promo, footer, and Open Sans. +- `app/page.tsx` renders the home page through `components/HomePageContent.tsx`. +- `app/about/page.tsx`, `app/testimonials/page.tsx`, and `app/contact/page.tsx` are static marketing/conversion pages. +- `app/contact/ContactContent.tsx` provides the contact page client experience. +- `app/questionnaire/page.tsx` renders the questionnaire flow. +- `app/nice-guy-university/page.tsx` renders the NGU promotional page and outbound CTAs. +- `app/blog/page.tsx` and `app/blog/[slug]/page.tsx` render blog index/detail pages with structured data. +- `app/sitemap.ts` and `app/robots.ts` expose Next-generated SEO metadata routes. + +### 4.2 API routes + +- `app/api/analyze/route.ts` accepts questionnaire submissions, applies honeypot/rate limiting/length checks, calls OpenAI (`gpt-5-mini` with fallback to `gpt-4o-mini`), and optionally emails the result via Brevo SMTP. +- `app/api/contact/route.ts` validates contact submissions, enforces per-IP rate limits, verifies Invisible reCAPTCHA v2, and sends contact email via Brevo. +- `app/api/ngu-coupon/route.ts` validates NGU coupon signups, verifies reCAPTCHA, sends the visitor coupon email, and sends the internal notification email. +- Shared server helpers live in `lib/server/`: contact and NGU normalization/validation/email builders, OpenAI client construction, and in-memory rate limiting. + +### 4.3 Components and client behavior + +- `components/navigation/` contains desktop/mobile navigation primitives used by `components/NavBar.tsx`. +- `components/sections/` contains major home-page content bands; keep visual changes consistent with the existing premium coaching brand. +- `components/questionnaire/` contains the questionnaire steps, fields, form, and analysis rendering. +- `components/blog/` contains blog filters, cards, hero, breadcrumbs, similar posts, and scroll-to-top behavior. +- `components/ContactForm.tsx` and `components/NguCouponSignupForm.tsx` load and execute Invisible reCAPTCHA v2 before posting to API routes. +- `components/TrackedLink.tsx` and `components/TrackedCtaLink.tsx` centralize CTA/link tracking. +- `components/hooks/` contains UI hooks for scroll tracking, fade-in behavior, and title visibility. + +### 4.4 Content, SEO, and analytics + +- Blog source content lives in `content/blog/posts.json`; `lib/blog.ts` normalizes slugs, excerpts, filters, dates, and similar posts. +- Site-wide brand/SEO constants live in `lib/site.ts`. +- Structured data helpers live in `lib/site-structured-data.ts` and `lib/blog-structured-data.ts`. +- `scripts/generate-sitemap.js` writes `public/sitemap.xml`; use `SITE_URL` to override the production base URL. +- Analytics scripts are hardcoded in `components/SiteAnalyticsScripts.tsx`; event dispatch lives in `lib/analytics.ts`. + +## 5. Environment + +No committed `.env.example` currently exists. Environment variables used by the app: + +- `OPENAI_API_KEY` — required for questionnaire analysis. +- `BREVO_SMTP_PASSWORD`, `BREVO_USER`, `BREVO_TO`, `BREVO_FROM` — required for contact and NGU email routes; optional for questionnaire result notification. +- `NEXT_PUBLIC_RECAPTCHA_SITE_KEY_V2` — public Invisible reCAPTCHA v2 site key used by browser forms. +- `RECAPTCHA_SECRET_KEY_V2` — server-side Invisible reCAPTCHA v2 secret used with Google `siteverify`. +- `SITE_URL` — optional sitemap generation override; defaults to `https://www.michaelzick.com`. +- `PORT` — used by `npm start` and Playwright web server startup. +- `CI` and `PLAYWRIGHT_SKIP_BUILD` — influence Playwright server reuse/build behavior. + +Do not commit `.env`, API keys, SMTP credentials, reCAPTCHA secrets, Vercel secrets, or production form exports. + +## 6. Commands + +```bash +npm run dev # Next dev server +npm run build # Production Next build +npm start # Start built Next app on $PORT +npm run lint # ESLint / Next core web vitals +npm run typecheck # tsc --noEmit +npm test # Compile test TS, run node --test, clean .test-dist +npm run test:e2e # Playwright E2E/mobile UI tests +npm run sitemap # Regenerate public/sitemap.xml +npm run agent-briefs:sync # Regenerate CLAUDE.md and GEMINI.md from AGENTS.md +npm run agent-briefs:check # Fail if CLAUDE.md or GEMINI.md drift from AGENTS.md +npm run check # Agent brief check + lint + typecheck + unit tests + build +``` + +CI runs the brief sync check, lint, typecheck, unit tests, production build, and Playwright Chromium E2E tests on the latest Node 24 patch. The security workflow runs Gitleaks, `npm audit --audit-level=moderate`, pull request dependency review via GitHub's Dependency Review API, and CodeQL. + +## 7. Conventions and coding standards + +- **Coding standards:** use `skills/coding-standards/SKILL.md` before implementation, refactors, API route work, UI state changes, security-sensitive code, analytics changes, and tests. +- **Package manager:** use npm and keep `package-lock.json`; do not introduce pnpm/yarn lockfiles. +- **Node version:** use Node 24, matching `.nvmrc`, `package.json#engines`, and GitHub Actions. +- **TypeScript:** the project is not yet strict (`strict: false`); keep new code strongly typed and avoid spreading `any` further. +- **Next boundaries:** keep browser-only code behind client components/hooks and server-only APIs in route handlers or `lib/server/*`. +- **API routes:** validate untrusted request bodies before use, enforce rate limits on public write routes, avoid logging secrets or full sensitive submissions, and return stable JSON errors. +- **Forms:** contact and NGU submissions must keep Invisible reCAPTCHA v2 verification and accessible failure states. +- **Analytics:** send events through `lib/analytics.ts` or tracked link components so GA4 and Amplitude payloads stay aligned. +- **SEO:** update metadata, structured data, sitemap generation, and canonical URLs when adding durable public pages or blog behavior. +- **Styling:** use Tailwind utility patterns already present in nearby components; keep pages responsive and verify mobile layouts when touching nav, hero, forms, CTAs, or promotional modals. +- **Testing:** unit-test pure helpers in `tests/*.test.ts`; use Playwright for routed UI behavior, mobile layout, reCAPTCHA flow mocks, and conversion-critical interactions. +- **Completion gate:** before marking meaningful work done, run `npm run lint`, `npm run typecheck`, and relevant tests. For PR-ready changes, run `npm run check`; add E2E when UI behavior changed. + +## 8. Key files map + +| Path | What lives here | +|---|---| +| [app/layout.tsx](app/layout.tsx) | Root metadata, scripts, global shell, nav, NGU promo, footer | +| [app/page.tsx](app/page.tsx) | Home route wrapper | +| [components/HomePageContent.tsx](components/HomePageContent.tsx) | Home page composition | +| [components/NavBar.tsx](components/NavBar.tsx) | Site navigation shell | +| [components/NguPromo.tsx](components/NguPromo.tsx) | Timed NGU promotion modal/banner behavior | +| [components/ContactForm.tsx](components/ContactForm.tsx) | Contact form client UX and reCAPTCHA execution | +| [components/NguCouponSignupForm.tsx](components/NguCouponSignupForm.tsx) | NGU coupon signup client UX | +| [components/questionnaire/QuestionnaireForm.tsx](components/questionnaire/QuestionnaireForm.tsx) | Questionnaire form state and submission | +| [components/questionnaire/steps.ts](components/questionnaire/steps.ts) | Questionnaire question definitions | +| [app/api/analyze/route.ts](app/api/analyze/route.ts) | OpenAI questionnaire analysis route | +| [app/api/contact/route.ts](app/api/contact/route.ts) | Contact email + reCAPTCHA route | +| [app/api/ngu-coupon/route.ts](app/api/ngu-coupon/route.ts) | NGU coupon email + reCAPTCHA route | +| [lib/server/contact.ts](lib/server/contact.ts) | Contact normalization, validation, config, email text | +| [lib/server/ngu-coupon.ts](lib/server/ngu-coupon.ts) | NGU coupon normalization, validation, config, email text | +| [lib/server/rate-limit.ts](lib/server/rate-limit.ts) | In-memory rate limiting helpers | +| [lib/blog.ts](lib/blog.ts) | Blog post normalization and filters | +| [lib/site.ts](lib/site.ts) | Site and brand constants | +| [lib/analytics.ts](lib/analytics.ts) | GA4/Amplitude event helpers | +| [scripts/generate-sitemap.js](scripts/generate-sitemap.js) | Static sitemap generation | +| [playwright.config.ts](playwright.config.ts) | Playwright web server and reporter configuration | +| [skills/coding-standards/SKILL.md](skills/coding-standards/SKILL.md) | Repo-local production coding standards | +| [skills/sync-agent-briefs/SKILL.md](skills/sync-agent-briefs/SKILL.md) | Workflow for syncing agent orientation files | + +--- + +## Maintaining this file + +**Whenever durable project facts change, update `AGENTS.md` in the same change.** Examples that require an update: + +- Adding, removing, renaming, or re-homing a route, API route, component group, content source, test category, or script. +- Changing form submission behavior, analytics conventions, SEO/structured-data behavior, reCAPTCHA/email/OpenAI integrations, or security posture. +- Changing root `package.json` scripts, CI/security workflows, Node version, environment variables, or completion workflow. +- Changing a file listed in [Key files map](#8-key-files-map), or adding something that belongs in it. + +Treat `AGENTS.md` as the canonical source for mirrored harness briefs. After updating it, run `npm run agent-briefs:sync` and `npm run agent-briefs:check` so [CLAUDE.md](CLAUDE.md) and [GEMINI.md](GEMINI.md) stay aligned. + +Do **not** use this file for ephemeral notes, in-flight TODOs, debugging logs, or session state. It is a map, not a journal. diff --git a/README.md b/README.md index eca74d8..1c61015 100644 --- a/README.md +++ b/README.md @@ -55,20 +55,20 @@ npm test ## Contact Form reCAPTCHA Setup -The contact form uses classic **reCAPTCHA v3**, not reCAPTCHA Enterprise. +The contact form uses classic **Invisible reCAPTCHA v2**, not reCAPTCHA Enterprise. -- Browser token generation uses `NEXT_PUBLIC_RECAPTCHA_SITE_KEY`. -- Server-side verification uses `RECAPTCHA_SECRET_KEY` and Google `siteverify`. +- Browser token generation uses `NEXT_PUBLIC_RECAPTCHA_SITE_KEY_V2`. +- Server-side verification uses `RECAPTCHA_SECRET_KEY_V2` and Google `siteverify`. - This setup does **not** use Google Cloud ADC, `gcloud auth application-default login`, service accounts, or reCAPTCHA Enterprise client libraries. Required local and deployment environment variables: ```bash -NEXT_PUBLIC_RECAPTCHA_SITE_KEY=your_v3_site_key -RECAPTCHA_SECRET_KEY=your_v3_secret_key +NEXT_PUBLIC_RECAPTCHA_SITE_KEY_V2=your_invisible_v2_site_key +RECAPTCHA_SECRET_KEY_V2=your_invisible_v2_secret_key ``` -In the Google reCAPTCHA admin console, make sure the key is a **v3** key and the allowed domains include every origin that can submit the contact form: +In the Google reCAPTCHA admin console, make sure the key is an **Invisible v2** key and the allowed domains include every origin that can submit the contact form: - `localhost` - the production domain diff --git a/app/about/page.tsx b/app/about/page.tsx index f0dcd2d..467cffd 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -42,7 +42,7 @@ export default function About() { return (
-
+

diff --git a/app/api/contact/route.ts b/app/api/contact/route.ts index 4679b23..24e7a90 100644 --- a/app/api/contact/route.ts +++ b/app/api/contact/route.ts @@ -85,11 +85,9 @@ export async function POST(req: NextRequest) { if (!captchaValidation.valid) { console.error('reCAPTCHA token invalid', { errorCodes: captchaValidation.errorCodes, - action: captchaValidation.action, - score: captchaValidation.score, }); return NextResponse.json( - { success: false, error: `Captcha verification failed: ${captchaValidation.errorCodes?.join(', ') || 'low score'} (score: ${captchaValidation.score})` }, + { success: false, error: `Captcha verification failed: ${captchaValidation.errorCodes?.join(', ') || 'invalid token'}` }, { status: 400 }, ); } diff --git a/app/api/ngu-coupon/route.ts b/app/api/ngu-coupon/route.ts new file mode 100644 index 0000000..f399c7e --- /dev/null +++ b/app/api/ngu-coupon/route.ts @@ -0,0 +1,119 @@ +import { NextRequest, NextResponse } from 'next/server'; +import nodemailer from 'nodemailer'; +import { RECAPTCHA_SITE_VERIFY_URL } from '../../../lib/recaptcha'; +import { consumeRateLimit, getClientIp } from '../../../lib/server/rate-limit'; +import { + buildNguCouponNotificationEmail, + buildNguCouponVisitorEmail, + getNguCouponConfig, + isValidNguRecaptchaResponse, + NGU_COUPON_RATE_LIMIT_MAX_REQUESTS, + NGU_COUPON_RATE_LIMIT_WINDOW, + normalizeNguCouponSubmission, + validateNguCouponSubmission, +} from '../../../lib/server/ngu-coupon'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +const rateLimitMap = new Map(); + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const submission = normalizeNguCouponSubmission(body); + + const rateLimit = consumeRateLimit({ + key: getClientIp(req.headers), + store: rateLimitMap, + windowMs: NGU_COUPON_RATE_LIMIT_WINDOW, + maxRequests: NGU_COUPON_RATE_LIMIT_MAX_REQUESTS, + }); + + if (!rateLimit.allowed) { + return NextResponse.json( + { success: false, error: 'Too many requests. Please try again in an hour.' }, + { status: 429 }, + ); + } + + const validationError = validateNguCouponSubmission(submission); + if (validationError) { + return NextResponse.json({ success: false, error: validationError }, { status: 400 }); + } + + const config = getNguCouponConfig(); + if (!config) { + console.error('NGU coupon service configuration is incomplete'); + return NextResponse.json( + { success: false, error: 'Email service not configured' }, + { status: 500 }, + ); + } + + const captchaResponse = await fetch(RECAPTCHA_SITE_VERIFY_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + secret: config.recaptchaSecretKey, + response: submission.captchaToken!, + }), + }); + + if (!captchaResponse.ok) { + console.error('NGU reCAPTCHA siteverify request failed', captchaResponse.status); + return NextResponse.json( + { success: false, error: `Captcha verification request failed (${captchaResponse.status})` }, + { status: 400 }, + ); + } + + const verification = await captchaResponse.json(); + const captchaValidation = isValidNguRecaptchaResponse(verification); + + if (!captchaValidation.valid) { + console.error('NGU reCAPTCHA token invalid', { + errorCodes: captchaValidation.errorCodes, + }); + return NextResponse.json( + { success: false, error: `Captcha verification failed: ${captchaValidation.errorCodes?.join(', ') || 'invalid token'}` }, + { status: 400 }, + ); + } + + const transporter = nodemailer.createTransport({ + host: 'smtp-relay.brevo.com', + port: 587, + auth: { + user: config.userName, + pass: config.password, + }, + }); + + const visitorEmail = buildNguCouponVisitorEmail(submission.email!); + const notificationEmail = buildNguCouponNotificationEmail(submission.email!); + + await transporter.sendMail({ + from: config.fromAddress, + to: submission.email, + subject: visitorEmail.subject, + text: visitorEmail.text, + }); + + await transporter.sendMail({ + from: config.fromAddress, + to: config.toAddress, + replyTo: submission.email, + subject: notificationEmail.subject, + text: notificationEmail.text, + }); + + return NextResponse.json({ success: true }); + } catch (err) { + console.error('Failed to send NGU coupon email', err); + return NextResponse.json( + { success: false, error: 'Failed to send coupon email' }, + { status: 500 }, + ); + } +} diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx index 3b91e1b..89ee79a 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/blog/[slug]/page.tsx @@ -82,7 +82,7 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) { ]; return ( -
+
diff --git a/app/contact/ContactContent.tsx b/app/contact/ContactContent.tsx index 0600001..1bb5e79 100644 --- a/app/contact/ContactContent.tsx +++ b/app/contact/ContactContent.tsx @@ -1,8 +1,8 @@ 'use client'; import { useRef, useState, useEffect } from 'react'; +import { OpenInNewWindowIcon } from '@radix-ui/react-icons'; import ContactForm from '../../components/ContactForm'; -import ContactRecaptchaLoader from '../../components/ContactRecaptchaLoader'; import { FadeInSection } from '../../components/FadeInSection'; import Image from 'next/image'; import { TestimonialsCarouselSection } from '../../components/sections/TestimonialsCarouselSection'; @@ -10,6 +10,7 @@ import TrackedCtaLink from '../../components/TrackedCtaLink'; import { trackLinkClick } from '../../lib/analytics'; export default function ContactContent() { + const bookingCtaLabel = 'Book a Strategy Call'; const testimonialsSectionRef = useRef(null); const testimonialsTitleRef = useRef(null); const [testimonialsVisible, setTestimonialsVisible] = useState(false); @@ -44,8 +45,7 @@ export default function ContactContent() { return (
- -
+

@@ -57,8 +57,8 @@ export default function ContactContent() {
Michael Zick - Book Your Free 45-Min Session + {bookingCtaLabel} +
diff --git a/app/globals.css b/app/globals.css index fb34ebb..3471242 100644 --- a/app/globals.css +++ b/app/globals.css @@ -81,6 +81,22 @@ html { } @layer components { + .page-top-offset { + padding-top: 11rem; + } + + @media (min-width: 930px) { + .page-top-offset { + padding-top: 12rem; + } + } + + @media (min-width: 1440px) { + .page-top-offset { + padding-top: 13rem; + } + } + .nav-link { @apply relative flex items-center text-white text-right font-light text-base leading-[28.8px] whitespace-nowrap px-0 py-[1.6px]; } @@ -130,6 +146,20 @@ html { padding: 9px 16px 10px; } + .nav-button-outline { + @apply inline-flex items-center justify-center rounded-lg border-2 border-cta-amber bg-transparent text-cta-amber font-semibold whitespace-nowrap transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cta-amber/50 focus-visible:ring-offset-2 text-base leading-4; + padding: 8px 15px 9px; + } + + .nav-button-outline:hover { + background: rgb(var(--cta-amber) / 0.1); + } + + .nav-button-outline:active { + transform: scale(0.97); + transition-duration: 100ms; + } + .blog-content { @apply text-lg leading-relaxed text-default-grey; } @@ -212,14 +242,28 @@ html { height: 88px; padding: 0 42px; white-space: nowrap; + max-width: 100%; + text-align: center; + } + + .cta-unified>span { + min-width: 0; } @media (max-width: 929px) { .cta-unified { font-size: 16px; - line-height: 16px; - height: 54px; - padding: 0 24px; + line-height: 1.15; + min-height: 54px; + height: auto; + padding: 14px 18px; + white-space: normal; + } + + .mobile-nav-link { + min-height: 44px; + padding-top: 0.375rem; + padding-bottom: 0.375rem; } } diff --git a/app/layout.tsx b/app/layout.tsx index 858753e..d57bc7d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,7 @@ import NavBar from '../components/NavBar'; import Footer from '../components/Footer'; import JsonLd from '../components/JsonLd'; import SiteAnalyticsScripts from '../components/SiteAnalyticsScripts'; +import NguPromo from '../components/NguPromo'; import { Open_Sans } from 'next/font/google'; import { siteConfig } from '../lib/site'; import { getSiteStructuredData } from '../lib/site-structured-data'; @@ -80,6 +81,7 @@ export default function RootLayout({ children }: { children: React.ReactNode; }) +
{children}