Typography-led portfolio. No frameworks I don't need, no bloat I can't justify.
Most developer portfolios are a scroll-jacking parallax hero, six animation libraries, and a 4 MB payload to say what a paragraph could.
This one is a column of text. Recruiters scan it in twenty seconds; anyone who wants depth can click into a case study. Every element that didn't earn its place is gone.
| Thing | What it actually means | |
|---|---|---|
| π | Typography-led | A single 644px column, one variable font, spacing on a fixed step scale. Hierarchy comes from type, not chrome. |
| β‘ | Fully static | output: "export" β pre-rendered HTML, no server, no runtime data fetching. |
| π€ | One self-hosted font | Inter subset to Latin, preloaded, under a private family name so a locally installed Inter can't paint first and reflow. |
| ποΈ | Typed content | Experience and projects are typed from Zod schemas; case studies are typed data, not MDX. |
| π | SEO that resolves | Generated sitemap and robots, JSON-LD Person + WebSite + Article, absolute canonical and og:image URLs. |
| βΏ | Keyboard-complete | Roving-tabindex tab list, skip link, visible focus rings, screen-reader labels on every external link. |
| πΌοΈ | Images that behave | WebP throughout; every image in public/ adds up to 428 KB. |
| π | Real headers | CSP, HSTS, X-Frame-Options, and immutable asset caching via vercel.json. |
| π§Ή | Dead code fails CI | Biome, tsc --noEmit, knip, and the build all gate every push. |
Next.js 16 App Router Β· React 19 Β· TypeScript (strict) Β· Sass Β· Zod Β· Biome Β· knip Β· pnpm
No Tailwind. No CSS-in-JS. No animation library. The interactions that exist are CSS.
app/
βββ components/ Identity, About, Directory, Contact
βββ case-studies/ index + [slug] route
βββ data/ experiences, projects, caseStudies
βββ lib/seo.ts one source of truth for metadata
βββ robots.ts generated, not static
βββ sitemap.ts generated from case-study dates
public/ fonts, WebP images, social card
Content lives in app/data/. Adding a case study means adding an object β the index page, the sitemap, and the JSON-LD all follow from it.
pnpm install
pnpm dev # localhost:3000| Task | Command |
|---|---|
Build (static export to ./out) |
pnpm build |
| Preview the built output | pnpm start |
| Lint + format check | pnpm lint |
| Typecheck | pnpm typecheck |
| Find dead code | pnpm lint:unused |
| Notify IndexNow of the live URLs | node scripts/indexnow.mjs |
Note
The social card is drawn in public/social-card.svg and shipped as PNG, because Twitter, LinkedIn, and Slack all ignore SVG. After editing the SVG:
rsvg-convert -w 1200 -h 630 public/social-card.svg -o public/social-card.pngTwo build-time env vars carry the ownership tokens, set in the Vercel project:
| Variable | Emits |
|---|---|
NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION |
<meta name="google-site-verification"> |
NEXT_PUBLIC_BING_SITE_VERIFICATION |
<meta name="msvalidate.01"> |
Unset variables emit no tag at all, so preview deploys and forks never claim ownership of the domain. Get the Google token from Search Console's URL prefix property and the Bing token from Webmaster Tools' add site flow; some hosts prefer the DNS TXT record instead, which works the same as far as the crawler is concerned and needs no deploy.
.github/workflows/indexnow.yml submits every URL in the live sitemap after each push to main (Bing, Yandex, Seznam, Naver β Google only uses the sitemap). Run it by hand with node scripts/indexnow.mjs, or preview with node scripts/indexnow.mjs --dry-run.
Longer write-ups on things I've built, framed around the decisions rather than the feature list.
- Working at Botpress β shipping and ownership at an AI startup
- Argus β live video review tooling Β· 2x hackathon winner
- Linky β a URL into a retrieval-backed knowledge base
- CropCare β sensors, MQTT, and Azure into one field workflow
- Email β mail@carsonspriggs.com
- LinkedIn β carsonspriggs
- GitHub β carsonSgit
Tip
Fork it if it's useful. If you ship something with it, I'd genuinely like to see it.
MIT Β· built in the open
