project
How I Built My Portfolio
A walk-through of the stack behind moaaz.be, Next.js 16, Tailwind v4, MDX content, Vercel hosting, and GitHub for version control, and the small decisions that shaped it.

Building this site was one of those side-of-the-desk projects that I kept coming back to between internship shifts. It started life as a plain resume page, and over time turned into a proper e-portfolio: a place that tells the story of what I work on instead of just listing it.
This post is a short tour of what's under the hood.
What I wanted from it
A few things were non-negotiable:
- It had to feel like a portfolio, not a CV. Every project, hackathon, talk, and milestone gets its own page rather than living as a bullet in a list.
- It had to be easy to update. New posts and projects should be a single Markdown-ish file, not a database write or a CMS click-through.
- It had to be fast and cheap to host. Static rendering for everything, no servers to babysit.
- It had to stay honest. No invented projects, no padded experience, content is traceable to things I actually did.
The stack
Framework
Next.js 16 on the App Router, with React 19 and TypeScript. Every route is pre-rendered at build time, so the site is essentially a folder of HTML and assets by the time it ships.
Styling
Tailwind CSS v4, paired with CSS custom properties for the light/dark theme switch. No design system library, just utilities and a small set of reusable components (cards, pills, section labels, page shell).
A tiny bit of motion: Motion (formerly Framer Motion) drives the floating nav and scroll fades.
Content
This is where the architecture actually lives.
- Posts and project case studies are MDX files under
src/content/posts/andsrc/content/projects/. - Frontmatter declares everything the site needs to render the card and the page, title, date, type, tags, cover image, gallery, links, status.
- A small loader (
src/lib/content.ts) parses the MDX, computes reading time, and figures out related posts. - The MDX body is rendered server-side through next-mdx-remote with a custom component map, so I can drop a
<Callout>or a tech-stack pill row straight into a post when I want one. - gray-matter handles frontmatter; remark-gfm unlocks GitHub-flavoured markdown; rehype-slug + rehype-pretty-code wire up anchor links and syntax highlighting.
Images
I shoot most event photos on a phone, which means HEIC and DNG files end up in the repo before they ever see the web. sharp handles all of that: it decodes the raw, rotates by EXIF orientation, resizes to a sensible max width, and saves a mozjpeg-encoded JPEG into public/. The result is the original quality without the original file size.
Icons & UI bits
lucide-react is the only icon set. clsx for conditional class strings. That's about it, the rest is hand-rolled components.
Hosting & version control
- Vercel hosts the site. Every push to
maintriggers a build, runs the type-checker, generates every static page, and ships it to the edge. Branches get their own preview URLs, which is great for trying out content changes without touching production. - GitHub holds the source, code and content together, version-controlled side by side. Repo lives at github.com/TwoEazy.
- Combell is where the
moaaz.bedomain was registered, picked up through the academic software programme.
This pairing means the workflow is: write a post in MDX, commit, push. The site rebuilds itself.
How the routes are organised
/ one-pager: intro, featured projects, latest posts, experience
/about bio, skills, languages
/portfolio every post in one place
/projects all project case studies
/projects/[slug] a single project + linked posts
/blog all blog posts
/blog/[slug] a single post with gallery and related posts
/experience full work + education timeline
/skills grouped skills + languages
/resume CV PDF view + download
/contact email, LinkedIn, GitHub
/colophon the longer-form version of this post
Anchor navigation stays on the home page; route navigation is in the sticky top bar.
Decisions I'd make the same way again
- Content as files. No CMS, no admin panel, no migrations. Posts are Markdown; assets sit next to them. Easy to read in five years.
- Static by default. No server-side data fetching unless I actually need it.
- Frontmatter as a contract. Every type-shaped MDX file gets type-checked through the loader, so a missing field shows up immediately rather than silently rendering blank.
What's next
A few things I haven't built yet but want to:
- A proper tag index at
/blog/tag/[tag]. - An RSS feed.
- JSON-LD
BlogPostingschema per post (thePersonschema is already inlayout.tsx). - A small search box over post titles and tags.
If you're curious about how a specific page works, the source is open at github.com/TwoEazy, or check the colophon for a more reference-style breakdown of the stack.