The personal site of Andrii Naidenko, a full-stack and mobile developer: one page with his
work, his open-source Claude Code plugins and a contact form. A second build of it,
toptal.naidenko.dev, is the address for Toptal's links: the same pages with no way to reach him
but Toptal.
- Next.js 16 (App Router) exported to static HTML, React 19, TypeScript
- Tailwind CSS 4
- Cloudflare Workers: each static export is served as assets by a Worker of its own; the
site's Worker answers
POST /api/contact, which checks Cloudflare Turnstile and mails the message with the Email Service binding - Two cookieless counters, so no consent banner: GoatCounter for its dashboard, and the
site's own counter (
POST /api/hitinto Cloudflare D1: each visit, its clicks and its visible time) with a password-protected/statspage - Vitest for the Worker and the helpers; Playwright and axe for end-to-end and accessibility tests
- Prettier (4 spaces) and ESLint; GitHub Actions runs every check
pnpm install
cp .dev.vars.example .dev.vars # Cloudflare's public Turnstile test keys
pnpm build && pnpm preview # the site plus the Worker on http://127.0.0.1:8788
pnpm build:toptal && pnpm preview:toptal # the Toptal build on http://127.0.0.1:8789pnpm dev runs the Next.js dev server on port 3000 for layout work. The contact form needs
the Worker, so test it with pnpm preview, where the email is only logged. The Toptal build's
Worker counts its visits through the site's: run both previews.
pnpm test # unit tests: validation, the Worker, analytics, scripts
pnpm test:e2e # builds both with e2e/e2e.env, starts both Workers, runs Playwright and axe
pnpm typecheck && pnpm lint && pnpm format:checkThe end-to-end tests run Playwright's headless Chromium, locally and in CI; install it once with
pnpm exec playwright install --only-shell chromium. They reach challenges.cloudflare.com for
Turnstile's test keys, and GoatCounter is stubbed.
| Setting | Where it goes | What it does |
|---|---|---|
NEXT_PUBLIC_TURNSTILE_SITE_KEY |
build: .env.production.local |
Turnstile widget key (public). pnpm run deploy refuses to run without it. |
NEXT_PUBLIC_GOATCOUNTER_URL |
build: .env.production.local |
GoatCounter's count endpoint, https://CODE.goatcounter.com/count. |
TURNSTILE_SECRET_KEY |
Worker secret | Verifies Turnstile tokens. |
CONTACT_TO |
Worker secret | The inbox that receives the form: a verified Email Routing destination. |
SLACK_WEBHOOK_URL |
Worker secret | A Slack incoming webhook: a copy of every message, and alerts when something fails. |
STATS_PASSWORD |
Worker secret | The password for /stats: a long random string used nowhere else (openssl rand -base64 24). Without it the page does not exist. An address gets ten tries a minute, and an IPv6 /64 counts as one address. |
VISITOR_KEY |
Worker secret | The key of the visitor hash, which tells a returning visitor from a new one: a long random string (openssl rand -hex 32). Changing it makes every visitor new. |
IGNORE_NETWORKS |
Worker secret (optional) | CIDR ranges or addresses, comma-separated, whose visits are not counted: the owner's own network. A secret, so no address reaches the public repository. |
STATS_DB |
wrangler.jsonc |
The D1 database naidenko-stats, created with wrangler d1 create. Its schema is in worker/migrations/. |
CONTACT_FROM |
wrangler.jsonc |
The sender address, on the site's domain. |
GOATCOUNTER_DASHBOARD |
wrangler.jsonc |
GoatCounter's dashboard, linked from /stats for the same dates. Empty means no link. |
TOPTAL_ORIGIN |
wrangler.jsonc |
The Toptal build's origin, where a page tagged ?ref=toptal… moves. Empty turns the move off. |
SITE |
wrangler.toptal.jsonc |
The Toptal build's service binding to the site's Worker (naidenko-dev), which counts its visits. |
NEXT_PUBLIC_SITE_VARIANT |
build: pnpm build:toptal |
toptal builds the Toptal variant into out-toptal/. |
.env.example lists the build settings. Locally, the Worker reads .dev.vars. Neither
.env*.local nor .dev.vars is committed.
Hosting, the TLS certificate, the email relay and Turnstile are free. The domain is the only cost.
-
Domain. Register it with Cloudflare Registrar, or add an existing one. Its DNS then lives in Cloudflare.
-
Email Routing (Email → Email Routing):
- enable it for the domain;
- add the destination inbox and click the link in the verification email;
- add a custom address, such as
hello@→ that inbox.
-
Turnstile (Turnstile → Add widget): mode Managed, hostname = the domain. Copy the site key and the secret key.
-
GoatCounter: sign up at goatcounter.com and copy the count endpoint. The free plan keeps six months; the site's own counter keeps everything.
-
Secrets:
pnpm exec wrangler login pnpm exec wrangler secret put TURNSTILE_SECRET_KEY pnpm exec wrangler secret put CONTACT_TO pnpm exec wrangler secret put SLACK_WEBHOOK_URL pnpm exec wrangler secret put STATS_PASSWORD pnpm exec wrangler secret put VISITOR_KEY pnpm exec wrangler secret put IGNORE_NETWORKS # optional
-
Build settings: create
.env.production.localwith the Turnstile site key and the GoatCounter endpoint. -
Deploy: run
pnpm run deploy. A barepnpm deployis pnpm's own command. The script:- refuses to run without the site key and the GoatCounter endpoint;
- rebuilds both builds;
- checks that neither carries a test value from
e2e/e2e.env, and that the Toptal build leads only to Toptal: no address but the privacy note's, nonaidenko.devoutside its own host, no LinkedIn or GitHub profile (scripts/check-build.mjs --toptal); - checks that every page, file and anchor either build links to exists
(
scripts/check-links.mjs; CI runs it too); - checks that the five required Worker secrets exist;
- applies new D1 migrations;
- deploys the Toptal build's Worker, then the site's, whose move of tagged pages points at the first.
The daily cron needs the account's
workers.devsubdomain: open Workers & Pages in the dashboard once. Without it the deploy exits with an error at "Cron schedules" (10063) after the new code is already live;pnpm exec wrangler triggers deployattaches the cron later.Never run
wrangler deploydirectly: it uploads whatever is inout/orout-toptal/, which afterpnpm test:e2eis a test build. The routes attach the custom domains,naidenko.dev(wrangler.jsonc) andtoptal.naidenko.dev(wrangler.toptal.jsonc), and Cloudflare creates their DNS records and certificates. -
Search: add the domain to Google Search Console (DNS verification) and submit
/sitemap.xml.
The Worker posts to Slack through SLACK_WEBHOOK_URL:
- a copy of every message, in case the email lands in spam. When the email does not go out, the post says so, and the visitor is still told the message went through;
- an order from
/audit's prices (?order=<id>, carried by the form's hiddenorderfield) as ":briefcase: Audit order: ", its email's subject "[naidenko.dev] Audit order: from ", and "Package:" first in the text, so a Gmail filter on the subject can label it; - Turnstile refuses the site's own check: a wrong secret, or siteverify out of reach;
- anything else fails unexpectedly;
- a broken outside link, found by the daily cron (
worker/links.ts). It reads the pages of the sitemap from the deployed build, checks the home page's links every day and the sample report's references fifteen a day, within the 50 requests a Free plan's cron may make. A link counts as broken on a 404 or 410, a network error, a timeout or a redirect loop, twice in a row. A 403, 429 or 503 is a bot wall, not a dead page: it goes to the logs as unverified. LinkedIn is not checked (it answers 999 to any script, a missing profile included), nor Toptal (its terms bar scripts). Run it locally withpnpm exec wrangler dev --test-scheduled, thencurl "http://127.0.0.1:8787/__scheduled?cron=17+3+*+*+*".
A visitor's bad or expired token raises no alert. When Slack cannot be reached, the alert goes to
the Worker's logs instead. Stream the live logs with pnpm exec wrangler tail naidenko-dev. Past
logs are in the dashboard under the Worker's Logs, because Wrangler's login cannot query them.
The page sends /api/hit its view, each section once as it scrolls into view, every tracked click
and the time it was visible. The Worker adds the place and network Cloudflare resolves, the
browser, system and language, and a keyed hash of the network and browser; it stores no IP
address. A daily cron erases the hashes older than 13 months, and the rest is kept.
- Link tags:
https://naidenko.dev/?ref=linkedin(orutm_source=) shows under Link tags on/stats, which filters by tag, country and dates (the last 30 days by default) and compares each total with the period of the same length just before. A bounce is a visit that left within 10 s. - Tagged links on LinkedIn: LinkedIn replaces a shared link with the page's canonical address,
so for its crawler (
LinkedInBot) the Worker adds the link's?ref=to the canonical link andog:urlof every HTML page (run_worker_firstsends every path without a dot to the Worker); everyone else sees the bare address, such ashttps://naidenko.dev/audit. Check a new link in Post Inspector: its Canonical URL must keep?ref=. LinkedIn caches the card, so a link added before a fix has to be removed and added again. - Tagged by the referring site: a visit GitHub sends without a tag counts as
github, since the profile links the bare address and GitHub sends its origin as the referrer. - Visits through Toptal: Toptal's profile guidance allows no link to a page "emphasizing your
contact information". The form, the email and the header's email icon render only in the
browser, so the static HTML and the Markdown never carry them, and the privacy note names
privacy@naidenko.devas text, with no link, as the GDPR asks. A page asked for with a tag?ref=toptal…orutm_source=toptal…(an old application'stoptal-<job id>, the profile'stoptal) moves with a 302 to the same path on the Toptal host (TOPTAL_ORIGIN), its tag in the form that host counts the same:?ref=toptalgoes,?ref=toptal-509168becomes?ref=509168. WithTOPTAL_ORIGINempty it stays, in the page's own Toptal mode: an inline script marks<html data-toptal>before the first paint, and the page shows no form, no email and no "Contact me" (src/lib/toptal.ts)./audithides its prices and order buttons, and its link home isnofollow; the Toptal build renders no prices at all. - The Toptal host:
toptal.naidenko.devserves the Toptal build (pnpm build:toptal,NEXT_PUBLIC_SITE_VARIANT=toptal) from a Worker of its own (worker/toptal-site.ts,wrangler.toptal.jsonc): no form, no email, no icon row under the name, no LinkedIn or GitHub profile, and no link tonaidenko.dev; the menu's last entry is "Hire", over the badge, and the sample report and its PDF link the Toptal host. Its pages carrynoindex, its Worker sendsX-Robots-Tag: noindexwith every answer, files included, and itsrobots.txtkeeps AI crawlers out. A link there takes a bare tag, such as a job's ID (/audit?ref=509168), counted astoptal-509168; a visit with no tag counts astoptal. Its own links carry a visit's tag in that bare form, so its addresses never showtoptal. Its Worker has no database or secrets: it hands/api/hitto the site's Worker through a service binding, so one D1, one visitor key and one list of ignored networks count both hosts. On/stats, the link tag "Every Toptal link" (?ref=toptal*) shows the visits that came through Toptal; a move within a site is a visit with no tag, so it is not among them. - Not counted: a load with
?preview=1; a browser that has opened/stats(it sets GoatCounter'sskipgcflag, which both counters honour), or any page with?skipgc=t, which sets the same flag; addresses inIGNORE_NETWORKS; bots, automated browsers, frames and prerendering, as GoatCounter's count.js skips them. The flag belongs to one host, and the Toptal host has no/stats: openhttps://toptal.naidenko.dev/?skipgc=tonce in each of your browsers there. - Testing from an ignored network: press "Count it again" on
/statsin that browser (a private window keeps it until closed), or open a page with?skipgc=f. Its hits then carryforce: trueand are counted even fromIGNORE_NETWORKS.
Every event goes to both counters. GoatCounter names it <event>-<values of its parameters>,
such as hire_me_toptal-badge; /stats lists it as hire_me_toptal · badge.
| Event | When | Parameters |
|---|---|---|
contact_click |
"Contact me" in the header | — |
hire_me_toptal |
"Hire me" on the Toptal badge | placement |
toptal_profile_click |
"View full résumé on Toptal" | placement |
profile_click |
GitHub, LinkedIn or Toptal icon | network |
email_click |
Any email link | placement (header, contact, form_error) |
client_site_click |
A client's name in Experience | company |
testimonial_click |
A client's name or photo under "What clients said" (their LinkedIn) | client |
store_click |
App Store or Google Play | store |
project_click |
A project's title in Projects | project |
service_click |
A service's title in Services that links a page of the site ("Code audits", to /audit) |
service |
sample_report_click |
A link from /audit to the sample report |
placement (hero, screenshot, section) |
sample_pdf |
The sample report's PDF | placement (audit, sample) |
sample_md |
The sample report's Markdown for AI agents | placement (audit, sample) |
prices_click |
"See prices" in /audit's hero (naidenko.dev only) |
— |
audit_order |
A package's order button under /audit's prices (naidenko.dev only) |
placement (launch, full, diligence, custom) |
github_auditdesk |
"Auditdesk on GitHub" on /audit |
— |
evidence_click |
The Semgrep benchmark's link under "What it covers" on /audit |
— |
sample_to_audit |
"How the audit works" on /audit/sample |
— |
sample_filter |
A click in the sample report's filters | — |
copy_install |
The install-command copy button | — |
generate_lead |
The form was sent | form |
form_error |
The form could not be sent | form, reason (rate_limit when limited) |
nav_click |
A link in the in-page menu (wide screens) | section (a section's id, or code-audit for "Code audit", which until 1.15.0 sent auditdesk) |
section_view |
A section scrolled into view, once per visit (/stats only) |
the section's id |
To track a new link or button, give it data-track="<event>" and any
data-track-<param>="<value>".
| Path | What lives there |
|---|---|
src/content/ |
All copy and links, one file per section |
src/components/ |
One component per block of the page |
src/app/ |
The pages, the link-preview image, robots.txt and sitemap.xml |
src/lib/ |
Form validation shared with the Worker; the Turnstile client; analytics |
worker/ |
The site's Worker: the contact endpoint, Turnstile verification, the visit counter and /stats; the Toptal build's (toptal-site.ts) |
e2e/ |
Playwright tests, accessibility checks, review screenshots |
assets/ |
Fonts and the photo for the generated images |
scripts/ |
The deploy guards, the env-file runner and the Toptal build's PDF |
- The layout is inspired by Brittany Chiang's site. The code is original.
- The frames in the claude-video-digest image are from Big Buck Bunny, © Blender Foundation, licensed under CC BY 3.0.
- The Geist font is © Vercel and licensed under the SIL Open Font License 1.1
(
assets/fonts/OFL.txt).
