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 ( + {title} + ); + } + return ( + <> + {/* Generated cover, one per theme */} + {title} + {title} + + ); +} + 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} -
- )} +
+ +

{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"] ---