Personal website of Kai Chen — production: kaichen.dev.
This repository is a Next.js 16 application using the App Router, React 19, TypeScript, and Tailwind CSS 4. It is deployed on Vercel.
| Resource | URL |
|---|---|
| Production site | https://kaichen.dev |
| Source | https://github.com/kaiiiichen/kaichen.dev |
- Overview
- Requirements
- Quick start
- npm scripts
- Repository layout
- Technology stack
- Routes and features
- API routes
- Environment variables
- External integrations
- Local development
- Testing
- CI, Dependabot, and auto-merge
- Git hooks
- Deployment
- Documentation map
- Forking this project
- License
The site combines:
- A marketing-style home page (identity, listening status, weather, GitHub pinned repositories via GraphQL with a static fallback, and Substack headlines).
- Dynamic data from Last.fm, GitHub, Open-Meteo, and optional Supabase-backed listening history.
- Notes are hosted externally on Notion (via the top navigation link).
- Optional observability via Sentry (client, server, edge) and Vercel Analytics / Speed Insights.
There is no middleware.ts (or proxy.ts) in this repo — every route is publicly accessible and rendered by the App Router directly.
| Tool | Version / notes |
|---|---|
| Node.js | 20.x (matches CI and @types/node) |
| npm | 9+; lockfile is package-lock.json — use npm ci for reproducible installs |
git clone https://github.com/kaiiiichen/kaichen.dev.git
cd kaichen.dev
npm install
cp .env.example .env.localEdit .env.local following Environment variables. You do not need every key to run the app locally; missing keys typically degrade or hide features rather than crash the build (exceptions: pages that import Supabase at module scope use placeholder values in CI — see below).
Start the dev server:
npm run devOpen http://localhost:3000.
Important: dev and build both pass --webpack to Next.js. Keep local and production behavior aligned.
| Script | Command | Purpose |
|---|---|---|
dev |
next dev --webpack |
Local development with Webpack. |
build |
next build --webpack |
Production bundle (also runs type checking as part of Next). |
start |
next start |
Serve the last build output (run build first). |
lint |
eslint |
ESLint across the repo (eslint.config.mjs). |
typecheck |
tsc --noEmit |
TypeScript without emitting JS. |
test |
vitest run |
Unit tests once (CI uses this). |
test:watch |
vitest |
Vitest in watch mode. |
postinstall |
git config core.hooksPath .githooks … |
Points Git at .githooks/ so the prepare-commit-msg hook runs after npm install (see Git hooks). |
Before opening a PR, run the same sequence as CI:
npm run lint && npm run typecheck && npm run test && npm run buildHigh-level map (not every file):
kaichen.dev/
├── app/ # App Router
│ ├── layout.tsx # Root layout: fonts, theme script, Nav, Providers, Analytics
│ ├── page.tsx # Home
│ ├── globals.css
│ ├── global-error.tsx # Root error boundary + Sentry
│ ├── opengraph-image.tsx # OG image for /
│ ├── about/ # Bio / CV-style page + OG
│ ├── projects/ # Projects + GitHub heatmap + OG
│ ├── api/ # Route handlers (Last.fm, GitHub, weather)
│ ├── components/ # UI: nav, cards, theme, weather, listening, GitHub heatmap, …
│ ├── hooks/ # e.g. use-now-playing.ts
│ └── lib/ # og.tsx, substack RSS, GitHub pinned repos (GraphQL)
├── lib/ # Shared server-oriented helpers + Vitest tests
│ ├── now-playing.ts # Types for Last.fm payload
│ ├── lastfm-now-playing-helpers.ts
│ ├── weather-open-meteo.ts
│ └── *.test.ts
├── mdx-components.tsx # MDX element mapping
├── next.config.ts # Webpack config + withSentryConfig
├── instrumentation.ts # Sentry Node/Edge registration
├── instrumentation-client.ts # Sentry browser + router transition hooks
├── sentry.server.config.ts
├── sentry.edge.config.ts
├── vitest.config.ts
├── eslint.config.mjs
├── .githooks/ # Git hooks (co-author trailer)
├── .github/
│ ├── workflows/ # ci.yml, auto-merge.yml
│ ├── dependabot.yml
│ ├── ISSUE_TEMPLATE/
│ └── pull_request_template.md
├── .env.example
├── AGENTS.md # AI agent / automation git rules
├── CLAUDE.md # Short context for Claude Code (points here + AGENTS)
├── CONTRIBUTING.md
├── SECURITY.md
├── CODE_OF_CONDUCT.md
└── LICENSE # GPL-3.0
| Layer | Choices |
|---|---|
| Framework | Next.js 16.2 (App Router), React 19, TypeScript 5 |
| Styling | Tailwind CSS 4 (@tailwindcss/postcss), custom CSS in app/globals.css |
| Content | Markdown/MDX pipeline available in config for future content routes |
| Fonts | @fontsource/* (Nunito, Bitter, JetBrains Mono), geist (sans/mono CSS variables) |
| Data | Supabase (@supabase/supabase-js) — optional listening history DB writes (service role) for /api/lastfm/now-playing |
| Monitoring | @sentry/nextjs (optional DSN), Vercel Analytics + Speed Insights |
| Testing | Vitest 3 |
Pinned versions are in package.json.
| Route | What it does |
|---|---|
/ |
Identity block, Last.fm line + card, Berkeley weather, pinned GitHub repos (GraphQL + fallback list), Substack RSS snippets |
/about |
Education, experience, courses, volunteering |
/projects |
Project cards + GitHub contribution calendar (client component, data from /api/github/contributions) |
External nav (no in-app route): the main nav includes Notes → Notion and Blog → Substack; there is no /notes or /blog route in this repo.
Open Graph: several routes ship opengraph-image route handlers for social previews. Set metadataBase in app/layout.tsx if you see build warnings about resolving OG image URLs.
All handlers live under app/api/.
| Method & path | Behavior | Caching / notes |
|---|---|---|
GET /api/lastfm/now-playing |
Last.fm user.getrecenttracks; optional iTunes artwork fallback; optional service-role writes to listening_history / listening_stats when a track is “now playing” |
Cache-Control: public, s-maxage=10, stale-while-revalidate=5; uses LASTFM_API_KEY; in-memory lastKnownTrack fallback |
GET /api/github/contributions |
GraphQL contribution calendar + REST search for latest commit + REST repo metadata for star counts | dynamic = force-dynamic; Cache-Control: no-store; requires GITHUB_TOKEN |
GET /api/github/stars?repo=owner/name |
Returns stargazers_count and archived for a repo |
revalidate = 3600; optional GITHUB_TOKEN for rate limits |
GET /api/weather |
Open-Meteo forecast for fixed Berkeley coordinates | fetch with next.revalidate = 600 |
Copy .env.example to .env.local. Never commit real secrets.
| Variable | Role |
|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Supabase project URL. Paired with SUPABASE_SERVICE_ROLE_KEY inside /api/lastfm/now-playing for the optional listening history. |
SUPABASE_SERVICE_ROLE_KEY |
Server-only. Used by /api/lastfm/now-playing for DB writes/reads against listening_history / listening_stats — keep off the client bundle. |
LASTFM_API_KEY |
Last.fm API. If unset, the now-playing API returns a graceful “not playing” / DB fallback without calling Last.fm. |
GITHUB_TOKEN |
Fine-grained or classic PAT for GitHub API (contributions + stars + pinned repos on the home page). If missing, contribution/stars features may error or return empty data; pinned projects fall back to a static list in app/lib/github-pinned.ts. |
GITHUB_LOGIN |
Optional. GitHub username for pinned repositories and related API calls (defaults to kaiiiichen if unset). Set when forking so the home page shows your pins. |
| Variable | Role |
|---|---|
NEXT_PUBLIC_SENTRY_DSN / SENTRY_DSN |
Error reporting; see instrumentation.ts and Sentry configs. |
SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT |
Build-time source map upload for readable stack traces in Sentry (configure on Vercel, not in git). |
vercel env pull .env.vercel.checkThat path is gitignored — do not commit it.
CI does not need any real Supabase keys to build — every Supabase client in this repo is constructed lazily inside a function body, so next build succeeds without NEXT_PUBLIC_SUPABASE_* set. See .github/workflows/ci.yml.
| Service | Use in this repo |
|---|---|
| Last.fm | Recent / now-playing track |
| Apple iTunes Search API | Album art fallback |
| GitHub GraphQL | Contribution calendar |
| GitHub REST | Repo stars, commit search |
| Open-Meteo | Weather (no API key) |
| Supabase | Optional listening_history / listening_stats writes (service role) for the now-playing route |
| Substack RSS | Home page “latest posts” (app/lib/substack.ts) |
| lib.berkeley.edu | Library hours HTML (scraped server-side; not an official API) |
- Node 20, npm install then
npm run dev. - Supabase: only
/api/lastfm/now-playinguses Supabase (service role) for the optional listening history. The site builds and serves every page without any Supabase env set; the route just falls back to live Last.fm + in-memory cache when the DB is unreachable.
| Symptom | Things to check |
|---|---|
| GitHub widgets empty | GITHUB_TOKEN set and not expired; API rate limits. |
| "Recently played" never persists across deploys | NEXT_PUBLIC_SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY set; tables listening_history / listening_stats exist with the expected columns. |
| Sentry noisy locally | DSN unset disables reporting; or lower sample rate in instrumentation-client.ts. |
Unit tests use Vitest and live next to helpers under lib/*.test.ts (RSS parsing, weather mapping, Last.fm helpers).
npm run test
npm run test:watchThere are currently no Playwright/E2E tests in this repo; manual browser checks matter for layout and visual polish.
Triggers on push and pull_request to main:
npm ci → lint → typecheck → test → build on ubuntu-latest, Node 20, with npm cache.
Dependabot (.github/dependabot.yml)
- npm and github-actions ecosystems, weekly (Monday 09:00 America/Los_Angeles).
- Grouped updates (fonts, Sentry, Supabase, MDX-related, Vercel, types, catch-all minor/patch).
- Ignored semver-major bumps for core tooling (
next,react,eslint,typescript,tailwindcss, …) so those upgrades stay manual.
Auto-merge (.github/workflows/auto-merge.yml)
Runs only when the PR author is dependabot[bot]:
- Reads semver classification via
dependabot/fetch-metadata. - For patch and minor updates: enables
gh pr merge --auto --squash(respects branch protection when checks pass). - On open / reopen, posts an idempotent PR comment that includes the official
@dependabot squash and mergeline (documentation + redundancy; primary merge path is still GitHub auto-merge).
pull_request types include synchronize so Dependabot force-pushes re-enable auto-merge. Concurrency is scoped per PR number to avoid overlapping runs.
After npm install, postinstall runs:
git config core.hooksPath .githooks.githooks/prepare-commit-msg appends:
Co-authored-by: Claude <noreply@anthropic.com>
to non-merge commits via git interpret-trailers (idempotent). Automation that cannot run hooks should add the same trailer manually — see AGENTS.md.
- Connect the GitHub repository to Vercel.
- Set environment variables in the Vercel project (production + preview as needed), especially
GITHUB_TOKEN,LASTFM_API_KEY, and the Supabase keys if you want listening history persistence. - Pushes to
maintypically deploy production; preview deployments use PR branches.
Manual CLI (after vercel link):
vercel --prod| File | Audience | Contents |
|---|---|---|
| README.md (this file) | Everyone | Setup, architecture, APIs, env, CI |
CONTRIBUTING.md |
Human contributors | How to PR, conventions, CI parity |
AGENTS.md |
AI agents / automation | Branch + PR only, co-author trailer, secrets |
CLAUDE.md |
Claude Code | Short pointer + stack summary |
SECURITY.md |
Security researchers | How to report issues responsibly |
CODE_OF_CONDUCT.md |
Contributors | Contributor Covenant |
.env.example |
Developers | Variable names and brief comments |
.github/pull_request_template.md |
PR authors | Checklist |
Cursor-specific rules live under .cursor/rules/ (IDE-only, not required reading for all contributors).
Replace at minimum:
| Area | Where to look |
|---|---|
| Copy, links, projects list | app/page.tsx, app/projects/page.tsx, app/about/page.tsx |
| Last.fm username | app/api/lastfm/now-playing/route.ts |
| GitHub login / repos / pins | app/api/github/contributions/route.ts, app/components/project-stars.tsx, app/lib/github-pinned.ts, env GITHUB_LOGIN |
| Supabase tables | app/api/lastfm/now-playing/route.ts, Supabase dashboard (listening_history, listening_stats) |
| Substack feeds | app/lib/substack.ts |
| Weather location | app/api/weather/route.ts, weather UI components |
| Theme / fonts | app/layout.tsx, app/globals.css, app/components/theme-provider.tsx |
Keep LICENSE compliance if you redistribute (GPL-3.0).
This project is licensed under the GNU General Public License v3.0 — see LICENSE.
Please read SECURITY.md before reporting vulnerabilities.