Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
# Copy to .env.local — never commit secrets.
# =============================================================================
# Copy this file to `.env.local` and fill in values. Never commit `.env.local`.
# Full documentation: README.md → "Environment variables"
# =============================================================================

# Site URL (used for OAuth redirectTo — set to production URL in Vercel env vars)
# --- Site & OAuth -------------------------------------------------------------
# Production: set to your canonical origin (e.g. https://kaichen.dev) in Vercel.
# Used by Supabase OAuth redirect URLs from the browser client.
NEXT_PUBLIC_SITE_URL=

# Supabase (listening_history, listening_stats, gallery_photos + storage)
# --- Supabase ----------------------------------------------------------------
# Project URL and anon key (safe to expose in the browser bundle).
# Used by getSupabaseAnon(), guestbook POST, gallery reads, /admin client, etc.
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=

# Server-only: bypasses Row Level Security — NEVER expose to the client.
# Required for /api/lastfm/now-playing DB writes (listening_history / listening_stats).
SUPABASE_SERVICE_ROLE_KEY=

# Last.fm (now playing / last played)
# --- Last.fm -----------------------------------------------------------------
# API key from https://www.last.fm/api/account/create
# If unset, /api/lastfm/now-playing still responds with graceful fallbacks.
LASTFM_API_KEY=

# GitHub GraphQL (contributions API + optional star counts on /projects)
# --- GitHub ------------------------------------------------------------------
# Personal access token or fine-grained token with repo read access.
# Powers /api/github/contributions and /api/github/stars.
GITHUB_TOKEN=

# Sentry (optional — error monitoring; omit for local dev / CI with no reporting)
# Client bundle reads NEXT_PUBLIC_SENTRY_DSN; Node/Edge may use SENTRY_DSN (falls back to NEXT_PUBLIC).
# --- Sentry (optional) -------------------------------------------------------
# Omit entirely for local dev / CI with no error reporting.
# Client bundle reads NEXT_PUBLIC_SENTRY_DSN; server/edge can use SENTRY_DSN.
NEXT_PUBLIC_SENTRY_DSN=
SENTRY_DSN=
# Build: upload source maps (readable stacks in Sentry) — org/project slugs from Sentry → Settings → Projects

# Build-time only: upload source maps during `next build` (e.g. on Vercel).
# Get org/project slugs from Sentry → Settings. Do not commit SENTRY_AUTH_TOKEN.
SENTRY_AUTH_TOKEN=
SENTRY_ORG=
SENTRY_PROJECT=
5 changes: 3 additions & 2 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## Summary

<!-- What does this PR change? (1–3 sentences) -->
<!-- What does this PR change? (1–3 sentences). For repo-wide context see README.md (table of contents). -->

## Type of change

Expand Down Expand Up @@ -34,4 +34,5 @@

- [ ] I have read [CONTRIBUTING.md](CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
- [ ] No secrets, `.env.local`, or production tokens are included in this PR.
- [ ] **README** / **`.env.example`** updated if user-facing behavior or required env vars changed.
- [ ] **[README.md](../README.md)** / **[`.env.example`](../.env.example)** updated if routes, APIs, user-facing behavior, or env vars changed.
- [ ] **[CONTRIBUTING.md](../CONTRIBUTING.md)** / **[AGENTS.md](../AGENTS.md)** updated if contribution or agent workflow rules changed.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
# misc
.DS_Store
*.pem
CLAUDE.md

# debug
npm-debug.log*
Expand Down
83 changes: 52 additions & 31 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,40 @@
# AGENTS.md — kaichen.dev

Shared instructions for any AI coding agent (Cursor, Claude Code, Codex, etc.)
working on this repo.
Instructions for **AI coding agents** (Cursor, Claude Code, Codex, Copilot, etc.) and anyone running **automated** edits against this repository.

## Git workflow — ALWAYS branch + PR
**Human-oriented** contributor docs: [CONTRIBUTING.md](CONTRIBUTING.md). **Full project reference:** [README.md](README.md) (architecture, APIs, environment variables, CI, MDX, deployment).

**Never push directly to `main`.** `main` is protected; pushes must go through
a pull request with required status checks. Even if the agent has bypass
permissions, use the PR flow.
---

For every change:
## Git workflow — always branch + PR

**Never push directly to `main`.** Treat `main` as protected: every change goes through a **pull request** with CI green. Even with bypass permissions, use the PR flow unless the repository owner explicitly overrides with a written reason.

### Steps

1. **Branch** from latest `main`:

1. Create a feature branch off the latest `main`:
```bash
git checkout main && git pull --ff-only
git checkout -b <type>/<short-description>
```
Branch name format: `fix/…`, `feat/…`, `chore/…`, `docs/…`, `refactor/…`.

2. Commit with a conventional-style message. **Every commit in this repo must
credit Claude as a co-author** — `.githooks/prepare-commit-msg` appends the
trailer automatically once `npm install` has run, but include it explicitly
anyway (e.g. when you commit via an API/web editor that skips git hooks):
Prefix examples: `fix/`, `feat/`, `chore/`, `docs/`, `refactor/`.

2. **Commit** using conventional-style messages:

```
<type>: <imperative summary>

<optional body explaining the why>
<optional body>

Co-authored-by: Claude <noreply@anthropic.com>
```

3. Push the branch and open a PR with `gh pr create`:
The `Co-authored-by` line is **required** for this repo’s policy (see [Git hooks](#git-hooks) below). If your tool cannot run hooks, append the trailer manually.

3. **Push** and **open a PR**:

```bash
git push -u origin HEAD
gh pr create --title "<type>: <summary>" --body "$(cat <<'EOF'
Expand All @@ -44,33 +47,51 @@ For every change:
)"
```

4. Wait for CI (lint + typecheck + test + build) to pass. Do **not** force-push
to `main`. Do **not** bypass required status checks.
4. **Wait for CI** — lint, typecheck, test, build. Do not force-push to `main` or skip required checks without owner approval.

5. If the user asks for a direct commit to `main`, push back on it and default
to the PR flow unless they explicitly override with a clear reason.
5. If the user asks to **commit straight to `main`**, default to explaining the PR workflow and ask for explicit exception.

---

## Before you commit

Run the same sequence CI runs — fail fast locally:
Mirror CI locally:

```bash
npm run lint && npm run typecheck && npm run test && npm run build
```

Update **[README.md](README.md)** and/or **[`.env.example`](.env.example)** when you change routes, APIs, or required environment variables.

---

## Git hooks (co-author trailer)

This repo ships a `prepare-commit-msg` hook at `.githooks/prepare-commit-msg`
that appends `Co-authored-by: Claude <noreply@anthropic.com>` on every commit
(skipped for merge commits). `npm install` sets `core.hooksPath = .githooks`
via the `postinstall` script, so the hook activates automatically after a
fresh clone + install.
- Hook path: **`.githooks/prepare-commit-msg`**
- Activation: `npm install` runs `postinstall` → `git config core.hooksPath .githooks` (best effort; may fail in some sandboxes — set manually if needed).
- Behavior: appends `Co-authored-by: Claude <noreply@anthropic.com>` via `git interpret-trailers` (skipped for **merge** commits; idempotent if trailer exists).

If hooks do not run (web editor, `--no-verify`, CI-only commits), **add the trailer by hand** so policy is not dropped silently.

---

## Secrets — never commit

Do not stage or commit:

- `.env.local`, `.env.vercel.check`, `.env.sentry-build-plugin`
- Any `*.local`, `*.secret`, or raw tokens

Run `git status` and `git diff --staged` before committing.

If you commit in an environment where the hook can't run (GitHub web editor,
some sandboxed agents, `-n` / `--no-verify` situations), add the trailer
manually so the policy is never silently dropped.
---

## Secrets
## Where to read more

Never commit `.env.local`, `.env.vercel.check`, `.env.sentry-build-plugin`, or
anything matching `*.local` / `*.secret`. Check `git status` before staging.
| Topic | Document |
| --- | --- |
| APIs, env, CI, MDX, fork checklist | [README.md](README.md) |
| Human contribution process | [CONTRIBUTING.md](CONTRIBUTING.md) |
| Short Claude Code stack summary | [CLAUDE.md](CLAUDE.md) |
| Security reporting | [SECURITY.md](SECURITY.md) |
| Cursor IDE git reminder | [`.cursor/rules/git-workflow.mdc`](.cursor/rules/git-workflow.mdc) |
54 changes: 54 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# kaichen.dev — Claude Code context

@AGENTS.md — **read first** for Git workflow (branch + PR only, co-author trailer, secrets).

---

## What this repo is

Personal website of Kai Chen — **Next.js 16** (App Router), **React 19**, **TypeScript**, **Tailwind CSS 4**, deployed on **Vercel**.

Canonical documentation: **[README.md](README.md)** (setup, routes, APIs, env, CI, MDX, forking).

---

## Architecture snapshot

| Concern | Implementation |
| --- | --- |
| **Now playing** | `GET /api/lastfm/now-playing` — Last.fm + optional iTunes art; optional Supabase writes (`listening_*`) with service role |
| **Client polling** | `app/hooks/use-now-playing.ts` — polls every **10s**; `Cache-Control` on API allows short CDN cache |
| **GitHub** | `GET /api/github/contributions` — GraphQL calendar + REST; `GET /api/github/stars` — star counts |
| **Weather** | `GET /api/weather` — Open-Meteo, fixed coordinates (Berkeley), revalidated fetch |
| **Gallery** | Supabase Storage + `gallery_photos`; admin at `/admin` (OAuth + allowlisted email in app — **RLS must match in Supabase**) |
| **Notes** | MDX under `app/notes/**/page.mdx`; pipeline in `next.config.ts` (Webpack + `@mdx-js/loader`); components in `mdx-components.tsx` and `components/notes/` |
| **Observability** | Sentry via `instrumentation*.ts` + `sentry.*.config.ts` (DSN optional); Vercel Analytics / Speed Insights in root layout |
| **Theme** | `app/components/theme-provider.tsx` + inline script in `app/layout.tsx` — default **light** when unset |

---

## Commands

```bash
npm install # also sets core.hooksPath → .githooks (co-author trailer)
npm run dev # next dev --webpack — required for MDX parity
npm run lint && npm run typecheck && npm run test && npm run build
```

---

## Important paths

- [`lib/supabase.ts`](lib/supabase.ts) — `getSupabaseAnon()` lazy singleton
- [`lib/now-playing.ts`](lib/now-playing.ts) — types for Last.fm payload
- [`app/lib/substack.ts`](app/lib/substack.ts) — RSS fetch + parse (tested)
- [`next.config.ts`](next.config.ts) — MDX webpack rule, `withSentryConfig`

---

## Conventions

- **Dark mode** — support `dark:` for new UI.
- **MDX** — do not assume Turbopack; dev/build use `--webpack`.
- **Secrets** — never commit `.env.local` or tokens; see AGENTS.md and README.
- **User prompts** — when the user sends copy-paste blocks, keep them as single fenced blocks when echoing back.
Loading
Loading