A production-oriented portfolio built with Next.js App Router, designed to feel personal while still being deployable, searchable, bilingual, and maintainable.
- It is more than a static portfolio. The site includes a grounded AI clone chat experience powered by OpenAI Responses API, optional
file_search, and a hosted vector store. - The content system is standardized around a single bilingual source of truth, which reduces drift and makes future updates safer.
- SEO is treated as a first-class concern: localized metadata, canonical URLs, alternate language URLs, Open Graph, Twitter cards,
robots.txt, andsitemap.xmlare all configured. - The chat path is hardened with request validation, request-size limits, best-effort rate limiting, timeout control, and abortable client requests.
- The site ships with baseline browser-facing security headers configured in Next.js.
- The UI keeps a distinctive personal tone while still following practical engineering standards such as reusable content/config layers and build-time checks.
- Image loading now uses Next.js image optimization, project covers are switched to lighter JPEG assets, and social/manifest assets are separated from oversized source images.
- The repo includes baseline automated tests for core helpers so regressions in language resolution and chat validation are easier to catch.
- Language is now route-native: English and Chinese pages live under
/enand/zh, with cookie-backed redirects so SEO and sharing stay clean.
- Home, Projects, Contact, and AI Chat pages built on the App Router
- Route-level bilingual URLs for English and Chinese with cookie-backed preference redirects
- OpenAI Responses API integration with vector-store-backed retrieval when
OPENAI_VECTOR_STORE_IDis configured - Static SEO routes for
robots.txt,sitemap.xml, andmanifest.webmanifest - Resume download, project showcase cards, and portfolio-specific structured data
- Single-source localized content in
src/content/portfolio-content.js - Shared site metadata in
src/config/site.js - URL-based language routing and helpers in
src/lib/language.jsplusproxy.js - Centralized chat validation and rate-limit logic in
src/lib/chat.js - Basic regression tests via Node's built-in test runner
- Lint, test, and production build checks wired into
npm run check
- Next.js 16
- React 19
- Tailwind CSS
- Framer Motion
- OpenAI JavaScript SDK
app/[lang]/ Localized pages, layout, and per-page metadata
app/ Route handlers and metadata routes
src/components/ Shared UI and page components
src/content/ Bilingual portfolio content
src/config/ Site-level configuration
src/lib/ Chat validation, metadata, and language helpers
knowledge/ Public AI clone knowledge cards
scripts/ Local tooling such as vector store uploads
test/ Node-based regression tests
npm install
npm run devOpen http://localhost:3000.
npm run lint
npm run test
npm run buildOr run everything in one pass:
npm run checkNEXT_PUBLIC_SITE_URL=https://your-domain.com
OPENAI_API_KEY=...
OPENAI_CHAT_MODEL=gpt-4o-mini
# Optional — hosted RAG through OpenAI file_search.
# Run `npm run sync:vector-store` after setting OPENAI_API_KEY, then copy the
# printed vector store id here.
OPENAI_VECTOR_STORE_ID=vs_...
OPENAI_FILE_SEARCH_MAX_RESULTS=5
# Optional — distributed rate limit on Vercel / multi-instance deployments.
# Without these, the chat route uses an in-memory bucket that does not
# survive across serverless instances.
UPSTASH_REDIS_REST_URL=...
UPSTASH_REDIS_REST_TOKEN=...The public clone knowledge lives in knowledge/*.md. These files are intentionally sanitized for public answers: no phone number, birthday, internal CV strategy, private application paths, or interview-only caveats.
For local development, the chat can still inline these Markdown files into the system prompt. For production RAG, upload them to an OpenAI vector store:
OPENAI_API_KEY=... npm run sync:vector-storeThen set the printed OPENAI_VECTOR_STORE_ID in Vercel. When that variable is present, the chat request attaches the Responses API file_search tool with the configured vector store. If it is missing, the app falls back to the local inlined knowledge path so development keeps working.
The chat API rate-limits per client IP (sliding window, defaults to 8 requests / minute).
- With
UPSTASH_REDIS_REST_URL+UPSTASH_REDIS_REST_TOKENset: uses Upstash Redis. Required for Vercel / any multi-instance deployment so limits stay consistent across functions. - Without them: falls back to an in-memory bucket. Fine for local development or a single long-running Node process, but ineffective on serverless.
To enable Upstash: create a free Redis database at console.upstash.com, copy the REST URL and token from the database page, and add both as environment variables in Vercel.
Each chat request logs a JSON line to stdout:
[chat] {"ts":"…","ip":"…","q":"…","chars":1234,"ms":2100,"ok":true}
On Vercel these show up under the project's Logs / Observability tab. For long-term querying, attach a Log Drain or write the same record to Upstash / a database from app/api/chat/route.js.
- Import this repository into Vercel.
- Keep the framework preset as
Next.js. - Set
NEXT_PUBLIC_SITE_URLto the final production domain. - Add
OPENAI_API_KEYand (optionally)OPENAI_CHAT_MODEL. - Add
UPSTASH_REDIS_REST_URLandUPSTASH_REDIS_REST_TOKENto enable distributed rate limiting (recommended). - Attach the custom domain in Vercel.
- Core pages are prerendered as static content.
- The chat API route is intentionally dynamic.
- Language preference is persisted via cookie and reinforced by route-based URLs.
- The
.env.localfile is ignored by Git and is intended for local secrets only.