GitHub

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-result classifying 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 build

bun 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 endpoints
  • app — shared page, layout, editor, and admin components rendered by the current routes
  • cli — authenticated blog CLI and GitHub device-flow login
  • lib/api.ts — Hono post, note, category, and CLI-auth API
  • schema.ts and migrations — D1 schema and forward migrations
  • src/kitty — Kitty theme editor, parser, gallery, and RPC implementation
  • components — shared UI and data visualizations

Read the original on github.com ↗