diff --git a/apps/web/src/app/api/blog/featured/route.ts b/apps/web/src/app/api/blog/featured/route.ts new file mode 100644 index 00000000..ce71c475 --- /dev/null +++ b/apps/web/src/app/api/blog/featured/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from 'next/server' +import { getFeaturedPosts } from '@/lib/blog' + +export async function GET() { + try { + const posts = await getFeaturedPosts() + return NextResponse.json(posts) + } catch (error) { + console.error('Error fetching featured posts:', error) + return NextResponse.json({ error: 'Failed to fetch featured posts' }, { status: 500 }) + } +} diff --git a/apps/web/src/app/blog/[slug]/page.tsx b/apps/web/src/app/blog/[slug]/page.tsx new file mode 100644 index 00000000..fbb2b910 --- /dev/null +++ b/apps/web/src/app/blog/[slug]/page.tsx @@ -0,0 +1,365 @@ +import type { Metadata } from 'next' +import { notFound } from 'next/navigation' +import Link from 'next/link' +import { getPostBySlug, getAllPostSlugs } from '@/lib/blog' +import { css } from '../../../../styled-system/css' + +interface Props { + params: { + slug: string + } +} + +// Generate static params for all blog posts +export async function generateStaticParams() { + const slugs = getAllPostSlugs() + return slugs.map((slug) => ({ slug })) +} + +// Generate metadata for SEO +export async function generateMetadata({ params }: Props): Promise { + const post = await getPostBySlug(params.slug) + + const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://abaci.one' + const postUrl = `${siteUrl}/blog/${params.slug}` + + return { + title: `${post.title} | Abaci.one Blog`, + description: post.description, + authors: [{ name: post.author }], + openGraph: { + title: post.title, + description: post.description, + url: postUrl, + siteName: 'Abaci.one', + type: 'article', + publishedTime: post.publishedAt, + modifiedTime: post.updatedAt, + authors: [post.author], + tags: post.tags, + }, + twitter: { + card: 'summary_large_image', + title: post.title, + description: post.description, + }, + alternates: { + canonical: postUrl, + }, + } +} + +export default async function BlogPost({ params }: Props) { + let post + try { + post = await getPostBySlug(params.slug) + } catch { + notFound() + } + + // Format date for display + const publishedDate = new Date(post.publishedAt).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }) + + const updatedDate = new Date(post.updatedAt).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }) + + const showUpdatedDate = post.publishedAt !== post.updatedAt + + return ( +
+ {/* Background pattern */} +
+ +
+ {/* Back link */} + + + Back to Blog + + + {/* Article */} +
+
+

+ {post.title} +

+ +

+ {post.description} +

+ +
+ {post.author} + + + {showUpdatedDate && ( + <> + + Updated: {updatedDate} + + )} +
+ + {post.tags.length > 0 && ( +
+ {post.tags.map((tag) => ( + + {tag} + + ))} +
+ )} +
+ + {/* Article Content */} +
+
+ + {/* JSON-LD Structured Data */} +