Marketing site for fynix.digital — a studio that helps cybersecurity companies turn their websites into growth engines through UI/UX, technical excellence, AI-ready SEO, and predictable lead generation.
The site is organised around four Acts — the studio's service model — each with its own landing page, deliverables, proof metrics, and linked case studies:
| Act | Slug | Focus |
|---|---|---|
| 01 · UI/UX | ui-ux |
Interfaces that earn trust before a line is read |
| 02 · Development | development |
Technical execution and performance |
| 03 · SEO | seo |
AI-ready organic visibility |
| 04 · Lead Generation | lead-generation |
Predictable, measurable pipeline |
Everything is statically prerendered except the two API routes, so pages ship as HTML and hydrate for motion and interaction.
| Layer | Choice | Version |
|---|---|---|
| Framework | Next.js (App Router, Turbopack) | 16.2.10 |
| UI runtime | React | 19.2.4 |
| Language | TypeScript | 5.9.3 |
| Styling | Tailwind CSS v4 (CSS-first @theme) |
4.3.2 |
| Animation | Motion | 12.42.2 |
| Smooth scroll | Lenis | 1.3.25 |
| WebGL | OGL | 1.0.11 |
| Map rendering | dotted-map | 3.1.0 |
| Error monitoring | @sentry/nextjs |
10.68.0 |
| Transactional email | Brevo API | — |
| Uptime | Better Stack | — |
| Hosting | Vercel | — |
| Linting | ESLint (eslint-config-next) |
9.39.5 |
Requirements: Node.js >=20.9.0 (enforced by Next 16) and npm.
git clone https://github.com/digibranders/fynix-digital-v2.git
cd fynix-digital-v2
npm install
cp .env.example .env # then fill in the values below
npm run devOpen http://localhost:3000.
| Variable | Scope | Required | Purpose |
|---|---|---|---|
BREVO_API_KEY |
Server | Yes, for the contact form | Sends the admin notification and user auto-reply |
SENTRY_AUTH_TOKEN |
Build | Production only | Uploads source maps so production stack traces are readable |
NEXT_PUBLIC_SITE_URL |
Client | Optional | Overrides the canonical URL used in metadata |
SENTRY_DSN / NEXT_PUBLIC_SENTRY_DSN |
Server / Client | Optional | Override the DSN; the project DSN is hardcoded as a fallback |
CMS_API_URL |
Server | For the blog | Origin of the fynix-cms read API. Vercel (marketing site) variable only |
CMS_READ_TOKEN |
Server | For the blog | Bearer token for the CMS read API. Vercel variable only; a secret |
REVALIDATE_SECRET |
Server | For the blog | Bearer secret for POST /api/revalidate. Vercel variable only; a secret |
VERCEL_ENV, NEXT_PUBLIC_VERCEL_ENV and VERCEL_PROJECT_PRODUCTION_URL are injected by Vercel and need no local configuration.
Never commit .env — it is gitignored, and SENTRY_AUTH_TOKEN and BREVO_API_KEY are both secrets.
Posts are written in fynix-cms and read from its API (lib/cms/). A post lives at an address the editor chooses, so one catch-all route (app/(site)/[...path]/page.tsx) renders it by its full path; existing routes always win. The blog index is /blog, with /blog/page/<n>, /blog/category/<slug> and /blog/author/<slug> (both also take /page/<n>).
The blog is dormant until CMS_API_URL and CMS_READ_TOKEN are both set: /blog and post addresses return 404 and the sitemap adds nothing, so it is safe to deploy before the CMS is live. Set CMS_API_URL, CMS_READ_TOKEN and REVALIDATE_SECRET on Vercel only; the VPS API does not serve these pages.
Cached CMS responses refresh after 5 minutes. To refresh sooner the CMS calls:
curl -X POST https://fynix.digital/api/revalidate \
-H "Authorization: Bearer $REVALIDATE_SECRET" \
-H "Content-Type: application/json" \
-d '{"paths": ["/blog/my-post"]}'paths is optional: the changed post addresses (send the old and the new address when one moved). Without it every CMS page is revalidated. Answers 200 {"revalidated": true, ...}, 401 for a missing or wrong secret, 400 for a malformed body. Cache tags: cms (all), cms:posts, cms:post:<path>, cms:categories, cms:authors, cms:sitemap, cms:redirects.
| Command | Does |
|---|---|
npm run dev |
Dev server with Turbopack |
npm run build |
Production build (uploads source maps when SENTRY_AUTH_TOKEN is set) |
npm start |
Serve the production build |
npm run lint |
ESLint |
npx tsc --noEmit |
Typecheck |
app/ App Router pages, all Server Components
api/contact/ Contact form → Brevo (admin + auto-reply)
api/lead-diagnostic/ Act-specific diagnostic capture
case-studies/[slug]/ Per-client case studies
services/[slug]/ Per-Act service pages
global-error.tsx Root-layout error boundary → Sentry
robots.ts Generated robots.txt
sitemap.ts Generated sitemap.xml
components/ Client Components ("use client")
lib/
content.ts Single source of truth: acts, case studies, siteConfig
email/ Brevo client, templates, HTML previews
emphasize.tsx Renders **bold** spans inside content strings
scripts/ One-off asset generation
instrumentation*.ts Sentry init per runtime
sentry.*.config.ts Sentry init per runtime
| Path | Type |
|---|---|
/ |
Static |
/about, /process, /faqs, /contact |
Static |
/privacy, /terms |
Static |
/services, /services/[slug] |
SSG (4 acts) |
/case-studies, /case-studies/[slug] |
SSG (5 case studies) |
/blog, /blog/page/[n], /blog/category/[slug], /blog/author/[slug] |
ISR, from the CMS (404 until configured) |
/[...path] |
ISR, a CMS post at its full address, or a CMS redirect, else 404 |
/robots.txt, /sitemap.xml |
Generated |
/api/revalidate |
Dynamic, POST, bearer REVALIDATE_SECRET |
/api/contact |
Dynamic (Node runtime) |
/api/lead-diagnostic |
Dynamic |
/monitoring |
Sentry tunnel (rewrite, bypasses ad blockers) |
- Page-level components are Server Components. Anything interactive lives in
components/behind"use client". - State is local. React hooks only — no global store.
- Content is data. Copy, metrics, deliverables and case studies live in
lib/content.ts, not in JSX.**double asterisks**in content strings are rendered as emphasis bylib/emphasize.tsx. - Design tokens are defined in
app/globals.cssvia Tailwind v4's@theme. Use the semantic names (--color-primary,--color-text-muted, …) rather than raw values. - See
CLAUDE.mdandDESIGN.mdfor the fuller architectural and visual direction.
Note:
AGENTS.mdflags that this is Next.js 16 — APIs and conventions differ from older versions. Checknode_modules/next/dist/docs/before relying on remembered patterns.
Sentry is wired across all three runtimes (browser, Node, Edge) via instrumentation.ts, with onRequestError capturing server component, route handler and server action failures, and app/global-error.tsx catching root-layout crashes.
| Signal | Status |
|---|---|
| Errors | On, unsampled |
| Tracing | On, 10% in production (100% in dev) |
| Logs | On, console capture at warn/error |
| Session Replay | Off |
| Profiling | Off |
Replay and profiling are deliberately disabled. Sentry bills quotas per organization, not per project, so the free plan's 50 replays/month are shared across every project in the org and are worth more on the app projects than on a marketing site. Continuous profiling is not included on the free plan at all.
Privacy: request bodies are excluded from every runtime (dataCollection: { httpBodies: [] }) because the contact and lead-diagnostic routes receive names, emails, phone numbers and free-text messages. Console capture is scoped to warn/error for the same reason — app/api/lead-diagnostic/route.ts logs submitter details at log level, which must not reach Sentry.
/api/contact validates the submission (required name, email, phone and at least one service; 2000-character field cap) then sends two transactional emails through Brevo: an admin notification and a user auto-reply.
All 15 outbound emails share one design system.
| File | Holds |
|---|---|
lib/email/design.ts |
Palette, type scale, document shell, and the primitives every template composes. The only file that touches raw colour or opens a <table>. |
lib/email/templates.ts |
Contact form and Technical SEO Audit, customer reply plus internal notification. |
lib/email/pavelTemplates.ts |
The workshop lifecycle: priority list, paid confirmation, reminders, certificate, recording, missed-you. |
To see them, render the gallery and open it:
npm run email:previewThat writes .email-preview/ (gitignored). Serve it with the email-preview entry in .claude/launch.json, or open .email-preview/index.html directly. Every template is rendered from source with sample data, alongside its plain-text alternative.
The masthead uses public/email/logo.png, exported from components/Logo.tsx. Re-run npx tsx scripts/generate-email-logo.tsx after any change to the logo component. It has to be a raster image: no mainstream mail client renders an SVG <img>.
lib/email/templates.test.ts asserts the invariants that hold across every template (escaping, one masthead and one footer, no 8-digit hex, no SVG images, subject and body agreeing on what they promise). Add a template, and it is covered automatically once it is listed there.
Buyers must receive our emails and only our emails. Zoom will otherwise send its own, and its registration confirmation carries the join link, which defeats the rule that the link is withheld until the one-hour reminder.
Create the Zoom webinar from the Semantic SEO Masterclass (Fynix emails only) template. Zoom copies every setting from a template except panelists, alternative host, and time, so a webinar created this way starts silenced. Created from scratch it does not: a new webinar defaults to sending a registration confirmation, and there is no account-level setting that changes that default. It is per webinar, every time.
If a webinar is ever created without the template, open Manage → Email Settings and confirm all five rows read "No ...":
| Row | Required |
|---|---|
| Panelist invite | No Invitation Email to Panelists |
| Confirmation email | No Confirmation Email Sent to Registrants |
| Reminder email | No reminder email to Attendees and Panelists |
| Attendee email | No follow-up email to Attendees |
| Absentee email | No follow-up email to Absentees |
Zoom shows a standing banner urging you to turn the confirmation email back on ("We recommend turning it on or sending your own confirmation email"). We do send our own. Ignore it.
To verify without the UI, read the live settings back:
npx tsx scripts/zoom-webinar-emails.ts <webinarId>Registration must also stay on Manually Approve. registerAttendee pushes paid buyers in with auto_approve, which Zoom only honours on a manually-approved webinar; switching to automatic approval opens the public registration page as a second, unpaid door into the session.
Designed, not built. No code for this exists yet.
A control layer for the company's own outgoings: a registry of every recurring
subscription, a ledger of the invoices each one is expected to produce, and
multi-channel collection (vendor API, billing mailbox, manual upload) that files
the documents in Cloudflare R2 and makes a missing one impossible to overlook.
It becomes a third section of the admin console at /admin/finance, using the
existing gateway, session and console primitives.
Note the naming trap: the existing invoices table means tax invoices Fynix
issues to workshop buyers. Everything in that system is prefixed finance_
and the inbound artifact is called a document, never an invoice.
| Document | Holds |
|---|---|
docs/finance/architecture.md |
Architecture review, gap analysis, target design, and the critique of the prior spec |
docs/finance/data-model.md |
Every table, column, index, constraint and state machine |
docs/finance/connector-architecture.md |
Capability model, connector interface, per-vendor research with sources |
docs/finance/failure-recovery.md |
Failure matrix, retry and backoff policy, the pre-implementation self-critique |
docs/finance/security-model.md |
Secrets, least privilege, audit, threat model, vendor-credential policy |
docs/finance/operations-runbook.md |
Vendor checklist, month-end close, what to do when each thing breaks |
docs/finance/implementation-plan.md |
Files, routes, workers, env vars, migrations, tests, rollout, rollback |
Hosted on Vercel. The Pavel API (api.fynix.digital) runs on the Firstfold VPS, which pulls main itself; the runbook is docs/vps/README.md in firstfold-platform. vercel.json restricts automatic deployments to main:
Work happens on development and reaches production by merging to main. SENTRY_AUTH_TOKEN must be set in the Vercel project environment for production source maps — Vercel sets CI=1, so the Sentry upload report appears in the build log.
/api/lead-diagnosticcurrently logs submissions and returns success. It does not yet dispatch email, hit a CRM webhook, or persist anything.components/Header.tsxhas a standing ESLint error (setStateinside an effect) and two unused-variable warnings.- The cron trigger is not in version control.
/api/pavel/cron/remindersis invoked on a schedule configured outside this repository:vercel.jsondeclares nocronsand there is no systemd unit or crontab here. A schedule nobody can see is a schedule nobody can verify, and nothing currently detects the timer having stopped.docs/finance/operations-runbook.md §6has a slot to record it, and the finance work adds a heartbeat so a stalled scheduler becomes visible.
{ "git": { "deploymentEnabled": { "*": false, "main": true } } }