Every developer needs a portfolio. Most reach for a template, and there is nothing wrong with that — but I wanted something that would grow with me. Something I could extend with features that showcase what I actually build. So I started from scratch with Next.js, TypeScript, and AWS Amplify.
This is the story of that foundation, and the first post in a series documenting the development journey of arkadiuszkulpa.co.uk.
Templates are fast but rigid. I knew from the start that this site would eventually host an AI chat assistant, a full CMS, and a media pipeline. A template would have fought me at every turn. Starting with a clean Next.js project gave me full control over the architecture, the styling system, and the deployment pipeline.
The other reason is simple: building it yourself teaches you things that using a template never will. Every decision — from how markdown gets rendered to how images get optimised — forced me to understand the tooling at a deeper level.
Next.js 14 with App Router was the obvious choice. Server-side rendering, file-based routing, and excellent TypeScript support made it the right foundation for a site that would eventually need ISR, API routes, and dynamic content.
TypeScript was non-negotiable. On a project I knew would grow in complexity, type safety would save me from myself months down the line.
AWS Amplify Gen2 handles hosting and CI/CD. Push to main, and the site deploys automatically. Push to dev, and a staging environment spins up. No manual deployment steps, no Docker containers to manage, no EC2 instances to babysit.
The blog started as a collection of markdown files in a content/posts/ directory. Each file had YAML frontmatter for metadata:
---
title: "My First Post"
date: "2025-05-30"
description: "A description for SEO."
---
# My First Post
Content here...
At build time, Next.js would read each file, parse the frontmatter with gray-matter, render the markdown with react-markdown and remark-gfm, and generate a static page for each slug. Simple, fast, and free of any database dependency.
The rendering pipeline supported GitHub Flavored Markdown — tables, strikethrough, task lists — plus syntax-highlighted code blocks via react-syntax-highlighter. This meant I could write technical content with real code examples and have them render beautifully without any extra effort.
A blog is just text on a page until you add the small details that make reading enjoyable.
Every post automatically generates a table of contents by scanning the rendered markdown for heading elements. The TOC sits in a sidebar on desktop and collapses above the content on mobile. It highlights the current section as you scroll, using the IntersectionObserver API to track which heading is in the viewport.
A thin progress bar at the top of each post page fills as you scroll through the article. It is a small touch, but it gives readers a sense of how much content remains — useful for longer technical posts.
Clicking any image in a post opens it in a modal overlay at full resolution. This was particularly important for architecture diagrams and screenshots where detail matters. The component handles keyboard navigation (Escape to close) and click-outside-to-dismiss.
Some of my earlier academic work existed as PDFs. Rather than linking out to a download, I built a <BlogPDF> component that embeds the PDF directly in the post with an "Open in New Window" button for full-screen reading. This kept readers on the site while still giving them access to the original documents.
Amplify Gen2 made deployment remarkably straightforward. The amplify.yml build spec defines the build commands, and Amplify handles the rest:
version: 1
frontend:
phases:
build:
commands:
- npm ci
- npm run build
artifacts:
baseDirectory: .next
files:
- '**/*'
cache:
paths:
- node_modules/**/*
- .next/cache/**/*
Two branches, two environments:
main → production at arkadiuszkulpa.co.ukdev → staging at a separate Amplify URLEvery push triggers a build. Every build runs the full Next.js compilation. If it fails, the previous version stays live. This gave me confidence to iterate quickly without worrying about breaking production.
If you are visiting this portfolio, what you are seeing is a site built entirely by hand. The blog you are reading renders markdown through a custom pipeline. The responsive layout adapts to your screen. The images zoom when you click them. Every feature was a deliberate choice, not a checkbox on a template.
More importantly, this foundation made everything that came after possible — the AI chat assistant, the CMS, the REST API, the media pipeline. Each of those features built on top of this base without fighting it.
With the foundation in place, I had a working portfolio with a static blog. But I wanted the site itself to showcase my AI/ML engineering skills. The next step was to add something no template could give me: an intelligent chat assistant powered by AWS Bedrock and retrieval-augmented generation.
That story is in Adding an AI Chat Assistant with RAG to My Portfolio.
This post is part of a series documenting the development of arkadiuszkulpa.co.uk.