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:
+25
-1
@@ -1,10 +1,12 @@
|
||||
import { and, asc, desc, eq } from "drizzle-orm";
|
||||
import { and, asc, desc, eq, ne } from "drizzle-orm";
|
||||
import type { DrizzleD1Database } from "drizzle-orm/d1";
|
||||
import {
|
||||
blogKeywords,
|
||||
cases,
|
||||
contentItems,
|
||||
posts,
|
||||
siteSettings,
|
||||
type BlogKeyword,
|
||||
type CaseStudy,
|
||||
type ContentItem,
|
||||
type ContentKind,
|
||||
@@ -62,6 +64,13 @@ export async function getPostBySlug(db: Db, slug: string): Promise<Post | null>
|
||||
return post ?? null;
|
||||
}
|
||||
|
||||
export async function getKeywords(db: Db): Promise<BlogKeyword[]> {
|
||||
return db
|
||||
.select()
|
||||
.from(blogKeywords)
|
||||
.orderBy(asc(blogKeywords.createdAt), asc(blogKeywords.id));
|
||||
}
|
||||
|
||||
export async function getHomeData(db: Db): Promise<HomeData> {
|
||||
const [settings, services, features, steps, faqs, caseList] = await Promise.all([
|
||||
getSettings(db),
|
||||
@@ -102,3 +111,18 @@ export function formatDate(d: Date | null | undefined): string {
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
/** slug 撞咗就加 -2、-3…;selfId 用喺更新自己時排除自己。 */
|
||||
export async function uniqueSlug(db: Db, base: string, selfId?: string): Promise<string> {
|
||||
let candidate = base;
|
||||
for (let i = 2; i < 50; i++) {
|
||||
const rows = await db
|
||||
.select({ id: posts.id })
|
||||
.from(posts)
|
||||
.where(selfId ? and(eq(posts.slug, candidate), ne(posts.id, selfId)) : eq(posts.slug, candidate))
|
||||
.limit(1);
|
||||
if (rows.length === 0) return candidate;
|
||||
candidate = `${base}-${i}`;
|
||||
}
|
||||
return `${base}-${Date.now()}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user