Marketing site for Tachyon, an AI-augmented digital-SAT tutoring business run by two 99th-percentile Cambridge, MA students. Built with Next.js (App Router) + TypeScript + Tailwind CSS, deploys cleanly to Vercel, with an optional Supabase lead-capture form.
Design system: "physics lab notebook meets editorial print" — warm paper, near-black ink, a single ultramarine accent, a graph-paper grid motif, hairline rules, and restrained monospace labels. Type: Newsreader (display) + Archivo (body) + Space Mono (labels), all via next/font.
pnpm install # (or: npm install)
pnpm dev # (or: npm run dev)Open http://localhost:3000.
The site runs with zero configuration — Calendly shows a "set your link" placeholder and the lead form shows a friendly "not connected yet" message until you add the values below. Nothing crashes; add config when you're ready.
Built and tested with
pnpm, but plainnpm install/npm run devwork identically.
Copy .env.local.example → .env.local and fill in:
| Variable | Required for | Notes |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Lead form | Supabase → Project Settings → API → Project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Lead form | Supabase → Project Settings → API → anon/publishable key. Public & safe to expose. |
There is no service-role key anywhere in this app — leads are read from the Supabase dashboard. Never put a service-role key in a NEXT_PUBLIC_* variable.
If these are absent, the booking section still works via Calendly; the request form just reports it isn't connected yet.
All site copy and data live in src/lib/site.ts. Edit the arrays and the components re-render — no JSX changes needed.
| What to change | Where in src/lib/site.ts |
|---|---|
| Business name, email, Instagram, URL | site object |
| Calendly link | site.calendlyUrl — replace REPLACE-ME |
| Nav links (and their routes) | nav |
| Hero trust line | trustLine |
| The "how it works" steps | systemSteps |
| Credential stats | stats |
| Tutor bios | tutors |
| Services table | services (set flag for a badge) |
| Pricing | pricing (set recommended: true / flag) + pricingNotes |
| Digital-SAT edge | edgePoints |
| How-we-use-AI copy | aiParagraph |
| Testimonials | testimonials — replace [Testimonial coming soon] or append more |
| FAQ | faqs — add/edit { q, a } objects |
| Form dropdown options | gradeOptions, sectionOptions, formatOptions |
The site is split across routes rather than one long scroll:
/— home: hero, credentials, a directory of links, testimonials, booking./how-it-works— the system, the digital-SAT edge, how we use AI./tutors— the two bios./pricing— services + pricing./faq— the FAQ accordion.
Each lives in src/app/<route>/page.tsx and composes the shared section components from src/components/sections/.
- Create an event type in Calendly and copy its link (e.g.
https://calendly.com/your-handle/diagnostic). - In
src/lib/site.ts, setcalendlyUrlto that link (search the repo forREPLACE-ME). - The placeholder is automatically replaced by the live inline scheduler (lazy-loaded for speed).
Design tokens live in tailwind.config.ts (color ramps, fonts, type scale) mirrored by CSS variables in src/app/globals.css. To change the accent, edit the accent ramp in the config and --accent in globals — the whole site follows. Fonts are wired via next/font in src/app/layout.tsx. The graph-paper grid is the .grid-paper / .grid-paper-fine utilities in globals; tune --grid to change its strength.
The hero's interactive dot field is src/components/TachyonField.tsx — it's a small canvas that reacts subtly to the cursor and is automatically static on mobile and when prefers-reduced-motion is set.
The "Request a free diagnostic" form inserts one row into a leads table using the public anon key. Row Level Security allows anonymous INSERT only — the public can submit, but no one can read leads from the browser. You read leads in the Supabase dashboard (Table Editor).
In the Supabase dashboard → SQL Editor, run:
-- Table
create table if not exists public.leads (
id uuid primary key default gen_random_uuid(),
created_at timestamptz not null default now(),
student_name text not null,
grade text,
current_score int,
target_score int,
sections text,
format text,
parent_email text not null,
message text
);
-- Enable Row Level Security
alter table public.leads enable row level security;
-- Allow anonymous INSERT only (public form can submit; no SELECT/UPDATE/DELETE)
create policy "Anon can insert leads"
on public.leads
for insert
to anon
with check (true);Because RLS is enabled and only an INSERT policy exists, the anon role cannot read, update, or delete rows — exactly right for a public form.
Put NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in .env.local (local) and in the Vercel dashboard (production — see below).
Open Supabase → Table Editor → leads. New submissions appear here.
- Push this repo to GitHub/GitLab/Bitbucket.
- In Vercel, Add New → Project and import the repo. Framework preset auto-detects Next.js — no config needed.
- Under Settings → Environment Variables, add (for Production, Preview, and Development):
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEY
- Deploy. Build command (
next build) and output are auto-configured. - After deploy: set your real
calendlyUrlinsrc/lib/site.tsand push (or edit it before first deploy).
If you deploy without the Supabase vars, the site still builds and runs — only the request form is disabled (Calendly still works). Add the vars and redeploy to enable it.
Vercel → Settings → Domains → add your domain and follow the DNS steps. Then update site.url in src/lib/site.ts so Open Graph / canonical URLs are correct.
src/
app/
layout.tsx # fonts, SEO metadata, Open Graph
page.tsx # home (hero, credentials, directory, testimonials, booking)
how-it-works/page.tsx
tutors/page.tsx
pricing/page.tsx
faq/page.tsx
icon.svg # favicon (Tachyon glyph)
opengraph-image.tsx # generated 1200×630 OG image
globals.css # tokens (CSS vars) + grid motif + base styles
components/
sections/ # Hero, System, WhyUs, Directory, Tutors, Services,
# Pricing, DigitalEdge, UseOfAI, Testimonials, Faq, Booking
Header, Footer, Wordmark, Reveal, TachyonField, SectionHeader,
CtaBand, FaqAccordion, LeadForm, CalendlyEmbed
lib/
site.ts # ALL content/data (edit here)
supabase.ts # browser Supabase client (graceful if unconfigured)
tailwind.config.ts # design tokens
- Semantic landmarks (
header/main/footer/nav), skip-to-content link, labeled form fields,aria-expandedaccordion + mobile menu, keyboard-focusable controls with visible focus rings. - Respects
prefers-reduced-motion(animations + hero field disabled). - The hero dot field is a single lightweight canvas, static on mobile; Calendly and its script are lazy-loaded on scroll; fonts use
next/fontwithdisplay: swap. - Decorative SVG/canvas are
aria-hidden; the wordmark exposes an accessible label.