diff --git a/src/app/api/public/posts/route.ts b/src/app/api/public/posts/route.ts new file mode 100644 index 0000000..e380f15 --- /dev/null +++ b/src/app/api/public/posts/route.ts @@ -0,0 +1,47 @@ +import { NextResponse } from "next/server"; +import { getLatestImportedArticles } from "@/lib/articles/legacy-source"; + +export const revalidate = 300; + +const MAX_LIMIT = 200; +const DEFAULT_LIMIT = 100; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const limit = Math.min( + Math.max(parseInt(searchParams.get("limit") || `${DEFAULT_LIMIT}`, 10) || DEFAULT_LIMIT, 1), + MAX_LIMIT, + ); + const page = Math.max(parseInt(searchParams.get("page") || "0", 10) || 0, 0); + const offset = page * limit; + + const { items, total } = await getLatestImportedArticles(limit, offset); + + const payload = items.map((a) => ({ + title: a.title, + excerpt: a.excerpt, + category: a.category, + date: a.date, + author: a.author, + authorSlug: a.authorSlug, + slug: a.slug, + image: a.image, + alt: a.alt, + source: "imported" as const, + })); + + return NextResponse.json( + { + items: payload, + page, + limit, + total, + pages: Math.ceil(total / limit), + }, + { + headers: { + "Cache-Control": "public, s-maxage=300, stale-while-revalidate=600", + }, + }, + ); +} diff --git a/src/app/home-page/components/ArticleFeed.tsx b/src/app/home-page/components/ArticleFeed.tsx index 577a409..228a0b6 100644 --- a/src/app/home-page/components/ArticleFeed.tsx +++ b/src/app/home-page/components/ArticleFeed.tsx @@ -106,6 +106,27 @@ export default function ArticleFeed() { } }, []); + // Fetch imported WordPress posts for the feed + useEffect(() => { + let cancelled = false; + (async () => { + try { + const res = await fetch("/api/public/posts?limit=200", { cache: "no-store" }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const json = await res.json(); + const items = Array.isArray(json?.items) ? (json.items as ArticleItem[]) : []; + if (!cancelled) setWpPosts(items); + } catch { + if (!cancelled) setWpPosts([]); + } finally { + if (!cancelled) setLoadingWp(false); + } + })(); + return () => { + cancelled = true; + }; + }, []); + const handleFeedModeChange = (mode: FeedMode) => { setFeedMode(mode); setPage(1); diff --git a/src/lib/articles/legacy-source.ts b/src/lib/articles/legacy-source.ts index ea18d5c..98abf18 100644 --- a/src/lib/articles/legacy-source.ts +++ b/src/lib/articles/legacy-source.ts @@ -414,3 +414,24 @@ export async function getRecentAiRewrites(limit = 20): Promise { const rows = await fetchPublishedRewrites(limit); return rows.map((r) => rewriteRowToArticle(r)); } + +export async function getLatestImportedArticles( + limit = 100, + offset = 0, +): Promise<{ items: Article[]; total: number }> { + try { + const { data, error, count } = await client() + .from("wp_imported_posts") + .select(SELECT_COLS, { count: "exact" }) + .eq("status", "published") + .order("wp_published_at", { ascending: false, nullsFirst: false }) + .range(offset, offset + limit - 1); + if (error || !data) return { items: [], total: 0 }; + return { + items: (data as ImportedPostRow[]).map((r) => rowToArticle(r)), + total: count ?? data.length, + }; + } catch { + return { items: [], total: 0 }; + } +}