I wanted a portfolio that felt clean, minimal, and intentional, so I started by recreating the style of a site I liked. At first, the goal was mostly visual: narrow content width, small navigation, strong typography, dark background, muted metadata, and compact post badges.
But once I had the homepage mostly matching the look I wanted, the bigger question became how to make the blog system work in a way that would scale.
I did not want to hardcode every article directly in React. That would make each post annoying to write and even more annoying to maintain. So the solution ended up being a combination of Next.js, App Router, Markdown content files, and a small server-side content pipeline.
Tech Stack
The project is built with:
- Next.js
- React
- TypeScript
- Tailwind CSS
- gray-matter for frontmatter parsing
- remark and remark-html for Markdown-to-HTML conversion
The visual layout lives in React components, but the blog content lives in Markdown files.
That separation is the whole point.
The Core Idea
Instead of writing blog posts in code, each post is stored as a Markdown file inside:
content/posts
Each file contains frontmatter at the top:
---
title: My Post Title
date: "2026-03-25"
tags:
- AI
- ENG
summary: A short summary of the article.
---
Then below that is the article body in Markdown.
That means I can write posts like documents instead of embedding paragraphs directly into TSX files.
File Structure
A simplified version of the project looks like this:
src/
app/
page.tsx
writing/
page.tsx
[slug]/
page.tsx
components/
badge.tsx
footer.tsx
navbar.tsx
posts-list.tsx
lib/
posts.ts
content/
posts/
how-we-built-this-portfolio.md
first-real-post.md
This gives the site two major layers:
- UI components
- content loading logic
How the Homepage Works
The homepage is a normal Next.js page, but instead of using a hardcoded array of post titles forever, it calls a function that reads the Markdown files.
That function lives in src/lib/posts.ts.
At a high level, the homepage does this:
- asks for all posts
- sorts them by date
- takes the most recent few
- renders them in a styled list
So the homepage does not need to be manually updated every time I publish something. It builds itself from the content directory.
Reading Posts from the File System
The content loader uses Node's file system API to read all Markdown files from content/posts.
Conceptually, the process is:
- read all filenames in the folder
- remove the
.mdextension to create the slug - read each file's contents
- parse the frontmatter
- return metadata for the homepage or full content for the article page
That is what makes the blog feel dynamic even though the content is stored locally.
The slug is based on the filename, so this file:
content/posts/how-we-built-this-portfolio.md
becomes this route:
/writing/how-we-built-this-portfolio
Why gray-matter Matters
Markdown alone is not enough if I want metadata like title, date, tags, and summary.
That is where gray-matter comes in.
It lets me split a Markdown file into two parts:
- frontmatter
- body content
So this:
---
title: How I Built This
date: "2026-03-25"
tags:
- TOOL
- ENG
summary: A technical breakdown of the portfolio.
---
This is the article body.
gets parsed into structured post data plus the raw Markdown body.
Without that, there would be no clean way to generate the post list, dates, badges, and metadata automatically.
Converting Markdown into HTML
Once the frontmatter is parsed, the article body is still just Markdown text.
To display it in the browser, I convert it into HTML using:
remarkremark-html
That gives me a final HTML string that can be rendered inside the article page.
So the pipeline looks like this:
Markdown file -> gray-matter -> metadata + body -> remark -> HTML -> article page
This is what lets me write naturally in Markdown while still rendering a styled blog page.
Dynamic Routes with App Router
The blog articles use a dynamic route:
src/app/writing/[slug]/page.tsx
This tells Next.js that anything after /writing/ should be treated as a slug.
So if the slug is:
how-we-built-this-portfolio
then the page route becomes:
/writing/how-we-built-this-portfolio
The page reads the slug from the URL, looks for the matching Markdown file, and renders that post.
That is what makes the site feel like a blog instead of just a homepage with placeholder links.
generateStaticParams() and Why It Exists
Because the blog posts are known ahead of time, the site can generate the routes in advance.
That is what generateStaticParams() does.
It gets all the slugs from the content folder and tells Next.js which pages to build.
So if I have three Markdown files, Next.js already knows to create three article pages.
That makes the site faster and cleaner because the routes are prepared ahead of time instead of guessed at runtime.
Reusable Components
I also broke the UI into reusable components so the code would not turn into a mess.
Navbar
Handles the top navigation links.
Footer
Handles the bottom external links like GitHub and RSS.
Badge
Renders the small colored tags such as AI, ENG, TOOL, or NEW.
PostsList
Takes a list of posts and renders the recent-post rows in the portfolio style.
This matters because the homepage and writing page both need to render post lists. Instead of duplicating the whole layout twice, one component handles it.
Matching the Portfolio Style
The visual style I was copying is deceptively simple.
It is not complicated because of TypeScript. It is mostly a combination of:
- narrow maximum width
- careful spacing
- small navigation text
- tighter heading tracking
- subtle gray metadata
- mono dates
- simple flex-based post rows
- tiny pill badges
- lots of restraint
The main layout is a centered column with dark background and controlled spacing.
That means most of the visual work came from:
max-width- top margins
- paragraph spacing
- muted text colors
- small typography adjustments
- flex alignment for post rows
The site looks simple because every spacing decision is deliberate.
Why the Dates Broke at One Point
One issue that came up was dates disappearing.
That happened because frontmatter parsers do not always treat dates as plain strings. Sometimes a value like:
date: 2026-03-21
can get interpreted differently than expected.
The fix was to normalize the date value properly and to quote the date in frontmatter:
date: "2026-03-21"
That keeps the parsing predictable and avoids the post list showing blank date fields.
Table of Contents on Article Pages
One of the reference article pages had a left-side table of contents listing the sections of the article.
To mimic that, I added logic that scans the Markdown for ## headings and builds a small table of contents array.
Then each heading gets a generated ID so the links can jump to the correct section.
That means the article page can show a side navigation like:
- Stop Forcing Determinism on Stochastic Machines
- Creative Workflows and Where LLMs Fit
- Examples of Faux Freedom
and clicking one of those links scrolls to that section.
This is a small feature, but it makes the article page feel much more like a long-form writing experience.
Why This Setup Is Better Than Hardcoding Posts
There are a few big reasons.
1. The content is separate from the design
I can redesign the site without rewriting every article.
2. Writing is easier
A blog post can be written in Markdown like a document instead of a React component.
3. Routes are automatic
The filename becomes the slug, so pages are created naturally.
4. The homepage updates itself
As long as a post file exists, it can appear in the recent posts list.
5. The code stays cleaner
The layout code does layout work, and the content files hold content.
That separation is one of the best decisions in the project.
What I Would Improve Next
This setup works well, but there are still obvious upgrades.
Add MDX
Markdown is great, but MDX would let me embed React components directly in posts.
That would be useful for:
- callout boxes
- custom image components
- code demos
- embedded media
Add a CMS
Right now the content is easier than TSX, but it still means editing Markdown files manually.
A Git-backed CMS would make it possible to write posts in a browser UI and still save them directly into the repo.
Add syntax highlighting
Right now code blocks render, but syntax highlighting would make technical posts look much better.
Add post pagination or filtering
Once the writing page gets bigger, it would help to filter by tags or sort by category.
Final Thoughts
What started as “I want my portfolio to look like this other site” turned into something better.
The project now has two distinct strengths:
- a portfolio layout I actually like
- a blog system that is practical to maintain
The important part technically is not just the visual clone. It is the architecture underneath:
- React components for layout
- Markdown for content
- file-system loading on the server
- frontmatter parsing
- Markdown transformation
- dynamic slug routes
- reusable rendering logic
That combination makes the site feel lightweight while still being flexible.
And most importantly, it means I can focus on writing instead of fighting the code every time I want to publish something.