A fast, accessible, SEO-strong portfolio built with Next.js (App Router) + TypeScript + Tailwind CSS, with MDX blog posts and live GitHub data. No database — all dynamic data comes from the GitHub API through server-side route handlers, cached hourly with ISR.
npm install
npm run dev # http://localhost:3000That's it. The site runs with zero configuration; a GitHub token just makes it better (see below).
All personal content lives in one file: lib/config.ts.
You should never need to touch a component to change what the site says.
| What | Where in config.ts |
|---|---|
| Name, role, tagline, bio paragraphs | site |
| GitHub username ( |
site.githubUsername |
| Email, location, availability badge, résumé link | site |
| Social links ( |
site.socials |
| About-section stats | highlights |
| Skill groups & chips | skills |
| Featured project cards | featuredProjects |
| Services cards | services |
Your résumé PDF is served from public/Mathew-Thomas-Resume.pdf; replace that
file to update it.
Create content/posts/my-post-slug.mdx with this frontmatter:
---
title: "My post title"
date: "2026-07-01"
excerpt: "One or two sentences shown on cards and in meta descriptions."
tags: ["Go", "Performance"]
readtime: "5 min"
---
Your **MDX** content here.The post automatically appears on the home page preview, /blog, the
sitemap, and gets its own statically-generated page at /blog/my-post-slug
with per-post SEO metadata. No registration step.
-
Create a token at https://github.com/settings/tokens — a classic token with no scopes is enough (public data only).
-
Copy
.env.exampleto.env.localand set:GITHUB_TOKEN=ghp_xxxx
What it changes:
| Without token | With token | |
|---|---|---|
| Repo cards | ✅ works (60 req/h limit) | ✅ 5,000 req/h |
| Contribution heatmap | SVG embed from ghchart.rshah.org | Native, theme-matched heatmap via the GraphQL API |
The token is read only in server code (lib/github.ts) and never reaches the
client bundle. GitHub responses are cached with next: { revalidate: 3600 },
so the API is hit at most once per hour per endpoint.
- Push this repo to GitHub.
- vercel.com/new → import the repo → Deploy. No build settings needed; Vercel auto-detects Next.js.
- In the Vercel project settings → Environment Variables, add:
GITHUB_TOKEN(optional, see above)NEXT_PUBLIC_SITE_URL— your production URL, e.g.https://mathewthomas.dev(used by the sitemap, robots.txt, and Open Graph tags)
Every push to main redeploys. The GitHub sections refresh hourly via ISR
without redeploying.
app/
layout.tsx # fonts, theme provider, global metadata, JSON-LD
page.tsx # home — composes all sections, Suspense for GitHub
blog/page.tsx # post index
blog/[slug]/page.tsx # SSG posts (generateStaticParams + generateMetadata)
api/github/route.ts # repos JSON (REST, ISR-cached)
api/github/contributions/route.ts # heatmap JSON (GraphQL) or fallback URL
api/subscribe/route.ts # newsletter stub w/ commented provider integrations
sitemap.ts / robots.ts
components/ # one component per section + shared UI
content/posts/*.mdx # blog posts (gray-matter frontmatter)
lib/
config.ts # ← ALL editable content
github.ts # server-only GitHub fetchers (token stays here)
posts.ts # MDX read/parse helpers
- GitHub data flow: server components call
lib/github.tsdirectly during render (no self-HTTP round-trip); the/api/github/*routes expose the same data as JSON for client-side or external use. - Contributions: GitHub's REST API doesn't expose contribution data, so
the heatmap uses the GraphQL
contributionsCollection.contributionCalendarquery when a token exists, and falls back to the tokenlessghchart.rshah.orgSVG when it doesn't. Both paths are commented incomponents/GithubActivity.tsx. - Newsletter:
app/api/subscribe/route.tsvalidates and logs signups; uncomment one of the Buttondown / ConvertKit / Mailchimp blocks and add its API key to.env.localto go live. - Dark mode:
next-themeswith the class strategy — respects system preference, persists the choice, no flash of the wrong theme. - Accessibility: semantic landmarks, skip link, focus-visible rings, aria-live form status, keyboard-navigable nav, reduced-motion support.