The Unexpected Hard Parts of Building a Portfolio in Next.js 16

I recently launched the first version of my portfolio, and it taught me more than I expected.

It looks simple from the outside: clean UI, gentle animations, a few mascots, but building it ended up being a long list of strange bugs, browser quirks, config mismatches, and debugging sessions that felt more like archaeology.

This post is a breakdown of everything that surprised me technically.
If you’re building a personal site or portfolio in Next.js 16, maybe this will save you an hour (or a day).

I wanted my portfolio to feel a little different, not just another “hero → projects → contact” layout, but something with a bit of personality.
So I designed an animated orb for the homepage, a curved S-timeline to show my journey, MDX-powered blog posts and case studies, and three mascots to give the site a sense of identity.

None of this felt complicated on paper.
But the moment I started building it in Next.js 16, I discovered a long list of small-but-painful issues that don’t appear in tutorials: animation desyncs, MDX pipeline mismatches, SVG quirks, browser inconsistencies, routing surprises, and one or two very confusing bugs that only appeared on iPhones.

This post is a breakdown of those unexpected problems… not just what broke, but why it broke and how I fixed it.
Even if you’re not building the same features, these might be helpful to anyone pushing beyond the standard layouts.

1. The Orb Animation

The homepage orb was meant to be the “wise guiding character” of the site; shifting shape as you scroll, pulsing softly, reacting as content switches sides.

GSAP + React + SVG morphing did not like that idea.

  • timelines desynced
  • scroll links jittered
  • states snapped back
  • RSC boundaries interfered

I didn’t understand enough yet to fix it properly without derailing the whole timeline.
So the orb is on hold for v2, and I’ll rebuild it once I’m deeper into GSAP and SVG animation patterns.

Sometimes the best engineering choice is postponing responsibly.

2. The S-Curve Timeline

Designing the S-curve in Figma took a while…
I was deciding what to design while designing and learning Figma at the same time.

Rebuilding it in code took even longer.

  • The curve had to stay responsive
  • Nodes needed to sit precisely on the SVG path
  • Labels had to flip sides on different breakpoints
  • Glow states couldn’t break on mobile
  • Scaling the curve couldn’t shift node positions
  • The layout had to follow a clean S-curve, not approximate it

I nearly rebuilt the entire component multiple times.
Eventually the visual matched the design exactly, and after that, wiring modals, JSON mapping, and interactions was much easier.

But timelines, especially curved, interactive ones, are their own kind of challenge.

3. MDX Broke Twice (For Two Different Reasons)

I used MDX in two parts of the site:

A. Case Studies

Each case study has:

  • its own folder
  • its own layout
  • its own MDX file

This needed a simple, isolated MDX pipeline.

B. Blog Posts

Blogs needed a more formal system:

  • posts/ directory
  • dynamic [slug] route
  • frontmatter parsing
  • server-side MDX compilation
  • a custom runtime evaluator
  • and styled prose

Here’s a simplified version of the compile logic:

const raw = fs.readFileSync(filePath, "utf-8");
const { data, content } = matter(raw);

const compiled = await compile(content, { outputFormat: "function-body" });

const mdxModule = new Function(
  "runtime",
  `${compiled.value}; return { default: MDXContent };`
)(runtime);

const MDXContent = mdxModule.default;

It works beautifully now, but both pipelines needed different setups, different overrides, and different expectations.

The MDX Config Problem

I currently have two MDX configs:

  • next.config.mjs using the createMDX wrapper
  • next.config.ts using the older MDX integration

Both work.
I don’t know which one Next 16 is using.

If you’re experienced with MDX + Next 16, feel free to check my repo and tell me which config I should keep.

4. Sprite Sheet Animations (Small Problem, Good Lesson)

I used pixel-style sprite sheets for some small animations.

Locally everything worked.
After deploying to Vercel, nothing animated.

Why?

Case sensitivity.

My local machine didn’t care about capitalization mismatches in file names.
Vercel did.

A tiny issue, but a surprisingly clean lesson in deployment differences, and in keeping asset naming consistent. A classic “aha” moment.

5. Navbar Glass Effect (A Browser Quirk)

My mobile navbar’s glass effect refused to render correctly in Chrome.
At first I thought my CSS was wrong, but the real issue was:

Chrome has trouble with nested backdrop-filter layers.

Safari handled it better, but I wouldn’t have known that without a StackOverflow thread explaining the issue (and showing the pseudo-element workaround).

The fix was to apply the blur on a ::before pseudo-element:

before:content-[''];
before:absolute;
before:inset-0;
before:backdrop-blur-[10px];

Suddenly, everything blurred correctly across browsers.

These are the kinds of bugs you’d never predict until you meet them.

6. Fonts (Much Harder Than Expected)

My approach should have worked:

  • import fonts
  • assign CSS variables
  • reference in Tailwind

But Tailwind v4 + Next 16 + MDX created conflicts.

My final working pipeline:

  • import fonts in layout.tsx (using Next Font)
  • expose each as variables: --font-inter, --font-montserrat, etc.
  • map them inside @theme inline in globals.css
  • reference using font-main, font-blog-body, font-blog-code

A single missing variable caused the entire MDX typography to fall apart.

One of the easier problems, but still time-consuming.

7. The [slug] Route Bug (Params as Promise)

This one confused me the most.

Next.js 16 passes params as a Promise.

Before I realized that, slug was coming in as undefined, so the loader tried fetching:

posts/undefined.mdx

What caused the bug?

In Next.js 16, route params are returned as a Promise.
If you do:

export default function Page({ params }) {
  console.log(params.slug)
}

params.slug will be undefined
(because params hasn’t been awaited).

The fix:

const { slug } = await params;

Everything worked instantly after that.
Sometimes the smallest syntax difference causes the largest confusion.

7.5 Post-Launch Bugs That Showed Up Right Before Releasing (Bonus Round)

Just when I thought I was finally ready to publish… three new issues appeared in the finishing stretch. Classic timing.

A. Blog Dynamic Route 404 Not Using the Global Not-Found Page

If someone visited:

/blog/nonexistent-slug

they should have seen the global app/not-found.tsx.

Instead, the blog layout rendered its own internal:

404 Not Found

Meanwhile, Turbopack kept warning:

sourceMapURL … negative timestamp

Cause

  • I was manually returning
    404 Not Found

    instead of using notFound()

  • The global 404 couldn’t trigger because the blog loader handled the error too early

Fix

  • Added:
import { notFound } from "next/navigation";
  • Removed all custom 404 JSX from the blog page
  • Called notFound() whenever a slug didn’t match
  • Ensured the only 404 file lives at:
app/not-found.tsx

Now invalid slugs correctly route to the global 404 page.

B. iPhone Safari Completely Broke My Timeline Nodes

On iPhones (Safari and Chrome), the timeline looked… cursed:

  • nodes floated away from the curve
  • labels clipped or disappeared
  • glows turned into square artifacts
  • everything felt misaligned

Cause

I was using inside SVG; something iOS Safari barely supports.

Safari struggles with:

  • nested HTML inside SVG
  • filters on foreignObject
  • overflow clipping rules

Fix

I rebuilt everything as pure SVG:

  • for nodes
  • + for labels
  • for grouping
  • added filterUnits="userSpaceOnUse"
  • set overflow="visible" on the main

Result: perfect alignment, consistent glow, and full iPhone compatibility.

C. Loader Showing Up in Places It Shouldn’t

Originally, the loader triggered during:

  • navigation from /#section/
  • certain anchor jumps
  • and sometimes didn’t trigger on actual blog loads

It felt inconsistent.

Fix

I mounted the loader only inside:

app/blog/layout.tsx

This kept it strictly scoped to blog routes, exactly where it belongs.

Blog pages now load with a smooth, predictable loader, and nowhere else.

8. I Even Added a Custom 404 Page

For fun, I made a 404 page that shows my three mascots walking across the screen. Will anyone ever see it?
Probably not.
But if they do, I hope it makes them smile.

404 page

9. Three Mascots: Orb, Samurai, Dog

Somewhere in the design phase, I decided the portfolio needed personality, not just UI.

So I added three mascots:

The Orb – Future Me

Calm, wise, 10-years-ahead Me.
Symbol of direction and clarity.

The Samurai – Discipline

The inner warrior spirit.
The code you live by.
Kaizen in character form.

The Dog – Curiosity & Playfulness

The innocent, excited part of me that simply enjoys making things.

They’re not random, they anchor the mindset I want to work with.

10. What I Didn’t Know About Building Projects (Until This Portfolio)

This part surprised me the most.

I’ve made portfolios before; sometimes alone, sometimes with friends.
But none of them ever felt like me.
They were functional, but they didn’t represent what I cared about, or how I think, or the way I build.

With this portfolio, I finally learned a few things I genuinely didn’t understand earlier:

a. Depth matters more than quantity

A single project that shows why and how you built something says more than ten average projects.

b. Case studies matter

Not long essays, just honest breakdowns:
what broke, what confused you, what you fixed.

That’s real signal.

c. Treating the portfolio like a product changes everything

UI, UX, accessibility, storytelling, consistency, typography,
these aren’t decoration.
They’re communication.

d. The process teaches more than the result

Planning, designing, coding, breaking, debugging, refining,
the workflow itself builds skill.

e. Mindset changes the outcome

I didn’t chase “perfection.”
I chased understanding.
I treated the portfolio as a chance to grow, not as a trophy.

That shift; me reflecting on my own past mistakes and what I finally learned while building this, made the difference.

11. The Big Picture

After designing for a week (that felt longer than it should’ve),
I started coding the site around November 11–12.

By November 14 night, I hit the frustrating middle, too much built to quit, not enough working to feel good.
I pulled an all-nighter from the 14th into the 15th, pushed through the wall, and after that the rest of the build (15–19) flowed naturally.

There was no magical “everything clicked” moment.

There was just a decision:

Ship v1.
Keep Learning.
Keep moving forward.

This portfolio wasn’t about showing off.
It was a deliberate learning experience; a test of my workflow, my habits, and my discipline.

And I’m glad I built it this way.

If You’ve Built Something Similar…

I’d love your feedback, especially on:

  • MDX config (mjs vs ts)
  • S-curve responsiveness
  • the orb animation (if you’ve done morphing SVGs in GSAP)
  • any timeline/animation advice
  • or anything strange you notice in the implementation

And if you’re curious about any part of the build:
MDX, design, the S-curve, mascots, animations, anything,
I’m always open to share more.

Thank you for reading

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

12 Free Business Planning Templates for Excel & Word

Next Post

Mastering the Upward Spiral of Improvement

Related Posts