A fast, SEO-first, destination-led marketing website for Sea Saba, a professional scuba diving operation on the island of Saba in the Dutch Caribbean. Production site: https://www.seasaba.com (deployed on Vercel from master).
| Doc | Contents |
|---|---|
| docs/TESTING.md | Test pyramid, commands, CI gates, accessibility and production smoke suites, performance budgets |
| docs/ANALYTICS_SEO.md | Analytics/GTM/consent architecture, tracked events, sitemap and indexing rules |
| docs/COOKIEBOT_CONSENT_SETUP.md | Cookiebot CMP + GTM consent-mode runbook |
| docs/SENTRY.md | Sentry error-monitoring baseline: activation gate, privacy posture, production verification |
| docs/OPERATIONS.md | Source of truth for operational facts (schedules, requirements, contact, Checkfront boundary) |
| docs/DEPENDENCIES.md | Dependabot update policy, dependency groups, and the major-version migration workflow |
| docs/design/THEME_UX_GUIDE.md | Brand colors, typography, spacing, imagery and UX rules |
| docs/design/TYPOGRAPHY_AUDIT.md | Issue #112 typography audit: measured font rendering vs declared system, canonical-scale proposal |
| docs/design/IMAGE_STANDARD.md | Image categories, ratios, naming and the PageHero/FeatureImage components |
| docs/design/Sea_Saba_Logo_Spec_DEC_21.pdf | Official logo specification (brand asset) |
| docs/historical/ | Completed-work records: Wix migration report, original IA plan, early homepage spec, unimplemented reviews setup |
| AI_INSTRUCTIONS.md | Guardrails for AI coding agents |
| SECURITY.md | Security policy and vulnerability reporting |
See the testing guide for the audit, coverage, fixtures, CI and required merge check. With Node 24, run npm ci, npx playwright install chromium webkit, then npm run test:ci. For fast feedback, use npm test or npm run test:watch.
This project is a migration from Wix to a custom Next.js stack.
The website is intentionally designed as a premium, destination-first marketing site, not a generic dive shop catalog.
The homepage is a positioning + routing page, while inner pages carry the SEO-rich informational depth.
The site should communicate:
- Why dive Saba
- Why choose Sea Saba
- What experiences are available
- How to take the next step
- Destination-first — sell Saba as a unique diving destination
- Experience-led — emphasize boat diving, signature sites, and trip planning
- Trust-first — calm, professional, safety-forward
- SEO-first — semantic, indexable, stable content architecture
- Conversion-ready — clear routing and strong Checkfront booking paths
This is not intended to be a retail-heavy or generic dive shop template.
- Framework: Next.js 16 (App Router)
- Language: TypeScript
- Styling: Tailwind CSS 4 + shadcn/ui
- Content: TSX pages for marketing / informational / dive site content
- Dynamic Data: Firestore (dives, boats, sites, species — public read-only)
- Booking: Checkfront (deep links, embedded widgets)
- Analytics: Vercel Analytics, Google Analytics 4 (optional), Google Tag Manager (optional)
- PDF Export: jsPDF (premium card-style dive log export)
- Deployment: Vercel
- Node.js 24 — pinned in
.nvmrc. Version managers that read.nvmrc(nvm on macOS/Linux, fnm everywhere) pick it up automatically (nvm use/fnm use). nvm-windows ignores.nvmrc— runnvm install 24once, thennvm use <installed-24.x>in each new shell. - npm — ships with Node. No global CLIs are required; Playwright,
Lighthouse and all other tooling are project dependencies run via
npm run/npx. - Playwright browsers — only for the browser test suites, not for
npm run dev:npx playwright install chromium webkit(~370 MB download). - Chrome/Chromium — only for
npm run test:perf: a system Chrome,CHROME_PATH, or the Playwright Chromium install are all discovered automatically.
npm ci # reproducible install from package-lock.json
cp .env.example .env.local # placeholders are fine — see below
npm run dev # http://localhost:3000Use npm ci, not npm install: CI and every documented test command assume
the exact lockfile dependency set. npm install may silently rewrite
package-lock.json.
.env.local can stay entirely placeholder: the site starts, every page
renders, and no analytics tags load. The only features that need real values
are live /dive-log data (Firebase) and the /cookie-policy declaration
widget (Cookiebot CBID). .env.example documents what each variable does and
which are optional. Do not use production credentials locally — a personal
Firebase project is enough for dive-log work.
- The dev server,
npm run build, lint, typecheck, unit/integration tests, coverage, browser E2E and performance budgets all work with placeholder env vars — the test build injects a demo Firebase project itself. /dive-logrenders its "unable to load" state when Firestore is unreachable; no other page depends on Firebase./bookloads the real Checkfront widget fromseasaba.checkfront.com(read-only browsing is safe — do not complete a real booking). If the vendor script is unreachable the page falls back to a direct booking link.- GTM/GA4/ads/Clarity/Meta load only when
NEXT_PUBLIC_GTM_IDis set, so local browsing sends no analytics. Vercel Analytics only reports from Vercel deployments; its/_vercel/*calls simply 404 on localhost.
EBADENGINEwarnings or strange install/test failures — checknode -v; it must be 24.x. On Windows,npmfromC:\Program Files\nodejspins its own bundled Node regardless of your version manager — make sure the manager's shim precedes it on PATH (where node).- nvm-windows didn't switch versions — it does not read
.nvmrc; runnvm install 24andnvm use <installed-24.x>explicitly. - Playwright "browser not found" — run
npx playwright install chromium webkit. On Linux also install the OS libraries (npx playwright install --with-deps chromium webkit); CI uses the official Playwright container which already has them. test:perfcan't find Chrome — install Chrome, setCHROME_PATH, or rely on the Playwright Chromium from the step above.- Port already in use — dev uses 3000;
test:e2e/test:smokestart their own server on 3100 andtest:perfon 3101, and they refuse to reuse a running server. Stop othernextprocesses first. /dive-logshows an error state — expected without real FirebaseNEXT_PUBLIC_*values; everything else is unaffected.test-public-firestore-read.mjsprints "read allowed (0 docs)" — it does not load.env.local; export the unprefixedFIREBASE_*variables first (seedocs/TESTING.md).test:smoke:prodhits the live site — it is a read-only post-deploy check against https://www.seasaba.com, not a local test. On PowerShell set the target with$env:SMOKE_BASE_URL="https://..."(POSIXVAR=valuesyntax does not work there).- Windows shells — snippets assume POSIX (
cp,export,VAR=value cmd). In PowerShell useCopy-Itemand$env:VAR="..."; cmd.exe has neithercpnorexport. - Stale
.nextoutput — after switching branches, delete.nextand rebuild (npm run build:testfor tests,npm run buildfor parity).
There are exactly two layouts in this project:
- Static hero image at top
- Minimal copy
- Destination positioning + routing
- Strong but restrained CTA hierarchy
- No video backgrounds (the former video section was removed for performance in September 2026; see
docs/design/THEME_UX_GUIDE.mdif video is ever reintroduced)
- Used for all non-home pages
- No video backgrounds
- Text-first, SEO-focused
- Breadcrumbs encouraged
- Semantic, content-rich, stable
Do not introduce additional layouts unless explicitly requested.
The homepage (app/(en)/page.tsx) currently follows this structure:
- Hero (Static Image)
- Why Saba — supporting static destination section
- The Dives That Made Saba Famous — dive-area showcase with CTAs
- Plan Your Trip — static routing sections
- Final CTA
The homepage should answer:
- Why dive Saba?
- Why Sea Saba?
- What can I choose?
- Where do I go next?
- The homepage is not a content dump
- The homepage is not the primary SEO text page for every topic
- Detailed content belongs on inner pages
- Motion is minimal and only used on the homepage
app/
├── layout.tsx # Root layout (header, footer, fonts, SEO, JSON-LD)
├── page.tsx # Homepage (destination-first layout)
├── not-found.tsx # Custom 404 page
├── robots.ts # robots.txt
├── sitemap.ts # sitemap.xml
├── book/page.tsx # Booking page (Checkfront widget + fallback)
├── (content)/ # Standard content layout group
│ ├── layout.tsx # Content layout (breadcrumbs, prose, SEO-first)
│ ├── about/
│ ├── contact/
│ ├── cookie-policy/
│ ├── courses/
│ ├── dive-log/
│ ├── dive-sites/
│ ├── diving/
│ ├── partners/
│ ├── plan-your-trip/
│ ├── privacy/
│ └── terms/
components:
├── ui/ # shadcn/ui components
├── header.tsx # Sticky header
├── footer.tsx # Site footer
├── footer-wrapper.tsx # Footer wrapper
├── hero.tsx # Homepage hero
├── breadcrumbs.tsx # Breadcrumb navigation
├── booking-cta.tsx # Reusable booking call-to-action
├── booking-widget.tsx # Checkfront embedded widget
├── dive-log-client.tsx # Interactive dive log UI
├── find-sea-saba.tsx # Map / location component
└── structured-data.tsx # JSON-LD LocalBusiness structured data
lib/
├── metadata.ts # SEO metadata helpers
├── constants.ts # Site-wide constants (URLs, nav items)
├── analytics.ts # Shared event tracking (GTM data layer + Vercel Analytics)
├── firebase.ts # Firebase client SDK setup
├── firestore/
│ └── dive-log.ts # Firestore dive log fetching and normalization
├── dive-log-export.ts # Premium PDF export for selected dives
└── ...
public/
└── images/ # Site images; optimized assets live in images/optimized/
Tests live under tests/ (unit, integration, e2e, production smoke) — see docs/TESTING.md.
The site is launched: all core pages, Checkfront booking integration, the
Firestore dive log, the legacy Wix redirect map, SEO metadata, analytics and
consent plumbing are live, with a full test/CI gate and post-deployment
production smoke suite (docs/TESTING.md). For the migration record see
docs/historical/MIGRATION_REPORT.md.
The site is deployed on Vercel from the master branch.
Set these in the Vercel project (Production + Preview). For local development
copy .env.example to .env.local — every value there may stay a
placeholder.
Critical: the browser-side Firebase SDK requires the public variables to use the NEXT_PUBLIC_ prefix:
NEXT_PUBLIC_FIREBASE_API_KEY
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN
NEXT_PUBLIC_FIREBASE_PROJECT_ID
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID
NEXT_PUBLIC_FIREBASE_APP_ID
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID
NEXT_PUBLIC_GTM_ID
NEXT_PUBLIC_COOKIEBOT_CBID
Production environment only (Sentry error monitoring — see docs/SENTRY.md; leaving these unset elsewhere keeps Sentry inert):
NEXT_PUBLIC_SENTRY_DSN
SENTRY_ORG
The canonical site URL is not an environment variable — it is the
SITE_URL constant in lib/constants.ts (used by metadata, sitemap, robots
and structured data). If the domain ever changes, update that constant.
These operational settings live outside the repository and must already exist (or be configured by an admin) — they cannot be inferred from the code:
- Vercel project linked to this repo, deploying
mastertohttps://www.seasaba.com(production domain alias + TLS managed there), with the env vars above configured and a Node version compatible with 24. - GitHub repository settings: branch ruleset/protection on
masterrequiring theCritical website testscheck (seedocs/TESTING.md), and CodeQL default setup for code scanning (seeSECURITY.md). - Firebase console: the production Firebase project and its public-read
Firestore rules for
dives/sites/species/boats(rules are not versioned here). - GTM container (
GTM-5PFMJFN) and Cookiebot account/domain group — all tag and consent configuration lives in those dashboards; seedocs/COOKIEBOT_CONSENT_SETUP.md. - Checkfront account (
seasaba.checkfront.com) — booking inventory, checkout and its own GTM integration are vendor-managed. - Sentry project (
sea-saba-web) — error monitoring target; the DSN is set as a Production-only env var (seedocs/SENTRY.md).
Public read access is required for the collections used by the live site:
match /dives/{docId} { allow read: if true; }
match /boats/{docId} { allow read: if true; }
match /sites/{docId} { allow read: if true; }
match /species/{docId} { allow read: if true; }
These rules are managed in the Firebase Console — they are not versioned in this repository.
- Ensure environment variables are set in Vercel (Production + Preview)
- Merge changes to
master - Vercel builds and deploys automatically
- Verify
/dive-logloads dives and/sitemap.xmlis valid
- Check Vercel Analytics for Core Web Vitals
- Monitor Search Console for crawl errors and 301 redirect coverage
- Keep 301 redirects in
data/redirects.tspopulated from the old Wix site - Verify GA4 / GTM events fire in Google Tag Assistant or browser DevTools Network tab
- Vercel Analytics: enabled independently through
@vercel/analytics/next. - Google Tag Manager: loaded via
AnalyticsLoaderonly whenNEXT_PUBLIC_GTM_IDis set; GA4 and marketing tags live inside GTM, not in the app. - Consent: Cookiebot CMP is deployed through GTM; see
docs/COOKIEBOT_CONSENT_SETUP.md.
Business events go through the trackEvent/trackLinkClick/trackBookingClick
helpers in lib/analytics.ts (booking clicks, contact handoffs, directions,
social/partner links, PDF export). The canonical event list, parameter
contract, and indexing rules are documented in
docs/ANALYTICS_SEO.md.
- Clarity over flash — text-first, SEO-focused content pages
- Trust over hype — calm, professional, conservation-minded tone
- Speed over spectacle — static generation, minimal JS, optimized assets
- Motion is minimal — only on homepage, degrades gracefully on mobile
- Destination over catalog — sell Saba and the Sea Saba experience first
- Understated sophistication — premium without being flashy
- Checkfront is the system of record
- Use:
- Deep links
- Embedded widgets
- Optional availability previews
- Do not move checkout logic into the website
- The website must still convert even if widgets fail
- Core marketing pages
- Diving overview pages
- Dive site pages
- Trip planning content
- FAQs and informational content
- Testimonials
- Reviews
- Staff profiles
- Dynamic operational / timely data
- Homepage hero uses a static full-bleed image
- No full-image color filter overlay by default
- Text readability should come from:
- image selection
- typography
- subtle local gradient behind text only
- light text-shadow
- Inner pages should remain calm, stable, and text-first
The site should feel like:
Premium expedition diving on a special island.
And the brand message should feel like:
Experienced. Deliberate. Professional.