Document R2 image upload in README and mark plan complete

This commit is contained in:
2026-09-12 10:16:27 +08:00
parent 00502e5e52
commit f6b885efe6
2 changed files with 59 additions and 56 deletions
+8 -5
View File
@@ -1,6 +1,6 @@
# 盈豐太陽能工程有限公司 — 官網(村屋太陽能)
**Astro 7 + React 19 + Chakra UI v3 + Cloudflare Workers + D1 + Drizzle。全部部署喺 Cloudflare,零維運。**
**Astro 7 + React 19 + Chakra UI v3 + Cloudflare Workers + D1 + KV + R2 + Drizzle。全部部署喺 Cloudflare,零維運。**
一頁式官網(村屋太陽能)+ Blog + 內容管理後台。
@@ -22,6 +22,7 @@
| Adapter | `@astrojs/cloudflare`(一個 Worker |
| DB | Cloudflare D1SQLite |
| ORM | Drizzle |
| 物件儲存 | Cloudflare R2(後台圖片上傳,binding `MEDIA` |
| Auth | HMAC-signed cookie`src/lib/auth.ts` |
| Markdown | `marked` |
| Icons | `lucide-react` |
@@ -53,7 +54,7 @@ npm run dev
```bash
npm run login # 瀏覽器授權
npm run setup # 建立 D1 / KV、寫入 wrangler.jsonc、設 secret、套 migration + seed
npm run setup # 建立 D1 / KV / R2、寫入 wrangler.jsonc、設 secret、套 migration + seed
```
`npm run setup` 會逐步問你:要唔要設定後台密碼、要唔要套 migration/seed、要唔要 build + deploy。若 `astro.config.mjs``site` 仍然係 `https://example.com`,會跳過 build + deploy 並提醒你改。
@@ -91,7 +92,7 @@ npm run deploy
1.`/admin`,輸入密碼登入
2. **內容總覽** → 服務 / 特色 / 流程 / 常見問題 / 完成案例 / 網站設定 / AI 生成
3. 每個項目可新增、修改、刪除、用 ↑↓ 排序、「已發布 / 草稿」控制前台顯示
4. 圖片:直接貼圖片 URL(暫時;之後可換成上傳)
4. 圖片:可揀檔即時上傳去 R2(瀏覽器端先縮圖轉 WebP),亦可貼圖片 URL
5. **AI 生成**:設定供應商同關鍵字佇列,生成草稿後去文章編輯頁覆核發布
## 結構
@@ -110,9 +111,11 @@ src/
├─ pages/
│ ├─ index.astro SSR 首頁
│ ├─ blog/… Blog
│ ├─ admin/… 後台(含 admin/ai
│ ├─ admin/… 後台(含 admin/ai、admin/upload 上傳 endpoint
│ ├─ media/[...key].ts R2 圖片出圖 endpoint
│ └─ sitemap.xml.ts / robots.txt.ts
├─ lib/{auth,db,env,form,ai,markdown}.ts
├─ components/admin/ImageField.astro 後台圖片欄(URL + 上傳 + 預覽)
├─ lib/{auth,db,env,form,media,ai,markdown}.ts
migrations/
├─ 0000_init.sql … Drizzle migrations
└─ seed.sql 種子資料(用 db:seed 另外執行)
@@ -1,6 +1,6 @@
# R2 Image Upload Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
**Goal:** 後台 4 個圖片欄位(`hero_image``og_image`、案例 `imageUrl`、文章 `coverImage`)支援揀檔即時上傳去 Cloudflare R2,同時保留貼 URL。
@@ -44,7 +44,7 @@
- Modify: `scripts/setup.mjs`
- Modify: `src/lib/env.ts`
- [ ] **Step 1: `wrangler.jsonc` 加 R2 binding**
- [x] **Step 1: `wrangler.jsonc` 加 R2 binding**
`"kv_namespaces": [...]` 之後(結尾 `]` 加逗號)加:
@@ -104,7 +104,7 @@
}
```
- [ ] **Step 2: `package.json` 加 `r2:create`**
- [x] **Step 2: `package.json` 加 `r2:create`**
`"kv:create"` 之後加一行:
@@ -113,7 +113,7 @@
"r2:create": "wrangler r2 bucket create yingfung-solar-media",
```
- [ ] **Step 3: `src/lib/env.ts` 加 `MEDIA`**
- [x] **Step 3: `src/lib/env.ts` 加 `MEDIA`**
`AppEnv` 改成:
@@ -128,7 +128,7 @@ export type AppEnv = {
};
```
- [ ] **Step 4: `scripts/setup.mjs` 加建 R2 bucket 步驟**
- [x] **Step 4: `scripts/setup.mjs` 加建 R2 bucket 步驟**
喺檔案上方 constant 區(`const KV_PLACEHOLDER = ...` 之後)加:
@@ -155,7 +155,7 @@ const R2_BUCKET = "yingfung-solar-media";
}
```
- [ ] **Step 5: 重新產生 binding 型別**
- [x] **Step 5: 重新產生 binding 型別**
Run: `npm run types`
Expected: 成功,`worker-configuration.d.ts``MEDIA: R2Bucket;` 出現喺 `Env`
@@ -163,12 +163,12 @@ Expected: 成功,`worker-configuration.d.ts` 內 `MEDIA: R2Bucket;` 出現喺
Run: `rg -n "MEDIA: R2Bucket" worker-configuration.d.ts`
Expected: 有一行 match(若 `wrangler types` 讀唔到 R2 binding,手動加都唔會影響 build,但正常會出)。
- [ ] **Step 6: Build 驗證**
- [x] **Step 6: Build 驗證**
Run: `npm run build`
Expected: 完成,冇 error。
- [ ] **Step 7: Commit**
- [x] **Step 7: Commit**
```bash
git add wrangler.jsonc package.json scripts/setup.mjs src/lib/env.ts worker-configuration.d.ts
@@ -182,7 +182,7 @@ git commit -m "Add R2 MEDIA binding and setup script"
**Files:**
- Create: `src/lib/media.ts`
- [ ] **Step 1: 建立 `src/lib/media.ts`**
- [x] **Step 1: 建立 `src/lib/media.ts`**
```ts
import type { AppEnv } from "./env";
@@ -215,12 +215,12 @@ export async function deleteMedia(
}
```
- [ ] **Step 2: Build 驗證**
- [x] **Step 2: Build 驗證**
Run: `npm run build`
Expected: 完成,冇 error。
- [ ] **Step 3: Commit**
- [x] **Step 3: Commit**
```bash
git add src/lib/media.ts
@@ -235,7 +235,7 @@ git commit -m "Add media helper for R2 keys and cleanup"
- Create: `src/pages/admin/upload.ts`
- Create: `src/pages/media/[...key].ts`
- [ ] **Step 1: 建立 `src/pages/admin/upload.ts`**
- [x] **Step 1: 建立 `src/pages/admin/upload.ts`**
```ts
import type { APIRoute } from "astro";
@@ -300,7 +300,7 @@ export const POST: APIRoute = async ({ request }) => {
};
```
- [ ] **Step 2: 建立 `src/pages/media/[...key].ts`**
- [x] **Step 2: 建立 `src/pages/media/[...key].ts`**
```ts
import type { APIRoute } from "astro";
@@ -329,12 +329,12 @@ export const GET: APIRoute = async ({ params, request }) => {
};
```
- [ ] **Step 3: Build 驗證**
- [x] **Step 3: Build 驗證**
Run: `npm run build`
Expected: 完成,冇 error,而且 `dist/` 唔會 build 出 `/media` 靜態頁(endpoint 係 SSR)。
- [ ] **Step 4: 手動測試 endpoints**
- [x] **Step 4: 手動測試 endpoints**
Run: `npm run dev`(另一個 terminal
Expected:
@@ -350,7 +350,7 @@ console.log(await (await fetch("/admin/upload", { method: "POST", body: fd })).j
Expected: `{ ok: true, url: "/media/cases/<uuid>.webp" }`,跟住開嗰個 url 有 responsebody 係嗰 4 bytes)。
- [ ] **Step 5: Commit**
- [x] **Step 5: Commit**
```bash
git add src/pages/admin/upload.ts "src/pages/media/[...key].ts"
@@ -365,7 +365,7 @@ git commit -m "Add R2 upload and media serving endpoints"
- Create: `src/components/admin/ImageField.astro`
- Modify: `src/layouts/AdminLayout.astro`
- [ ] **Step 1: 建立 `src/components/admin/ImageField.astro`**
- [x] **Step 1: 建立 `src/components/admin/ImageField.astro`**
```astro
---
@@ -398,7 +398,7 @@ const { name, label, value = "", scope, id = name } = Astro.props;
</div>
```
- [ ] **Step 2: `AdminLayout.astro` 加 client script**
- [x] **Step 2: `AdminLayout.astro` 加 client script**
`</main>` 之後、`</body>` 之前加:
@@ -472,7 +472,7 @@ const { name, label, value = "", scope, id = name } = Astro.props;
</script>
```
- [ ] **Step 3: `AdminLayout.astro` 加樣式**
- [x] **Step 3: `AdminLayout.astro` 加樣式**
`<style is:global>``.row { ... }` 之前加:
@@ -485,12 +485,12 @@ const { name, label, value = "", scope, id = name } = Astro.props;
.image-preview[hidden] { display: none; }
```
- [ ] **Step 4: Build 驗證**
- [x] **Step 4: Build 驗證**
Run: `npm run build`
Expected: 完成,冇 error。
- [ ] **Step 5: Commit**
- [x] **Step 5: Commit**
```bash
git add src/components/admin/ImageField.astro src/layouts/AdminLayout.astro
@@ -505,7 +505,7 @@ git commit -m "Add ImageField component and client-side upload script"
- Modify: `src/data/settings-fields.ts`
- Modify: `src/pages/admin/settings.astro`
- [ ] **Step 1: `settings-fields.ts` 加 `image` 型別**
- [x] **Step 1: `settings-fields.ts` 加 `image` 型別**
將第一行改成:
@@ -523,7 +523,7 @@ export type FieldType = "text" | "textarea" | "url" | "image";
{ key: "og_image", label: "分享圖片 (OG image)", type: "image" },
```
- [ ] **Step 2: `settings.astro` 加 import 同刪舊邏輯**
- [x] **Step 2: `settings.astro` 加 import 同刪舊邏輯**
將 frontmatter 改成:
@@ -584,7 +584,7 @@ const settings = await getSettings(db);
---
```
- [ ] **Step 3: `settings.astro` 欄位 render 加 ImageField**
- [x] **Step 3: `settings.astro` 欄位 render 加 ImageField**
將 template 內 `SETTINGS_GROUPS.map((group) => ( ... ))` 的欄位 map 改成(其餘不變):
@@ -621,17 +621,17 @@ const settings = await getSettings(db);
))}
```
- [ ] **Step 4: Build 驗證**
- [x] **Step 4: Build 驗證**
Run: `npm run build`
Expected: 完成,冇 error。
- [ ] **Step 5: 手動測試**
- [x] **Step 5: 手動測試**
Run: `npm run dev`
Expected: `/admin/settings` 的「首頁 Hero」同「SEO 設定」入面,主視覺圖片同 OG 圖變咗有檔案選擇 + 預覽;上傳後 URL 欄填 `/media/settings/<uuid>.webp`;儲存後前台首頁圖片更新;再換一張,舊 `/media/...` 變 404。
- [ ] **Step 6: Commit**
- [x] **Step 6: Commit**
```bash
git add src/data/settings-fields.ts src/pages/admin/settings.astro
@@ -645,7 +645,7 @@ git commit -m "Add image upload to site settings with old-file cleanup"
**Files:**
- Modify: `src/pages/admin/cases.astro`
- [ ] **Step 1: 加 import 同 `env`**
- [x] **Step 1: 加 import 同 `env`**
喺 import 區加:
@@ -661,7 +661,7 @@ const env = getEnv();
const db = getDb(env.DB);
```
- [ ] **Step 2: save 時比對並刪舊圖**
- [x] **Step 2: save 時比對並刪舊圖**
`if (action === "add" || action === "save")` 內嘅 `} else {`(更新分支)改成:
@@ -688,7 +688,7 @@ const db = getDb(env.DB);
}
```
- [ ] **Step 3: delete 時刪圖**
- [x] **Step 3: delete 時刪圖**
`} else if (action === "delete") { ... }` 改成:
@@ -701,7 +701,7 @@ const db = getDb(env.DB);
}
```
- [ ] **Step 4: 新增表單改用 ImageField**
- [x] **Step 4: 新增表單改用 ImageField**
將新增表單內:
@@ -720,7 +720,7 @@ const db = getDb(env.DB);
</div>
```
- [ ] **Step 5: 每張卡改用 ImageField 並移除舊預覽**
- [x] **Step 5: 每張卡改用 ImageField 並移除舊預覽**
將每張卡內:
@@ -753,17 +753,17 @@ const db = getDb(env.DB);
)}
```
- [ ] **Step 6: Build 驗證**
- [x] **Step 6: Build 驗證**
Run: `npm run build`
Expected: 完成,冇 error。
- [ ] **Step 7: 手動測試**
- [x] **Step 7: 手動測試**
Run: `npm run dev`
Expected: `/admin/cases` 新增同每張卡都有上傳;上傳後前台「完成案例」顯示新圖;換圖後舊圖 404;刪除案例後其圖 404。
- [ ] **Step 8: Commit**
- [x] **Step 8: Commit**
```bash
git add src/pages/admin/cases.astro
@@ -777,7 +777,7 @@ git commit -m "Add image upload to cases with cleanup"
**Files:**
- Modify: `src/pages/admin/post/[id].astro`
- [ ] **Step 1: 加 import 同 `env`**
- [x] **Step 1: 加 import 同 `env`**
喺 import 區加:
@@ -793,7 +793,7 @@ const env = getEnv();
const db = getDb(env.DB);
```
- [ ] **Step 2: 刪文時刪封面**
- [x] **Step 2: 刪文時刪封面**
將:
@@ -814,7 +814,7 @@ const db = getDb(env.DB);
}
```
- [ ] **Step 3: 更新時比對並刪舊封面**
- [x] **Step 3: 更新時比對並刪舊封面**
`} else {`(更新分支)內、`await db.update(posts)` 之前加:
@@ -838,7 +838,7 @@ const db = getDb(env.DB);
...
```
- [ ] **Step 4: 封面欄改用 ImageField**
- [x] **Step 4: 封面欄改用 ImageField**
將:
@@ -867,17 +867,17 @@ const db = getDb(env.DB);
/>
```
- [ ] **Step 5: Build 驗證**
- [x] **Step 5: Build 驗證**
Run: `npm run build`
Expected: 完成,冇 error。
- [ ] **Step 6: 手動測試**
- [x] **Step 6: 手動測試**
Run: `npm run dev`
Expected: `/admin/post/new` 同上傳封面;發布後 Blog 列表/文章頁顯示新封面;換封面舊圖 404;刪文封面 404。
- [ ] **Step 7: Commit**
- [x] **Step 7: Commit**
```bash
git add "src/pages/admin/post/[id].astro"
@@ -894,7 +894,7 @@ git commit -m "Add cover image upload to blog posts with cleanup"
- Modify: `src/data/AGENTS.md`
- Modify: `AGENTS.md`
- [ ] **Step 1: 更新 `src/AGENTS.md`**
- [x] **Step 1: 更新 `src/AGENTS.md`**
Admin 段「- **圖片**:一律以 URL 字串處理(暫無上傳)。」改成:
@@ -908,7 +908,7 @@ Admin 段「- **圖片**:一律以 URL 字串處理(暫無上傳)。」改
- **圖片 endpoints**`/admin/upload`POST,上傳去 R2 `MEDIA`);`/media/[...key]`GET,讀 R2 出圖,設 `Cache-Control: public, max-age=31536000, immutable` 同 ETag304)。
```
- [ ] **Step 2: 更新 `src/lib/AGENTS.md`**
- [x] **Step 2: 更新 `src/lib/AGENTS.md`**
Ownership 段改成「擁有 `src/lib/` 七個檔案:`env.ts``db.ts``auth.ts``form.ts``media.ts``ai.ts``markdown.ts`。」
@@ -920,7 +920,7 @@ Ownership 段改成「擁有 `src/lib/` 七個檔案:`env.ts`、`db.ts`、`aut
- **`media.ts`**`MEDIA_PREFIX``/media/`)、`mediaUrl(key)``mediaKey(url)``deleteMedia(env, url)``deleteMedia` 只處理 `/media/` 開頭嘅值,外部 URL/空值唔理、唔 throw。
```
- [ ] **Step 3: 更新 `src/data/AGENTS.md`**
- [x] **Step 3: 更新 `src/data/AGENTS.md`**
`settings-fields.ts` 相關:Local Contracts 內「**設定欄位**」bullet 補一句:
@@ -928,7 +928,7 @@ Ownership 段改成「擁有 `src/lib/` 七個檔案:`env.ts`、`db.ts`、`aut
- **圖片欄位**`FieldType``"image"``hero_image` / `og_image` 用呢個型別,`/admin/settings` 會 render `ImageField`(可上傳去 R2)。
```
- [ ] **Step 4: 更新根 `AGENTS.md`**
- [x] **Step 4: 更新根 `AGENTS.md`**
「關鍵慣例與陷阱」內補一條(放喺環境變數 / 資料層附近):
@@ -938,12 +938,12 @@ Ownership 段改成「擁有 `src/lib/` 七個檔案:`env.ts`、`db.ts`、`aut
`scripts/` 是 project-level files owned by root,唔使另加 Child DOX。
- [ ] **Step 5: Build 驗證**
- [x] **Step 5: Build 驗證**
Run: `npm run build`
Expected: 完成,冇 error。
- [ ] **Step 6: Commit**
- [x] **Step 6: Commit**
```bash
git add AGENTS.md src/AGENTS.md src/lib/AGENTS.md src/data/AGENTS.md
@@ -956,12 +956,12 @@ git commit -m "Update DOX docs for R2 image upload"
**Files:** 無(只驗證)
- [ ] **Step 1: 全量 build**
- [x] **Step 1: 全量 build**
Run: `npm run build`
Expected: 成功。
- [ ] **Step 2: 端到端手動檢查(`npm run dev`**
- [x] **Step 2: 端到端手動檢查(`npm run dev`**
逐項確認:
@@ -974,7 +974,7 @@ Expected: 成功。
7. 上傳 SVG 或 >8MB 檔 → 顯示中文錯誤、唔會寫入。
8. 冇 JS(停用 JavaScript)→ URL 欄仍可手動貼,主表單可儲存。
- [ ] **Step 3: 確認 git 狀態乾淨**
- [x] **Step 3: 確認 git 狀態乾淨**
Run: `git status --short`
Expected: 冇未 commit 嘅改動(或只有預期內嘅檔案)。