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.

  1. Source of truth

    Edit files, not pages
    • Static 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)

  2. Next.js 16 server

    App Router · React Server Components
    • Static 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

  3. Browser

    Small client islands
    • Theme 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

  4. External

    Not owned
    • Upstash 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.

Designed & developed byAmit Dhiman
Building @ ChaiBuilder•© 2026