How I Built My Data Science Portfolio with Next.js and GitHub Pages (Free)
A step-by-step guide to building a free data science portfolio with Next.js static export, Tailwind CSS, and GitHub Pages — plus what to put in it so recruiters actually care.
An effective data science portfolio does two things: it proves you can build, and it proves you can explain.
Mine lives at rishabhsingh.me and costs $0 to host. The stack is Next.js 16 with static export
(output: "export"), Tailwind CSS v4, and TypeScript, deployed to GitHub Pages through a GitHub
Actions workflow, with a custom domain wired up via a single CNAME file. The site includes lazy-loaded
sections, a dark/light theme with a five-accent color picker, an interactive regression simulator built on
canvas, and 23 blog posts migrated from Medium. What makes it effective isn't the framework — it's the
content strategy: no Titanic or MNIST toy projects, verifiable receipts like merged pull requests to CPython,
conda-build, and librosa, interactive demos instead of static screenshots, and explainer posts that
demonstrate communication skill. This post walks through exactly how I built it, step by step.
What should a data science portfolio include?
Before touching a framework, decide what the portfolio has to prove. I'm a Data Scientist at Chryselys with a BS in Data Science & Applications from IIT Madras, and I've reviewed enough portfolios to know the pattern that fails: a grid of notebook screenshots — Titanic survival, MNIST digits, maybe an Iris scatter plot. Everyone applying for the same role has the same grid. A recruiter who has seen four hundred Titanic classifiers cannot tell yours apart, and worse, these projects prove nothing except that you can follow a tutorial.
Here's what I put in mine instead, in rough order of how much weight I think each carries:
- Verifiable receipts. Work that a stranger can independently confirm. For me that's merged pull requests to real open-source projects: CPython (#134804), conda-build (#4782), and librosa (#1850). Nobody can fake a merged PR to CPython. A maintainer of the world's most-used programming language reviewed my code and accepted it — that's a signal no course certificate can match, and it cost me nothing but time.
- Interactive projects, not screenshots. Where possible, let the visitor touch the work. I built a confidence-interval-vs-prediction-interval regression simulator as a canvas widget embedded directly in a blog post. Readers drag the sample size and noise sliders and watch the bands respond. A static PNG of the same chart would communicate a tenth of it.
- Explainer writing. Data science is half communication. Every explainer post on the site — 23 of them, migrated from Medium — is evidence I can take something technical and make it land for a non-specialist. When an interviewer asks "explain a p-value to a stakeholder," I can point at a URL.
- One clear identity. Name, role, employer, education, and a way to contact you — above the fold, no hunting.
If you only take one thing from this section: delete the toy projects. An empty portfolio with two merged PRs beats a full portfolio of tutorials.
Why Next.js static export + GitHub Pages?
GitHub Pages only serves static files. It cannot run a Node server, so a default Next.js app won't deploy
there. The bridge is Next.js's static export mode: set output: "export" in the config and
next build emits a folder of plain HTML, CSS, and JS — no server required. You keep the entire
Next.js developer experience (file-based routing, React components, TypeScript, image handling, metadata API)
and ship an artifact any static host can serve.
Why does that combination make sense for a data science portfolio specifically?
- A portfolio is inherently static. There's no user auth, no database, no server-side personalization. Paying for compute you don't use is pointless.
- Static export forces good habits. Every page is prerendered HTML, which means fast first paint, trivially cacheable pages, and content that crawlers — including AI crawlers — can read without executing JavaScript.
- The code lives where your work lives. My portfolio repo sits next to the open-source contributions it talks about. One GitHub profile, one story.
- Interactive widgets still work. "Static" describes the hosting, not the page. My canvas regression simulator, the theme toggle, and the accent color picker are all client-side React and vanilla JS running on statically served pages.
I paired it with Tailwind CSS v4 for styling and TypeScript throughout. Tailwind keeps the CSS colocated with components, which matters when you're the only maintainer and come back to the code three months later.
GitHub Pages vs Vercel vs Netlify: which should you use?
All three host a static portfolio for free. The honest answer is that any of them works; here's how they compare for this specific use case:
| Feature | GitHub Pages | Vercel (Hobby) | Netlify (Free) |
|---|---|---|---|
| Build minutes | 2,000/mo via GitHub Actions (free tier) | Included, per-deployment limits | 300 build minutes/mo |
| Custom domain | Free, via CNAME file + DNS | Free | Free |
| Bandwidth | 100 GB/mo soft limit | 100 GB/mo | 100 GB/mo |
| HTTPS | Free, automatic | Free, automatic | Free, automatic |
| Server-side rendering | No (static only) | Yes | Yes |
| Cost for a portfolio | $0 | $0 (non-commercial use) | $0 |
I chose GitHub Pages for three reasons: my code and open-source work already live on GitHub, so it's one less account and one less dashboard; the Hobby tiers of Vercel and Netlify carry usage terms and upgrade nudges I didn't want to think about; and the static-only constraint is a feature, not a limitation — it guarantees I never accidentally build something that needs a server. If you want server-side rendering, image optimization at request time, or preview deployments per pull request, Vercel is the natural pick. For a static portfolio, GitHub Pages does everything required.
How do you build it, step by step?
Here's the full path from empty folder to live site on a custom domain. Four steps.
1. Scaffold the project
Create a Next.js app with TypeScript and Tailwind. The interactive prompts handle both:
npx create-next-app@latest portfolio --typescript --tailwind --app
cd portfolio
npm run dev
Build your pages as ordinary React components under src/app/. Blog posts, project pages, and the
home page are all just routes. Two portfolio-relevant details from my build: images go in as WebP (smaller
files, universally supported now), and anything below the fold — heavy sections, embedded widgets — gets
lazy-loaded so the first paint stays fast.
2. Configure static export
One line in next.config.ts turns the app into a static site generator:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
images: { unoptimized: true },
trailingSlash: true,
};
export default nextConfig;
Three notes. output: "export" makes next build write plain files to
out/. images.unoptimized is required because GitHub Pages has no server to run
Next.js's on-demand image optimizer — which is why I pre-convert everything to WebP myself.
trailingSlash makes every route a real index.html inside a folder, so URLs like
/blog/my-post/ resolve correctly on a dumb file server with no rewrite rules.
3. Set up the GitHub Actions workflow
In the repo settings, set Pages → Source to "GitHub Actions." Then add
.github/workflows/deploy.yml:
name: Deploy to GitHub Pages
on:
push:
branches: [main]
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm run build
- uses: actions/upload-pages-artifact@v3
with: { path: ./out }
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
From now on, every push to main builds the site and deploys it. No CLI, no manual upload, no
"did I forget to publish" moments. The deploy takes about two minutes end to end.
4. Point a custom domain at it
Buy a domain (~$10/year — the only money in this entire stack). Then:
- Put a file named
CNAMEin yourpublic/folder containing exactly one line: your domain (mine saysrishabhsingh.me). Next.js copiespublic/into the export, so the file survives every deploy. - At your DNS provider, add A records for the apex domain pointing at GitHub Pages' IPs, plus a CNAME record
for
wwwpointing to<username>.github.io. - In repo settings, enter the domain under Pages → Custom domain and tick "Enforce HTTPS." GitHub provisions the certificate automatically.
DNS propagation takes minutes to a few hours. After that, the site is live on your own name, with HTTPS, for free.
How much does it cost?
$0 for hosting, plus roughly $10/year for the domain. That's the complete bill. GitHub Pages
hosting is free, GitHub Actions' free tier (2,000 minutes/month) covers hundreds of deploys of a site this
size, the SSL certificate is free, and Next.js, Tailwind, and TypeScript are open source. The domain is
technically optional — username.github.io works — but $10/year for a name you control, that
outlives any employer or platform, is the best money-per-signal ratio in this whole project.
What about SEO and AI search?
A portfolio nobody finds is a diary. Since the site is already static HTML, most of the SEO work is just filling in metadata that Next.js makes first-class:
- JSON-LD structured data — Person schema on the homepage, Blog/BlogPosting on posts, and BreadcrumbList for navigation, so search engines understand who I am and how pages relate.
sitemap.tsandrobots.ts— Next.js generatessitemap.xmlandrobots.txtfrom typed route handlers at build time; new blog posts join the sitemap automatically.llms.txt— a plain-text map of the site for AI crawlers. As more discovery happens through ChatGPT and Perplexity rather than Google, making your site legible to language models is cheap insurance.- An Open Graph social card — so links shared on LinkedIn or X render a proper preview instead of a bare URL. For a portfolio, links get shared in exactly the contexts that matter: recruiters, hiring managers, group chats.
- WebP images and lazy loading — page speed is a ranking factor, and a static export with small images is about as fast as the web gets.
Was it worth building instead of using a template?
Yes — because the site itself became a project. The theme system (dark/light plus five accent colors,
persisted in localStorage), the lazy-loading strategy, and the canvas regression simulator are
all things I can point to in an interview as engineering work, not just design choices. A Notion page or a
template can hold the same links, but it can't demonstrate that you shipped something. If you're
time-constrained, start with the four steps above and a single page — you can be live this afternoon. Then
replace one toy project with one real contribution, write one explainer post, and repeat. That loop, not the
framework, is what makes a data science portfolio work.