Personal blog of moikapy — AI Engineer x Gamer.
Built with Next.js 16, Tailwind CSS v4, and shadcn/ui. Deployed on Cloudflare Pages with Cloudflare D1 for blog post storage.
- 📝 Blog with admin panel — write and publish from any device at
/admin - 🔐 Password-protected admin — login from your phone, tablet, anywhere
- 🗄️ Cloudflare D1 database — no git commits needed to publish posts
- 📡 RSS feed at
/feed/rss.xml - ⚡ Nostr feed at
/feed/nostr.json(NIP-23 long-form content) - 🌑 Dark mode by default
- 🔍 SEO optimized (sitemap, robots.txt, Open Graph)
- 🚀 Deployed on Cloudflare with OpenNext
bun install
bunx wrangler login bun run db:create
Copy the database_id from the output and paste it into wrangler.toml:
[[d1_databases]] binding = "DB" database_name = "moikapy-blog" database_id = "YOUR_ID_HERE"
Generate a password hash:
bun run auth:hash-password your-password-here
This outputs something like ADMIN_PASSWORD_HASH=abc123.... Set it as a Cloudflare secret:
# Set the password hash wrangler secret put ADMIN_PASSWORD_HASH # Paste the hash when prompted # Generate and set a session signing secret openssl rand -hex 32 wrangler secret put SESSION_SECRET # Paste the generated hex string
For local development, add both to .env.local:
ADMIN_PASSWORD_HASH=your-hash-here
SESSION_SECRET=your-random-hex-here
bun run db:migrate:local # Local development bun run db:migrate:remote # Production (after first deploy)
bun run dev # Next.js dev server (no D1 — admin/blog won't fully work) bun run preview # Cloudflare dev server with D1 (full stack)
bun run build bun run build:cf bun run deploy
- Visit
moikapy.dev/admin - Enter your password
- Click "+ New Post"
- Write your post in markdown, add tags, toggle "Published"
- Save — it's live
Works on any device — phone, tablet, laptop.
# Create a post (needs session cookie from login) curl -X POST https://moikapy.dev/api/posts \ -H "Content-Type: application/json" \ -b cookies.txt \ -d '{ "slug": "my-post", "title": "My Post", "excerpt": "A short description", "content": "# Hello\n\nThis is my post...", "tags": ["ai", "gaming"], "published": true }' # Update a post curl -X PATCH https://moikapy.dev/api/posts/my-post \ -H "Content-Type: application/json" \ -d '{"published": false}' # Delete a post curl -X DELETE https://moikapy.dev/api/posts/my-post
bun run nostr:generate-keys
Save the nsec to .env and add the npub to src/lib/config.ts.
NSEC=nsec1... bun run nostr:publish
| What | Where | Why |
|---|---|---|
| Blog posts | Cloudflare D1 (SQLite) | Write from anywhere, no git required |
| Images/media | Cloudflare R2 (optional) | S3-compatible, zero egress fees |
- Password: SHA-256 hashed, stored as Cloudflare secret (
ADMIN_PASSWORD_HASH) - Session: HMAC-SHA256 signed token, stored in httpOnly cookie (7 day expiry)
- Protected routes:
/admin/*(redirects to/admin/login) and/api/posts/*(returns 401) - Public routes: Blog pages, RSS, Nostr, login page
├── drizzle/ # D1 migration SQL
├── scripts/ # Auth, Nostr, key generation
├── src/
│ ├── app/
│ │ ├── admin/ # Admin panel (login + post editor)
│ │ ├── api/
│ │ │ ├── auth/ # Login/logout API
│ │ │ └── posts/ # REST API for posts CRUD
│ │ ├── blog/ # Public blog pages
│ │ └── feed/ # RSS + Nostr feed endpoints
│ ├── components/ # React components
│ ├── db/ # Drizzle ORM + D1 schema
│ └── lib/ # Config, auth, posts API
├── middleware.ts # Auth middleware (protects admin + API)
├── open-next.config.ts # Cloudflare OpenNext config
└── wrangler.toml # Cloudflare Workers config
| Command | Description |
|---|---|
bun run dev |
Next.js dev server |
bun run preview |
Cloudflare dev server (with D1) |
bun run build |
Production build |
bun run build:cf |
Build for Cloudflare |
bun run deploy |
Deploy to Cloudflare |
bun run db:create |
Create D1 database |
bun run db:migrate:local |
Run migrations locally |
bun run db:migrate:remote |
Run migrations on production D1 |
bun run auth:hash-password |
Hash a password for admin login |
bun run nostr:generate-keys |
Generate Nostr key pair |
bun run nostr:publish |
Publish posts to Nostr relays |