Add Zod validation layer and AI blog generation
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.
This commit is contained in:
@@ -12,7 +12,10 @@ const links = [
|
||||
{ href: "/admin/content/faq", label: "常見問題" },
|
||||
{ href: "/admin/cases", label: "案例" },
|
||||
{ href: "/admin/settings", label: "設定" },
|
||||
{ href: "/admin/ai", label: "AI 生成" },
|
||||
];
|
||||
const isActive = (href: string) =>
|
||||
href === "/admin" ? path === "/admin" : path === href || path.startsWith(href + "/");
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
@@ -33,7 +36,7 @@ const links = [
|
||||
<nav>
|
||||
{
|
||||
links.map((l) => (
|
||||
<a href={l.href} class={path === l.href || path.startsWith(l.href + "/") ? "active" : ""}>
|
||||
<a href={l.href} class={isActive(l.href) ? "active" : ""}>
|
||||
{l.label}
|
||||
</a>
|
||||
))
|
||||
@@ -107,6 +110,8 @@ const links = [
|
||||
.badge.published { background: #e1f5ee; color: #0f6e56; }
|
||||
.badge.draft { background: #faeeda; color: #854f0b; }
|
||||
.muted { color: #8a9793; font-size: 13px; }
|
||||
.field-error { color: #a32d2d; font-size: 12.5px; font-weight: 600; margin-left: 8px; }
|
||||
.err { color: #a32d2d; font-size: 13px; font-weight: 600; }
|
||||
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||
@media (max-width: 640px) { .grid2 { grid-template-columns: 1fr; } }
|
||||
.row { display: grid; gap: 12px; grid-template-columns: 1fr; }
|
||||
|
||||
Reference in New Issue
Block a user