Why write this down
A portfolio is a small system, but small systems are where you can afford to be principled: to make every choice on purpose and defend it. This is the architecture record for mepritam.dev: what runs it, why those tools, the trade-offs taken, and the patterns that keep it cheap to change. I wrote it the way I'd expect to defend a design in review.
The stack at a glance
Here is the stack in one view.
- Framework: Next.js 15 (App Router): file-system routing, RSC, and first-class static export.
- Language: TypeScript (strict): types as the cheapest test you can run.
- Rendering: Static export (SSG): no server to run, attack, or pay for; CDN-fast.
- Styling: Tailwind + design tokens: one source of truth; no CSS entropy.
- Content: MDX + gray-matter, Zod-validated: content/template separation with a schema guard.
- UI primitives: CVA + a small component layer: variants without a heavyweight UI framework.
- Icons: lucide-react + inline brand SVGs: tree-shakeable; brand marks under our control.
- SEO/LLM: Schema + llms.txt as code: machine legibility is a build artifact, not an afterthought.
Rendering: static export, and why
The whole site is output: "export": pre-rendered HTML to a CDN. For content
that changes on deploy, not per request, SSG is the correct default, not a
limitation.
- Performance is structural, not tuned. There's no server round-trip or hydration-blocking data fetch on the critical path; TTFB is CDN latency.
- The security surface is almost nil. No runtime, no server secrets, no request handlers to harden.
- Operations are trivial. Rollback is a previous immutable build; there is no capacity to plan.
The trade-off is honest: no per-request personalization and a full rebuild to publish. For a portfolio those costs are ~zero, so SSG dominates SSR here.
The discipline it forces matters too: push interactive behaviour to the client edges (the tools, theme, contact form) and keep everything else static. A 90+ Lighthouse score on mobile in 2026 is the baseline I hold the build to.
TypeScript in strict mode: types as a test budget
strict: true is non-negotiable. On a one-person project the cheapest test that
exists is the compiler. Frontmatter flows through Zod schemas
(workFrontmatterSchema, etc.) so malformed content fails the build instead of
shipping a broken page. Parse at the boundary, then trust the types. The type that comes out of Zod is
the type the templates consume, so content and rendering can't disagree.
Styling: a token-driven system, not utility soup
Tailwind is the engine; design/tokens.ts is the source of truth. Colour,
type hierarchy, spacing, radii, and motion live as tokens that tailwind.config.ts
imports. Components compose utilities but never invent raw hex or one-off pixel
values. Two consequences a senior reviewer cares about:
- Theming is a variable swap. Light/dark is CSS custom properties; there is no duplicated "dark stylesheet" to drift.
- The system resists entropy. New components inherit the scale instead of adding a 13th shade of grey. This is the difference between a design system and a pile of class names.
Variants use class-variance-authority (see components/ui/button.tsx): typed,
exhaustive variant maps instead of ad-hoc conditional className strings.
Content architecture: separation with a schema
Content is data; components are presentation. Long-form lives in content/*.mdx.
Page copy lives in typed modules like data/home.ts. Site facts live in data/site.ts.
The pipeline (lib/content.ts) reads MDX, validates frontmatter with Zod, and renders
through a unified/remark/rehype chain with sanitisation (rehype-sanitize).
Even my own Markdown stays untrusted-by-default, because that habit prevents the one time the input is not mine.
Why this matters: editing words never touches JSX, and adding a page is adding a file, not refactoring a component. That's the property that lets a codebase age without rotting.
SEO and LLM visibility as build artifacts
Discoverability is engineering output, not marketing dust.
- Structured data as code (
lib/seo.tsx):Person,WebSite,ProfessionalService,FAQPage,BreadcrumbList,ItemList,ProfilePage, andArticlecompose per page with a shared@idgraph so entities resolve. llms.txt,llms-full.txt, a dedicated profile, and per-page Markdown companions ship at build time (scripts/generate-llms.ts) and link via<link rel="alternate" type="text/markdown">. Assistants get clean, authoritative context instead of scraping rendered HTML.
This is a deliberate bet: the page now has a machine audience, and machine legibility is something you compile, not something you hope for.
Applied AI & Context Retrieval (The Vectorless RAG Choice)
For conversational retrieval (such as Nykaa's chatbot), we built a lightweight, vectorless RAG pipeline instead of standard database routing.
- The Tradeoff: A database like pgvector is the correct choice when scale grows. But for a small catalog, serializing OpenAI
text-embedding-3-smallvector coordinates into flat JSON files and performing cosine similarity in memory is faster. It eliminates external database hops, reduces search latency to<5ms, and saves $1,000s in hosting fees. - LangGraph State Loops: Rather than free-form loops, LangGraph organizes conversational states. An intent node classifies query boundaries, a tool router fetches contextual embeddings, and a custom reranker formats context. This guarantees deterministic paths and clean error boundaries.
Quality Pipelines: Vale, Jest, and class sorting
We treat code cleanliness and content consistency as automated build assertions:
- Tailwind Class Order:
prettier-plugin-tailwindcssformats class ordering. This keeps styles readable and prevents CSS size duplicates. - Vale Prose Linting: The Vale linter validates writing in a prebuild script. Vale checks markdown guidelines, grammatical mistakes, and naming consistency so tech notes stay professional.
- Boundary Testing: Unit tests target deterministic code boundaries: frontmatter schemas, utility functions, custom sitemaps, and search rankings. We maintain 95%+ coverage on critical client routes to ensure components fail-safe.
Containerized Edge Deployment (Docker & Traefik)
We build and package the site using modern container patterns:
- Standalone Node Build: Next.js compiles to a standalone folder, packing only files required for runtime. This reduces the Docker image size from 1.2GB to 140MB.
- Dynamic Edge Routing: The container deploys behind a central Traefik gateway. Instead of publishing host ports (like
80or443), Traefik maps traffic via isolated Docker network namespaces using container labels read at runtime. - Unprivileged Execution: Containers drop root privileges immediately at startup, running on a read-only layer to block unauthorized filesystem writes.
Patterns and principles applied
- Single source of truth: tokens for design, Zod types for content,
site.tsfor facts. Every fact has exactly one home. - Parse at the boundary: untrusted input becomes a typed value at the edge; the interior assumes correctness.
- Composition over configuration: small primitives (
Section,Card,Reveal,Button) compose into pages; no mega-components with twenty props. - Progressive enhancement: content renders without JS; interactivity (theme, reveals, tools) is additive and degrades cleanly.
- Performance as a constraint, not a phase: motion composites only, scripts
load
lazyOnload, fontsswap. The budget gets defended at design time. - Accessibility by construction: semantic landmarks, focus management, and
prefers-reduced-motionare part of the components, not a later audit.
What I'd revisit at scale
Good architecture also names its own seams. If this grew into a multi-author publication I'd add: a typed content layer with incremental builds (e.g. a content collection API) so full rebuilds don't bound publish time; visual regression tests on the component library; an OG-image generation step; and a small e2e smoke suite on the interactive tools. None are worth their complexity today, and knowing when a pattern starts paying for itself is the actual job.