diff --git a/docs/app/api/og-blog/route.tsx b/docs/app/api/og-blog/route.tsx
new file mode 100644
index 0000000000..262c6375a3
--- /dev/null
+++ b/docs/app/api/og-blog/route.tsx
@@ -0,0 +1,152 @@
+import { ImageResponse } from "@vercel/og";
+import * as z from "zod";
+
+export const runtime = "edge";
+
+const ogSchema = z.object({
+ title: z.string().trim().min(1).max(200),
+ date: z.string().trim().max(40).optional(),
+ theme: z.enum(["light", "dark"]).default("dark"),
+});
+
+const themes = {
+ dark: {
+ background: "radial-gradient(120% 120% at 80% 0%, #111111 0%, #000000 55%)",
+ titleGradient:
+ "linear-gradient(100deg, #fafafa 0%, #d4d4d4 55%, #8a8a8a 100%)",
+ date: "#8f8f8f",
+ outline: "rgba(255, 255, 255, 0.09)",
+ speck: "rgba(255, 255, 255, 0.35)",
+ },
+ light: {
+ background: "radial-gradient(120% 120% at 80% 0%, #ffffff 0%, #f1f1f0 55%)",
+ titleGradient:
+ "linear-gradient(100deg, #0a0a0a 0%, #2e2e2e 55%, #6f6f6f 100%)",
+ date: "#7a7a7a",
+ outline: "rgba(0, 0, 0, 0.09)",
+ speck: "rgba(0, 0, 0, 0.3)",
+ },
+} as const;
+
+const specks: Array<{ top: string; left: string; size: number }> = [
+ { top: "6%", left: "43%", size: 3 },
+ { top: "13%", left: "61%", size: 2 },
+ { top: "4%", left: "76%", size: 2 },
+ { top: "27%", left: "52%", size: 2 },
+ { top: "9%", left: "30%", size: 2 },
+ { top: "31%", left: "88%", size: 3 },
+];
+
+function titleFontSize(title: string) {
+ if (title.length <= 40) return 72;
+ if (title.length <= 80) return 62;
+ if (title.length <= 120) return 54;
+ return 46;
+}
+
+export async function GET(req: Request) {
+ try {
+ const url = new URL(req.url);
+ const { title, date, theme } = ogSchema.parse(
+ Object.fromEntries(url.searchParams),
+ );
+
+ const geist = await fetch(
+ new URL("../../../assets/Geist.ttf", import.meta.url),
+ ).then((res) => res.arrayBuffer());
+
+ const colors = themes[theme];
+ const trimmedTitle =
+ title.length > 140 ? `${title.substring(0, 137)}...` : title;
+
+ return new ImageResponse(
+
+ {specks.map((speck, i) => (
+
+ ))}
+
+ {/* Rotated rounded-rect outline, bleeding off the top-right corner */}
+
+
+
+
+ {trimmedTitle}
+
+ {date ? (
+
+ {date}
+
+ ) : null}
+
+
,
+ {
+ width: 1200,
+ height: 630,
+ fonts: [
+ {
+ name: "Geist",
+ data: geist,
+ weight: 400,
+ style: "normal",
+ },
+ ],
+ headers: {
+ "Cache-Control":
+ "public, max-age=86400, s-maxage=31536000, immutable",
+ },
+ },
+ );
+ } catch (err) {
+ console.log({ err });
+ return new Response("Failed to generate the OG image", { status: 500 });
+ }
+}
diff --git a/docs/app/blog/[[...slug]]/page.tsx b/docs/app/blog/[[...slug]]/page.tsx
index 24ade7ade7..78dcd69d72 100644
--- a/docs/app/blog/[[...slug]]/page.tsx
+++ b/docs/app/blog/[[...slug]]/page.tsx
@@ -20,6 +20,69 @@ function formatDate(date: Date) {
});
}
+function generatedCoverUrl(
+ title: string,
+ date: Date | undefined,
+ theme: "light" | "dark",
+) {
+ const params = new URLSearchParams();
+ params.set("title", title);
+ if (date) {
+ params.set(
+ "date",
+ new Date(date).toLocaleDateString("en-US", {
+ month: "long",
+ year: "numeric",
+ }),
+ );
+ }
+ params.set("theme", theme);
+ return `/api/og-blog?${params.toString()}`;
+}
+
+function BlogCover({
+ title,
+ date,
+ image,
+}: {
+ title: string;
+ date: Date;
+ image?: string;
+}) {
+ if (image) {
+ return (
+
+ );
+ }
+ return (
+ <>
+ {/* Generated cover, one per theme */}
+
+
+ >
+ );
+}
+
function BlogList() {
const posts = blogs
.getPages()
@@ -49,17 +112,13 @@ function BlogList() {
className="group block border-b border-dashed border-foreground/[0.06] px-5 sm:px-6 lg:px-8 py-5 transition-colors hover:bg-foreground/[0.02]"
>
- {post.data?.image && (
-
-
-
- )}
+
+
+
{post.data.title}
@@ -253,22 +312,8 @@ export async function generateMetadata({
if (!page || page.data.draft) return notFound();
const { title, description, image, date } = page.data;
- const ogSearchParams = new URLSearchParams();
- ogSearchParams.set("heading", title);
- if (description) ogSearchParams.set("description", description);
- if (date) {
- ogSearchParams.set(
- "date",
- new Date(date).toLocaleDateString("en-US", {
- month: "short",
- day: "numeric",
- year: "numeric",
- }),
- );
- }
- const ogUrl = `/api/og-release?${ogSearchParams.toString()}`;
-
- const ogImage = image || ogUrl;
+ // Social cards have no theme preference; always use the dark variant.
+ const ogImage = image || generatedCoverUrl(title, date, "dark");
return createMetadata({
title,
diff --git a/docs/content/blogs/0-supabase-auth-to-planetscale-migration.mdx b/docs/content/blogs/0-supabase-auth-to-planetscale-migration.mdx
index 4b1c5cea87..d44e790007 100644
--- a/docs/content/blogs/0-supabase-auth-to-planetscale-migration.mdx
+++ b/docs/content/blogs/0-supabase-auth-to-planetscale-migration.mdx
@@ -5,7 +5,6 @@ date: 2025-08-25
author:
name: "Dagmawi Esayas"
avatar: ""
-image: "https://docs.better-auth.com/blogs/supabase-ps.png"
tags: ["migration", "guides", "supabase", "planetscale"]
---
diff --git a/docs/content/blogs/1-3.mdx b/docs/content/blogs/1-3.mdx
index f4d93f5616..d47070874d 100644
--- a/docs/content/blogs/1-3.mdx
+++ b/docs/content/blogs/1-3.mdx
@@ -6,7 +6,6 @@ author:
name: "Bereket Engida"
avatar: "/avatars/beka.jpg"
twitter: "bekacru"
-image: "https://docs.better-auth.com/release-og/1-3.png"
tags: ["1.3", "authentication", "oidc", "mcp", "sso", "organization"]
---
diff --git a/docs/content/blogs/1-4.mdx b/docs/content/blogs/1-4.mdx
index 24abb9df6f..4c2e5385fb 100644
--- a/docs/content/blogs/1-4.mdx
+++ b/docs/content/blogs/1-4.mdx
@@ -6,7 +6,6 @@ author:
name: "Bereket Engida"
avatar: "/avatars/beka.jpg"
twitter: "bekacru"
-image: "https://docs.better-auth.com/release-og/1-4.png"
tags: ["1.4", "stateless sessions", "SCIM", "database indexing", "database join", "SSO domain verification", "uuid", "device authorization"]
---
diff --git a/docs/content/blogs/1-5.mdx b/docs/content/blogs/1-5.mdx
index 50f9fa6f25..0804b609b6 100644
--- a/docs/content/blogs/1-5.mdx
+++ b/docs/content/blogs/1-5.mdx
@@ -6,7 +6,6 @@ author:
name: "Alex Yang"
avatar: "/avatars/alex.png"
twitter: "himseIf_65"
-image: "https://docs.better-auth.com/release-og/v1.5-release.png"
tags: ["1.5", "CLI", "MCP", "oauth-provider", "electron", "i18n", "adapters", "cloudflare", "d1", "stripe", "SSO", "SCIM", "test-utils"]
---
diff --git a/docs/content/blogs/1-6.mdx b/docs/content/blogs/1-6.mdx
index 522be5df15..41b22cec40 100644
--- a/docs/content/blogs/1-6.mdx
+++ b/docs/content/blogs/1-6.mdx
@@ -5,7 +5,6 @@ date: 2026-04-07
author:
name: "Taesu Kim"
avatar: ""
-image: "/release-og/1-6.png"
tags: ["1.6", "OpenTelemetry", "passkey", "performance", "release workflow"]
---
diff --git a/docs/content/blogs/authjs-joins-better-auth.mdx b/docs/content/blogs/authjs-joins-better-auth.mdx
index ef7bab81a5..0ca5e63dbe 100644
--- a/docs/content/blogs/authjs-joins-better-auth.mdx
+++ b/docs/content/blogs/authjs-joins-better-auth.mdx
@@ -6,7 +6,6 @@ author:
name: "Bereket Engida"
avatar: "/avatars/beka.jpg"
twitter: "bekacru"
-image: "https://docs.better-auth.com/blogs/authjs-joins.png"
tags: ["seed round", "authentication", "funding"]
---
diff --git a/docs/content/blogs/security-update-june-2026.mdx b/docs/content/blogs/security-update-june-2026.mdx
index 7c8188cc18..4ef3d13eda 100644
--- a/docs/content/blogs/security-update-june-2026.mdx
+++ b/docs/content/blogs/security-update-june-2026.mdx
@@ -5,7 +5,6 @@ date: 2026-06-02
author:
name: "Better Auth Maintainers"
avatar: ""
-image: "/blogs/security-june-2026.png"
tags: ["security", "advisories", "release"]
---
diff --git a/docs/content/blogs/seed-round.mdx b/docs/content/blogs/seed-round.mdx
index d74fa2d54d..f54c31604b 100644
--- a/docs/content/blogs/seed-round.mdx
+++ b/docs/content/blogs/seed-round.mdx
@@ -6,7 +6,6 @@ author:
name: "Bereket Engida"
avatar: "/avatars/beka.jpg"
twitter: "bekacru"
-image: "https://docs.better-auth.com/blogs/seed-round.png"
tags: ["seed round", "authentication", "funding"]
---