TRAINYOURAGENT

Seven Lights Counseling: a therapy practice site with zero third-party trackers

We built a psychotherapy practice site on Next.js 15 and React 19: twelve routes including a No Surprises Act Good Faith Estimate page, a first-party lead route, generated sitemap and robots, and a dependency tree with no analytics package, no tag manager and no third-party tracking script in it.

The problem

A therapy practice site has a visitor problem no other small-business site has. Someone researching a psychotherapist is disclosing something about themselves by the act of reading the page. The default modern stack — a tag manager, an analytics script, a remarketing pixel, and a hosted font — means that visit is shared with four companies before the person has typed anything. Most practice sites do this without noticing, because the tooling is the default and nobody frames a pixel as a disclosure. For this build we treated it as one, and the constraint drove the architecture: no tag manager, no analytics package, no remarketing pixel, self-hosted fonts rather than a font CDN. The second requirement was regulatory rather than technical. Under the No Surprises Act, self-pay clients are entitled to a Good Faith Estimate of expected charges, and a practice site that discusses rates has a specific obligation to surface it. That is a page with legal content requirements, not a pricing page with a disclaimer.

Twelve routes, statically rendered, self-hosted everything

The App Router carries twelve `page.tsx` routes: home, about, approach, services, a dynamic `services/[slug]`, rates, faq, contact, privacy, good-faith-estimate, and a two-step `begin` funnel with its own thank-you page. Twenty-eight components sit behind them. There are ten runtime dependencies and it is worth listing them, because the list is the claim: Next, React, React DOM, Tailwind Merge, clsx, GSAP, Lenis, sharp, and two Fontsource packages for Fraunces and Inter. There is no `@vercel/analytics`, no `react-ga`, no `posthog-js`, no `@segment/analytics`, no tag manager. The two Fontsource packages are the specific reason fonts are self-hosted: a Google Fonts link is a request to a third party carrying the visitor's IP and referrer, and on a page about seeking therapy that referrer is the sensitive part. `sitemap.ts` and `robots.ts` are generated route handlers rather than static files, so the indexable surface derives from the routes rather than being maintained alongside them — the same principle we apply on our own site, at a twentieth of the scale.

The Good Faith Estimate is a route, not a footnote

`src/app/good-faith-estimate/page.tsx` is a first-class page with its own URL, linked from the rates page. That placement is deliberate: an estimate obligation that only exists as a paragraph inside a pricing page is easy for a visitor to miss and hard for a practice to point at. This is the sort of requirement that separates a site built for a regulated practice from a template with the practice's name on it. Alongside it sit the privacy page and an FAQ that carries the practical questions — insurance, sliding scale, cancellation — that determine whether someone books.

Motion that is felt rather than watched, and a first-party funnel

The motion system is GSAP with Lenis for smooth scrolling, plus an `Atmosphere` component on the home page. On most sites this would be decoration. Here it is doing tonal work: the visual register a therapy practice needs is calm and unhurried, and that is communicated by easing curves and pacing far more than by copy. The build discipline around media is worth noting — a `prebuild` script fetches and processes media through sharp, so images are optimised at build time and served from the same origin rather than from an image CDN. That is consistent with the no-third-parties position rather than an unrelated performance choice. Lead capture is first-party: `src/app/api/lead/route.ts` is a route handler in the same deployment, so an enquiry goes to the practice's own backend rather than through a form vendor. `MatchFlow`, `StickyConsult` and `ExitIntent` make up the conversion surface. Exit-intent on a therapy site is a judgement call and we would defend it as implemented — it offers a way to start rather than interrupting with an offer. The first-party form is the other half of the no-trackers position, and the half people forget. A site can have no analytics script and still route every enquiry through a third-party form builder, which means the most sensitive submission on the whole site — someone describing why they are seeking therapy — passes through a vendor nobody named in the privacy policy. A route handler in the same deployment means the data goes from the visitor's browser to the practice, and nowhere else.

A services taxonomy that has to work for two readers at once

`services/page.tsx` and `services/[slug]/page.tsx` carry the practice's modalities as a dynamic route family, which is the structurally correct shape for a set of pages that share a template and differ in content — the same pattern we use at far larger scale on our own site. The content problem underneath it is more interesting than the routing. A therapy services page has two readers with opposite needs. One is a search engine that wants a clear, conventional, keyword-legible description of a modality. The other is a person in distress who does not know what the modality is called and is describing a feeling rather than a treatment. Writing pages that serve both without sounding like either a textbook or a brochure is the actual work, and it is why the FAQ and the `approach` page exist as separate routes rather than as sections — someone who wants the clinical framing and someone who wants to know whether they will be judged are looking for different pages. `ThemeToggle`, `PageHero`, `CtaBand` and `Accordion` make up a small, deliberately conventional component set. Twenty-eight components for twelve routes is a low ratio, and that is the intent: a site this size does not need a design system, it needs a handful of components used consistently.

The numbers, and the command behind each one

What is genuinely hard about this

Convincing anyone to give up analytics. The instinct that a site without measurement is a site flying blind is correct in general and wrong here, and arguing that takes longer than building the site. The resolution we would offer to any practice in a sensitive category: server-side request logs on your own infrastructure answer most of the questions you actually have, and none of them involve handing a visitor's identity to an advertising company. If richer measurement is genuinely needed later, it can be added deliberately, with a privacy policy that describes it honestly — which is the opposite of the usual order. Motion on a site like this is harder to get right than motion on a marketing site, because the failure mode is not 'boring', it is 'inappropriate'. A parallax flourish that would read as polished on a SaaS landing page reads as glib on a page about grief. Most of the tuning work went into making the motion slower and smaller than instinct suggested. And the ordinary compliance difficulty: legal-content pages have to be correct, current, and written to be understood by someone who is anxious. A Good Faith Estimate page that is technically compliant and incomprehensible has met the letter of the requirement and failed the person it exists for.

What this entry does not claim

Stack and status

Built with Next.js 15.5, React 19, TypeScript 5.9, Tailwind CSS 3.4, GSAP, Lenis, sharp and Fontsource (self-hosted Fraunces + Inter). Relationship: Client build. Stage: In production. Period: 2026. Our role: Site architecture, motion system, compliance pages, funnel Figures re-derived on 2026-08-23.