# Mac VPN Guide — Developer Documentation

An editorial VPN comparison site operated by **VPNSuperFast**, the makers of **VPNFast**.

## Why the site is shaped this way

Google Ads rejects campaigns that promote an app or an App Store link directly. So `/` is
written as a genuine comparison page — six Mac VPNs ranked on privacy, network, Mac support
and value — and every CTA for our own product points at
**https://vpnproxymaster.app/vpn-for-macos/**, never at the App Store.

Two rules follow from that, and both matter more than any styling detail:

1. **No App Store links anywhere.** `grep -rn "apps.apple.com" app components lib public`
   must come back empty.
2. **Nothing is fabricated.** Competitor ratings are their live App Store scores; every
   other figure is the provider's own published number. VPNFast is shown *without* a star
   rating because its macOS listing has none. Our ownership is disclosed in the top bar, on
   the card, in the methodology section and in the footer.

## Tech Stack

| Technology | Version | Purpose |
|---|---|---|
| **Next.js** | 16.2.9 | React framework (App Router, Server Components, Turbopack) |
| **React** | 19.2.4 | UI library |
| **TypeScript** | 5.x | Type safety |
| **Tailwind CSS** | 4.x | Theme lives in `app/globals.css` under `@theme inline {}` — there is no `tailwind.config.js` |
| **Lucide React** | 1.17.0 | Icons (`Award`, `Check`) |
| **CVA + clsx + tailwind-merge** | — | Variants and class merging (`lib/cn.ts`) |

`framer-motion` and `@radix-ui/react-accordion` are still in `package.json` but nothing
imports them. Animation is CSS-only; the FAQ uses native `<details>`.

## Project Structure

```
├── app/
│   ├── layout.tsx           # Metadata, fonts, GTM/GA, JSON-LD built from lib/vpn-data
│   ├── page.tsx             # Composes the guide (Server Component)
│   ├── globals.css          # Tailwind v4 theme, .reveal, reduced-motion + print rules
│   ├── contact/             # Email only — no form, there is no backend
│   ├── disclaimer/          # Advertising disclosure + ownership
│   ├── privacy-policy/      # Covers this website, not the app
│   └── terms/
│
├── components/
│   ├── sections/
│   │   ├── DisclosureBar.tsx    # Advertising + ownership disclosure strip
│   │   ├── GuideHeader.tsx      # Sticky nav. variant="home" | "page"
│   │   ├── GuideHero.tsx        # #top — headline, lead, last-updated pill
│   │   ├── EditorsPick.tsx      # #picks — featured VPNFast card
│   │   ├── Alternatives.tsx     # Ranked competitors 1–5
│   │   ├── ComparisonTable.tsx  # #compare — 9-column table, scrolls on mobile
│   │   ├── Methodology.tsx      # Ranking criteria + transparency statement
│   │   ├── GuideFAQ.tsx         # #faq — native <details>
│   │   ├── GuideFooter.tsx      # Disclosure box, links, trademark notices
│   │   └── LegalLayout.tsx     # Shared shell + LegalSection/LegalList for the 4 legal pages
│   └── ui/
│       ├── VpnCard.tsx          # One card, `featured` when provider.isOurs
│       ├── StarRating.tsx       # Renders nothing when rating is null
│       ├── Reveal.tsx           # IntersectionObserver fade-up
│       ├── SectionHeading.tsx   # eyebrow + h2 + lead
│       ├── Badge.tsx, Container.tsx, SectionWrapper.tsx
│
├── lib/
│   ├── vpn-data.ts          # ALL page content — providers, criteria, FAQ
│   ├── types.ts             # VpnProvider, FAQItem, Criterion
│   ├── site-config.ts       # Brand names and the product URL
│   └── cn.ts
│
└── public/images/vpn-logos/ # Official 512px App Store icons
```

## Editing content

Everything on the page comes from **`lib/vpn-data.ts`**. Add or reorder a provider there and
the cards, the comparison table and the JSON-LD `ItemList` all follow. The FAQ answers feed
the `FAQPage` schema from the same array, so the two can't drift apart.

Each provider carries two refund fields on purpose: `refund` is the short value for the
card's spec panel ("30 days") and `guarantee` is the full phrasing for the comparison
table ("30-day money-back"). Keep `summary` to roughly 130–155 characters — the cards
are scannable at that length and turn into a wall of text beyond it.

`rating: null` is meaningful — it suppresses the stars rather than rendering zero. Keep it
that way for any product we can't cite a real rating for.

### Refreshing the figures

Ratings and rating counts come from Apple's public lookup API:

```bash
curl -s "https://itunes.apple.com/lookup?id=905953485&country=us" | python3 -m json.tool
```

App Store IDs for the current line-up:

| Provider | App ID | | Provider | App ID |
|---|---|---|---|---|
| NordVPN | `905953485` | | CyberGhost | `583009522` |
| ExpressVPN | `886492891` | | Hotspot Shield | `443369807` |
| Surfshark | `1391782046` | | Turbo VPN | `1365309175` |
| Proton VPN | `1437005085` | | VPN Proxy Master | `1025707485` |
| Private Internet Access | `955626407` | | | |

**Do not hardcode provider names anywhere else.** The trademark notice in the footer and
on `/disclaimer` is generated from `trademarkList` in `lib/vpn-data.ts` — both previously
drifted out of sync after providers were swapped. `public/llms.txt` is the one remaining
hand-maintained copy; update it whenever the line-up changes.

Server counts, country counts, device limits and refund windows change with promotions —
re-check them against each provider before a campaign and bump `siteConfig.lastUpdated`,
which is printed in the hero and under the table.

## Legal pages

Contact, Disclaimer, Privacy and Terms all render through `LegalLayout`, which supplies the
header, title, "last updated" line, the back/email footer row and `GuideFooter`. Write a new
one as a list of `<LegalSection heading="…">` blocks — don't hand-roll the chrome.

The Contact page is deliberately **email only**. There is no backend and no form endpoint, so
a contact form would silently fail; the page links `siteConfig.supportEmail` instead.

Dates on these pages come from `siteConfig.lastUpdated`, so they move together with the hero
pill and the table footnote.

## Two layout details worth knowing

**The comparison table sits in a wider wrapper than the rest of the page.** Body copy uses
`Container` (1080px), but the table needs ~1140px to show all nine columns, so it gets its own
`max-w-[1200px]` wrapper. It fits without scrolling from 1024px up and scrolls inside its own
container below that. If you add a column, re-check that fit.

**The comparison table wrapper needs `relative`.** A statically-positioned scroll container
lets a `<table>`'s overflow escape to the document root in Chrome, which widened the whole
page on mobile. `<main>` also carries `min-w-0` because it is a flex item of
`body.flex-col`, where `min-width: auto` would otherwise refuse to shrink.

**`.reveal` hides content only behind `@media (scripting: enabled)`.** Without that guard,
a JS failure, a printer or a non-rendering crawler would see `opacity: 0` on the page's
entire content. Reduced-motion and print both force it visible.

## Development

```bash
npm install
npm run dev      # localhost:3000
npm run build
npm run start
npm run lint     # currently clean: 0 errors, 0 warnings
```

Accessing the dev server over a network IP requires adding it to `allowedDevOrigins` in
`next.config.ts`, or Next 16 blocks the JS bundles and interactivity fails silently.

## Before deploying

1. `npm run build`, `npm run lint` and `npx tsc --noEmit` all clean.
2. `grep -rn "apps.apple.com" app components lib public` → empty.
3. Re-verify every figure in `lib/vpn-data.ts`; update `siteConfig.lastUpdated`.
4. Check `/`, `/terms` and `/privacy-policy` for horizontal overflow from 320px up.
5. Run the JSON-LD through Google's Rich Results Test.
