Skip to content

DylanHias/SoRelax

Repository files navigation

So'Relax — Massagetherapie Website

Marketing site for So'Relax, a solo massage-therapy practice run by Tanja in Aarschot, Belgium. The site's job is simple: explain the treatments, let visitors book a session (through Salonized, in a new tab), and be editable by a non-technical owner without a developer in the loop.

It's not a spa booking platform, not a blog, not a storefront. Eight treatments on one page, a few supporting pages (about, contact, gift cards, terms), and a CMS that exposes exactly the fields Tanja needs to edit.

What this is, concretely

  • A static Next.js 15 siteoutput: 'export' → plain HTML/CSS/JS, no server runtime, hosted on Cloudflare Pages' free tier. Deliberate: the brief (§2) rejects SSR, edge runtimes, ISR, and route handlers.
  • TinaCMS with three collectionstreatments, testimonials, settings. Editing happens at /admin (Git-backed; changes commit to main and trigger a Pages rebuild). Everything else is hardcoded Dutch copy in components, migrated from the old site per brief §7.
  • Dutch only (nl-BE) — UI, slugs (/over-mij, /behandelingen, /afspraak, /cadeaubon), CMS labels, error messages. The only English phrase is the intentional hero tagline "Your me-time starts with… me".
  • "Therapist, not spa" — warm neutrals (#FAF7F2 / #2F5D5A), Fraunces
    • Inter, borders over shadows, no candles/stones/lotus stock. The palette and type are load-bearing brand decisions, not cosmetic defaults — see CLAUDE.md for the full list of things not to do.
  • Salonized is external-only — every booking CTA opens Salonized in a new tab. No iframe, no modal, no embed script on our origin. This is why LCP is fast and why there's no third-party to gate behind cookie consent.
  • Cookie consent is DIY — a small client-side banner (CookieConsent.tsx) stores { necessary, analytics, marketing } in localStorage under sorelax-consent and emits sorelax-consent-change events. useConsent() is the hook any future third-party script should gate on. No iubenda / Cookiebot.
  • No contact form. /contact lists address, phone, email, and opening hours; inquiries go through Salonized booking or direct phone/email. There is no form backend and no plan to add one.

Source-of-truth documents:

  • project-brief.md — the authoritative spec (tech stack, page list, design system, CMS schema, perf/a11y budgets, build order). Read it before making architectural decisions.
  • CLAUDE.md — the non-obvious constraints that are easy to violate accidentally (the list of "don't reintroduce X" items).
  • websitecontent.md — raw Dutch copy from the previous site, used to seed the CMS and hardcoded pages.

Getting started

pnpm install
pnpm dev          # TinaCMS local + Next.js at http://localhost:3000
                  # /admin → local Tina editor (writes to content/**/*.json on disk)
pnpm build        # tinacms build --local → next build → static export to ./out
pnpm typecheck
pnpm lint

Node 20+ (see .nvmrc); package manager pinned to pnpm@10.

  • pnpm dev:next / pnpm build:next skip the Tina wrapper — useful when iterating on components without regenerating the admin bundle.
  • pnpm build:cloud builds the admin against Tina Cloud instead of the local filesystem — switch to this once NEXT_PUBLIC_TINA_CLIENT_ID and TINA_TOKEN are provisioned.

Project layout

tina/
  config.ts               # TinaCMS schema (3 collections — treatments, testimonials, settings)
content/                  # JSON files owned by TinaCMS — edited via /admin
  treatments/treatments.json
  testimonials/testimonials.json
  settings/settings.json
src/
  app/                    # 9 routes (home + 8 pages) + sitemap, robots
                          # /admin is generated by tinacms build into public/admin/
  components/
    ui/                   # Button, Card, Container, Section, PullQuote, Accordion, Icons…
    sections/             # Home-page sections (Hero, Specializations, …)
    Nav.tsx / Footer.tsx / FloatingBookingButton.tsx / CookieConsent.tsx
  content/                # Typed readers over the TinaCMS JSON files
    treatments.ts         # getTreatments, getTreatmentsByCategory
    testimonials.ts       # getTestimonials, getFeaturedTestimonials
    settings.ts           # getSettings (with env-var fallback for Salonized / social)
  lib/
    fonts.ts              # next/font self-hosted Fraunces + Inter
    site.ts               # siteConfig, nav structure
    schema.ts             # JSON-LD builders (LocalBusiness, Person, Service)
    format.ts
docs/
  editor-handleiding.md   # Dutch editor guide for Tanja (login, price updates, hours)

Design tokens live in both src/app/globals.css (as CSS custom properties under :root) and the Tailwind @theme inline block in the same file. Don't add a token to one without the other.

TinaCMS collections

Exactly three, per brief §6 — adding more is a maintenance tax for a solo non-technical editor.

Collection File Shape
treatments content/treatments/treatments.json Ordered list of 8 services (therapeutic + relaxation). Drag-reorder in the editor. Each entry has an optional salonizedLink override.
testimonials content/testimonials/testimonials.json Ordered list of reviews. featured: true = shown on the home carousel.
settings content/settings/settings.json Single document: opening hours, Salonized widget URLs, Instagram / Facebook handles.

Field labels and help text in the admin UI are Dutch — Tanja is the editor. The Dutch editor guide is in docs/editor-handleiding.md.

Environment

Copy .env.example.env.local and fill in what you have. The site still builds with everything empty — CMS-backed fields fall back to env vars, which fall back to sane placeholders.

Variable Needed for
NEXT_PUBLIC_SITE_URL Absolute URLs in metadata, sitemap, OG tags
NEXT_PUBLIC_TINA_CLIENT_ID, TINA_TOKEN /admin against Tina Cloud (prod) — not needed for local dev
NEXT_PUBLIC_SALONIZED_WIDGET_URL, NEXT_PUBLIC_SALONIZED_GIFTCARD_URL Fallback for booking widget + gift-card CTA when the CMS field is empty
NEXT_PUBLIC_INSTAGRAM_URL, NEXT_PUBLIC_FACEBOOK_URL Fallback for footer social links when the CMS field is empty

About

Website for a massage therapist and pain coach

Topics

Resources

Stars

Watchers

Forks

Contributors