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
- MDX lecture notes
- 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, Substack headlines, and links to news.kaichen.dev and the Berkeley library hours page).
- Dynamic data from Last.fm, GitHub, Open-Meteo, optional Supabase-backed listening history, and UC Berkeley library hours scraped from the official hours page (cached, JSON API available).
- MDX-powered notes under
/noteswith math (KaTeX), GitHub-flavored Markdown, and syntax-highlighted code blocks — multiple course segments (UC Berkeley and SUSTech); see MDX lecture notes. - 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. MDX is wired through a custom webpack() block in next.config.ts (@mdx-js/loader + remark/rehype plugins). Using Webpack for dev/build keeps MDX behavior aligned with production. Do not assume Turbopack-only behavior for .mdx files.
| Script | Command | Purpose |
|---|---|---|
dev |
next dev --webpack |
Local development with Webpack (MDX-compatible). |
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
│ ├── notes/ # Notes index, course pages, MDX note routes
│ ├── api/ # Route handlers (Last.fm, GitHub, weather, UCB libraries)
│ ├── berkeley-libraries/ # UC Berkeley library hours (HTML from lib.berkeley.edu)
│ ├── 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)
├── components/notes/ # MDX shortcodes: Theorem, Proof, Definition, Example, NoteBlock
├── lib/ # Shared server-oriented helpers + Vitest tests
│ ├── now-playing.ts # Types for Last.fm payload
│ ├── lastfm-now-playing-helpers.ts
│ ├── ucb-library-hours.ts # Fetch + parse lib.berkeley.edu/hours (Cheerio)
│ ├── weather-open-meteo.ts
│ └── *.test.ts
├── mdx-components.tsx # MDX element mapping + shortcode registration
├── next.config.ts # MDX webpack rule + 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 | MDX via @mdx-js/loader + remark-gfm, remark-math, rehype-katex, rehype-highlight |
| Scraping | cheerio — parses UC Berkeley library hours HTML server-side |
| 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, links to news.kaichen.dev and /berkeley-libraries |
/about |
Education, experience, courses, volunteering |
/projects |
Project cards + GitHub contribution calendar (client component, data from /api/github/contributions) |
/notes |
Index of courses (see MDX lecture notes); links to external SUSTech-Kai-Notes for broader collections |
/notes/... |
Nested segments per course (e.g. cs61a, data100, cs217, ma121–ma337); individual notes are page.mdx |
/berkeley-libraries |
UC Berkeley library open/closed status and hours, parsed from lib.berkeley.edu/hours; data revalidates every 15 minutes |
External nav (no in-app route): the main nav includes News → news.kaichen.dev and Blog → Substack; there is no /blog or /news 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 |
GET /api/ucb-libraries |
Same payload as /berkeley-libraries: JSON with libraries, fetchedAt, sourceUrl, or ok: false + error |
Uses getUCBLibraryHours(); upstream fetch revalidate: 900 (15 minutes) |
Berkeley libraries: parsing depends on the HTML structure of lib.berkeley.edu. If the upstream page changes, lib/ucb-library-hours.ts may need updates (see error responses when zero libraries parse).
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.
- Notes are route segments with
page.mdxfiles (e.g.app/notes/cs61a/scheme-quote/page.mdx), not a separatecontent/directory. - The index at
/noteslists courses by code (examples: CS61A, Data 100, CS217, MA121–MA337). Each course has apage.tsxhub and nested folders for individual notes. - Shared layout:
app/notes/layout.tsx(imports KaTeX CSS, width/padding). - MDX components and typography are centralized in
mdx-components.tsx. - Custom shortcodes (Theorem, Definition, Proof, Example, NoteBlock) live in
components/notes/and are registered globally for MDX. - Metadata in MDX files often uses
export const metadata = { title, description }(Next.js metadata), not always YAML frontmatter. - Large PDFs or archives for a course may live under
public/notes/...and be linked from MDX; keep binary paths in sync if you move files.
To add a new course: add a card on the notes index, create app/notes/<slug>/page.tsx plus note folders with page.mdx, match existing note headers (breadcrumb, title block) for visual consistency, and run npm run build to validate the MDX pipeline.
| 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. - If MDX fails to compile, confirm you did not remove the
--webpackflag from scripts. - 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 |
|---|---|
| MDX differs between dev and prod | Ensure both use Webpack (--webpack). |
| 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 |
| Berkeley library page | lib/ucb-library-hours.ts, app/berkeley-libraries/page.tsx, app/api/ucb-libraries/route.ts |
| 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.