Roopa Ram

Roopa Ram — Portfolio

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.


Table of contents

  1. Quick start
  2. Tech stack
  3. Project architecture
  4. Folder structure
  5. Content management (JSON CMS)
  6. Adding a project / skill / testimonial
  7. Backend & API options
  8. Database schema (when you upgrade)
  9. Deployment
  10. Environment variables
  11. SEO strategy
  12. Performance & accessibility
  13. Admin / CMS roadmap
  14. Bonus features roadmap
  15. Scripts

Quick start

# 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.


Tech stack

Frontend

Content / CMS

SEO & PWA


Project architecture

                      ┌─────────────────────────────┐
                      │   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

Folder structure

.
├─ .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

Content management (JSON CMS)

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.


Adding content

Add a project

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.

Add a skill / testimonial / service / experience

Same pattern — append a typed entry to the matching file in src/data/. TypeScript will tell you what’s required.

Add a blog post

  1. Add metadata to src/data/blog.ts.
  2. (Optional) Put the markdown body in content/blog/<slug>.md and wire a markdown loader. The gray-matter and reading-time dependencies are already installed.

Backend & API options

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.


Database schema (when you upgrade)

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)

Deployment

GitHub Pages (default)

The repo ships with .github/workflows/deploy.yml. On every push to main it:

  1. Installs dependencies
  2. Runs next build with output: "export" and the right basePath
  3. Adds .nojekyll
  4. Uploads ./out and deploys to GitHub Pages

One-time setup

  1. Push the repo to GitHub.
  2. In the repo: Settings → Pages → Build and deployment → Source: GitHub Actions.
  3. (Optional) Set repository variable 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.
  4. Push to 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)

Limitations of GitHub Pages

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

Vercel

The fastest upgrade. Vercel runs the same Next.js app with server actions, API routes, ISR, and image optimization. Steps:

  1. Import the repo into Vercel.
  2. Remove output: "export" from next.config.mjs (or set an env that bypasses it).
  3. Vercel auto-detects the framework and deploys.

Recommended if you want server actions, the admin panel, or a real /api/contact endpoint.

Netlify

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.

Railway / Render (full stack)

When you want the admin dashboard + MongoDB + REST API:

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)

Environment variables

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

SEO strategy

What’s already wired:

Recommended next:

  1. Generate a real OG image at public/og-image.png (1200×630). Tools: next/og (works on Vercel) or Figma export.
  2. Add structured data for projects (SoftwareApplication schema) once you have real screenshots.
  3. Submit your sitemap to Google Search Console + Bing Webmaster Tools.
  4. Add Plausible or Umami analytics — both are GDPR-safe and don’t slow the site down.

Performance & accessibility


Admin / CMS roadmap

When you’re ready for an admin panel:

  1. Move the JSON files into MongoDB collections using the schema above.
  2. Build an admin app at src/app/(admin)/ — protected by NextAuth.js or Clerk.
  3. Wire CRUD via Next.js server actions (when hosted on Vercel) or REST endpoints (when hosted on Railway).
  4. Image upload: Cloudinary or UploadThing, store URLs in MongoDB.
  5. Rich text editor: Tiptap (already React, plays well with Tailwind).
  6. Dashboard analytics: Plausible API + Recharts.

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.)

Bonus features roadmap

These are wired in spirit (libraries installed, hooks in place) and easy to enable:


Scripts

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)

License

MIT — fork it, remix it, ship your own. If it helps you land a gig, an attribution back is appreciated but not required.