PortoHub: Designing & Building a Full-Stack Portfolio Platform
- 24
- Data Models
- 30+
- Admin Pages
- 47+
- API Routes
- 9
- Themes
- 0critical
- WCAG Violations
- $0/mo
- Third-party CMS Cost
Project Overview
PortoHub is a full-stack, production-ready personal portfolio system I designed and built from scratch. It combines an enterprise-grade admin CMS with a public-facing portfolio site — built with Next.js 15, MongoDB, Cloudinary, and Tailwind CSS v4.
The goal was simple but ambitious: build a portfolio platform that is flexible enough to serve any creative professional, fast enough to rank in search, accessible enough to meet WCAG 2.1 AA, and beautiful enough to win clients.
- 24Mongoose schemas
- Data Models
- 30+management screens
- Admin Pages
- 47+REST endpoints
- API Routes
- 9switchable layouts
- Themes
- 13+rich content blocks
- Block Types
- WCAG 2.1AA compliant
- Accessibility
💡 Why I built this
Most portfolio platforms are either too rigid (Squarespace, Webflow) or too bare-bones (a static JSON resume). PortoHub fills the gap: a fully owned, fully customisable system with real CMS power, appointment booking, a media library, and multi-theme rendering — while still being a joy to maintain.
The Problem
As a senior UX/UI designer working across enterprise SaaS, I needed a portfolio that matched the depth and quality of the work I was presenting. The friction points with existing solutions were consistent:
- No ownership — platforms like Behance or Dribbble own your content and can change or remove it at any time
- Rigid structure — most portfolio builders force a fixed page structure; I needed full editorial control over how each case study was told
- Poor SEO — JavaScript-heavy SPA portfolios perform poorly in search without SSR
- No appointment scheduling — every inbound client inquiry still required manual back-and-forth
- Inaccessible defaults — most free themes fail WCAG basics: missing alt text, broken tab order, skipped heading levels
A designer's portfolio is their most important product. It should be held to the same standard as the work it showcases.
— Shahriar Shanto
Objectives & Success Criteria
The project had five non-negotiable objectives:
- Full ownership — self-hosted, own database, own media storage
- Rich editorial control — a block-based CMS that handles text, images, code, metrics, UX-specific blocks
- Switchable themes — one dataset, multiple visual identities selectable without rebuilding
- Integrated booking — end-to-end appointment scheduling with categories, time slots, and payment tracking
- Accessibility first — WCAG 2.1 AA compliance audited and enforced at every component level
✅ Success criteria
Every admin action under 3 clicks. Every public page passing axe-core scan with zero critical violations. Core Web Vitals green on Vercel Analytics. Zero dependency on third-party portfolio platforms.
Research & Discovery
Competitive Analysis
I audited 12 portfolio systems — ranging from no-code builders to developer-focused static site generators — across five dimensions:
| Platform | CMS Control | SEO | Accessibility | Booking |
|---|---|---|---|---|
| Squarespace | Limited | Good | Partial | Paid add-on |
| Webflow | Good | Good | Manual | None |
| Next.js + Contentful | Good | Excellent | Manual | None |
| PortoHub | Full | Excellent | WCAG AA | Built-in |
User Persona: The Solo Creative Professional
Primary user for the admin CMS: a senior designer or developer who publishes 1-3 case studies per month, manages 2-4 active client conversations per week, and wants to be found by hiring managers through organic search.
Key needs:
- Write and publish case studies without touching code
- Showcase process work (wireframes, research artifacts, metrics) alongside narrative
- Accept discovery calls and consultations without Calendly fees
- Control exactly how their brand appears — theme, colors, social links
Design & Development Process
Information Architecture
The first design decision was separating the system into two distinct user journeys with very different information needs:
- Public visitor — needs to discover work, assess credibility, and contact the owner
- Admin (portfolio owner) — needs to create and manage all content with minimal friction
This separation drove the route structure: a (public) route group for visitor-facing pages and a protected /admin tree for the CMS.
Design System Decisions
Rather than building a custom component library from scratch, I combined two proven systems:
- Tailwind CSS v4 for utility-first styling with native CSS variable theming
- Flowbite React 0.12 as the component base for form elements, modals, and data tables
- Lucide React for a consistent, accessible icon set
All interactive components were audited against WCAG 2.1 AA — adding aria-label to icon-only buttons, aria-expanded/aria-controls to disclosure patterns, and enforcing logical heading hierarchy on every page.
Theme Architecture
One of the key architectural decisions was the ThemeRouter pattern. Instead of storing layout logic in settings, all nine themes receive the same ThemeData object — a typed interface containing settings, projects, blogs, skills, achievements, testimonials, services, and gallery items.
This means switching themes in the admin panel is a single settings write. No content migration, no page re-builds. The active theme is read at request time and the correct component is rendered server-side.
// src/themes/ThemeRouter.tsx
export default function ThemeRouter({ data }: { data: ThemeData }) {
const theme = data.settings.activeTheme;
switch (theme) {
case "dark": return <DarkTheme data={data} />;
case "creative": return <CreativeTheme data={data} />;
case "minimal": return <MinimalTheme data={data} />;
case "glass": return <GlassTheme data={data} />;
case "bento": return <BentoTheme data={data} />;
case "sidebar": return <SidebarTheme data={data} />;
case "urbanist": return <UrbanistTheme data={data} />;
default: return <DefaultTheme data={data} />;
}
}Block Editor Design
The block editor is the heart of the CMS. The design requirement was: every editorial action should feel like using Notion, not like filling a database form.
Key UX decisions in the editor:
- Drag-to-reorder using dnd-kit — blocks snap into position with visual feedback
- Block picker modal — categorised into Basic, Advanced, and UX-specific blocks so the editor knows exactly what tool does what job
- Inline editing — each block is edited in context, not in a separate panel
- Rich text via Tiptap — supports bold, italic, links, code, bullet lists, and ordered lists within text blocks
Database Schema Design
MongoDB was chosen over a relational database for one specific reason: the block content system. Content blocks have heterogeneous shapes — a metrics block looks nothing like a quote block. Mongoose's Schema.Types.Mixed for the data field allows each block type to store exactly the fields it needs without null-padded columns.
The 24 models were designed with three guiding principles:
- Lean reads — all public-facing queries use
.lean()for plain JavaScript objects, avoiding Mongoose overhead - Compound indexes — every list query is backed by a compound index on
status + featuredorstatus + createdAt - Embedded SEO — every content model embeds its SEO metadata (title, description, og:image, canonical) rather than joining to a separate collection
Appointment System
The appointment booking system was the most complex feature — it needed to handle:
- Weekly recurring time slots (available hours per day of the week)
- Holiday/blackout dates that override the weekly schedule
- Multiple appointment categories with different fees and descriptions
- A clean booking form with calendar + time slot selection
- Payment status tracking (unpaid/paid/partial)
Rather than integrate a third-party scheduler (which adds cost and a third-party dependency), I built the full booking flow as a first-party feature using 5 models: Appointment, AppointmentCategory, AppointmentSetting, TimeSlot, and Holiday.
The Solution
PortoHub is deployed at portohub.shahriarshanto.online on Vercel (Singapore region) with MongoDB Atlas as the database and Cloudinary for media storage. The architecture is:
Architecture Overview
─────────────────────────────────────────
Public Site (SSR, Next.js App Router)
└─ 8 public routes (/blog, /projects, /contact, etc.)
└─ 9 switchable themes (DefaultTheme, DarkTheme, BentoTheme…)
└─ Force-dynamic rendering (always up-to-date from DB)
Admin CMS (Protected, JWT session)
└─ 30+ admin pages (/admin/blogs, /admin/settings, etc.)
└─ Block editor (13+ content block types, drag-to-reorder)
└─ Appointment booking system (full scheduling + payment)
└─ Media library (Cloudinary, usage tracking)
└─ Multi-CV management (6 template styles, analytics)
Data Layer
└─ MongoDB Atlas (24 Mongoose models)
└─ Cloudinary (images + files)
└─ Vercel Edge (static assets, CDN)
Monitoring / Analytics
└─ Vercel Analytics (Core Web Vitals)
└─ Google Analytics integration (configurable in settings)🚀 Key technical achievements
Server-side rendering on every public page (no hydration flash). Nine themes sharing one typed data interface. Full appointment booking without any third-party scheduler dependency. WCAG 2.1 AA compliance enforced at the component level with automated axe-core scanning.
Accessibility Engineering
Accessibility was treated as a first-class engineering concern, not a post-launch audit. The work covered four areas:
Semantic Structure
- Every page has a single
<h1>that describes the page purpose - Heading levels increment by one — a custom
normaliseHeadingLevels()function enforces this at render time for blog posts - Landmark regions (
<main>,<nav>,<footer>) on every page
Interactive Elements
- All icon-only buttons have explicit
aria-label(Edit, Delete, Close, etc.) - Disclosure patterns use
aria-expanded+aria-controls - Form inputs all have associated
<label>elements viahtmlFor/idwiring - Required inputs have
aria-required="true"
Color & Contrast
- All text/background pairings tested against WCAG AA contrast ratios (4.5:1 minimum)
- Ghost/outline buttons updated to Flowbite's
lightcolor spec for visible borders at rest - Dark mode tested separately — dark: variants applied consistently
- 0after audit
- Critical violations
- 0on public pages
- axe-core findings
- 0resolved
- Heading order issues
- 100%coverage
- Icon-only buttons labelled
Outcomes & Results
- < 15min (was: hours)
- Time to publish case study
- ≤ 3clicks to any task
- Admin actions
- 9without rebuilding
- Themes available
- 0critical on launch
- Accessibility violations
- $0vs $15-50/mo
- Third-party CMS cost
- < 2min on Vercel
- Deployment time
The most meaningful outcome is qualitative: the portfolio now feels like a product I'm proud of, not a workaround. Publishing a new case study takes under 15 minutes. Switching visual themes takes under 30 seconds. And every page passes automated accessibility scanning — which matters for the clients I work with in enterprise healthcare (HCBS platforms) where accessibility is a contractual requirement, not a nice-to-have.
Learnings & What I'd Do Differently
Every project teaches something. Here's what building PortoHub reinforced or changed in my thinking:
What worked well
- MongoDB + block content — the flexible schema was the right call. Structured content blocks with heterogeneous
datafields are far easier to iterate than fixed columns - Force-dynamic SSR — always-fresh data with no cache invalidation complexity. The performance cost is acceptable given the content update frequency
- ThemeRouter pattern — one of the simplest ideas in the codebase, but one of the most powerful. New themes can be added without touching the data layer
What I'd change
- Start with a design system audit earlier — several accessibility fixes (icon labels, heading order) could have been baked into initial components rather than fixed in a second pass
- Add image alt-text validation at upload time — currently alt text is optional; it should be required for uploaded images
- Consider Incremental Static Regeneration (ISR) for blog posts and case studies — SSR on every request is fine now, but ISR would scale better at higher traffic
🎓 Key takeaway for UX engineers
The best portfolio platform is the one you own completely. Build it like you'd build a client product — with user research (even when you are the user), an information architecture decision, and a tested accessibility baseline. Your portfolio is always-on evidence of how you work.