How it works
A map of this portfolio: where content lives, what the server renders, what runs in the browser, and which services it calls.
Source of truth
Edit files, not pagesStatic assets
public/
Images, logo, favicons and manifest icons.
- Served as is from the site root.
- Pages load them through next/image, so sizes and formats are optimized.
- Fonts are not here: they live next to the theme code.
Sends to Static pages (next/image)
Content config
src/config/*
Hero, projects, experience, skills and resume as typed data.
- Every section of the site reads from one typed file.
- Resume-only fields (short bullets, hidden entries, skill groups) sit beside the full text.
- Changing content never means touching a component.
Sends to Static pages (typed data), Resume page (getResumeData())
Theme registry
src/theme/*
One file per theme: light and dark tokens plus fonts.
- A theme is a typed object with separate light and dark color tokens.
- defineTheme() fills optional tokens, so a theme can stay short.
- Fonts are registered once in fonts.ts and loaded with next/font/local, self-hosted.
Sends to ThemeStyles (tokens to CSS)
Env vars
.env
Site URL, Upstash credentials and the resume phone number.
- NEXT_PUBLIC_URL sets canonical URLs and the origin the PDF is rendered from.
- UPSTASH_REDIS_REST_URL and _TOKEN connect the view counter.
- RESUME_PHONE keeps the phone number out of the repository.
Sends to Server actions (Upstash keys)
Next.js 16 server
App Router · React Server ComponentsStatic pages
/ · /projects · /work-experience
Built once per deploy from the content config.
- Server components read config and render plain HTML.
- No data fetching at request time, so each page is plain static HTML.
- Each route sets its own metadata through generateMetadata().
Resume page
/resume
An A4 HTML resume generated from the same config.
- getResumeData() maps config to one page: latest projects, short bullets, grouped skills.
- getResumeVersion() hashes that data into a version string.
- Dev shows a banner when the content no longer fits one page.
Server actions
src/app/actions/views.ts
getViews, incrementViews and getUniqueVisitors.
- Called straight from the navbar counter, with no API route in between.
- Reads the visitor IP from request headers to count unique visitors.
- Failures return 0, so a Redis outage never breaks the page.
Sends to Upstash Redis
Root layout
src/app/layout.tsx
Providers, navbar, footer and font variables.
- Wraps every route in next-themes, the color theme provider, Lenis and View Transitions.
- Sets the font CSS variables on <html>.
- The navbar and footer are hidden when printing, so the resume prints clean.
Sends to Motion & scroll (hydrates)
ThemeStyles
src/theme/ThemeStyles.tsx
Turns the theme registry into CSS in the document head.
- The default theme owns :root; every other theme sits on [data-theme].
- A tiny inline script restores the saved theme before first paint, so nothing flashes.
- No cookies are read, so pages stay static.
Sends to Theme layer (CSS + init script)
PDF route
/api/resume/pdf?v=hash
Prints /resume to a PDF with headless Chromium.
- One valid URL per resume version: a wrong v gets a redirect to the right one.
- The version is a hash of the data, so the cache key changes only when content does.
- Renders from the configured site URL, never from the request Host header.
Sends to Resume page (loads), Chromium
Browser
Small client islandsTheme layer
Light or dark plus a color theme.
- next-themes toggles the .dark class for the mode.
- ColorThemeProvider sets data-theme on <html> and saves the choice in localStorage.
- Switching runs inside a View Transition for a smooth crossfade.
Motion & scroll
Smooth scroll and route transitions.
- Lenis gives the page smooth scrolling.
- next-view-transitions animates moving between routes.
- Motion drives reveals and small interactions.
Download PDF
One click fetches the PDF.
- Fetches /api/resume/pdf with the current version hash.
- Saves the response as a file.
- Falls back to the browser print dialog if the route fails.
Sends to PDF route (GET ?v=hash)
View counter
Counts a visit once per session.
- Uses sessionStorage so a refresh does not count twice.
- The first visit calls incrementViews; later pages only read the total.
- Hover shows the unique visitor count.
Sends to Server actions (server action)
GitHub calendar
Contribution graph, fetched client-side.
- Loaded with next/dynamic and ssr: false, so it never blocks the first render.
- Shows a loading and an error state.
- Colors follow light and dark mode.
Sends to Contributions API
External
Not ownedUpstash Redis
REST
Views and unique visitors.
- INCR on portfolio:views counts visits.
- SADD and SCARD on portfolio:visitors track unique IPs.
- Talks to Redis over REST, so it works in serverless functions.
Chromium
headless
Prints the resume page.
- Serverless uses @sparticuz/chromium; local dev uses installed Chrome.
- Concurrent requests share one browser run.
- Sizes the page from the CSS @page rule, so the PDF is A4.
Contributions API
GitHub feed
Public GitHub activity JSON.
- Called from the browser with the GitHub username.
- The site keeps no copy: the calendar shows the last year only.
Design decisions
Content is data
Every section reads from a typed file in src/config. The resume uses the same files, so there is no second copy to keep in sync.
Static first
Pages render at build time and read no cookies or headers. Only the view counter and the PDF route do work per request.
Themes without flash
Color themes are plain TypeScript objects turned into CSS at build time. A pre-paint script applies the saved choice, so there are no cookies and no flash.
Cache by content
The PDF URL carries a hash of the resume data. The cache key changes when the content does, and never otherwise.
Small client islands
Only the parts that need a browser run there: the theme switch, the counter, the GitHub graph and the download button.
Fail soft
Redis, the GitHub feed and the PDF route can each fail without breaking a page. The counter shows 0, the graph shows an error card, and the PDF button falls back to print.