# VPN Website — Lessons Learned

> Updated after corrections or important discoveries. Read at session start.

---

## Project-Specific

### 1. Next.js version mismatch
- **PDF spec says Next.js 15, but project uses Next.js 16.2.9**
- Always check `node_modules/next/dist/docs/` for breaking changes before writing code
- AGENTS.md explicitly warns about this

### 2. Tailwind CSS v4 — no config file
- Theme goes in `globals.css` via `@theme inline {}`, NOT in `tailwind.config.js`
- Already set up in the boilerplate — extend it, don't replace the pattern

### 3. Font choice
- User wants ProtonVPN-style fonts, globally configurable
- ~~Original: Bricolage Grotesque (headings) + Inter (body)~~ — USER REJECTED, "doesn't look good"
- **Final: Inter for both headings and body** (same as ProtonVPN). Single variable `--font-inter`.
- CSS variables `--font-heading` and `--font-body` both point to `--font-inter` for easy future swapping

### 4. Brand color discrepancy
- HTML reference uses `#0054FC`, PDF spec says `#0A84FF`
- **Use PDF spec color `#0A84FF`** as primary brand color

### 5. Brand name is placeholder
- "SuperVPN Proxy" is temporary — must be stored in `lib/site-config.ts` as a global constant
- Company: "GREEN DIGITAL CONSULTING LLP" — also in site-config

### 6. Content source
- All copy comes from `/Users/p21-0059/Downloads/vpn-mac-site_2.html`
- Pricing is in USD, not INR
- 7 FAQ items, 3 reviews, 3 pricing plans

### 7. Shadcn UI + Tailwind v4 compatibility
- Don't run `npx shadcn@latest init` — may conflict with Tailwind v4
- Instead: manually install Radix primitives + CVA + clsx + tailwind-merge

### 8. HTML has extra sections not in PDF spec
- Sticky header/nav bar
- Promo countdown timer in hero
- "Laurel" trusted worldwide badge
- These should all be implemented (they're in the reference HTML)

---

## General Patterns

### 9. Framer Motion + React type conflicts
- `motion.button` with `...React.ButtonHTMLAttributes<HTMLButtonElement>` spread causes `onDrag` type incompatibility
- **Fix:** Don't extend `HTMLButtonAttributes`. Use an explicit prop interface listing only the props you need (children, className, onClick, type, disabled, aria-label)
- This applies to any `motion.*` component where you spread native HTML props

### 10. Next.js 16 scroll behavior
- Next.js 16 no longer overrides `scroll-behavior: smooth` during navigation
- Must add `data-scroll-behavior="smooth"` to `<html>` element to restore smooth scrolling
- Also added CSS `scroll-behavior: smooth` on `html` in globals.css for anchor links

---

## Comparison-page rebuild (Sept 2026)

### 11. `<table>` overflow escapes an unpositioned scroll container
- A `<table min-width:880px>` inside `<div class="overflow-x-auto">` still widened
  `documentElement.scrollWidth` to 855px on a 390px viewport — the wrapper clipped
  visually but the overflow propagated to the root.
- **Fix:** add `relative` to the scroll wrapper. `contain: paint` also works;
  `max-width: 100%` and `min-width: 0` on the wrapper do **not**.
- Verified by measuring `scrollWidth - clientWidth` in an iframe at 15 widths, not by
  eyeballing screenshots.

### 12. Headless screenshots below ~500px lie
- `--window-size=390,2300 --screenshot` produced a 390px-wide PNG with content visibly
  cut off, on a page that measured zero overflow. Chrome enforces a minimum window width
  and lays the page out wider than the capture canvas.
- **Fix:** render the page in an `<iframe>` of the exact width inside a wide window, and
  screenshot that. Trust `scrollWidth`/`clientWidth` over the image.

### 13. Don't mutate `<html>` className for a no-JS guard
- `document.documentElement.classList.add('js')` in a blocking script caused a React 19
  hydration attribute mismatch.
- **Fix:** `@media (scripting: enabled)` in CSS — no script, no mismatch, and it fails
  open (content visible) on browsers without support.

### 14. JSX can swallow the space after an expression
- `Figures verified {siteConfig.lastUpdated} from each…` rendered as `2026from`.
- **Fix:** explicit `{" "}`. Worth grepping the rendered HTML for `\w<!-- -->\w` to catch
  every instance rather than fixing one by eye.

### 15. Reveal-on-scroll must never gate real content invisibly
- Wrapping the table, methodology and FAQ in an `opacity-0` reveal meant a print render
  and any no-JS client saw blank sections — on a page whose entire value is its content.
- Always pair a scroll reveal with print + reduced-motion + no-JS escape hatches.

### 16. Providers block server-side fetches; Apple doesn't
- nordvpn.com and cyberghostvpn.com return 403 to WebFetch and to curl with a browser UA.
- Apple's `itunes.apple.com/lookup` and `/search` are open, need no auth, and return the
  official listing name, seller, rating, rating count and a 512px icon — first-party data
  for competitor comparisons.

### 17. Never let a scroll animation own the *hidden* state in CSS
- `.reveal:not(.reveal-in){opacity:0}` meant that if the client effect never ran, every
  wrapped section stayed permanently blank — the cards, table, methodology and FAQ all
  vanished while their headings stayed visible.
- **Fix:** visible is the default; the component adds `.reveal-hidden` only after it has
  confirmed the element is below the fold, and a 4s failsafe timer always removes it.
  Elements already on screen are never hidden at all.
- Rule: an animation must never be the thing that decides whether content is readable.

### 18. Moving a project directory invalidates `.next`
- After `mv`-ing the repo, `.next/dev/**` still contained 6 files with absolute paths to
  the old location. Stale dev chunks break hydration, which (combined with #17) rendered
  the page's whole body invisible.
- **Fix:** `rm -rf .next` after moving a project. Do it before debugging anything else.

### 19. IntersectionObserver does not work in old headless Chrome
- `--headless` (old): the IO callback never fires, not even the initial one.
- `--headless=new`: the initial callback fires, but `window.scrollTo` under
  `--virtual-time-budget` still produces no further callbacks.
- Consequence: headless cannot validate scroll-reveal behaviour. Assert on the DOM
  (`.reveal-hidden` count) and on a failsafe path instead, and confirm visually.
