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
| Field | Required | Notes |
|---|---|---|
title | yes | Post title |
body_md / body | yes | Markdown; raw HTML stripped on render |
category | yes | General, Language, Thoughts, Visual Novel |
category2 | no | Optional 2nd category, max 2 total |
published_at / date | yes | Must be YYYY-MM-DD — drives filters |
status | no | published or draft (default) |
storage_key | no | R2 key of full featured image |
thumb_key | no | R2 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
- SQL apostrophes: inside a single-quoted SQL string, write
'as''(e.g.'don''t'). The.sql-file approach avoids most shell-quoting pain. published_atmust beYYYY-MM-DD— the year/month filters slice this string, so a full timestamp or other format breaks filtering.- Test locally first if unsure: same commands with
--local, then view viawrangler devbefore running--remote. - Categories are fixed and in canonical display order regardless of DB order: General · Language · Thoughts · Visual Novel. Max 2 per post.
- Changes show up within ~5 minutes (edge cache).
idis auto-assigned — never set it, and never setcreated_atorfb_post_id(leftover, unused).