anna.maria.dev cover
Personal portfolio and open source template for full-stack engineers. Built as an editor metaphor: titlebar with tabs, sidebar navigation, status bar, all styled with the entrepta design system. Dark first, TypeScript strict.
Live: annamaria.app
- Home: bento grid with a hero card, Spotify Now Playing widget, an Apple Watch activity card (via wristkit), GitHub contributions, a featured project, and career stats
- About: career timeline, education, tech stack grid, GitHub contributions calendar
- Blog: MDX posts with syntax highlighting (Shiki), reading progress bar, and tag filtering
- Projects: case studies with sidebar metadata and rich MDX content
- Piano: a small interactive piano
- Log: one feed for everything you finish — films, series, books, albums, podcasts, games — with ratings, favourites and notes, plus an admin behind WorkOS AuthKit to manage it
- Contact: email form powered by Resend and React Email, with a honeypot
- Command palette: ⌘K navigation across the whole site
- Editor chrome: titlebar, sidebar, and status bar shared across every page
- Themes: dark by default, light toggle, 6 brand color presets, no flash
- SEO: dynamic OG images, sitemap, robots.txt, canonical URLs
Spotify, wristkit and the log are all optional. Without their environment variables the site still builds and runs: the widgets show an empty or error state, /log renders empty, and /admin is unreachable.
| Layer | Tech |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript (strict) |
| Styling | Tailwind CSS v4 |
| Design system | entrepta (components copied in, no SDK) |
| Content | MDX via Velite |
| State | Zustand |
| Animations | Motion v12 |
| Resend + React Email | |
| Syntax highlight | Shiki |
| Themes | next-themes + entrepta ThemeSwitcher |
| OG images | @vercel/og |
| SEO | next-sitemap |
| Icons | Phosphor Icons, simple-icons |
| wristkit storage | Postgres via Drizzle ORM |
| Deploy | Vercel |
git clone https://github.com/imnotannamaria/anna.maria.dev.git my-portfolio
cd my-portfolio
npm installCopy the example and fill in your values:
cp .env.example .env.local(The live site manages its own secrets with Infisical instead of a
committed .env.local, which is why npm run dev is wrapped in it — see step 4 for the fork
path.)
# Required: get yours at resend.com
RESEND_API_KEY=re_xxxxxxxxxxxx
# Your public URL (used for sitemap and OG images)
NEXT_PUBLIC_BASE_URL=https://yourdomain.com
# Optional: Spotify Now Playing widget (Client Credentials flow)
SPOTIFY_CLIENT_ID=xxx
SPOTIFY_CLIENT_SECRET=xxx
SPOTIFY_PLAYLIST_ID=xxx
# Optional: Postgres, shared by the wristkit card and /log
# On Supabase use the transaction pooler string (port 6543), not the direct one
DATABASE_URL=postgresql://user:pass@host:6543/postgres
# Optional: wristkit ingest endpoint
WRISTKIT_API_KEY=replace-with-32-random-bytes
# Optional: WorkOS AuthKit, guards /admin
WORKOS_API_KEY=sk_test_xxx
WORKOS_CLIENT_ID=client_xxx
WORKOS_COOKIE_PASSWORD= # 32+ chars: openssl rand -base64 32
NEXT_PUBLIC_WORKOS_REDIRECT_URI=https://yourdomain.com/api/auth/callback
# Optional: comma-separated emails allowed into /admin
ADMIN_EMAILS=you@example.com
# Optional: GitHub GraphQL, for the contributions grid on / and /about
# Classic PAT, `read:user` scope
GITHUB_TOKEN=ghp_xxxxxxxxxxxxLeave out whatever you don't need. Only RESEND_API_KEY and NEXT_PUBLIC_BASE_URL are required.
Edit the following files with your own data:
| File | What to change |
|---|---|
lib/site-config.ts |
Name, email, GitHub/LinkedIn/X handles |
app/page.tsx |
Bio, stats, sections shown on home |
app/about/page.tsx |
Full bio, timeline, stack, interests |
app/layout.tsx |
Site title, description, theme presets |
app/api/contact/route.ts |
Email from and to addresses |
components/about/github-calendar.tsx |
Your GitHub username |
lib/metadata.ts |
baseUrl fallback |
npm run devdev is wrapped in Infisical, which is how this repo keeps its own
secrets out of the working tree. On a fork, that is not what you want — use the script that
reads your .env.local instead:
npm run dev:local(Or run infisical login and point .infisical.json at your own workspace, if you'd rather
have a secrets manager too.)
Open localhost:3000.
Create content/blog/your-post-slug.mdx:
---
title: "Your post title"
description: "A short description for SEO and cards."
date: "2026-01-01"
tags: ["next.js", "typescript"]
published: true
---
Your content here.Create content/projects/your-project-slug.mdx:
---
title: "Project Name"
description: "What it does in one sentence."
date: "2026-01-01"
tags: ["react", "typescript"]
github: "https://github.com/you/project"
live: "https://project.vercel.app"
featured: true
published: true
---
## Overview
...Set featured: true to show it as the featured project on the home page. Only the most recent featured project is shown there.
- Create an account at resend.com
- Add and verify your domain
- Create an API key and add it to
.env.local - Update
fromandtoinapp/api/contact/route.ts:
from: "Portfolio <hello@yourdomain.com>",
to: ["you@yourdomain.com"],- Create an app at the Spotify Developer Dashboard
- Grab the client ID and secret, and the ID of a public playlist
- Add
SPOTIFY_CLIENT_ID,SPOTIFY_CLIENT_SECRET, andSPOTIFY_PLAYLIST_IDto.env.local
The widget uses the Client Credentials flow, server to server, so the secret never reaches the browser.
The Apple Watch activity card reads from your own Postgres database. See wristkit for the full setup: the SQL migration, the iOS Shortcut, and the sync endpoint at /api/v1/wristkit/sync.
/log is a single feed for everything you finish, with an admin at /admin/log to manage it. It shares the same Postgres database as wristkit.
- Set
DATABASE_URLand run[docs/sql/001-log-entries.sql](docs/sql/001-log-entries.sql)against it. - Optionally seed it with sample entries:
npm run seed:log. - For the admin, create an application at workos.com, register
<your-domain>/api/auth/callbackas a redirect URI, and fill in the fourWORKOS_*variables. - Put your own email in
ADMIN_EMAILS.
That last step is not optional if you want the admin. AuthKit decides who is signed in, not who is allowed — without an allowlist, anyone who creates an account in your WorkOS organisation reaches your admin. lib/auth/require-admin.ts is what actually guards it.
Without DATABASE_URL the page renders empty and the build still passes. Without the WorkOS variables /admin is simply unreachable.
The design decisions behind all of it, phase by phase, are in docs/log-plan.md.
/roadmap is a board of what the site is going to become — to do, in progress, shipped — with an admin at /admin/roadmap. It rides on the same database and the same allowlist as the log, so if you already did the steps above there are only two left:
- Run
[docs/sql/003-roadmap-items.sql](docs/sql/003-roadmap-items.sql)againstDATABASE_URL. - Optionally seed it:
npm run seed:roadmap.
Items start with status raw, which never renders publicly — that is the holding pen for an idea you don't want to lose but haven't decided anything about. Promote one to todo when it becomes real.
Without DATABASE_URL the board renders empty and the build still passes.
The reasoning, phase by phase, is in docs/roadmap-component-plan.md.
npm run test:allThat is the whole thing: it starts a throwaway Postgres, builds content, then runs all three layers — 123 unit, 38 integration, 9 end-to-end — and stops the database afterwards. Docker (or OrbStack) needs to be running; nothing else has to be set up, and no environment variable has to be exported.
npm run test:all -- --keep # leave the database up, so the next run is faster
npm run test:all -- --no-e2e # skip the browser layer and its production buildThe layers can still be run one at a time. Only the first needs no infrastructure:
npm test # unit — pure functions and the API surface
npm run test:watch # the same, in watch mode
npm run test:integration # + a real Postgres
npm run test:e2e # + Playwright, builds and starts the app itselfRun on their own, the last two expect the database and the e2e secret to already be in the
environment — npm run test:all exists precisely because wiring that up by hand every time
was five steps of ceremony:
docker compose -f docker-compose.test.yml up -d
export DATABASE_URL=postgresql://postgres:postgres@localhost:5433/postgres
export TEST_WORKOS_COOKIE_PASSWORD=$(openssl rand -base64 32)Both suites are destructive. The integration setup TRUNCATEs log_entries,
roadmap_items and wristkit_samples between every test, and the e2e suite writes and
deletes real rows. Both refuse to run unless DATABASE_URL points at localhost, so
pointing them at Supabase fails loudly instead of erasing it — override with
ALLOW_NONLOCAL_TEST_DB=true only for a database you are willing to lose.
TEST_WORKOS_COOKIE_PASSWORD is any 32+ character string. It seals throwaway admin sessions,
signed against a local stand-in for WorkOS's JWKS rather than the real WorkOS, and is
unrelated to the production WORKOS_COOKIE_PASSWORD. In CI it comes from a repo secret of
the same name, which the workflow checks for before doing any work.
The e2e layer also needs a browser once: npx playwright install --with-deps chromium.
The full reasoning — why a mocked DB was ruled out, why the e2e auth setup runs a local JWKS server instead of stubbing WorkOS, what each test is actually guarding against — is in docs/tests-plan.md.
- Push to GitHub
- Import the repo at vercel.com/new
- Add the environment variables from
.env.localin the Vercel dashboard - Deploy: sitemap and robots.txt are generated automatically at build time
app/
page.tsx # Home
about/page.tsx # About
blog/ # Blog list + [slug]
projects/ # Projects list + [slug]
piano/ # Piano
contact/page.tsx # Contact form
log/ # Public log feed
admin/ # Log CRUD, behind AuthKit
components/entrepta/ # entrepta design system components
api/
contact/route.ts # Email via Resend
og/route.tsx # Dynamic OG images
now-playing/route.ts # Spotify Now Playing
auth/callback/route.ts # WorkOS AuthKit callback
v1/[[...route]]/ # Hono app: wristkit ingest + admin CRUD
layout.tsx # Root layout (editor chrome)
proxy.ts # AuthKit proxy, scoped to /admin
content/
blog/*.mdx # Blog posts
projects/*.mdx # Project case studies
components/
chrome/ # Titlebar, sidebar, command palette
home/ # Bento grid cards (stack, mini piano, GitHub, log)
log/ # Log feed card, star rating
admin/ # Log entry form, table, dialogs
spotify/ # Now Playing widget
wristkit/ # Apple Watch activity card
blog/ # MDX renderer, reading progress
projects/ # Project card
about/ # GitHub calendar
contact/ # Contact form
ui/ # Shared UI helpers
emails/
contact-email.tsx # React Email template
lib/
api/ # Hono app, routes, middleware
auth/ # Admin email allowlist
db/client.ts # Shared Postgres client
log/ # Schema, validation, queries, mutations
velite.ts # Content query helpers
site-config.ts # Name, email, socials
experience.ts # Career start date, years of experience
spotify.ts # Spotify token + playlist fetch
wristkit/ # wristkit schema + queries
utils.ts # cn(), formatDate(), estimateReadingTime()
metadata.ts # createMetadata() helper
docs/
log-plan.md # /log design decisions, phase by phase
sql/ # Hand-run migrations
MIT. Fork freely, customize, make it yours.