Building this portfolio
The first question one should always ask before building something is: "Why bother?" In a world of portfolio builders and templates, creating a site from scratch is a valid question with a simple answer: I wanted to.
At work, I've been diving deeper into full-stack development with React and Vue, while at home, my personal homelab has become a passion project. These two interests went hand-in-hand when I decided I wanted a space to write about my projects. What started as a simple blog idea quickly grew into a full-fledged portfolio, and the homelab was the perfect place to build and host it.
The Technology Stack
I chose a modern, performant, and scalable stack that reflects the tools I use daily and wanted to showcase and learn more about.
Frontend Application
- Framework: Next.js 15 with the App Router, chosen for its powerful server-side rendering capabilities, performance-first approach, and seamless developer experience.
- Styling: Tailwind CSS v4 with a custom, theme-aware design system using CSS variables.
- Components: Headless UI for accessible components like the modals and mobile menu.
- Animations: Framer Motion for subtle, polished UI animations.
- Blog: Statically generated from local Markdown files, parsed with
unifiedandrehype, with syntax highlighting powered byshiki.
Self-Hosted Infrastructure
- Hypervisor: Proxmox VE running on an ASUS PN64 Mini PC.
- Web Server: A lightweight Nginx container serves the static site files.
- Proxy: Nginx Proxy Manager handles SSL and acts as the secure gateway for all traffic.
- Analytics: A self-hosted instance of Plausible Analytics provides privacy-focused site tracking.
Design and Layout
My goal was a clean, modern, and professional aesthetic that felt personal and unique.
- The Landing Page: I opted for an "Atmospheric Focus" design—a minimalist portal with a dynamic, multi-layered background and interactive buttons to guide visitors to the main sections of the site.
- The Portfolio: A single, filterable grid inspired by modern portfolio sites. Users can filter by category (Code, Art, Game) and view project details in a tabbed modal without leaving the page.
- The About Page: A portfolio needs a CV portion (at least so I was thinking). This is heavily inspired by the elegant, scroll-spy navigation of designers like Brittany Chiang. A sticky left column provides context and navigation, while the right column tells my professional story.
- The Blog: A minimalist, typography-focused layout designed for maximum readability, powered by the
@tailwindcss/typographyplugin.
Components of Note
Beyond the overall layour, a lot of effort went into crafting the core interactive components to be reusable, theme-aware and user-friendly. A core principle for this project was clean architecture. I intentionally separated the presentation layer (React components) from the business logic and data sources. All project details, work experience, and social links are stored in dedicated .js files, which are then fetched and passed into the components as props. This approach makes the site easier to update and maintain without touching the component code itself, and sticking to the DRY principle.
- The Project Card: The project card is the centerpiece of the portfolio. My goal was to make it more than just a static link. It needed to be interactive and visually tied to the content it represents:
- Dynamic Theming: Instead of a single color, the card's border and hover glow are dynamically colored based on its primary tag (e.g., 'Code', 'Art', 'Game'). This is handled by a helper function that pulls from a centralized set of CSS theme variables, ensuring colors are consistent and theme-aware.
- Interactive Feedback: On hover, the card gently scales up and emits a soft glow matching its category color, providing clear and satisfying feedback.
- The Project Modal: Clicking a project card doesn't navigate away from the page. Instead, it opens a detailed modal, keeping the user in context. This component had to be flexible enough to handle different types of media.
- Dynamic Tabs: To avoid a cluttered, long-scrolling modal, I implemented a dynamic tab system using Headless UI. The modal checks if a project has extra media, like a Sketchfab model or a YouTube video. If it does, it automatically generates tabs for "Details," "Sketchfab," and "YouTube," keeping the content neatly organized.
- Scrollable Content: For projects with a lot of text and images, the main content area of the modal becomes scrollable while the header image and footer button remain fixed. This was achieved with a nested flexbox layout, ensuring the modal is always easy to use and never overflows the screen.
- The Experience Item: These are used in the
Aboutpage to create the different experience items: such as the different employment and education cards.
Challenges & Solutions
Building from scratch is never without its hurdles. Here are a few of the most interesting challenges I encountered:
Making up my mind
Probably the biggest challenge was to find out "what did I want with this"; what could I attempt to make that would be fun enough to build and yet something good looking enough to be "worth" showcasing. The entire layout went through several iterations and there were many occasions I was ready to say "well this was fun enough, but on to the next thing". One of the earliest challenges was translating a 'blank canvas' into a concrete design. My approach was iterative; I went through several layout concepts, wireframes, and color palettes. Sticking with key decisions, like the 'Atmospheric Focus' landing page, created a foundation that guided the rest of the site's design.
The Bane of CSS
Now I am not a frontend developer by trade: I am dabbling more and more in it at work, but the frontend is just an aspect of many of the things I touch upon at work. But whenever I do, there is this constant battle of getting things center, sitting nicely in place, or just in general behaving as I envision it. Now TailwindCSS was AMAZING, don't get me wrong, but there was still a constant battle of having one div formatted one way, while the next div formatted differently to get it to look as I want it. Then we add on embedding from other sites like Sketchfab, and making that play nicely within my format. It did not help that I did the rookie mistake of first hard-coding all the colors when setting up, which gave me a good cleanup job when I remembered that "light mode" is a thing.
It works on my computer
The unending hurdles one has to face going from running npm run dev to having the site finish built accessible through the web. First hurdle was getting the URL to work properly: This is not the first container that I have opened to the web, but it is by far the hardest one I have had to get working. I ran into constant "502 Bad Gateway" that would randomly take the site down mid debugging and setups. Only for me to figure out I did the rookie mistake of forgetting to reserve the static IP for the container, meaning my Pixel Tablet would sometime operate on the same IP. I also ran into the other classic gatchas with running an SPA: with how the routing works differently, especially with the dynamicly generated content for the blog. But like anything, I got to it one problem at a time, setting up the proxy correctly, automating the deployment so no typos would creap in and ensuring routing and refreshing works as you expect on a modern site.
Pragmatic Decisions & Trade-offs
Every project requires balancing ideal solutions with practical constraints. For this portfolio, a key architectural decision was how to handle media assets, particularly the high-resolution images from my art projects. While Next.js offers a fantastic built-in image optimization pipeline, I made a strategic choice to serve these assets from their original sources, like Artstation. Now that might sound a bit weird considering a big portfion of the portfolio section is dedicated to my artistic projects. However, the primary benefit of this approach is a significantly smaller and more lightweight repository. This simplifies the build and deployment pipeline, reduces hosting costs, and avoids content duplication. The trade-off, of course, is a dependency on external services and a potential increase in load time as browsers fetch images or render embeds from third-party domains. For the goals of this project, I determined that the operational simplicity and maintainability benefits outweighed the minor performance impact for the end-user.
Final Thoughts
This project was an incredible learning experience that went far beyond just building a React application. It was a journey through networking, server administration, design systems, and debugging deep, complex issues. The result is a site that I have 100% control over, from the hardware it runs on to the last line of CSS.
And that, to me, is why you bother.
Thank you for reading.
PS: Below I have pasted the project layout for those interested in seeing how all of this is organized
Project Structure
frontend-root/
├── src/
│ ├── app/
│ │ ├── (main)/ # Route Group for pages with the main layout
│ │ │ ├── about/
│ │ │ │ └── [id]/ # Slug for generated blog articles from .md
│ │ │ ├── blog/
│ │ │ ├── portfolio/
│ │ │ └── layout.js # Layout WITH the Navbar
│ │ │
│ │ ├── globals.css # Global styles, Tailwind config, and theme variables
│ │ ├── layout.js # Root layout (WITHOUT Navbar) for the splash page
│ │ └── page.jsx # The main landing/splash page
│ │
│ ├── components/
│ │ ├── about/
│ │ │ ├── EducationItem.jsx # Reusable Eductation element to use in the about page
│ │ │ ├── VolunteerExperienceItem.jsx # Reusable Volunteering Experience element to use in the about page
│ │ │ └── WorkExperienceItem.jsx # Reusable Work Experience element to use in the about page
│ │ ├── portfolio/
│ │ │ ├── PortfolioFilters.jsx # Project Filtering Component to filter per category
│ │ │ ├── ProjectCard.jsx # Reusable card component for projects
│ │ │ └── ProjectModal.jsx # Reusable modal component for projects
│ │ ├── shared/
│ │ │ ├── HeroButton.jsx # The main animated buttons for the landing page
│ │ │ ├── Navbar.jsx # The site's main navigation bar
│ │ │ └── ThemeSwitch.jsx
│ │ │
│ │ └── ThemeProvider.jsx # Light / Dark
│ │
│ ├── data/
│ │ ├── experiences.js # JS file holding objects with all the experiences for the about page
│ │ ├── projects.js # JS file holding objects with all the projects
│ │ └── socials.js # JS file holding objects with all the social links and icons
│ │
│ └── lib/
│ └── color-helper.js # Helper function that returns colors based on tags
|
├── posts/ # Where all the blog post are stored in .md
|
├── package.json
└── README.md