Introduce a schema-first validation layer and an AI blog generation feature, plus one-command Cloudflare provisioning. - src/schemas/ holds Zod input schemas for post, content, case, settings, AI, and keywords; parseForm() in src/lib/form.ts validates FormData and returns per-field errors. - Migrate all admin POST handlers to parseForm, showing field-level errors and only redirecting once validation passes. - Add blog_keywords table and posts.focus_keyword (migration 0002); uniqueSlug() centralised in src/data/content.ts. - Add src/lib/ai.ts (OpenAI-compatible chat completions + optional Tavily research) and /admin/ai for AI settings, keyword queue, and draft generation. - Add scripts/setup.mjs (npm run setup) to provision D1/KV, set secrets, and optionally migrate, seed, and deploy. - Document AI secrets, Workers Builds deploy flow, and new schemas across README and AGENTS docs.
250 lines
10 KiB
Markdown
250 lines
10 KiB
Markdown
# 盈豐太陽能 — 結構優化 + AI Blog + 部署便利 (Design Spec)
|
||
|
||
- **日期**:2026-09-11
|
||
- **狀態**:已與客戶確認方向
|
||
- **背景**:現有網站(見 `2026-09-11-ying-fung-solar-design.md`)功能完整,但想改善三樣嘢:
|
||
1. 資料/表單處理散落手砌,欠型別化驗證層
|
||
2. 想有 AI 生成 Blog 文章功能(參考 webtemplate,但簡化、唔要 Google keyword search)
|
||
3. 部署到 Cloudflare 嘅流程手續多,想更方便
|
||
- **本 spec 取代**先前 spec 嘅部署一節;其餘架構決策不變。
|
||
|
||
---
|
||
|
||
## 1. 目標與非目標
|
||
|
||
### 目標
|
||
|
||
1. **輕量 schema-first 驗證層**:用 Zod 做「admin 表單輸入 + DB 寫入」嘅單一真相來源,得到 typed contract 好處,但零 codegen、零 API layer。
|
||
2. **AI Blog 生成**:後台一鍵生成完整 Markdown 草稿(DeepSeek via OpenAI 相容端點),可選 Tavily 上網研究,支援關鍵字佇列。
|
||
3. **部署便利**:一鍵本機 setup script + Cloudflare Workers Builds(Git push 自動 deploy)。
|
||
|
||
### 非目標 (Out of scope)
|
||
|
||
- 唔引入真 OpenAPI spec / codegen / 前後端分離 SPA(已評估唔啱一個一頁式官網)。
|
||
- AI **封面圖**生成(唔做)。
|
||
- **定時/排程**自動生成(唔做;只做後台手動觸發)。
|
||
- **Google Ads keyword discovery**(唔做)。
|
||
- 圖片上傳(維持以 URL 貼圖)。
|
||
- 雙語、聯絡表單(維持現狀)。
|
||
|
||
---
|
||
|
||
## 2. Thread 1 — 輕量 schema-first 驗證層
|
||
|
||
### 2.1 依賴
|
||
|
||
新增 `zod`(v4,`^4`)。
|
||
|
||
### 2.2 結構
|
||
|
||
```
|
||
src/schemas/
|
||
├─ post.ts postInput
|
||
├─ content.ts contentItemInput(kind enum 用 CONTENT_KINDS)
|
||
├─ case.ts caseInput
|
||
├─ settings.ts 由 SETTINGS_GROUPS 動態砌出可選 key 驗證
|
||
├─ ai.ts aiSettingsInput + AI 輸出格式 schema
|
||
├─ keyword.ts keywordInput
|
||
└─ index.ts barrel(集中 re-export schema 同 z.infer 型別)
|
||
src/lib/form.ts parseForm(formData, schema)
|
||
```
|
||
|
||
- Schema 係「表單輸入 + DB 寫入」嘅唯一真相來源;型別一律 `z.infer`。
|
||
- `settings-fields.ts` 繼續做 **UI metadata**(標籤、分組),Zod 只負責 **驗證**,唔重複定義。
|
||
- `parseForm` 介面:
|
||
```ts
|
||
type ParseResult<T> =
|
||
| { ok: true; data: T }
|
||
| { ok: false; errors: Record<string, string> };
|
||
function parseForm<T>(form: FormData, schema: ZodType<T>): ParseResult<T>;
|
||
```
|
||
錯誤以 `field -> 訊息` 形式回傳,admin 表單喺對應欄位下顯示;未對應欄位嘅錯誤顯示喺表單頂。
|
||
|
||
### 2.3 改動
|
||
|
||
- `pages/admin/post/[id].astro`、`content/[kind].astro`、`cases.astro`、`settings.astro`:POST handler 改用 `parseForm` 取代手砌 `String(form.get(...))` 同手寫 `if (!title)`。
|
||
- `data/content.ts`:改用 `src/schemas` 匯出嘅型別(如 `NewPost` 形狀),與 schema 對齊。
|
||
- 排序(`up`/`down`)、`delete` 等非表單欄位動作維持現狀(唔屬 schema 範圍)。
|
||
|
||
### 2.4 唔做
|
||
|
||
- 唔改前台元件介面、唔改 DB schema(純驗證層)。
|
||
- 唔引入 `openapi.json`、唔引入 client codegen。
|
||
|
||
---
|
||
|
||
## 3. Thread 2 — AI Blog 生成
|
||
|
||
### 3.1 資料模型(改 `src/db/schema.ts` → `npm run db:generate`)
|
||
|
||
**`posts` 加欄位:**
|
||
|
||
| 欄位 | 型別 | 說明 |
|
||
|---|---|---|
|
||
| `focusKeyword` | text nullable | 生成時帶入嘅焦點關鍵字(SEO 用) |
|
||
|
||
**新表 `blog_keywords`:**
|
||
|
||
| 欄位 | 型別 | 說明 |
|
||
|---|---|---|
|
||
| `id` | text PK | UUID |
|
||
| `keyword` | text notNull | 關鍵字 / 主題 |
|
||
| `status` | text enum `pending` \| `generated` \| `skipped`,預設 `pending` | 狀態 |
|
||
| `createdAt` | integer (timestamp_ms) | |
|
||
| `usedAt` | integer (timestamp_ms) nullable | 生成時間 |
|
||
| `postId` | text nullable | 生成出嚟嘅文章 id(可選連結) |
|
||
|
||
索引:`idx_keywords_status(status)`。
|
||
|
||
> 比 webtemplate 減去 Google Ads 專屬欄位(`avg_monthly_searches`、`competition`、`source`)。
|
||
|
||
### 3.2 Secrets 與設定
|
||
|
||
**Secrets(`wrangler secret` / `.dev.vars`,加入 `AppEnv`):**
|
||
|
||
- `AI_API_KEY`
|
||
- `TAVILY_API_KEY`
|
||
|
||
**設定(D1 `site_settings`,後台可改):**
|
||
|
||
| Key | 預設 | 說明 |
|
||
|---|---|---|
|
||
| `ai_enabled` | `1` | 總開關 |
|
||
| `ai_base_url` | `https://api.deepinfra.com/v1/openai` | OpenAI 相容端點 |
|
||
| `ai_chat_model` | `deepseek-ai/DeepSeek-V3-0324` | 模型 |
|
||
| `ai_context_prompt` | `""` | 寫作風格 / 語氣 |
|
||
| `ai_business_context` | seed 由公司資料砌 | 公司背景(grounding) |
|
||
| `ai_web_search_enabled` | `0` | Tavily 研究開關 |
|
||
| `ai_web_search_max_results` | `5` | Tavily 結果數 |
|
||
|
||
### 3.3 生成流程(`src/lib/ai.ts`)
|
||
|
||
```
|
||
POST /admin/ai
|
||
action = "generate-topic"(自由輸入 topic)
|
||
| "generate-next"(攞下一個 pending keyword)
|
||
│
|
||
▼
|
||
generateBlogPost({ db, env, topic })
|
||
1. 讀 ai_* 設定;檢查 ai_enabled + AI_API_KEY
|
||
2. 若係 generate-next → 攞最舊一個 pending keyword,mark 佢 "generated" + usedAt + postId;
|
||
generate-topic(自由輸入)唔會寫入佇列
|
||
3. 若 ai_web_search_enabled + TAVILY_API_KEY → Tavily search → research_brief
|
||
4. 組 system prompt:
|
||
ai_context_prompt + ai_business_context + research_brief
|
||
+ SEO/GEO 硬規則 + 輸出格式合約
|
||
5. 呼叫 DeepSeek chat completions(OpenAI 相容 /chat/completions)
|
||
6. 解析輸出(TITLE / CONTENT / META_DESC),用 Zod 驗證格式;失敗回錯誤
|
||
7. slugify + uniqueSlug;autoExcerpt 補 excerpt
|
||
8. 插入 posts(status = "draft"、focusKeyword = topic)→ 回 post id
|
||
│
|
||
▼
|
||
redirect 去 /admin/post/<id> 覆核
|
||
```
|
||
|
||
**執行方式:同步 await。** POST handler 直接等生成完成(約 10–40 秒)再 redirect。原因:最簡單、admin 流程直觀,符合「唔使太複雜」;Workers 對 outbound fetch 等待唔計 CPU limit。若日後遇到超時,先改 `Astro.locals.cfContext.waitUntil` + 狀態欄(已知 adapter 有注入 `locals.cfContext`)。
|
||
|
||
**提示合約:**
|
||
|
||
- 輸出用固定分隔格式(TITLE / CONTENT / META_DESC),比純 JSON 容忍度更高。
|
||
- 內建規則:繁體中文(廣東話書面)、800–1200 字、H2/H3 結構、關鍵字放標題及首段、meta ≤ 160 字、GEO(summary-first + Q&A)。
|
||
|
||
### 3.4 Admin UI(新頁 `/admin/ai`)
|
||
|
||
一頁過包含:
|
||
|
||
1. **AI 設定表單**(`ai_*` keys,密碼類 secret 只顯示「已設定 / 未設定」,唔顯示值)
|
||
2. **關鍵字佇列**:新增、刪除、標記 skipped;顯示狀態
|
||
3. **生成掣**:`generate-topic`(自由輸入)+ `generate-next`(下一個 pending)
|
||
|
||
- 生成掣用 POST 表單 + 提交時 JS 顯示「生成中…」(純漸進增強,唔引入框架)。
|
||
- `/admin` 首頁加入口連結去 `/admin/ai`。
|
||
|
||
### 3.5 唔做
|
||
|
||
- AI 圖、排程、Google Ads、多語。
|
||
|
||
---
|
||
|
||
## 4. Thread 3 — 部署便利
|
||
|
||
### 4.1 一鍵本機 setup — `scripts/setup.mjs`(`npm run setup`)
|
||
|
||
互動式 Node script,逐步做:
|
||
|
||
1. 檢查 `wrangler whoami`(未登入提示 `npm run login`)。
|
||
2. `wrangler d1 create yingfung-solar-db`(若已存在/已填 id 則略過)→ 解析 output 拎 `database_id`。
|
||
3. `wrangler kv namespace create CACHE` → 解析 id。
|
||
4. 將 id 寫入 `wrangler.jsonc`:以字串替換 `PASTE_D1_DATABASE_ID_HERE` / `PASTE_KV_NAMESPACE_ID_HERE`(**保留 JSONC 註解同行距**,唔用 JSON.parse/stringify)。
|
||
5. 提示輸入後台密碼 → `wrangler secret put ADMIN_PASSWORD`。
|
||
6. 套 migration + seed(remote)。
|
||
7. 可選:`npm run build` + `npx wrangler deploy`。
|
||
|
||
- 每一步 idempotent(重跑唔會整爛已設定嘅嘢)。
|
||
- 唔會自動改 `astro.config.mjs` 嘅 `site`(提示用戶手動改)。
|
||
|
||
### 4.2 Cloudflare Workers Builds(Git push 自動 deploy)
|
||
|
||
- 喺 Cloudflare Dashboard → Workers & Pages → 連接 Git repo。
|
||
- **Build command**:`npm run build`
|
||
- **Deploy command**:`npx wrangler deploy`
|
||
- Secrets(`ADMIN_PASSWORD`、`AI_API_KEY`、`TAVILY_API_KEY`)喺 Dashboard 設定。
|
||
- **Migration 唔入 CI**:改 schema 時手動跑 `npm run db:migrate`(避免每次 push 亂郁 DB)。README 寫明呢個分工。
|
||
|
||
### 4.3 唔做
|
||
|
||
- 唔加 GitHub Actions。
|
||
- 唔自動化 Cloudflare Dashboard 連 repo(需要人手授權)。
|
||
|
||
---
|
||
|
||
## 5. 分階段實作
|
||
|
||
| 階段 | 內容 | 完成標準 |
|
||
|---|---|---|
|
||
| **Phase 1** | Zod schema-first 驗證層 | `npm run build` 過;admin 表單驗證錯誤正常顯示 |
|
||
| **Phase 2** | AI Blog + 關鍵字佇列 | `npm run db:generate` 出 migration;`npm run dev` 實測生成草稿 |
|
||
| **Phase 3** | setup script + Workers Builds 文件 | 新 clone 行 `npm run setup` 可完成 provision;README 更新 |
|
||
|
||
三階段相對獨立,逐階段完成並 `npm run build`。
|
||
|
||
---
|
||
|
||
## 6. 風險與對策
|
||
|
||
| 風險 | 對策 |
|
||
|---|---|
|
||
| Zod 加入 admin POST 改動面大 | 逐個 admin POST handler 遷移;`parseForm` 提供一致 fallback,build 驗證 |
|
||
| LLM 輸出格式唔穩定 | 用容錯分隔格式 + Zod 驗證,失敗回明確錯誤、唔插入壞資料 |
|
||
| 同步生成請求時間長 | Workers I/O 等待唔計 CPU;若超時先改 `cfContext.waitUntil` + 狀態欄 |
|
||
| `wrangler.jsonc` 有註解,程式改寫易壞 | 只做精準字串替換,唔 parse 整個檔 |
|
||
| Workers Builds 跑 migration 風險 | 明確唔放 CI,migration 手動執行 |
|
||
| 外部 AI 供應商停机 | 生成失敗只回錯誤,唔影響網站;設定可改 `ai_base_url`/model |
|
||
|
||
---
|
||
|
||
## 7. 驗證
|
||
|
||
- 每階段:`npm run build`(唯一 build/type 檢查)。
|
||
- Phase 2:`npm run db:generate`;`npm run dev` 手動生成一篇草稿並覆核。
|
||
- Phase 3:`npm run setup` 喺未 provision 環境 dry-run(或已有環境重跑,確認 idempotent)。
|
||
|
||
---
|
||
|
||
## 8. 文件同步
|
||
|
||
- `AGENTS.md`(root):指令、部署流程、AI secret。
|
||
- `src/AGENTS.md`:新 `schemas/`、`/admin/ai` 路由、AI 執行方式。
|
||
- `src/db/AGENTS.md`:新 `blog_keywords` 表、`posts.focusKeyword`。
|
||
- `src/data/AGENTS.md`:關鍵字查詢、型別來源改為 schemas。
|
||
- `src/lib/AGENTS.md`:`ai.ts`、`form.ts`、`env.ts` 新 secret。
|
||
- `README.md`:setup script、Workers Builds、AI 設定步驟。
|
||
- `migrations/AGENTS.md`:新 migration。
|
||
|
||
---
|
||
|
||
## 9. 待客戶提供
|
||
|
||
- `AI_API_KEY`(DeepInfra 或同類 OpenAI 相容供應商)。
|
||
- `TAVILY_API_KEY`(若用上網研究)。
|