# 盈豐太陽能 — 結構優化 + 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 = | { ok: true; data: T } | { ok: false; errors: Record }; function parseForm(form: FormData, schema: ZodType): ParseResult; ``` 錯誤以 `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/ 覆核 ``` **執行方式:同步 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`(若用上網研究)。