Skip to content

nickcatal/website

Repository files navigation

nickcatalano.com

🔗 www.nickcatalano.com

My personal site. It's a quiet little page — a photo, a short bio, a few links — with one playful trick up its sleeve.

The fun part

Below the biography there's a button: Generate Fun Versions. Click it and a small AI (GPT-5 Nano) dreams up five tongue-in-cheek concepts for retelling the bio — Fantasy Wizard CTO, Suspicious Pigeon Expert, Medieval Town Crier, that kind of thing. Pick one and it rewrites the bio in that voice, right below the original. The real bio never goes away; the rewrite is just an alternate telling.

The concepts change every time, the facts stay the same (the model is told to keep jobs, places, and people intact and only play with tone), and the whole thing is built to stay cheap and safe to run.

Stack

  • Next.js 16 (App Router) + React 19, mostly Server Components
  • TypeScript + Tailwind CSS v4
  • Server Actions for the two AI calls — no API routes, no client-side keys
  • OpenAI SDK with Structured Outputs (Zod schemas, no hand-parsed JSON)
  • Redis for rate limiting and a response cache, behind a small abstraction so it's local Redis in dev and Upstash in production
  • PostHog for web analytics + LLM observability (lightweight posthog-node capture — no LangChain, no extra AI SDK)
  • Deployed on Vercel

Quickstart

npm install
docker compose up -d                 # local Redis on :6379
cp .env.example .env.local           # then add your OPENAI_API_KEY
npm run dev

Open http://localhost:3000.

You only strictly need an OPENAI_API_KEY to try the feature. Redis is optional in development — if it's not running, rate limiting simply turns off with a warning and everything else works.

Environment variables

Variable Required What it's for
OPENAI_API_KEY yes (prod) Calls GPT-5 Nano for suggestions and rewrites.
OPENAI_MODEL no Override the model id. Defaults to gpt-5-nano.
BIO_SIGNING_SECRET yes (prod) HMAC secret for signing rewrite prompts. Dev uses a fallback.
USE_LOCAL_REDIS dev Set true to use a local Redis via REDIS_URL.
REDIS_URL dev e.g. redis://localhost:6379.
KV_REST_API_URL prod Upstash REST URL (rate limiting + cache). Vercel's Redis integration injects it.
KV_REST_API_TOKEN prod Upstash REST token (injected alongside the URL).
POSTHOG_TOKEN no PostHog project token. Unset → analytics are a no-op (e.g. locally).
POSTHOG_HOST no Server-side PostHog host. Defaults to https://us.i.posthog.com.

In development the app is lenient: a fallback signing secret is allowed and Redis is optional. In production it fails fast — if a required variable is missing, it throws rather than running without protection.

Scripts

Command Does
npm run dev Start the dev server.
npm run build Production build.
npm run start Serve the production build.
npm run lint ESLint (Next core-web-vitals).
npm run typecheck tsc --noEmit.
npm test Jest (signing/security tests).

CI runs lint + typecheck + test on every push and PR to main (.github/workflows/ci.yml). None of it needs secrets.

How it works

  1. The page is a Server Component. It renders the image, bio, and links from lib/bio.ts — the single source of truth for all content. No AI calls happen on load.
  2. Generate Fun Versions calls the generateSuggestions Server Action. It rate-limits the caller, asks GPT-5 Nano (via Structured Outputs) for five { title, prompt } concepts, and signs each prompt with HMAC-SHA256 before sending it to the browser.
  3. Picking a concept calls rewriteBio with the prompt and its signed token. The server re-verifies the signature and expiry, confirms the prompt matches the signed payload, rate-limits, checks the cache, and only then asks the model to rewrite the bio.

Security model

The browser may show a prompt but is never trusted to supply one. Every prompt is signed when generated and re-verified before any rewrite runs — the signed payload, not the request body, is authoritative. Tokens expire after 15 minutes. See lib/signing.ts (and its tests).

Cost control

  • Suggestions are generated fresh each click (cheap — five short items from a nano model) so the ideas always feel new.
  • Rewrites are cached in Redis for 30 days, keyed by the biography + the exact prompt, so a repeated concept is served from cache instead of the model.
  • Daily per-IP limits: 5 suggestion generations and 25 rewrites.

Swappable Redis

Nothing outside lib/cache/ and lib/rate-limit/ knows whether it's talking to a local Redis or Upstash. The factory picks the implementation from the environment. Caching fails open (a backend hiccup is just a cache miss); rate limiting fails closed in production (it never silently disables protection) and degrades to "allow with a warning" in development.

Analytics

PostHog handles both web analytics and LLM observability, and it's entirely gated on POSTHOG_TOKEN — unset (e.g. local dev), nothing fires.

  • Web: posthog-js via a small provider (components/posthog-provider.tsx), with manual $pageview capture for App Router navigations. Browser events go through a first-party /ingest reverse proxy (rewrites in next.config.ts) so ad-blockers don't eat them.
  • LLM: each OpenAI call emits a $ai_generation event from the server (lib/posthog.ts) with model, token counts, and latency — using posthog-node directly, so PostHog's LLM dashboards light up without pulling in any AI framework. Events flush via next/server's after(), so analytics never add latency to a response.

(Google Analytics from the original site is preserved alongside PostHog.)

Deploying to Vercel

  1. Import the repo into Vercel. It auto-detects Next.js — no build config needed.
  2. Add a Redis store from the project's Storage tab (Vercel's Upstash/KV integration). It injects KV_REST_API_URL and KV_REST_API_TOKEN automatically — the app reads those directly, so there's nothing to copy.
  3. Set the remaining environment variables in Project Settings → Environment Variables:
    • OPENAI_API_KEY
    • BIO_SIGNING_SECRET (openssl rand -hex 32)
    • POSTHOG_TOKEN (optional — enables analytics + LLM observability)
    • Redis: the KV_REST_API_* pair from step 2 is injected automatically. Leave USE_LOCAL_REDIS unset.
  4. Deploy. main is production; pull requests get preview deployments.

Project layout

app/            layout (+ analytics), homepage, server actions
components/      BioRewriter + PostHog provider/pageview
lib/            bio (content), env, signing, schemas, openai, posthog
  cache/        cache interface + local (ioredis) and Upstash impls
  rate-limit/   limiter interface + local and Upstash impls
public/         ntr600.jpg

See AGENTS.md for conventions if you're contributing (human or AI).

About

Nick Catalano's Homepage

Resources

Stars

Watchers

Forks

Contributors