Modern Portfolio Website - Next.js, TypeScript, TailwindCSS, Framer Motion, Shadcn UI, i18next, FullStack Project (My Personal Official Portfolio)
A production portfolio at https://www.arnobmahmud.com built with Next.js 16.3 (App Router), React 19.2, TypeScript, TailwindCSS, and Framer Motion. It is a public site (no login): server-rendered pages, a contact form, an AI FAQ chatbot, English/German i18n, and SEO metadata.
- Live Demo: https://www.arnobmahmud.com/
- Security: private reports → SECURITY.md · contact@arnobmahmud.com
- Author: Arnob Mahmud · GitHub @arnobt78 · LinkedIn @arnob-mahmud-05839655
- Overview
- Features
- Technology stack
- Keywords
- Project structure
- How it works
- Installation
- Environment variables
- Scripts
- Routes
- API endpoints
- Components you can reuse
- Hooks and context
- i18n
- Chatbot
- SEO and metadata
- Deployment
- Troubleshooting
- Contributing
- License
This repository is the source for Arnob Mahmud’s personal site. Next.js App Router keeps each URL as a small server page.tsx that only exports metadata and renders a client page component. That split matters: Google sees titles and canonicals from the server; buttons, forms, and the chatbot stay in the browser.
You can clone it to learn App Router, i18n cookies, Edge SSE, or SMTP mail — or fork it as a starting point for your own portfolio. Optional APIs (chat, email, Redis, Sentry) are off unless you add keys. The UI still runs without them.
- App Router pages: Home, About, Resume, Work, Services, FAQ, Contact, Privacy, Terms
- i18n: English and German via
i18next+ cookieselectedLanguage - Request interceptor:
proxy.tssetsx-initial-languageandx-pathname(Next.js 16 name; do not restoremiddleware.ts) - Contact form: SMTP through Nodemailer (
/api/send-email,/api/send-auto-reply) - AI chatbot: Edge stream
/api/chat, Redis sessions, FAQ RAG - Analytics: GA4 + Vercel Analytics
- Observability: Sentry (
tunnelRoute: /monitoring) - SEO:
metadataon every route, Open Graph image/img-8-1200.png, JSON-LD Person + LocalBusiness,app/sitemap.ts - Motion: Framer Motion page + stair transitions
- UI: Tailwind + Radix/shadcn primitives
| Layer | What | Why it is here |
|---|---|---|
| Next.js 16.3 | App Router, proxy.ts, Turbopack next build |
File-based routes, metadata, Route Handlers |
| React 19.2 | UI | Client islands only where needed |
| TypeScript 5.9 | Types | npx tsc --noEmit |
| Tailwind 3.4 | Utility CSS | Fast layout without a second CSS framework |
| Framer Motion | Animation | Page enter/exit without full SPA router |
| Radix / shadcn | Dialog, tabs, sheet | Accessible primitives |
| Nodemailer 9 | SMTP | Contact + auto-reply (Node runtime, not Edge) |
| Upstash Redis | Sessions + FAQ vectors | Chat history cookie chatbot_session |
| Vercel AI SDK | streamText |
Model fallback in lib/ai.ts |
| ESLint 9 | eslint . |
Flat config eslint.config.mjs |
Beginner note: a Server Component (default in app/) can await data and set metadata. A Client Component ("use client") can use useState and click handlers. This project never turns a whole route into a client file just because one section is interactive.
Full-Stack Software Engineer, Software Engineer Germany, Frankfurt, React, Next.js, Angular, Node.js, Python, .NET, REST APIs, PostgreSQL, MongoDB, AWS, Docker, Kubernetes, CI/CD, portfolio, i18n, RAG chatbot.
Used in app/layout.tsx and per-page metadata.keywords so search engines see both the person and the tech.
app/ # Routes + API (App Router)
layout.tsx # Root metadata, lang, providers
page.tsx # Home
about|resume|work|services|faq|contact|privacy|terms/page.tsx
api/chat|history|send-email|send-auto-reply|feedback|seed/route.ts
sitemap.ts
components/ # Header, Footer, pages/*, chatbot/*, ui/*
context/ # LanguageContext, widget-settings-context
hooks/ # use-chat, useTypewriter, use-is-client, use-synced-storage
lib/ # ai, rag, redis, translations, logger, faqs
proxy.ts # Next 16 interceptor (language + pathname headers)
eslint.config.mjs # ESLint 9 + eslint-config-next 16
.env.example # Placeholder names only — copy to .env.local
SECURITY.md # Private vulnerability reporting
- A request hits
proxy.ts(Node). CookieselectedLanguageorAccept-Languagebecomes headerx-initial-language. Pathname becomesx-pathname. app/layout.tsxawait headers()and sets<html lang>plus the canonical<link>.- The matching
app/.../page.tsxsupplies SEOmetadataand renders a page component fromcomponents/pages/. - Client trees (nav, forms, chat) hydrate. Language is also stored in
localStorage/ cookie so the toggle survives refresh.
Chat (optional keys): widget → POST /api/chat { message } → rate limit → Redis session → searchFAQ → getAIResponse (Gemini → OpenRouter :free → Groq → Hugging Face → optional OpenAI) → SSE data: { response } then data: [DONE].
Contact: ContactPage → axios POST /api/send-email then /api/send-auto-reply. Needs EMAIL_USER / EMAIL_PASS.
Need: Node.js 20.9+ (Next 16 floor). npm is enough.
git clone https://github.com/arnobt78/Portfolio--NextJS-FullStack.git
cd Portfolio--NextJS-FullStack
npm install
cp .env.example .env.local # optional — see below
npm run devOpen http://localhost:3000.
The site runs without any env vars. Chat, email, Redis, Sentry, and Search Console tags stay inactive until you fill .env.local.
Template: .env.example. Copy to .env.local (gitignored). Never commit real keys. .env at the repo root is also gitignored.
| Name | Required to run UI? | How to get it |
|---|---|---|
GOOGLE_GEMINI_API_KEY |
No | Google AI Studio |
OPENROUTER_API_KEY |
No | OpenRouter — this app uses :free model IDs only |
GROQ_API_KEY |
No | Groq console |
HUGGING_FACE_API_KEY |
No | Hugging Face |
OPENAI_API_KEY |
No | Optional paid last resort |
UPSTASH_REDIS_URL / UPSTASH_REDIS_TOKEN |
No | Upstash Redis |
SESSION_TTL |
No | Seconds (default 2592000) |
EMAIL_USER / EMAIL_PASS |
No | Gmail (or SMTP) + App Password |
FEEDBACK_EMAIL |
No | Inbox for chatbot feedback |
NEXT_PUBLIC_GA_MEASUREMENT_ID |
No | GA4 G-... |
NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION |
No | Search Console meta content |
NEXT_PUBLIC_BING_SITE_VERIFICATION |
No | Bing Webmaster meta content |
NEXT_PUBLIC_SENTRY_DSN |
No | Sentry project DSN |
SENTRY_ORG / SENTRY_PROJECT |
No | Source maps on next build |
Legacy aliases still read by lib/ai.ts: OpenRouter_API_KEY, Groq_Llama_API_KEY, Hugging_Face_Inference_API_KEY.
On Vercel, add the same names in Project → Settings → Environment Variables (Production + Preview).
Already in package.json:
{
"dev": "next dev",
"dev:clean": "rm -rf .next && next dev",
"build": "next build",
"start": "next start",
"lint": "eslint ."
}npm run lint # ESLint 9 flat config
npx tsc --noEmit # types
npm run build # Turbopack production build
npm run start # serve `.next`| Path | File | Role |
|---|---|---|
/ |
app/page.tsx |
Hero, stats, photo |
/about |
app/about/page.tsx |
Bio |
/resume |
app/resume/page.tsx |
Experience / education / skills (hash tabs) |
/work |
app/work/page.tsx |
Project grid + Swiper |
/services |
app/services/page.tsx |
Offerings |
/faq |
app/faq/page.tsx |
FAQ accordion |
/contact |
app/contact/page.tsx |
Form |
/privacy /terms |
matching page.tsx |
Legal |
Each page.tsx is a server file: export metadata, return <SomePage />. Put interactive UI in components/pages/.
Example (home):
import type { Metadata } from "next";
import HomePage from "../components/pages/HomePage";
export const metadata: Metadata = {
title:
"Arnob Mahmud | Full-Stack Engineer | Web, API, SaaS, & Cloud Solutions",
alternates: { canonical: "https://www.arnobmahmud.com/" },
};
export default function Home() {
return <HomePage />;
}| Method | Path | Runtime | Body | Purpose |
|---|---|---|---|---|
| POST | /api/chat |
Edge | { "message": "..." } |
SSE chatbot |
| GET | /api/history |
Edge | cookie | Chat history |
| POST | /api/send-email |
Node | contact fields | Mail to you |
| POST | /api/send-auto-reply |
Node | contact fields | Visitor thank-you |
| POST | /api/feedback |
Node | rating / comment | Chatbot feedback |
| POST | /api/seed |
Node | — | Embed FAQs into Redis |
Chat smoke test (after npm run build && npm run start and a Gemini/OpenRouter key):
curl -X POST http://localhost:3000/api/chat \
-H 'Content-Type: application/json' \
-d '{"message":"Say hi in one short sentence."}'You should see text/event-stream and data: [DONE].
Copy the folder, keep the "use client" line, and pass props or wrap with the same context.
| Component | Path | Reuse idea |
|---|---|---|
Photo |
components/Photo.tsx |
Circular crop + dashed ring |
Stats |
components/Stats.tsx |
Count-up after hydration (useIsClient) |
LanguageSelector |
components/LanguageSelector/ |
Cookie + setLanguage |
ChatbotWidget |
components/chatbot/ |
Needs Providers + Redis/AI keys for live answers |
SafeImage |
components/ui/SafeImage.tsx |
Next/Image with fallback |
| UI kit | components/ui/* |
shadcn Button, Dialog, Tabs, Sheet |
Example: use the button elsewhere:
import { Button } from "@/components/ui/button";
<Button variant="outline">Hire me</Button>;@/ maps to the repo root (tsconfig.json paths). In another Next app, copy components/ui + lib/utils.ts (cn = clsx + tailwind-merge).
LanguageContext—language,setLanguage,t(key). Server initial lang fromproxyheaders; client may prefer cookie.use-chat.ts— React Query["chat-history"];sendMessagestreams then invalidates history so the widget updates without a full page reload.useTypewriter— hero name animation.use-is-client/use-synced-storage— hydration-safe client flag and localStorage enums (theme, font size, widget position).
This is not a CRUD admin app. Static pages do not need global cache densify. Chat is the only live mutation path.
Strings live in lib/translations.ts (en / de). LanguageSelector writes cookie selectedLanguage. proxy.ts reads it on the next request so html lang is correct on first paint.
Add a key in both language objects, then t("nav.home") in a client component.
Widget: components/chatbot/chatbot-widget.tsx. FAQ text: lib/faqs.ts. After editing FAQs, call POST /api/seed so Redis vectors update.
Fallback order (see lib/ai.ts): Gemini Flash → OpenRouter free IDs → Groq OSS → Hugging Face gpt-oss → optional OpenAI.
Root: app/layout.tsx — metadataBase, title, description, keywords, authors (Arnob Mahmud + https://www.arnobmahmud.com), creator, publisher, icons (/favicon.ico, /img-8-1200.png), Open Graph, Twitter card, JSON-LD (Person email contact@arnobmahmud.com).
Each route adds its own title / canonical. Do not drop per-page canonicals; /services indexing depends on them.
Vercel project my-portfolio. next.config.mjs 308-redirects arnobmahmud.com and *.vercel.app → https://www.arnobmahmud.com.
npm run build
# or: vercel --prod (only with owner approval)- Language stuck on English: check cookie
selectedLanguage=deand thatproxy.tsexists (Next 16). There is nomiddleware.ts. - Chat 400
Message required: body must be{ "message": "..." }, not an OpenAI-stylemessagesarray. - Email fails: Node runtime routes need
EMAIL_USER/EMAIL_PASS; Edge cannot run Nodemailer. - Lint:
npm run lint(ESLint 9). Config:eslint.config.mjs.
Fork, branch, PR. Keep App Router pages as Server Components. Do not restore middleware.ts on Next 16.
Report security issues privately — see SECURITY.md (contact@arnobmahmud.com).
This project is licensed under the MIT License. Feel free to use, modify, and distribute the code as per the terms of the license.
This is an open-source project - feel free to use, enhance, and extend this project further!
If you have any questions or want to share your work, reach out via GitHub or my portfolio at https://www.arnobmahmud.com.




