Personal portfolio and content platform built on Next.js App Router with a provider-switched CMS runtime (Notion primary, local fallback), migrated from Tailwind Plus Spotlight and customized with production features (article search/filtering, Hermes AI chat, contact workflow, SEO routes, and custom content pages).
- Overview
- Tech Stack
- Quick Features
- Environment Variables
- Local Development
- Build and Run
- Testing
- Documentation Map
- Troubleshooting
This project is the active codebase for brandonperfetti.com and includes:
- Content-driven article system with provider switch:
local: fallback mode (non-Notion providers + empty article-safe states)notion: Notion CMS article projection + canonical Source Article page blocks
- Search in two places:
- Header modal (
Cmd/Ctrl + K) with title/description/full article body matching. - Articles page explorer with topic chips + query-string syncing.
- Header modal (
- Dynamic article route at
/articles/[slug]withgenerateStaticParams,dynamicParams=true, and route-levelgenerateMetadata(). - Hermes chat experience with streaming OpenAI responses and image generation.
- Contact form integration through SendGrid mail API.
- SEO endpoints: sitemap, robots, RSS feed metadata.
- Next.js 16 (App Router)
- React 19
- TypeScript 5
- Tailwind CSS 4
- GSAP for motion primitives and choreography
- Headless UI (menu/popover primitives)
- SendGrid for contact + marketing list APIs
- OpenAI API for Hermes chat + image generation
- Heroicons + project-local icon components
- Home page with article highlights, contact card, and work/resume summary.
- Reusable motion system for headline, reveal, parallax, and hover animations.
- Articles route with full-text + topic filtering.
- Global header modal search (
Cmd/Ctrl + K). - Hermes AI chat with streaming text and image generation modes.
- Hermes input supports multiline prompts (
Enterto send,Shift+Enterfor newline). - SendGrid-backed contact workflow (newsletter API is present; home-page newsletter UI is currently hidden).
- SEO routes: sitemap, robots, and feed endpoint metadata.
Start from .env.example and copy into .env.local (or .env) in project root.
cp .env.example .env.localThis file includes the minimal runtime env contract. Full Notion CMS setup and operational details live in docs/NOTION_CMS.md.
NEXT_PUBLIC_SITE_URL=...
OPENAI_API_KEY=...
SENDGRID_API_KEY=...# Newsletter list destination (either key supported)
SENDGRID_MAILING_ID=...
# or
SENDGRID_LIST_ID=...
# Regional SendGrid API base (optional)
# set to "eu" for EU residency account routing
SENDGRID_DATA_RESIDENCY=eu
# Contact form routing overrides
CONTACT_TO_EMAIL=you@example.com
CONTACT_FROM_EMAIL=no-reply@example.com# local = fallback mode (hard-coded providers + no local article corpus)
# notion = Notion CMS providers
CMS_PROVIDER=local# Manual sync endpoint auth
CMS_REVALIDATE_SECRET=...
# Cron endpoint auth
CRON_SECRET=...
# Webhook auth/verification
NOTION_WEBHOOK_VERIFICATION_TOKEN=...
NOTION_WEBHOOK_SECRET=...For full Notion configuration (all NOTION_* env vars, webhook/revalidate secrets, projection sync, runbooks), see:
docs/NOTION_CMS.md
The Portfolio CMS - Tech database is maintained by the tech curation cron using GitHub signals.
- Recency is gated with
GITHUB_TECH_MAX_REPO_AGE_MONTHS(default:24) so old repos do not dominate current stack telemetry. - Auto-create remains catalog-driven (high-confidence technologies only), then rows are enriched with summary/reference/logo and Cloudinary-hosted
Logo URL. - Site visibility is controlled in Notion:
Status = Publishedto show on the site.Featured = trueonly for highlighted tech sections.
Install dependencies:
npm installRun standard dev server:
npm run devApp default URL: http://localhost:3000
Production build:
npm run buildStart production server:
npm run startLint:
npm run lintUses ESLint flat config in eslint.config.mjs (not legacy .eslintrc*).
Format:
npm run formatFormat check (no writes):
npm run format:checkType check:
npm run typecheckUnit/integration tests (Vitest):
npm run testWatch mode:
npm run test:watchCoverage:
npm run test:coverageE2E smoke (Playwright):
npm run test:e2eThis repo uses Husky hooks for local quality gates:
pre-commit:npm run format:checkandnpm run lintpre-push:npm run typecheckandnpm run test
Hooks are installed via:
npm run prepareThis repo uses progressive disclosure docs for coding agents and collaborators.
Primary instruction entrypoint:
.github/copilot-instructions.md
Compatibility entry files (symlinked):
AGENTS.mdCLAUDE.md
Detailed topic docs live in docs/:
docs/ARCHITECTURE.mddocs/FEATURES.mddocs/STYLING.mddocs/STATE.mddocs/NAVIGATION.mddocs/SEO.mddocs/DEPENDENCIES.mddocs/WORKFLOW.mddocs/ACCESSIBILITY.mddocs/TESTING.mddocs/MAINTENANCE.mddocs/DOCUMENTATION.mddocs/NOTION_CMS.mddocs/notion-integration.mddocs/agent-notion-operations.md
If you need implementation internals first, start with:
docs/ARCHITECTURE.mddocs/STATE.mddocs/NAVIGATION.md
If newsletter subscribe fails with access/scope errors, verify your SendGrid key has marketing contacts permissions and that SENDGRID_MAILING_ID / SENDGRID_LIST_ID is set.
Confirm OPENAI_API_KEY is present and valid. Chat and image endpoints are server-side and return explicit JSON errors for missing keys.
If public Hermes routes start returning 429/403, verify Hermes guardrail env settings (HERMES_*) and TURNSTILE_SECRET_KEY behavior.
If memory pressure is observed under high-cardinality traffic, tune optional in-memory guardrail controls: HERMES_GUARDRAILS_MAX_BUCKETS and HERMES_GUARDRAILS_BUCKET_TTL_MS.