Personal portfolio website for Matt Black — senior software engineer specializing in full-stack web and mobile development.
Built with Next.js (Pages Router), Tailwind CSS, and Framer Motion. Deployed on Netlify.
yarn install
yarn devOpen http://localhost:3000.
yarn build
yarn startResume content lives in utils/section-data.js. Site-wide settings (name, bio, contact) are in utils/global-data.js and utils/site-config.js.
| Variable | Description |
|---|---|
NEXT_PUBLIC_SITE_URL |
Canonical site URL (default: https://mattblack.io) |
BLOG_NAME |
Display name |
BLOG_JOB_TITLE |
Job title for SEO / structured data |
BLOG_FOOTER_TEXT |
Footer text |
BLOG_EMAIL |
Contact email |
BLOG_LINKEDIN |
LinkedIn profile URL |
OPENAI_API_KEY |
Required for the résumé chat assistant. Server-side only — never exposed to the browser. |
OPENAI_MODEL |
Optional. Chat model for the assistant (default: gpt-4o-mini). |
CHAT_RATE_LIMIT_PER_MINUTE |
Optional. Max chat requests per visitor IP per minute (default: 8). |
CHAT_RATE_LIMIT_PER_DAY |
Optional. Max chat requests per visitor IP per day (default: 40). |
CHAT_GLOBAL_DAILY_LIMIT |
Optional. Max chat requests across all visitors per day (site-wide circuit breaker, default: 1000). |
NEXT_PUBLIC_CHAT_MESSAGE_LIMIT |
Optional. Soft per-browser daily message cap enforced in the UI (default: 20). |
The homepage includes a subtle floating action button (bottom-right) that opens a chat window powered by components/ChatWidget.js. Questions are sent to the serverless route pages/api/chat.js, which grounds the LLM in the résumé content (via utils/resume-context.js) and instructs it to answer only questions about Matt's background, declining anything off-topic.
To enable it, set OPENAI_API_KEY in your environment:
- Local: add it to a
.env.localfile (git-ignored):OPENAI_API_KEY=sk-... - Netlify: add it under Site settings → Environment variables. Netlify's Next.js runtime runs
pages/api/*as serverless functions automatically.
To use a different provider (e.g. Anthropic), swap the request logic in pages/api/chat.js.
The assistant has several layers of protection against abuse and runaway API costs:
- Per-visitor rate limiting —
pages/api/chat.jslimits each client IP (per-minute and per-day) viautils/rate-limit.jsand returns429 Too Many Requestswith aRetry-Afterheader when exceeded. - Site-wide daily circuit breaker — a global daily request cap (
CHAT_GLOBAL_DAILY_LIMIT) stops serving once the whole site hits its ceiling for the day. - Client-side daily cap — the widget caps messages per browser per day (
NEXT_PUBLIC_CHAT_MESSAGE_LIMIT) to curb casual/accidental overuse and give clear feedback. - Bounded requests — conversation history, per-message length, and
max_tokensare all capped so no single request can be expensive.
Important
The in-memory rate limiter is best-effort: on serverless (Netlify Functions) counters live in a single warm instance and reset on cold starts, so they are not shared across concurrent instances. The guaranteed ceiling on your bill is an OpenAI account-level spend limit — set a monthly budget and usage limit under platform.openai.com → Settings → Limits. For strict, shared rate limiting, back utils/rate-limit.js with a durable store such as Upstash Redis.