Publish to Production

Posts are added manually — no automated import. A post is one row in blog_posts (D1 kagamix); a featured image is one or two objects in R2 kagamix-media, served from https://img.kagamix.com. Run every command from web/ in the kagamix repo unless noted.

Easiest way — npm run blog:add

Write the post as a .md file with frontmatter (use the Post Generator to build it), put the image next to it if any, then run one command from the repo root:

npm run blog:add -- path/to/my-post.md

Compresses the image to WebP (display rendition + ≤400px thumbnail), uploads both to R2 as media/<sha256>.webp, inserts the DB row, prints the post URL. Always writes to production — there's no --local flag for this script.

Field reference

FieldRequiredNotes
titleyesPost title
body_md / bodyyesMarkdown; raw HTML stripped on render
categoryyesGeneral, Language, Thoughts, Visual Novel
category2noOptional 2nd category, max 2 total
published_at / dateyesMust be YYYY-MM-DD — drives filters
statusnopublished or draft (default)
storage_keynoR2 key of full featured image
thumb_keynoR2 key of thumbnail

Manual: text-only post

Write the INSERT into a .sql file (avoids shell quoting pain with apostrophes/newlines):

INSERT INTO blog_posts (title, body_md, category, published_at, status) VALUES (
  'ชื่อบทความ',
  '# หัวข้อ

เนื้อหาย่อหน้าแรก',
  'General',
  '2026-07-04',
  'published'
);

Apply

npx wrangler d1 execute kagamix --remote --file new-post.sql

Manual: post with a featured image

Upload to R2

npx wrangler r2 object put kagamix-media/blog/my-post.webp \
  --file ./my-post.webp --content-type image/webp --remote

Confirm it serves: https://img.kagamix.com/blog/my-post.webp. Simplest: use the same file for both full image and thumbnail. Optional lighter thumbnail:

magick my-post.webp -resize 400x400 my-post-thumb.webp
npx wrangler r2 object put kagamix-media/blog/my-post-thumb.webp \
  --file ./my-post-thumb.webp --content-type image/webp --remote

Insert the row

INSERT INTO blog_posts
  (title, body_md, category, category2, published_at, status,
   storage_key, thumb_key, width, height)
VALUES (
  'รีวิวเกม ...',
  'เนื้อหา markdown ...',
  'Visual Novel',
  'Language',
  '2026-07-04',
  'published',
  'blog/my-post.webp',
  'blog/my-post-thumb.webp',
  1280, 720
);

Store storage_key/thumb_key as the R2 key only (blog/my-post.webp), not the full URL — the site prepends the domain.

Draft-then-publish workflow

npm run blog:drafts                      # list every draft
npm run blog:publish -- 284              # publish post 284
npm run blog:publish -- 284 "Visual Novel" Language   # publish + set categories

Omit status (or set draft) to stage privately first — drafts 404 on /blog/:id and never appear in listings. blog:publish validates categories.

Editing, unpublishing, deleting

npx wrangler d1 execute kagamix --remote \
  --command "UPDATE blog_posts SET title = 'ชื่อใหม่' WHERE id = 284"

npx wrangler d1 execute kagamix --remote \
  --command "UPDATE blog_posts SET status = 'draft' WHERE id = 284"

npx wrangler d1 execute kagamix --remote \
  --command "DELETE FROM blog_posts WHERE id = 284"

Deleting a row leaves its image in R2 (harmless). To remove it too:

npx wrangler r2 object delete kagamix-media/blog/my-post.webp --remote

Gotchas