Premium, production-ready portfolio website for Roopa Ram — Flutter & Full Stack Developer.
Built with Next.js 15 (App Router), TypeScript, Tailwind CSS, Framer Motion, ShadCN UI, and Lucide React. Static-exported and deployable to GitHub Pages via a single push to main.
Goals: feels modern, loads fast, ranks well, is fully content-managed via JSON, and runs on a free GitHub Pages plan with optional upgrade paths to Vercel / Netlify / Railway / Render when you need a real backend.
# 1. Install
npm install
# 2. Copy env
cp .env.example .env.local
# 3. Run
npm run dev # http://localhost:3000
# 4. Production build (static export)
npm run build # outputs to ./out
The build emits a fully static site in
./out. That’s what GitHub Pages serves.
Frontend
output: "export")next-themes (system-aware dark/light mode)embla-carousel-react (testimonials slider)react-hook-form + zod (typed, validated contact form)Content / CMS
src/data/*.ts — type-safe, version-controlled, zero infra.SEO & PWA
metadata, Open Graph, Twitter cards, JSON-LD Person schemasitemap.xml + robots.txt via app/sitemap.ts / app/robots.tsmanifest.webmanifest for PWA install prompts ┌─────────────────────────────┐
│ Next.js 15 App Router │
│ src/app/page.tsx │
└──────────────┬──────────────┘
│ composes
┌────────────────────┼────────────────────┐
▼ ▼ ▼
Feature sections UI primitives Lib utilities
src/features/* src/components/ui src/lib/*
│
▼
JSON CMS (typed)
src/data/*.ts
src/types/cms.ts
build → static export → ./out → GitHub Pages
src/features/<name>/<name>-section.tsx. They consume data from src/data and primitives from src/components/ui.#home, #about, …) so the whole experience is one fast, SEO-friendly route.mailto: fallback so it works on GitHub Pages out of the box..
├─ .github/workflows/deploy.yml # GitHub Pages CI/CD
├─ public/ # static assets (resume, og image, icons)
│ ├─ resume/ # drop Roopa-Ram-Resume.pdf here
│ ├─ projects/ # project covers & screenshots
│ ├─ testimonials/ # avatars
│ ├─ blog/ # cover images
│ ├─ manifest.webmanifest
│ └─ .nojekyll
├─ src/
│ ├─ app/
│ │ ├─ layout.tsx # fonts, theme, navbar, footer, providers
│ │ ├─ page.tsx # composes the sections
│ │ ├─ globals.css # tailwind layers + design tokens
│ │ ├─ loading.tsx # loading screen
│ │ ├─ not-found.tsx # branded 404
│ │ ├─ sitemap.ts # static sitemap
│ │ └─ robots.ts # robots.txt
│ ├─ components/
│ │ ├─ layout/ # navbar, footer, back-to-top, background, cursor glow
│ │ ├─ ui/ # ShadCN-style primitives
│ │ ├─ theme-provider.tsx
│ │ ├─ theme-toggle.tsx
│ │ └─ typing-effect.tsx
│ ├─ features/ # one folder per section
│ │ ├─ hero/
│ │ ├─ about/
│ │ ├─ skills/
│ │ ├─ projects/
│ │ ├─ experience/
│ │ ├─ services/
│ │ ├─ testimonials/
│ │ ├─ blog/
│ │ └─ contact/
│ ├─ data/ # JSON-based CMS (typed)
│ │ ├─ site.ts # site config + nav + socials
│ │ ├─ skills.ts
│ │ ├─ projects.ts
│ │ ├─ experience.ts
│ │ ├─ services.ts
│ │ ├─ testimonials.ts
│ │ ├─ blog.ts
│ │ └─ stats.ts
│ ├─ hooks/ # reusable hooks
│ ├─ lib/ # cn(), motion variants, SEO, icon registry
│ └─ types/ # shared TS types
├─ next.config.mjs # output: 'export' + basePath for GH Pages
├─ tailwind.config.ts
├─ postcss.config.mjs
├─ tsconfig.json
├─ .env.example
└─ package.json
Every piece of dynamic content lives in src/data/*.ts, fully typed against src/types/cms.ts. There is no database to spin up, no admin panel to authenticate against — just edit a file, commit, push, and CI re-deploys.
src/data/
site.ts # name, tagline, socials, nav, resume URL
skills.ts # categorized skills with levels and icons
projects.ts # project cards + categories
experience.ts # timeline items
services.ts # service offerings
testimonials.ts # client/teammate quotes
blog.ts # post metadata (extend with markdown later)
stats.ts # stat cards used on hero & about
Why JSON-based and not a database?
| Concern | JSON CMS | DB-backed CMS |
|---|---|---|
| GitHub Pages compatible | ✅ yes | ❌ needs a server |
| Free to host | ✅ yes | 💸 hosting cost |
| Versioned / auditable | ✅ git history | ❌ separate audit trail |
| Editable by non-devs | ❌ needs git | ✅ admin UI |
| Build-time SEO | ✅ everything in HTML | ⚠ needs SSR/ISR |
When you outgrow JSON (typically when a non-developer needs to edit content), follow the database schema and the upgrade path further down.
Append an entry in src/data/projects.ts:
{
id: "my-new-app",
title: "My New App",
category: "Flutter", // 'Flutter' | 'Full Stack' | 'Backend' | 'UI/UX'
description: "Short one-liner shown on the card.",
longDescription: "Longer paragraph for detail views.",
thumbnail: "/projects/my-new-app/cover.png",
screenshots: ["/projects/my-new-app/shot-1.png"],
tech: ["Flutter", "Firebase"],
features: ["Feature A", "Feature B"],
links: {
github: "https://github.com/...",
live: "",
playStore: "",
appStore: "",
},
featured: true,
year: 2025,
}
Drop the cover/screenshot files under public/projects/my-new-app/. The card resolves URLs through withBasePath so it also works on GitHub Pages project sites.
Same pattern — append a typed entry to the matching file in src/data/. TypeScript will tell you what’s required.
src/data/blog.ts.content/blog/<slug>.md and wire a markdown loader. The gray-matter and reading-time dependencies are already installed.This site is statically exported, so by default it has no backend. The contact form ships with three drop-in providers — pick whichever fits your goal.
| Provider | Where it runs | GitHub Pages? | Best for |
|---|---|---|---|
| Formspree (default) | 3rd-party | ✅ yes | Contact form on a static site |
| EmailJS | Pure client | ✅ yes | Same, no signup needed on backend |
| Resend / Nodemailer | Server (Vercel/Netlify functions) | ❌ no | Full control, transactional email |
For Formspree, set:
NEXT_PUBLIC_FORMSPREE_ENDPOINT=https://formspree.io/f/<id>
For EmailJS, wire the public keys (already templated in .env.example).
If you don’t set any provider, the form falls back to opening the visitor’s default mail client with the message pre-filled — still functional, never broken.
When you migrate to MongoDB (or any document DB), this is the schema the JSON CMS already implies:
// projects
{
_id: ObjectId,
slug: string, // unique
title: string,
category: "Flutter" | "Full Stack" | "Backend" | "UI/UX",
description: string,
longDescription?: string,
thumbnail: string, // CDN URL
screenshots: string[],
tech: string[],
features: string[],
links: { github?: string; live?: string; playStore?: string; appStore?: string },
featured: boolean,
year: number,
publishedAt: Date,
updatedAt: Date,
}
// skills (one document per category)
{
_id: ObjectId,
slug: string, // "mobile" | "frontend" | ...
title: string,
description: string,
icon: string,
items: { name: string; level: number; icon: string }[],
}
// experience
{
_id, role, company, location, period, type,
summary, responsibilities: string[], tech: string[],
}
// testimonials
{ _id, name, role, company, avatar, rating, feedback, approved: boolean }
// services
{ _id, title, icon, description, bullets: string[] }
// blogPosts
{
_id, slug, title, excerpt, cover, tags, category,
readingMinutes, date, author, content, // markdown
status: "draft" | "published",
}
// contactMessages
{ _id, name, email, subject, message, createdAt, ip, userAgent, status }
// adminUsers
{ _id, email, passwordHash, role: "owner" | "editor", createdAt }
Suggested REST routes (Next.js API or Express):
GET /api/projects
GET /api/projects/:slug
POST /api/projects (auth: owner|editor)
PATCH /api/projects/:slug (auth: owner|editor)
DELETE /api/projects/:slug (auth: owner)
GET /api/skills
GET /api/services
GET /api/testimonials (only approved=true)
POST /api/testimonials (open, marked approved=false)
GET /api/blog
GET /api/blog/:slug
POST /api/contact (rate-limited)
POST /api/auth/login
GET /api/admin/messages (auth)
The repo ships with .github/workflows/deploy.yml. On every push to main it:
next build with output: "export" and the right basePath.nojekyll./out and deploys to GitHub PagesOne-time setup
NEXT_PUBLIC_SITE_URL to your final URL — the workflow already infers it from the repo name, but a custom domain or override goes here.main. The workflow finishes in ~2 min and your site is live at https://<user>.github.io/<repo>/.Custom domain (e.g. roopa-ram.dev)
CNAME file under public/ with your domain (one line, no protocol):
roopa-ram.dev
GITHUB_PAGES=false in CI so basePath is dropped (root-served on a custom domain). Easiest way: don’t set GH_PAGES_REPO — next.config.mjs already gates basePath on its presence.NEXT_PUBLIC_SITE_URL to https://roopa-ram.dev.| Want to… | Works on GH Pages? | Workaround |
|---|---|---|
| Static HTML/CSS/JS | ✅ | — |
| Client-side React / Next.js export | ✅ | output: 'export' (this repo) |
| Contact form submissions | ⚠ | Formspree / EmailJS (no server needed) |
| API routes / server actions | ❌ | Move to Vercel/Netlify, or proxy through Railway |
| Auth-gated admin panel | ❌ | Host admin on Vercel; keep the public site on Pages |
| ISR / SSR / Edge | ❌ | Vercel or Netlify |
Image optimization via next/image loader |
❌ | unoptimized: true (already set) |
| Privacy-friendly analytics | ✅ | Plausible / Umami via env var |
The fastest upgrade. Vercel runs the same Next.js app with server actions, API routes, ISR, and image optimization. Steps:
output: "export" from next.config.mjs (or set an env that bypasses it).Recommended if you want server actions, the admin panel, or a real /api/contact endpoint.
Similar to Vercel — works great with Next.js via the Netlify Next.js Runtime. Drop the project, set NEXT_PUBLIC_SITE_URL, and deploy. Netlify Forms is a strong alternative to Formspree for contact submissions.
When you want the admin dashboard + MongoDB + REST API:
NEXT_PUBLIC_API_URL=https://api.roopa-ram.dev.A reasonable split:
roopa-ram.dev → Vercel (Next.js)
admin.roopa-ram.dev → Vercel (Next.js, admin app)
api.roopa-ram.dev → Railway (Express + MongoDB)
See .env.example. The variables actually used:
| Var | Where | Required? |
|---|---|---|
NEXT_PUBLIC_SITE_URL |
metadata, OG, JSON-LD, sitemap | recommended |
GITHUB_PAGES |
next.config.mjs (gates basePath) |
only on GH Pages |
GH_PAGES_REPO |
next.config.mjs (sets basePath) |
only on GH Pages |
NEXT_PUBLIC_FORMSPREE_ENDPOINT |
contact form | optional |
NEXT_PUBLIC_EMAILJS_* |
contact form (alt) | optional |
NEXT_PUBLIC_GITHUB_USERNAME |
GitHub stats widget (when you wire it) | optional |
What’s already wired:
<title> template, OG, Twitter cards, canonical via metadataBase, keywords, author.Person schema in the root layout — helps Google build a knowledge card.app/sitemap.ts and app/robots.ts.viewport.themeColor).<h1> per page, <section> per concept, <ol> for the experience timeline.next/font, lazy image loading, prefers-reduced-motion respected, unoptimized images sized via aspect-ratio to avoid CLS.Recommended next:
public/og-image.png (1200×630). Tools: next/og (works on Vercel) or Figma export.SoftwareApplication schema) once you have real screenshots.globals.css short-circuits animations under prefers-reduced-motion.<button> or <a>, with visible focus-visible rings.<img> tags hide cleanly if the asset is missing (no broken-image icons).scroll-mt-24 on every section keeps anchor jumps clear of the fixed navbar.When you’re ready for an admin panel:
src/app/(admin)/ — protected by NextAuth.js or Clerk.Folder hint:
src/app/(admin)/
layout.tsx # admin shell, auth guard
page.tsx # dashboard
projects/ # CRUD
blog/ # CRUD with Tiptap
testimonials/ # approval queue
messages/ # contact inbox
src/services/ # API client (fetchProjects, etc.)
These are wired in spirit (libraries installed, hooks in place) and easy to enable:
src/data/site.ts.cmdk package + global hotkey; route to #section IDs.xterm.js or a custom mini-shell rendering portfolio commands.src/components/layout/cursor-glow.tsx).tsparticles if you want more than the gradient blobs.next-intl (requires moving off pure static export or pre-rendering all locales).next-pwa (works on Vercel) or a hand-rolled service worker (works on GH Pages).@bytemd/react-gh-contributions or github-readme-stats via image embed.npm run dev # local development
npm run build # static export → ./out
npm run start # serve build locally (only useful if you remove output:'export')
npm run lint # eslint
npm run typecheck # tsc --noEmit
npm run format # prettier write
npm run deploy:gh # build + add .nojekyll (CI also does this)
MIT — fork it, remix it, ship your own. If it helps you land a gig, an attribution back is appreciated but not required.