Skip to content

kaiiiichen/kaichen.dev

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

204 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

kaichen.dev

CI Vitest ESLint
Next.js 16 React 19 TypeScript 5 Tailwind CSS 4 Node.js 20
Deployed on Vercel License GPL-3.0 Dependabot kaichen.dev

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

Table of contents

  1. Overview
  2. Requirements
  3. Quick start
  4. npm scripts
  5. Repository layout
  6. Technology stack
  7. Routes and features
  8. API routes
  9. Environment variables
  10. External integrations
  11. Local development
  12. Testing
  13. CI, Dependabot, and auto-merge
  14. Git hooks
  15. Deployment
  16. Documentation map
  17. Forking this project
  18. License

Overview

The site combines:

  • A marketing-style home page (identity + Giving What We Can pledge mark, social icons with brand-colored hovers (Email, Signal, GitHub, LinkedIn, Spotify), a Spotify listening card, Berkeley weather/clock card, GitHub pinned repositories via GraphQL with a static fallback, and Substack headlines).
  • Dynamic data from Spotify, GitHub, Open-Meteo, and optional Supabase-backed listening history.
  • A /misc page with curated lists (news worth attention, favorite tools & content creators, remembrance).
  • Notes are hosted externally on Notion (via the top navigation link).
  • Optional observability via Sentry (client, server, edge) and Vercel Analytics / Speed Insights.

UI / typography: The root body uses Geist Sans (font-sans). Nunito (via @fontsource/nunito) is used for the top nav, magazine-style cards (.mag-card / .mag-label), and most English copy inside those surfaces so the chrome stays consistent. Theme defaults to light when unset; .dark on <html> comes from the theme script + provider.

Now playing: The client hook app/hooks/use-now-playing.ts polls GET /api/spotify/now-playing about every 10s (cache: "no-store").

There is no middleware.ts (or proxy.ts) in this repo — every route is publicly accessible and rendered by the App Router directly.


Requirements

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

Quick start

git clone https://github.com/kaiiiichen/kaichen.dev.git
cd kaichen.dev
npm install
cp .env.example .env.local

Edit .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 dev

Open http://localhost:3000.

dev and build use Next.js' default Turbopack bundler.


npm scripts

Script Command Purpose
dev next dev Local development (Turbopack).
build next build 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 build

Repository layout

High-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, education, focus, experience, volunteering + OG
│   ├── projects/                 # Projects + GitHub heatmap + OG
│   ├── misc/                     # Curated lists: news, tools, creators, remembrance + OG
│   ├── api/                      # Route handlers (Spotify, GitHub, weather)
│   ├── components/               # UI: nav, cards, theme, weather, listening, GitHub heatmap, …
│   ├── hooks/                    # use-now-playing.ts (Spotify poll)
│   ├── lib/                      # og.tsx, substack RSS, GitHub pinned repos (GraphQL)
│   ├── sitemap.ts                # /sitemap.xml
│   └── robots.ts                 # /robots.txt
├── lib/                          # Shared server-oriented helpers + Vitest tests
│   ├── now-playing.ts            # Types for now-playing payload
│   ├── spotify-now-playing-helpers.ts
│   ├── spotify-access-token.ts
│   ├── listening-supabase.ts
│   ├── weather-open-meteo.ts
│   └── *.test.ts
├── public/                       # avatar.jpg (home photo + OG images)
├── next.config.ts                # Image remote patterns + 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

Technology stack

Layer Choices
Framework Next.js 16.2 (App Router, Turbopack), React 19, TypeScript 5
Styling Tailwind CSS 4 (@tailwindcss/postcss), shared UI tokens + magazine cards in app/globals.css (.mag-card, .mag-label)
Fonts @fontsource/* (Nunito, JetBrains Mono), geist (Geist Sans / Mono as CSS variables on <html>, default font-sans on <body>)
Data Supabase (@supabase/supabase-js) — optional listening history DB writes (service role) for /api/spotify/now-playing
Monitoring @sentry/nextjs (optional DSN), Vercel Analytics + Speed Insights
Testing Vitest 4

Pinned versions are in package.json.


Routes and features

Route What it does
/ Identity block (name + Giving What We Can 🔸), social links (mailto, Signal, GitHub, LinkedIn, Spotify), Listening + Location cards (Spotify + Open‑Meteo: temp with °C/°F toggle, condition, feels-like, humidity, local America/Los_Angeles clock via berkeley-time.tsx), pinned GitHub repos (GraphQL + fallback list), Substack RSS snippets
/about Personality intro, education, Focus (current courses), experience, volunteering
/projects Project cards mirroring GitHub profile pins (stars / archived badges from the same GraphQL query) + GitHub contribution calendar (client component, data from /api/github/contributions)
/misc Curated lists: News (call for attention, sorted newest-first), Things I Love (productivity / developer / travel tools, content creators), Remembrance

External nav (no in-app route): the main nav order is About · Projects · Notes · Blog · Misc, where NotesNotion and BlogSubstack are external; there is no /notes or /blog route in this repo.

Open Graph: every route ships an opengraph-image route handler (shared template in app/lib/og.tsx, rendering public/avatar.jpg + a text panel). metadataBase is set to https://kaichen.dev in app/layout.tsx, and each page exports its own title / description.

SEO: app/sitemap.ts and app/robots.ts serve /sitemap.xml and /robots.txt (API routes disallowed); the home page embeds JSON-LD Person structured data.


API routes

All handlers live under app/api/.

Method & path Behavior Caching / notes
GET /api/spotify/now-playing Spotify me/player/currently-playing + recently-played; optional service-role merges legacy listening_stats rows into spotify:<trackId>; writes listening_* while is_playing Cache-Control: public, s-maxage=10, stale-while-revalidate=5; SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, SPOTIFY_REFRESH_TOKEN; in-memory lastKnownTrack fallback
GET /api/github/contributions GraphQL contribution calendar (last 12 months); returns { weeks, totalContributions } revalidate = 300; Cache-Control: public, s-maxage=300, stale-while-revalidate=600; requires GITHUB_TOKEN
GET /api/weather Open-Meteo current conditions for fixed Berkeley, CA coordinates (temperature_2m, weathercode, apparent_temperature, relative_humidity_2m, hourly rain chance) fetch with next.revalidate = 600; parsed in lib/weather-open-meteo.ts

Star counts and archived badges come from the pinned-repositories GraphQL query (app/lib/github-pinned.ts, stargazerCount / isArchived, server-side, revalidate = 120) — there is no public stars proxy endpoint.


Environment variables

Copy .env.example to .env.local. Never commit real secrets.

Always safe to document (names only)

Variable Role
NEXT_PUBLIC_SUPABASE_URL Supabase project URL. Paired with SUPABASE_SERVICE_ROLE_KEY inside /api/spotify/now-playing for the optional listening history.
SUPABASE_SERVICE_ROLE_KEY Server-only. Used by /api/spotify/now-playing for DB reads/writes against listening_history / listening_stats — keep off the client bundle.
SPOTIFY_CLIENT_ID / SPOTIFY_CLIENT_SECRET / SPOTIFY_REFRESH_TOKEN Spotify app + user refresh token (scopes: user-read-currently-playing, user-read-recently-played). If unset, the route falls back to memory + DB for “last played.”
GITHUB_TOKEN Fine-grained or classic PAT for GitHub API (contributions + pinned repos incl. star counts). If missing, the contribution calendar errors and pinned projects fall back to a static list in app/lib/github-pinned.ts (no star badges).
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.

Sentry (optional)

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 CLI (optional)

vercel env pull .env.vercel.check

That path is gitignored — do not commit it.

CI builds without live Supabase

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.


External integrations

Service Use in this repo
Spotify Web API Current + recently played track
GitHub GraphQL Contribution calendar + profile pinned repositories incl. stars / archived (app/lib/github-pinned.ts, revalidate = 120)
Open-Meteo Weather (no API key); Berkeley, CA lat/long in app/api/weather/route.ts
Supabase Optional listening_history / listening_stats writes (service role) for /api/spotify/now-playing
Substack RSS Home page “latest posts” (app/lib/substack.ts)

Local development

  • Node 20, npm install then npm run dev.
  • Supabase: only /api/spotify/now-playing uses Supabase (service role) for the optional listening history. The site builds without Supabase env; when it is unset, the route skips DB reads/writes and still uses Spotify if SPOTIFY_* is configured.

Common issues

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.

Testing

Unit tests use Vitest and live under lib/*.test.ts: lib/substack-rss.test.ts, lib/weather-open-meteo.test.ts, lib/spotify-now-playing-helpers.test.ts.

npm run test
npm run test:watch

There are currently no Playwright/E2E tests in this repo; manual browser checks matter for layout and visual polish.


CI, Dependabot, and auto-merge

Triggers on push and pull_request to main:

npm cilinttypechecktestbuild on ubuntu-latest, Node 20, with npm cache.

  • npm and github-actions ecosystems, weekly (Monday 09:00 America/Los_Angeles).
  • Grouped updates (fonts, Sentry, Supabase, Vercel, types, catch-all minor/patch).
  • Ignored semver-major bumps for core tooling (next, react, eslint, typescript, tailwindcss, …) so those upgrades stay manual.

Runs only when the PR author is dependabot[bot]:

  1. Reads semver classification via dependabot/fetch-metadata.
  2. For patch and minor updates: enables gh pr merge --auto --squash (respects branch protection when checks pass).
  3. On open / reopen, posts an idempotent PR comment that includes the official @dependabot squash and merge line (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.


Git hooks

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.


Deployment

  1. Connect the GitHub repository to Vercel.
  2. Set environment variables in the Vercel project (production + preview as needed), especially GITHUB_TOKEN, Spotify keys, and the Supabase variables if you want listening history persistence.
  3. Pushes to main typically deploy production; preview deployments use PR branches.

Manual CLI (after vercel link):

vercel --prod

Documentation map

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).


Forking this project

Replace at minimum:

Area Where to look
Copy, links, projects list, social URLs app/page.tsx, app/projects/page.tsx, app/about/page.tsx
Spotify OAuth app + refresh token Spotify Developer Dashboard; env SPOTIFY_* consumed in lib/spotify-access-token.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 lib/listening-supabase.ts, app/api/spotify/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
Misc lists (tools, creators, news, remembrance) app/misc/page.tsx
Theme / fonts app/layout.tsx, app/globals.css, app/components/theme-provider.tsx

Keep LICENSE compliance if you redistribute (GPL-3.0).


License

This project is licensed under the GNU General Public License v3.0 — see LICENSE.


Security

Please read SECURITY.md before reporting vulnerabilities.

About

my personal space at a corner of human made internet :D

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages