From d119bee7715a8a7350f0a43e95ef379e6574383c Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Sat, 15 Nov 2025 19:45:47 +0000 Subject: [PATCH] Create blog list API endpoint cgen-37fbdc1ff50a4c7faadbeb3b860d9384 --- api/blog/index.ts | 138 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 api/blog/index.ts diff --git a/api/blog/index.ts b/api/blog/index.ts new file mode 100644 index 00000000..c486ad40 --- /dev/null +++ b/api/blog/index.ts @@ -0,0 +1,138 @@ +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = process.env.SUPABASE_URL || ""; +const supabaseServiceRole = process.env.SUPABASE_SERVICE_ROLE || ""; + +const supabase = + supabaseUrl && supabaseServiceRole + ? createClient(supabaseUrl, supabaseServiceRole) + : null; + +interface GhostPost { + id: string; + title: string; + slug: string; + excerpt?: string; + html?: string; + feature_image?: string; + authors?: Array<{ name: string }>; + published_at: string; + reading_time?: number; + tags?: Array<{ name: string }>; +} + +interface GhostApiResponse { + posts: GhostPost[]; + meta?: { + pagination: { + page: number; + limit: number; + pages: number; + total: number; + }; + }; +} + +async function fetchFromGhost(limit: number = 50): Promise { + const ghostUrl = process.env.VITE_GHOST_API_URL; + const ghostKey = process.env.VITE_GHOST_CONTENT_API_KEY; + + if (!ghostUrl || !ghostKey) { + return []; + } + + try { + const params = new URLSearchParams({ + key: ghostKey, + limit: String(limit), + include: "authors,tags", + fields: + "id,title,slug,excerpt,html,feature_image,published_at,reading_time,authors,tags", + }); + + const url = `${ghostUrl}/ghost/api/content/posts/?${params.toString()}`; + const response = await fetch(url); + + if (!response.ok) { + console.error(`Ghost API error: ${response.statusText}`); + return []; + } + + const data: GhostApiResponse = await response.json(); + return ( + data.posts?.map((post) => ({ + id: post.id, + slug: post.slug, + title: post.title, + excerpt: post.excerpt || "", + body_html: post.html || "", + author: + post.authors && post.authors.length > 0 + ? post.authors[0].name + : "AeThex Team", + published_at: post.published_at, + read_time: post.reading_time || null, + category: + post.tags && post.tags.length > 0 ? post.tags[0].name : "General", + image: post.feature_image || null, + source: "ghost", + })) || [] + ); + } catch (error) { + console.error("Failed to fetch from Ghost:", error); + return []; + } +} + +async function fetchFromSupabase(limit: number = 50): Promise { + if (!supabase) { + return []; + } + + try { + const { data, error } = await supabase + .from("blog_posts") + .select( + "id,slug,title,excerpt,author,date,read_time,category,image,likes,comments,published_at,body_html", + ) + .order("published_at", { ascending: false }) + .limit(limit); + + if (error) { + console.error("Supabase query error:", error); + return []; + } + + return ( + data?.map((post) => ({ + ...post, + source: "supabase", + })) || [] + ); + } catch (error) { + console.error("Failed to fetch from Supabase:", error); + return []; + } +} + +export default async function handler(req: any, res: any) { + if (req.method !== "GET") { + return res.status(405).json({ error: "Method not allowed" }); + } + + try { + const limit = Math.min(parseInt(req.query.limit as string) || 50, 100); + + // Try Ghost first, then Supabase + let posts = await fetchFromGhost(limit); + + if (!posts.length) { + posts = await fetchFromSupabase(limit); + } + + return res.status(200).json(posts); + } catch (error) { + console.error("Blog API error:", error); + return res.status(500).json({ error: "Failed to fetch blog posts" }); + } +}