ermalbujupaj.dev
This site — a monorepo platform for a personal brand, built like production software.
Design, engineering, content · 2026 — ongoing · Live
Overview
The site you are reading is itself a project: a Turborepo monorepo where the portfolio is just the first app. Future side projects ship as sibling apps on subdomains — spotifystats.ermalbujupaj.dev, trading.ermalbujupaj.dev — each independently deployable, all sharing one design system.
It is built the way I build production software: strictly-scoped phases, each verified and reviewed before the next starts, with architecture decisions captured as ADRs in the repo.
Problem
Personal sites rot. They get built once from a template, drift out of date, and can't absorb new ideas — a new side project means a new deployment story, a new design, a new repo. I wanted a personal platform with the properties of good production software: one brand, one design system, room to grow for a decade.
Solution
A pnpm + Turborepo workspace with a shared @ermal/ui package holding the entire visual identity — design tokens, fonts, primitives. The accent color is a single CSS variable; changing two lines re-brands every current and future app.
Content is typed data, never hardcoded: skills, experience and these case studies live in schema-enforced TypeScript files, so updating the CV is a data edit, not a refactor.
Architecture
Tailwind CSS v4 runs CSS-first: tokens are custom properties mapped through @theme, with shadcn-compatible naming so registry components drop in unmodified. Dark mode is a class toggled by next-themes before first paint — no theme flash.
React Server Components by default. Only five components hydrate on the client (theme toggle, mobile menu, two interactive sections, motion wrappers); everything else is server-rendered and statically prerendered.
Animations run through LazyMotion in strict mode — roughly half the bundle of full Motion — behind a MotionConfig that honors prefers-reduced-motion globally.
Engineering challenges
The mobile menu was swallowing navigation
Radix's dialog holds a body scroll lock until its exit animation ends — and a hash navigation issued during the lock silently never scrolls. The fix defers router.push until the sheet has finished closing; the debugging path (aria state fine, scroll position frozen) is the kind of bug that only shows up when you verify in a real browser.
lint-staged in a monorepo
Pre-commit hooks kept failing with ENOENT: the root config invoked ESLint, but ESLint only exists inside the workspace packages. The answer is lint-staged's closest-config-wins model — per-package .lintstagedrc files whose commands resolve from the package that owns the binary.
Design tokens across package boundaries
Tailwind v4's automatic source detection only scans the consuming app, so classes used inside @ermal/ui would silently vanish from the build. The token stylesheet declares its own scan root with @source — the package stays self-contained.
Lessons learned
- Tokens-first pays compound interest: every section since Phase 1 was styled purely with semantic utilities, and dark mode has never needed a special case.
- Server-Components discipline is a performance budget you set once — deciding 'client components need a reason' keeps the bundle honest forever.
- A green build is not a working app: the two real bugs in this codebase were both invisible to build, lint and typecheck, and only surfaced in browser verification.
What's next
- MDX blog with syntax highlighting, RSS and search (Phase 6).
- Live GitHub integration: pinned repos, contribution graph, language stats.
- First subdomain app to prove the platform thesis end to end.
Tech stack
- Next.js
- React Server Components
- TypeScript
- Tailwind CSS v4
- Turborepo
- pnpm
- Motion
- Vercel