A personal blog + AI agent deployed to Cloudflare Workers. The homepage has a real-time activity dashboard (GitHub contributions + top languages) refreshed every 30 min by a cron worker.
- Blog — Markdown articles in D1, syntax-highlighted code
- HN News Archive — Top 5 HN stories fetched every 30 min, with full-length AI rewrites (Gemma 4 via 3-min heartbeat cron)
- GitHub Contributions Heatmap — Real-time activity ribbon in the hero
- Top Languages Donut — 5-segment SVG donut with hover interaction (shows language name + percentage in center), color-coded legend (hero sidebar)
- Visitor Terminal — Typewriter effect showing your IP/location/ISP at the top of the hero
- KurAgent — AI assistant with tool calling (web search, time, JS eval) at
agent.krsz.in - LLM Leaderboard — Full-screen modal showing top 50 models from Artificial Analysis (V2 Free endpoint, refreshed at most every 2 h to respect the 100 req/day quota)
- Guestbook — Visitor messages on the homepage (flow layout + modal)
- News Archive — Paginated HN news index at
/newswith full-text AI rewrites - Particle Background — Canvas character rain (ku/ra/shi/zu)
- Three Themes — dark / deep-blue / deep-green, toggle in the header
Three Cloudflare Workers, deployed in parallel by GitHub Actions on push to main:
| Worker | Path | URL | Purpose |
|---|---|---|---|
cf-blog |
./ |
https://blog.krsz.in | Main blog (Next.js) |
cf-agent |
agent-worker/ |
https://agent.krsz.in | AI agent with tool calling |
cf-blog-cache |
cache-worker/ |
(cron-only) | Homepage cache refresh (30-min) + News rewrite heartbeat (3-min) |
Apex domain is centralized in
shared/site-config.ts(APEX_DOMAIN, currentlykrsz.in). All worker URLs fan out from a single constant — change one line to migrate to a new apex.
The homepage never calls GitHub / Artificial Analysis on user requests. cf-blog-cache pre-fetches data into D1 (posts, news, github_repos, leaderboard, contributions); the homepage reads everything from D1. Cold cache = empty hero; ISR revalidates every 5 min.
CI runs on push to main: migrate-db (database/schema.sql) first, then deploys all three workers in parallel.
- Frontend: Next.js 15 (App Router) + React 19
- Styling: Tailwind CSS (utility classes in JSX) +
tokens.css/base.css(CSS variables + body effects) + component-level CSS (activity.css,nes.css) wherecolor-mix()/pseudo-elements can't be expressed in Tailwind - Storage: Cloudflare D1 (articles, news, github_repos, leaderboard, contributions, guestbook) + Cloudflare KV (sessions, rate limits)
- AI: Gemini (with quota-based model fallback for TPD/RPM 429s)
- Deploy: Cloudflare Workers via
@opennextjs/cloudflare
npm ci --legacy-peer-deps # Install
npm run dev # Local dev server on :3000
npm run build:cf # Build for Cloudflare Workers
npm run lint # ESLintDirect deploy (skips CI):
npx wrangler deploy # cf-blog
cd agent-worker && npm run build:cf && npx wrangler deploy # cf-agent
cd cache-worker && npx wrangler deploy # cf-blog-cachePOST /api/upload — Generate a presigned URL to upload directly to the public-files R2 bucket. Files are publicly accessible at https://bucket.krsz.in/<filename> (driven by BUCKET_URL in shared/site-config.ts).
# Get an upload link
curl -X POST https://blog.krsz.in/api/upload \
-H "Authorization: Bearer <UPLOAD_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"filename":"avatar.png","contentType":"image/png"}'
# Response
# { "url": "...", "key": "avatar.png", "publicUrl": "https://bucket.krsz.in/avatar.png", "expiresIn": 300 }
# Upload directly to R2 (URL valid for 5 minutes)
curl -X PUT "<url>" -H "Content-Type: image/png" --data-binary @avatar.pngGET /api/upload — List files in the bucket
curl https://blog.krsz.in/api/upload \
-H "Authorization: Bearer <UPLOAD_API_KEY>"
# Optional prefix filter
curl "https://blog.krsz.in/api/upload?prefix=images/" \
-H "Authorization: Bearer <UPLOAD_API_KEY>"KurAgent supports three tools via the /api/tool and /api/chat endpoints:
@web_search <query>— Brave Search API@get_time <timezone>— Current time in any IANA timezone@eval_expression <code>— Safe JS expression evaluator (recursive-descent parser, noeval/Function)
Default model: gemma-4-31b-it. Streams via SSE. Max 5 tool-call iterations per turn.
. # cf-blog (main blog)
├── app/ # Next.js App Router
├── components/ # React components (UI, activity, agent, llm, theme, layout, providers)
├── lib/ # Backend logic (D1, articles, guestbook, model pool, languages)
├── database/ # D1 schema (schema.sql — all tables: posts, news_items, github_repos, etc.)
├── public/ # Static assets served by Next.js
├── agent-worker/ # cf-agent (separate Next.js worker)
├── cache-worker/ # cf-blog-cache (cron refresher)
└── AGENTS.md # AI agent orientation
AGENTS.md— Multi-worker architecture, hero features, conventions, D1/R2 storage layout