Migrating a Flask Portfolio to Next.js: What I Kept, What I Dropped
My Flask portfolio worked. It had a contact form, a projects section, smooth scroll behavior, and a custom CSS design system I was reasonably happy with. So why rebuild it?
The short answer: adding content was too much friction. The longer answer follows.
The Flask Version's Actual Problem
Flask is a fine framework. The portfolio used Jinja2 templates, a contact form via Flask-Mail, and vanilla CSS. No database — just static content baked into templates.
The problem was that "static content baked into templates" means content changes require code changes. Adding a project meant editing a Python file, testing locally, and redeploying. It felt heavier than it should for what was essentially a file update.
I wanted to add a blog and make project writeups richer without that overhead. File-based MDX content was the answer — drop a .mdx file in the right directory and it appears on the site, frontmatter and all.
Why Next.js Specifically
A few reasons:
App Router server components make filesystem reads at build time trivial. fs.readFileSync in a server component, no API route required, no data fetching complexity.
Static export means the deployment story stays simple. next build produces a ./out directory. Deploy it anywhere. No Node server to manage at runtime — same operational footprint as the Flask version on Render.
TypeScript throughout catches the kind of errors that are annoying to debug in production: wrong field names, missing frontmatter properties, incorrect interface shapes.
Tailwind CSS v4 — I wanted to try the new CSS-first configuration. It's cleaner than the JS config approach. Less boilerplate for the same utility-first output.
The Migration Itself
The actual work was straightforward:
- Rebuilt the layout and components in React, matching the design from the Flask version
- Moved project content into
.mdxfiles withgray-matterfrontmatter - Wrote
lib/content.tsto read and parse those files at build time - Replaced the Flask-Mail contact form with a client-side form that posts to a serverless endpoint
- Configured
next.config.tsfor static export withoutput: "export"
The hardest part was getting the fonts right. The Flask version used @font-face declarations in CSS. Next.js wants next/font/google for optimal loading. The migration is mechanical but requires touching the layout file and removing the old CSS font declarations.
What I Kept
The design system: dark background, electric blue accent, the same type scale. No reason to redesign just because the stack changed.
The general structure: projects, about, contact. Simple is correct for a portfolio.
What I Dropped
The Flask-Mail contact form backend. Replaced with a form that submits to a third-party endpoint — less infrastructure to maintain.
Jinja2 template inheritance. React components with TypeScript interfaces are a strict upgrade for maintainability.
Worth It?
Yes. The friction of adding content is gone. Writing this post is the proof — I created a content/blog/ directory, dropped in a .mdx file, and it shows up on the site. No code changes, no redeployment trigger, just content.
For a portfolio that I actually want to update, that matters.