A typography-led Nuxt 4 portfolio (saaranshmenon.me): warm dark broadsheet, hairline rules, one hot accent, and a hero sentence annotated like the transaction trace my payment systems produce. Production ships zero framework JavaScript — the whole client runtime is ~30 inlined lines.
- Nuxt 4 — static generation via
nuxt generate,features.noScripts: 'production' - Vue 3 + TypeScript (strict) — dev-time only; no hydration ships
- Sass — hand-rolled token system (no Tailwind, no modules, no icon/image libs)
- Fraunces + JetBrains Mono, variable woff2, self-hosted in
public/fonts/(latin subset, downloaded once from Google Fonts)
- Content lives in
app/data/*.ts(typed; components never hardcode copy). Update projects/bio/case studies there. - Design system:
app/assets/scss/_tokens.scssis the single source — every color/space/shape/motion value is a CSS custom property on:root._typography.scssholds the three type roles (display serif / mono metadata / system body) and the@font-facerules._base.scssholds the hairline section grammar, link/button behaviors, reveal grammar, grain. - Motion: one inline script (
MOTION_JSinnuxt.config.ts) — IntersectionObserver reveals (60ms sibling stagger) + the kinetic hero weight (pointer-x →wght320–640). Everything sits fully visible with no JS and underprefers-reduced-motion. - Signature: the hero sub-sentence renders spans with dotted underlines and tiny mono tags (
uptime/auth ok/settle t+1), typeset like a payment transaction trace. The Instagram Comment Analyzer case study stays published, framed explicitly as past after-hours work. - Fonts:
fraunces-latin-var.woff2(67KB, preloaded) ·jetbrains-mono-latin-var.woff2(40KB) ·fraunces-italic-sub.woff2(24KB, lowercase-only subset for the single accent-italic word — keep italics lowercase or glyphs fall back to Georgia italic). - Machine layer:
public/llms.txt, JSON-LDPerson(app.vue), data-drivenserver/routes/sitemap.xml.ts, OG image rendered in the site's own type system.
npm install
npm run dev # dev server at http://localhost:3000 (scripts/HMR on)
npm run generate # prerender static site to .output/public (no scripts)
npm run preview # serve the generated output locally
npm run typecheck # vue-tsc via nuxt typecheckFirebase Hosting (project vue-express-54b2e), serving prerendered .output/public — fully static. Hashed assets (/_nuxt) and /fonts/** get immutable cache headers; HTML revalidates every request.
npm run deploy:preview # generate + deploy to the "staging" preview channel
npm run deploy # generate + deploy to productionEvery TODO(saaransh) in app/data/ — resolve before launch:
-
profile.ts— hero metric strip: three real outcomes (payments scale · one before→after number · team growth) -
profile.ts— the signature'suptime todotag: the real uptime number (e.g.99.99%) -
profile.ts— status line wording (open to …) -
caseStudies.ts— confirm the year indated: 'after-hours project · 2025 · past work' -
profile.ts— drop the resume PDF intopublic/resume.pdf -
caseStudies.ts— unsnooze: two outcome metrics (downloads/week · sessions revived), the AI-vs-judgment honest note, confirm the rejected-alternatives framing -
caseStudies.ts— instagram-comment-analyzer: scale paragraph (context), the real constraint, how the fusion reconciles (decision), the two actually-rejected alternatives, three outcome metrics, honest notes -
projects.ts— expand the Vue Account Manager description (everything else verified against the GitHub API 2026-07-11: recency order, repo names, live URLs) -
socials.ts— confirm the Medium account is still active