diff --git a/landing-next/app/blog/[slug]/opengraph-image.tsx b/landing-next/app/blog/[slug]/opengraph-image.tsx new file mode 100644 index 0000000..d5c56e7 --- /dev/null +++ b/landing-next/app/blog/[slug]/opengraph-image.tsx @@ -0,0 +1,110 @@ +import { ImageResponse } from "next/og"; +import { getPostBySlug } from "@/lib/notion"; + +export const runtime = "edge"; + +export const size = { width: 1200, height: 630 }; +export const contentType = "image/png"; + +interface Props { + params: Promise<{ slug: string }>; +} + +export default async function OgImage({ params }: Props) { + const { slug } = await params; + const post = await getPostBySlug(slug); + + const title = post?.title ?? "AgentRail Blog"; + + return new ImageResponse( + ( +
+ {/* Orange accent bar at top */} +
+ + {/* AgentRail wordmark top left */} +
+ + AgentRail + +
+ + {/* Main content — centered vertically */} +
+
60 ? "48px" : "60px", + fontWeight: 700, + lineHeight: 1.15, + letterSpacing: "-0.025em", + maxWidth: "960px", + }} + > + {title} +
+
+ + {/* Bottom right URL */} +
+ agentrail.app/blog +
+
+ ), + { ...size } + ); +} diff --git a/landing-next/app/blog/[slug]/page.tsx b/landing-next/app/blog/[slug]/page.tsx new file mode 100644 index 0000000..4ab178b --- /dev/null +++ b/landing-next/app/blog/[slug]/page.tsx @@ -0,0 +1,206 @@ +import { getPublishedPosts, getPostBySlug } from "@/lib/notion"; +import Nav from "@/components/Nav"; +import Footer from "@/components/Footer"; +import PostBody from "@/components/blog/PostBody"; +import { notFound } from "next/navigation"; +import type { Metadata } from "next"; +import Link from "next/link"; + +export const revalidate = 3600; +export const dynamicParams = true; + +interface Props { + params: Promise<{ slug: string }>; +} + +export async function generateStaticParams() { + try { + const posts = await getPublishedPosts(); + return posts.map((post) => ({ slug: post.slug })); + } catch { + return []; + } +} + +export async function generateMetadata({ params }: Props): Promise { + const { slug } = await params; + const post = await getPostBySlug(slug); + if (!post) return {}; + + const url = `https://agentrail.app/blog/${slug}`; + + return { + title: `${post.title} — AgentRail`, + description: post.description, + alternates: { canonical: url }, + openGraph: { + title: post.title, + description: post.description, + url, + siteName: "AgentRail", + type: "article", + publishedTime: post.publishedDate, + tags: post.tags, + }, + twitter: { + card: "summary_large_image", + title: post.title, + description: post.description, + }, + }; +} + +function formatDate(dateStr: string): string { + if (!dateStr) return ""; + const date = new Date(dateStr); + return date.toLocaleDateString("en-GB", { + day: "2-digit", + month: "short", + year: "numeric", + }); +} + +export default async function BlogPostPage({ params }: Props) { + const { slug } = await params; + const post = await getPostBySlug(slug); + + if (!post) notFound(); + + return ( + <> +