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 be able to 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. It is written the way I'd expect to defend a design in review.
The stack at a glance
| Concern | Choice | One-line reason |
|---|---|---|
| Framework | Next.js 15 (App Router) | File-system routing + RSC + 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 tiny 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 — push dynamic behaviour to the client edges (the tools, theme, contact form) and keep everything else static — is exactly the boundary you want anyway.
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 is parsed through Zod schemas
(workFrontmatterSchema, etc.) so malformed content fails the build instead of
shipping a broken page — parse, don't validate. 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 in typed modules like data/home.ts, site facts 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) —
untrusted-by-default even for my own Markdown, because that's the habit that
prevents the one time it isn't 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 treated as engineering output, not marketing dust:
- Structured data as code (
lib/seo.tsx):Person,WebSite,ProfessionalService,FAQPage,BreadcrumbList,ItemList,ProfilePage,Article— composed per page with a shared@idgraph so entities resolve. llms.txt,llms-full.txt, a dedicated profile, and per-page Markdown companions are generated at build (scripts/generate-llms.ts) and linked 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.
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, don't validate — untrusted input becomes a typed value at the boundary; 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 is 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.