diff --git a/frontend/app/blog.md/route.ts b/frontend/app/blog.md/route.ts new file mode 100644 index 0000000..6dbbab5 --- /dev/null +++ b/frontend/app/blog.md/route.ts @@ -0,0 +1,10 @@ +import { buildBlogMarkdown } from "@/lib/llms"; + +export const revalidate = 300; + +export async function GET() { + const body = await buildBlogMarkdown(); + return new Response(body, { + headers: { "Content-Type": "text/markdown; charset=utf-8" }, + }); +} diff --git a/frontend/app/blog/page.tsx b/frontend/app/blog/page.tsx index 2827568..847c3a0 100644 --- a/frontend/app/blog/page.tsx +++ b/frontend/app/blog/page.tsx @@ -1,109 +1,42 @@ -"use client"; - -import { useState, useEffect } from "react"; -import Link from "next/link"; -import { ArrowRight, ArrowLeft, ChevronRight } from "lucide-react"; -import { api } from "@/lib/api"; -import { formatDate } from "@/lib/utils"; import { Navbar } from "@/components/public/Navbar"; import { Footer } from "@/components/public/Footer"; +import { + BlogIndex, + type BlogPost, + type BlogCategory, +} from "@/components/public/BlogIndex"; +import { BreadcrumbJsonLd } from "@/components/public/JsonLd"; +import { apiUrl } from "@/lib/api-base"; -interface Post { - id: string; - slug: string; - title: string; - excerpt?: string; - content?: string; - author?: string; - authorPubkey?: string; - publishedAt?: string; - createdAt?: string; - categories?: { category: { id: string; name: string; slug: string } }[]; - featured?: boolean; +const LIMIT = 9; + +async function fetchJson(path: string, fallback: T): Promise { + try { + const res = await fetch(apiUrl(path), { next: { revalidate: 300 } }); + if (!res.ok) return fallback; + return (await res.json()) as T; + } catch { + return fallback; + } } -interface Category { - id: string; - name: string; - slug: string; -} - -function PostCardSkeleton() { - return ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ); -} - -function FeaturedPostSkeleton() { - return ( -
-
-
-
-
-
-
-
-
-
-
-
- ); -} - -export default function BlogPage() { - const [posts, setPosts] = useState([]); - const [categories, setCategories] = useState([]); - const [activeCategory, setActiveCategory] = useState("all"); - const [page, setPage] = useState(1); - const [total, setTotal] = useState(0); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const limit = 9; - - useEffect(() => { - api.getCategories().then(setCategories).catch(() => {}); - }, []); - - useEffect(() => { - setLoading(true); - setError(null); - api - .getPosts({ - category: activeCategory === "all" ? undefined : activeCategory, - page, - limit, - }) - .then(({ posts: data, total: t }) => { - setPosts(data); - setTotal(t); - }) - .catch((err) => setError(err.message)) - .finally(() => setLoading(false)); - }, [activeCategory, page]); - - const totalPages = Math.ceil(total / limit); - const featured = posts.find((p) => p.featured); - const regularPosts = featured ? posts.filter((p) => p.id !== featured.id) : posts; +export default async function BlogPage() { + const [{ posts, total }, categories] = await Promise.all([ + fetchJson<{ posts: BlogPost[]; total: number }>(`/posts?page=1&limit=${LIMIT}`, { + posts: [], + total: 0, + }), + fetchJson("/categories", []), + ]); return ( <> +
@@ -121,160 +54,12 @@ export default function BlogPage() {
-
-
- - {categories.map((cat) => ( - - ))} -
-
- -
- {error && ( -
- Failed to load posts: {error} -
- )} - - {loading ? ( - <> - -
- {Array.from({ length: 6 }).map((_, i) => ( - - ))} -
- - ) : posts.length === 0 ? ( -
-

- No posts yet -

-

- Check back soon for curated Bitcoin content. -

-
- ) : ( - <> - {featured && page === 1 && ( - -
- - Featured - -

- {featured.title} -

- {featured.excerpt && ( -

- {featured.excerpt} -

- )} -
- {featured.author && {featured.author}} - {featured.publishedAt && ( - {formatDate(featured.publishedAt)} - )} -
-
- - )} - -
- {regularPosts.map((post) => ( - - {post.categories && post.categories.length > 0 && ( -
- {post.categories.map((pc) => ( - - {pc.category.name} - - ))} -
- )} - -

- {post.title} -

- - {post.excerpt && ( -

- {post.excerpt} -

- )} - -
-
- {post.author && {post.author}} - {post.author && (post.publishedAt || post.createdAt) && ·} - {(post.publishedAt || post.createdAt) && ( - - {formatDate(post.publishedAt || post.createdAt!)} - - )} -
- - Read - -
- - ))} -
- - {totalPages > 1 && ( -
- - - Page {page} of {totalPages} - - -
- )} - - )} -
+