solberg.is
Source for solberg.is, my personal blog and projects page.
The site includes the public blog, an authenticated Markdown editor and admin area, a CLI for managing posts, and the Kitty terminal-theme editor.
Stack
- TanStack Start with React Server Components and React 19
- Vite and the Cloudflare Vite plugin
- Cloudflare Workers and Cloudflare D1
- Kysely for typed queries, with
db-resultclassifying failures into Result tags - Markdown and MDX rendered with
safe-mdx, with server-side Shiki highlighting - Monaco for post editing
- Tailwind CSS v4
- Hono for the authenticated blog API
- Bun, TypeScript, oxlint, and oxfmt
Local development
Install dependencies and start Vite:
bun install bun run dev
Local configuration lives in .env, which is ignored by Git. The application expects:
NODE_ENV=development SITE_URL=http://localhost:5173 GITHUB_CLIENT_ID=... GITHUB_CLIENT_SECRET=... ONEDOLLARSTATS_API_KEY=...
The development app bypasses GitHub OAuth through /api/dev-auth, but still needs a sufficiently
long GITHUB_CLIENT_SECRET for the encrypted admin session. Use the URL printed by Vite if it
chooses a port other than 5173.
Local D1 database
To initialize a fresh local D1 database, apply the checked-in migrations in timestamp order:
for migration in migrations/*/migration.sql; do bunx wrangler d1 execute solberg-blog --local --file "$migration" done
After changing schema.ts, generate and inspect a new migration:
bun run generate:migration
Apply one generated migration locally:
bunx wrangler d1 execute solberg-blog \ --local \ --file migrations/<timestamp_name>/migration.sql
Apply it to production explicitly:
bunx wrangler d1 execute solberg-blog \ --remote \ --file migrations/<timestamp_name>/migration.sql \ --yes
Production schema changes must land before application code that reads the new schema. D1 schema work is not applied automatically by a Worker deployment.
Blog CLI
The Bun CLI in cli/blog.ts manages posts and notes through the authenticated
Hono API.
It targets the Vite development server at http://localhost:5173 by default. Set BLOG_API_URL
if Vite chooses another port, or use the production shortcut:
bun run blog:prod login bun run blog:prod list bun run blog:prod get how-i-use-claude-code
Login uses GitHub's device flow and stores the resulting app token in ~/.blog-cli-session.
Create a post
New posts are drafts unless --publish is supplied. The body can come from a file or stdin:
bun run blog:prod create \ --slug my-post \ --title "My Post" \ --body-file - \ --publish < post.md
Add --dry-run to inspect the post without creating it, or --diff to print the content before
creation.
Safely edit a post
get --json returns the post and its ETag. Pass that ETag back with --if-match to avoid
overwriting an edit made after the post was fetched:
bun run blog:prod get my-post --json > /tmp/my-post.json jq -r '.post.markdown' /tmp/my-post.json > /tmp/my-post.md # Edit /tmp/my-post.md, then update only if the original revision is still current. bun run blog:prod update my-post \ --body-file /tmp/my-post.md \ --if-match "$(jq -r '.etag' /tmp/my-post.json)" \ --diff
Other useful forms:
# Print only Markdown, suitable for a pipe or redirect. bun run blog:prod get my-post --body-only # Preview metadata and Markdown changes without writing. bun run blog:prod update my-post --body-file post.md --dry-run # Publish or return a post to draft state. bun run blog:prod update my-post --publish bun run blog:prod update my-post --unpublish # Back up every post to iCloud Documents on macOS. bun run blog:prod backup
Run bun run blog --help for the complete command and option list.
Checks
bun run test
bun run lint
bun run format:check
bun run buildbun run build runs the production Vite build and tsc --noEmit. Lefthook runs formatting and
type-aware linting before each commit.
Deployment
Pushing main triggers Cloudflare Workers Builds. A successful
Workers Builds: solberg-blog check is the production deployment signal.
git push origin main gh api "repos/jokull/blog/commits/$(git rev-parse HEAD)/check-runs" \ --jq '.check_runs[] | {name, status, conclusion, details_url}'
A failed build leaves the previous production deployment active. Manual deployment is also available:
bun run deploy
Repository map
src/routes— TanStack Start file routes and API endpointsapp— shared page, layout, editor, and admin components rendered by the current routescli— authenticated blog CLI and GitHub device-flow loginlib/api.ts— Hono post, note, category, and CLI-auth APIschema.tsandmigrations— D1 schema and forward migrationssrc/kitty— Kitty theme editor, parser, gallery, and RPC implementationcomponents— shared UI and data visualizations