
Lean Portfolio
My earlier dngioi.com: a service-first portfolio for freelance work, with a services carousel, a work archive, a blog, reviews, a Start a Project form and its own admin CMS. Built with Next.js and MongoDB.
About the project
Lean Portfolio was the first version of dngioi.com, a freelance and business site for individuals, small businesses and teams that need practical software help. The homepage sells services through a full-screen banner carousel, Work is the proof library of projects, solutions and products, and Contact is a structured Start a Project form. A blog, an About page and visitor reviews round it out, and everything is managed from a custom admin area.
It started as a Vite and React app and moved to Next.js with a MongoDB backend, then grew in tracked milestones: the admin CMS, an image pipeline on Cloudflare R2, a product catalog, the home carousel, the work archive with service banners, and a light and dark theme. It was later replaced on dngioi.com by Personal Hub.
The screenshots show the app running locally with demo content.
Features
- Service banners: a full-screen home carousel, each banner with its audience, headline, summary and calls to action.
- Work archive: projects, solutions and products with type filters, image galleries, key features and a link to start a similar project.
- Start a Project: a structured inquiry form (business type, budget, timeline, preferred contact) that lands in the admin inbox.
- Blog with rich-text editing, tags and pagination; reviews from visitors; an About page with skills and experience.
- Admin CMS for the profile, skills, experience, posts, work, banners and messages.
- Light and dark themes built on semantic colour tokens, without a flash on load.
Screenshots
Architecture
- Next.js App Router with server components for public pages and route handlers for the JSON API.
- MongoDB through the official driver (it ran on Firestore's MongoDB-compatible API); request bodies validated with Zod schemas.
- Admin sign-in: a password exchanged for a JWT in an HTTP-only cookie (signed with
jose); every write route checks it. - Images compressed on the server with
sharpand stored on Cloudflare R2. - Shipped as a standalone Docker image and auto-deployed to Firebase App Hosting from GitHub Actions.
Challenges & what I learned
- Moving from a Vite single-page app to the Next.js App Router without losing content or URLs (old product pages redirect to Work).
- Keeping partial updates from wiping fields: Zod's
.partial()still applies defaults, so updates keep only the fields the caller sent. - Stopping spam on public forms with a honeypot, a minimum fill time and a rate limit per visitor.
- Reaching AA contrast in both themes by moving every colour to semantic tokens.
- A small CMS covers most of what a personal site needs, and owning it makes changes fast.
- Validation schemas need care at the edges: defaults and partial updates behave differently than they look.
- Tracking work as issues with a spec and a plan per change keeps a side project moving; that habit carried over to Personal Hub.
