A modern, production-ready developer portfolio built with Next.js 15, React 19, and TypeScript. Content is fully managed through Hygraph (GraphCMS) via GraphQL, with server-side rendering, incremental static regeneration, and a dark theme UI powered by Tailwind CSS and Framer Motion animations.
Live content updates without redeployment. Type-safe from CMS schema to UI. Optimized for performance, security, and accessibility.
- Next.js 15 App Router with React Server Components, ISR (1-day revalidation), and SSG via
generateStaticParams - Hygraph CMS integration — all content (projects, experience, tech stack, bio) managed through a headless CMS with a single GraphQL fetcher and retry logic (exponential backoff)
- Security hardened — CSP headers, XSS-safe SVG rendering via sanitize-html, Zod validation on client + server, rate limiting, CORS origin checks
- Accessible — skip-to-content link, focus-visible outlines, ARIA attributes, semantic heading hierarchy, screen-reader labels
- SEO optimized — dynamic Open Graph metadata, canonical URLs, JSON-LD structured data (Schema.org), XML sitemap, robots.txt
- Polished UX — Framer Motion page transitions, animated mobile menu (AnimatePresence), loading skeletons, error boundaries, toast notifications, empty states
- Contact form — React Hook Form + Zod validation, forwards messages to Discord via webhook
- Container-ready —
output: 'standalone'for Docker / Vercel / Railway deployment
| Layer | Technologies |
|---|---|
| Framework | Next.js 15, React 19, TypeScript 5.9 |
| Styling | Tailwind CSS 3.4, Framer Motion 12 |
| CMS | Hygraph (GraphQL), @graphcms/rich-text-react-renderer |
| Forms | React Hook Form, Zod 4, @hookform/resolvers |
| Security | sanitize-html (SVG whitelist), CSP headers, rate limiting |
| Tooling | pnpm, ESLint, Prettier, Husky, lint-staged, Bundle Analyzer |
| Node | 22.x |
app/
├── api/contact/route.ts # Contact form API (Discord webhook)
├── components/
│ ├── back-to-top/ # Scroll-to-top button
│ ├── button/ # Reusable button with a11y focus ring
│ ├── cms-icon/ # SVG renderer with XSS sanitization
│ ├── contact-form/ # Form with validation & error feedback
│ ├── header/ # Responsive nav + animated mobile menu
│ ├── json-ld/ # Schema.org structured data
│ ├── rich-text/ # CMS rich text (headings, code, quotes...)
│ ├── section-title/ # Animated section headers (h2/h3)
│ └── pages/ # Route-specific section components
│ ├── home/ # Hero, techs, projects, experience
│ ├── project/ # Project detail sections
│ └── projects/ # Project listing + cards
├── lib/
│ ├── animations.ts # Framer Motion presets
│ └── utils.ts # cn() — clsx + tailwind-merge
├── types/ # TypeScript types (Hygraph schema)
├── utils/
│ ├── fetch-hygraph-query.ts # GraphQL fetcher with retry & ISR
│ └── get-relative-time.ts # Intl.RelativeTimeFormat utility
├── layout.tsx # Root layout, fonts, metadata, skip-link
├── page.tsx # Home page
├── projects/page.tsx # Projects listing
├── projects/[slug]/page.tsx # Project detail (SSG + ISR)
├── error.tsx & global-error.tsx # Error boundaries
├── loading.tsx # Loading skeletons
├── sitemap.ts # Dynamic XML sitemap
└── robots.ts # robots.txt generation
- Node.js 22.x (see
.nvmrc) - pnpm >= 8.0
git clone https://github.com/your-username/portfolio.git
cd portfoliopnpm installCopy the example file and fill in your credentials:
cp .env.example .env.local# Hygraph — your GraphQL endpoint and read-only token
HYGRAPH_URL="https://...cdn.hygraph.com/content/.../master"
HYGRAPH_TOKEN="your-hygraph-token"
# Discord — webhook URL for contact form messages
# Create one at: Server Settings > Integrations > Webhooks > New Webhook
WEBHOOK_URL="https://discord.com/api/webhooks/your-id/your-token"
# Site — public URL (used for metadata, canonical URLs, origin validation)
NEXT_PUBLIC_SITE_URL="http://localhost:3000"pnpm devOpen http://localhost:3000 in your browser.
pnpm build
pnpm start| Command | Description |
|---|---|
pnpm dev |
Start development server |
pnpm build |
Create production build |
pnpm start |
Serve production build |
pnpm lint |
Run ESLint |
pnpm type-check |
Run TypeScript type checking |
pnpm validate |
Lint + type-check |
pnpm format:check |
Check code formatting |
pnpm format:fix |
Auto-fix formatting |
ANALYZE=true pnpm build |
Build with bundle analysis |
All CMS data flows through a single utility (fetchHygraphQuery) that handles:
- GraphQL POST to Hygraph with Bearer token auth
- Retry with exponential backoff — 3 attempts (1s, 2s, 4s delays)
- ISR caching — configurable revalidation per query (default: 1 day)
- Type-safe generics —
fetchHygraphQuery<T>(query, revalidate?, variables?)
- Server Components by default — only components requiring interactivity use
'use client' - SSG for project pages via
generateStaticParams()with ISR fallback - Streaming with loading skeletons per route segment
| Layer | Implementation |
|---|---|
| CSP | Strict policy; unsafe-eval only in dev (needed for HMR) |
| SVG Sanitization | Whitelist of safe SVG tags/attributes; blocks href, xlink:href |
| Input Validation | Zod schemas on both client and server with length limits |
| Rate Limiting | 5 requests/minute per IP on contact endpoint |
| Origin Validation | Checks Origin header against NEXT_PUBLIC_SITE_URL |
| Slug Validation | Regex check before GraphQL query to prevent injection |
| Headers | X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy |
The project outputs a standalone build (output: 'standalone' in next.config.js), making it ready for:
- Vercel — zero-config deployment
- Docker — single Node.js process, no
node_modulesneeded at runtime - Railway / Fly.io — standalone output works out of the box
- TypeScript strict mode with
no-explicit-anyenforced - ESLint with Next.js core-web-vitals + TypeScript rules
- Prettier — no semicolons, single quotes, 2-space indent
- Pre-commit hooks — Husky + lint-staged auto-fix on every commit
- Path aliases —
@/*maps to project root
This project is for portfolio and educational purposes.