This is my personal portfolio — built from scratch, no templates, no "I'm passionate about synergising innovative solutions" energy.
Clean UI, real projects with actual case studies, and a live blog powered by the Dev.to API.
Facts only, checked against the build in this repo (last verified 18 Oct 2026):
| Area | What the build actually does |
|---|---|
| Code splitting | Every page and the heavy components lazy-load; React, router, motion, styled-components and Helmet each get a vendor chunk |
| Compression | Gzip and Brotli files are pre-generated at build time |
| Prerendering | /, /about, /projects, /blog, /contact and every case study at /projects/<slug> ship as real static HTML, plus a prerendered 404.html |
| Images | AVIF banners with 400w/800w variants and srcset; the hero portrait has six widths |
| Accessibility | Skip link, focus traps with background inert, route-change focus, :focus-visible rings, reduced-motion support, WCAG AA text contrast |
| Security headers | CSP without inline scripts, HSTS, X-Frame-Options: DENY, CORP/COOP, Permissions-Policy |
Measured against the production build on this machine (npm run preview, Lighthouse v13.5.0, 19 Oct 2026):
| Page | Preset | Performance | Accessibility | Best Practices | SEO |
|---|---|---|---|---|---|
/ |
Desktop | 98 | 100 | 100 | 100 |
/ |
Mobile | 82 | 100 | 100 | 100 |
/projects |
Desktop | 98 | 100 | 100 | 100 |
| Tool | Why I picked it |
|---|---|
| React 19 | Bleeding edge. I like living dangerously. |
| TypeScript 5.7 | Because undefined is not a function ruins lives. |
| Vite 6 + Terser | Fast builds, aggressive minification. |
| Styled Components v6 | CSS-in-JS. Judge me. I regret nothing. |
| Framer Motion 12 | The secret behind all the whoosh effects. |
| React Router v7 | URL routing without ceremony. |
| React Helmet Async | The only owner of per-page meta tags and JSON-LD. |
| Puppeteer (build-time only) | Prerenders every route to static HTML. |
| Dev.to API | Live blog posts, no API key needed. |
src/
├── pages/ # The main "rooms" of the site
│ ├── Home.tsx # Hero, stats, selected work, latest writing
│ ├── About.tsx # Timeline, skills, focus areas
│ ├── Projects.tsx # Filters, featured rows, project table
│ ├── projects/ # Styled components + case-study modal
│ ├── Blog.tsx # Posts fetched from Dev.to
│ ├── Contact.tsx # Form, details, socials, visible FAQ
│ └── NotFound.tsx # 404
│
├── components/ # Reusable pieces
│ ├── Navbar.tsx # Focus-trapped, portaled mobile menu
│ ├── Footer.tsx # Nav, socials, copy-email
│ ├── SEO.tsx # Meta tags, OG/Twitter cards, JSON-LD
│ ├── Breadcrumb.tsx # BreadcrumbList for known sub-pages only
│ ├── FAQSchema.tsx # FAQPage markup backed by visible content
│ ├── DecryptText.tsx # Text scramble with an sr-only real label
│ ├── LoadingSpinner.tsx # Full-screen loader while chunks arrive
│ ├── ErrorBoundary.tsx # Route + app error fallbacks
│ ├── ExitIntentPopup.tsx # Suppressed on /contact and over dialogs
│ ├── ResumeDownload.tsx # Hover/focus/Escape resume preview
│ ├── BlogCard.tsx # Dev.to article rows
│ ├── ScrollToTop.tsx # Scroll reset + focus handoff
│ ├── icons.tsx # Shared SVG glyphs
│ └── layout/primitives.tsx # SectionHeading / MicroLabel / TechPill
│
├── data/
│ ├── projects.ts # All project data (edit this to add projects)
│ └── faq.ts # One FAQ list for UI + structured data
│
├── hooks/
│ ├── useFocusTrap.ts # Tab cycling, Esc to close, focus restore
│ ├── useScrollLock.ts # Reference-counted page scroll lock
│ ├── useBackgroundInert.ts # Inert background while a dialog is open
│ ├── useDevtoPosts.ts # Shared Dev.to cache subscription
│ └── useMediaQuery.ts # Resize-reactive media query
│
├── utils/
│ ├── devto.ts # Dev.to fetch + validated memory/localStorage cache
│ ├── site.ts # URL, email, socials, nav, shared copy
│ └── routes.ts # Typed route constants
│
├── styles/
│ ├── critical.css # Tokens, resets, global focus styles
│ ├── GlobalStyle.ts # Container, Button, Badge
│ └── surfaces.ts # Surface mixins
│
├── assets/ # Project images (plus generated 400w/800w variants)
├── App.tsx # Routing, lazy loading, shell
└── index.tsx # Entry point
npm run build does three things:
- Type check —
tsc -b - Bundle — Vite + Terser, gzip/Brotli compression, manual vendor chunks
- Postbuild:
generate-sitemap.mjswritesbuild/sitemap.xml, taking eachlastmodfrom the last git commit that touched that pageprerender.mjsserves the clean SPA shell, visits every route, waits for the rendered<h1>(and for the blog to settle), inlines styled-components CSS, and writes static HTML — including404.htmlverify-build.mjsthen asserts every written document is crawlable: exactly one title, description, canonical and robots tag, one<h1>on each page, anoindexand canonical-free404.html, a sitemap that lists every indexable URL, and a present social card. A regression fails the build.
Security headers and the 404 rule live in netlify.toml. There is no second _headers / _redirects file to drift out of sync.
npm run lint # ESLint (flat config)
npm test # Vitest: project data, site config, date formatting
npm run build # type check → bundle → sitemap + prerenderCI runs the same three commands on every push to master and every pull request.
You'll need: Node.js 20+ and the urge to poke around someone else's code.
git clone https://github.com/rolan-r-n-r/RNR.git
cd RNR
npm install
npm run devOpen http://localhost:5173 and you're in.
npm run preview # preview the production build locallyHeads up: the prerender step needs Chrome. It auto-detects the usual locations on Windows/macOS/Linux, or you can point it somewhere with the
CHROME_PATHenv var.npm run devdoesn't need any of this.
cp .env.example .env.localOnly one variable matters: VITE_DEVTO_USERNAME. Set it to your Dev.to handle and the blog page is live. Production values belong in the Netlify dashboard.
- Portfolio — rolan-rnr.netlify.app
- GitHub — @rolan-r-n-r
- Blog — dev.to/rolan_r_n_r
- Email — rolanlobo901@gmail.com
- Based in — India
Made with care, an embarrassing amount of coffee, and way too many console.logs — which are now gone in prod, RIP.
— Rolan
If you liked it, a star goes a long way.