Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions landing-next/app/blog/[slug]/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -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(
(
<div
style={{
width: "100%",
height: "100%",
background: "#0D1B2A",
display: "flex",
flexDirection: "column",
position: "relative",
fontFamily: "sans-serif",
}}
>
{/* Orange accent bar at top */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "6px",
background: "#FF6B35",
}}
/>

{/* AgentRail wordmark top left */}
<div
style={{
position: "absolute",
top: "40px",
left: "56px",
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
<span
style={{
color: "#FF6B35",
fontSize: "22px",
fontWeight: 700,
letterSpacing: "-0.02em",
}}
>
AgentRail
</span>
</div>

{/* Main content — centered vertically */}
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
padding: "0 56px",
flex: 1,
marginTop: "24px",
}}
>
<div
style={{
color: "#ffffff",
fontSize: title.length > 60 ? "48px" : "60px",
fontWeight: 700,
lineHeight: 1.15,
letterSpacing: "-0.025em",
maxWidth: "960px",
}}
>
{title}
</div>
</div>

{/* Bottom right URL */}
<div
style={{
position: "absolute",
bottom: "40px",
right: "56px",
color: "#4a5568",
fontSize: "16px",
letterSpacing: "0.02em",
fontFamily: "monospace",
}}
>
agentrail.app/blog
</div>
</div>
),
{ ...size }
);
}
206 changes: 206 additions & 0 deletions landing-next/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Metadata> {
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 (
<>
<Nav />
<main
style={{
background: "var(--bg)",
minHeight: "100vh",
paddingBottom: "100px",
}}
>
{/* Accent bar */}
<div
style={{ height: "3px", background: "var(--accent)", width: "100%" }}
/>

{/* Header */}
<section style={{ padding: "60px 0 48px" }}>
<div
style={{
maxWidth: "760px",
margin: "0 auto",
padding: "0 32px",
}}
>
{/* Back link */}
<Link
href="/blog"
style={{
fontFamily: "var(--font-mono)",
fontSize: "11px",
letterSpacing: "0.06em",
color: "var(--accent)",
textDecoration: "none",
textTransform: "uppercase",
display: "inline-flex",
alignItems: "center",
gap: "6px",
marginBottom: "32px",
}}
>
← Blog
</Link>

{/* Tags */}
{post.tags.length > 0 && (
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: "6px",
marginBottom: "20px",
}}
>
{post.tags.map((tag) => (
<span
key={tag}
style={{
fontFamily: "var(--font-mono)",
fontSize: "10px",
letterSpacing: "0.06em",
background: "rgba(0,135,90,0.12)",
color: "var(--accent)",
border: "1px solid rgba(0,135,90,0.3)",
borderRadius: "4px",
padding: "2px 8px",
textTransform: "uppercase",
}}
>
{tag}
</span>
))}
</div>
)}

{/* Title */}
<h1
style={{
fontFamily: "var(--font-sans)",
fontWeight: 700,
fontSize: "clamp(28px, 3.5vw, 48px)",
lineHeight: 1.15,
letterSpacing: "-0.025em",
color: "var(--ink)",
margin: "0 0 16px",
}}
>
{post.title}
</h1>

{/* Date */}
<div
style={{
fontFamily: "var(--font-mono)",
fontSize: "12px",
color: "var(--ink-3)",
letterSpacing: "0.04em",
}}
>
{formatDate(post.publishedDate)}
</div>

{/* Description */}
{post.description && (
<p
style={{
fontFamily: "var(--font-sans)",
fontSize: "18px",
lineHeight: 1.6,
color: "var(--ink-2)",
marginTop: "24px",
marginBottom: 0,
borderTop: "1px solid var(--line)",
paddingTop: "24px",
}}
>
{post.description}
</p>
)}
</div>
</section>

{/* Content */}
<section>
<div
style={{
maxWidth: "760px",
margin: "0 auto",
padding: "0 32px",
}}
>
{post.content && <PostBody content={post.content} />}
</div>
</section>
</main>
<Footer />
</>
);
}
Loading
Loading