A free, modern developer portfolio built with Next.js 16, TypeScript, and Tailwind CSS — deployed on Vercel for the same price as GitHub Pages, but with features GitHub Pages can't touch.
GitHub Pages is free and simple, but it's static. You can't run server-side logic, which means:
- You can't have a contact form without exposing your email address
- You can't add any backend features later
Vercel's Hobby tier is also free, supports custom domains, automatic SSL, and CI/CD from GitHub — but it also runs serverless functions. That's how this template handles the contact form without ever revealing your email.
Same price. Way more capable.
Most portfolio contact forms either link to mailto:you@email.com (exposing your address to scrapers) or use a third-party service that puts their branding on your site.
This template uses Resend — a free email API — as a serverless function on Vercel. Here's what happens:
- Visitor fills out the form and hits Send
- The form submits to a Next.js Route Handler (
/api/contact) running on Vercel's servers - That function calls Resend with your API key — stored secretly as an environment variable, never visible in the browser
- Resend delivers the message to your inbox with the sender's email as a
replyToheader - You hit Reply — it goes straight to them
- Your email address is never in the code, never in the browser, never visible to anyone
| Tool | Purpose | Cost |
|---|---|---|
| Next.js 16 | Framework (App Router) | Free |
| TypeScript | Type safety | Free |
| Tailwind CSS 4 | Styling | Free |
| Resend | Contact form emails (3,000/month free) | Free |
| Vercel Hobby | Hosting, SSL, CI/CD, serverless functions | Free |
| Custom domain | From any registrar | ~$10–15/year |
Total annual cost: ~$10–15 (just the domain)
This template was deployed to Vercel and run through Google PageSpeed Insights to verify the claims. These are actual scores from the live deployment, not synthetic benchmarks.
Desktop
Mobile
| Mobile | Desktop | |
|---|---|---|
| Performance | 98 | 100 |
| Accessibility | 96 | 96 |
| Best Practices | 100 | 100 |
| SEO | 100 | 100 |
| First Contentful Paint | 0.8s | 0.2s |
| Largest Contentful Paint | 2.0s | 0.4s |
These scores are a direct result of how the template is built: no client-side data fetching, fonts self-hosted via next/font (eliminating the render-blocking Google Fonts CDN request), minimal JavaScript bundle, and server-rendered HTML on every request.
Click Fork in the top right on GitHub.
Edit data/profile.ts — this is the primary file you'll change to make it yours:
export const profile: Profile = {
name: "Your Name",
tagline: "Your Title",
location: "Your City",
bio: "A short bio about yourself.",
description: "A one-line description for search engines and social previews.",
linkedin: "https://linkedin.com/in/yourhandle",
github: "https://github.com/yourhandle",
siteUrl: "https://yourdomain.com",
resumePath: "/resume.pdf",
};
export const experience: ExperienceItem[] = [
{
id: "your-role",
company: "Your Company",
title: "Your Title",
date: "2022 — Present",
description: "What you did there.",
tags: ["TypeScript", "React"],
},
// add more...
];Place your resume PDF at:
public/resume.pdf
The button in the Resume section links to /resume.pdf automatically. Keep the PDF free of contact details — the contact form handles that.
- Create a free account at resend.com
- Go to API Keys → Create API Key → name it
portfolio-prod - Select Sending access, All Domains
- Copy the key — you only see it once
- Go to vercel.com and sign in with GitHub
- Click Add New Project → import your forked repo
- Vercel auto-detects Next.js — don't change any build settings
- Before deploying, expand Environment Variables and add:
| Name | Value |
|---|---|
RESEND_API_KEY |
re_xxxxxxxxxxxx (from Resend) |
CONTACT_EMAIL |
you@youremail.com (where you want messages delivered) |
RESEND_FROM |
Portfolio Contact <onboarding@resend.dev> (see note below) |
RESEND_FROMnote: The defaultonboarding@resend.devsender works immediately in Resend's sandbox but is limited to sending to your own verified email only. Once you've verified your own domain in Resend, update this to something likePortfolio Contact <hello@yourdomain.com>and it will work for all recipients.
- Click Deploy — your site will be live on a
.vercel.appURL in ~60 seconds
In Vercel → your project → Domains → add your domain. Vercel shows you the DNS records to set at your registrar. SSL is automatic.
Once your domain is live, update siteUrl in data/profile.ts to your real URL — this feeds into the sitemap, SEO metadata, and social share previews.
All colors are CSS variables in app/globals.css:
@theme {
--color-background: #f8f7f4; /* page background */
--color-ink: #1a1a2e; /* primary text */
--color-ink-muted: #4a4a6a; /* secondary text */
--color-accent: #d97706; /* amber — change this to your color */
--color-accent-light: #fef3c7; /* light tint of accent */
--color-surface: #ffffff; /* card backgrounds */
--color-border: #e2e0da; /* borders and dividers */
}Change --color-accent and --color-accent-light to match your personal brand.
Fonts are loaded via next/font/google in app/layout.tsx. To swap fonts:
- Replace
InterandPlayfair_Displaywith any fonts from fonts.google.com - Update the import and the two
constdeclarations at the top ofapp/layout.tsx - The CSS variables
--font-sansand--font-displayinapp/globals.csswill pick up the new fonts automatically — no changes needed there
vercel-portfolio-starter/
├── app/
│ ├── api/
│ │ └── contact/
│ │ └── route.ts # Serverless contact form handler
│ ├── globals.css # Design tokens + Tailwind config
│ ├── layout.tsx # Root layout + fonts + SEO metadata
│ └── page.tsx # Home page
├── components/
│ ├── Hero.tsx # Name, tagline, bio, nav links
│ ├── Experience.tsx # Work history list
│ ├── Resume.tsx # Resume download button
│ ├── Contact.tsx # Contact form (client component)
│ └── Footer.tsx # Footer with social links
├── data/
│ └── profile.ts # ← Edit this file to customize content
└── public/
└── resume.pdf # ← Drop your resume PDF here
All content lives in data/profile.ts. To add a new experience entry, add an object to the experience array. No code changes required elsewhere — the component reads the data automatically.
Social platforms (LinkedIn, Twitter/X, Slack) display a preview card when your URL is shared. The card pulls from the OG image metadata in app/layout.tsx, which points to {siteUrl}/og-image.png.
An OG image is not included in this template because it should contain your name, title, and personal branding — it can't be meaningfully pre-made for someone else. OG images also require an absolute public URL to your deployed domain, so they can't be tested until the site is live anyway.
To add one (recommended):
- Create a 1200×630 PNG with your name and title — tools like Figma, Canva, or og-image.vercel.app make this straightforward
- Save it to
public/og-image.png - Make sure
siteUrlindata/profile.tsis set to your real domain
Once deployed, you can verify your card at cards-dev.twitter.com/validator or LinkedIn's Post Inspector.
MIT — use it, fork it, make it your own.
Built by Colin Rhys

