diff --git a/landing/app/api/og-release/route.tsx b/landing/app/api/og-release/route.tsx
new file mode 100644
index 0000000000..4d76eb8070
--- /dev/null
+++ b/landing/app/api/og-release/route.tsx
@@ -0,0 +1,171 @@
+import { ImageResponse } from "@vercel/og";
+import * as z from "zod";
+export const runtime = "edge";
+
+const ogSchema = z.object({
+ heading: z.string(),
+ description: z.string().optional(),
+ date: z.string().optional(),
+});
+
+export async function GET(req: Request) {
+ try {
+ const geist = await fetch(
+ new URL("../../../assets/Geist.ttf", import.meta.url),
+ ).then((res) => res.arrayBuffer());
+
+ const url = new URL(req.url);
+ const urlParamsValues = Object.fromEntries(url.searchParams);
+ const validParams = ogSchema.parse(urlParamsValues);
+
+ const { heading, description, date } = validParams;
+ const trueHeading =
+ heading.length > 140 ? `${heading.substring(0, 140)}...` : heading;
+
+ return new ImageResponse(
+
+
+
+
+
+
+
+ {trueHeading}
+
+
+
+ {description}
+
+
+
+ {date}
+
+
+
+ {/* Lines */}
+
+
+
+
+
+
,
+ {
+ width: 1200,
+ height: 630,
+ fonts: [
+ {
+ name: "Geist",
+ data: geist,
+ weight: 400,
+ style: "normal",
+ },
+ ],
+ },
+ );
+ } catch (err) {
+ console.log({ err });
+ return new Response("Failed to generate the OG image", { status: 500 });
+ }
+}
diff --git a/landing/app/api/og/route.tsx b/landing/app/api/og/route.tsx
new file mode 100644
index 0000000000..8aacaff5e1
--- /dev/null
+++ b/landing/app/api/og/route.tsx
@@ -0,0 +1,208 @@
+import { ImageResponse } from "@vercel/og";
+import * as z from "zod";
+export const runtime = "edge";
+
+const ogSchema = z.object({
+ heading: z.string().default("Better Auth Documentation"),
+ mode: z.string().default("dark"),
+ type: z.string().default("documentation"),
+});
+export async function GET(req: Request) {
+ try {
+ const geist = await fetch(
+ new URL("../../../assets/Geist.ttf", import.meta.url),
+ ).then((res) => res.arrayBuffer());
+ const geistMono = await fetch(
+ new URL("../../../assets/GeistMono.ttf", import.meta.url),
+ ).then((res) => res.arrayBuffer());
+ const url = new URL(req.url);
+ const urlParamsValues = Object.fromEntries(url.searchParams);
+ const validParams = ogSchema.parse(urlParamsValues);
+ const { heading, type } = validParams;
+ const trueHeading =
+ heading.length > 140 ? `${heading.substring(0, 140)}...` : heading;
+
+ const paint = "#fff";
+
+ const fontSize = trueHeading.length > 100 ? "30px" : "60px";
+ return new ImageResponse(
+
+
+
+
+
+
+
+
+
+
+ {type === "documentation" ? (
+
+ ) : null}
+ {type}
+
+
+ {trueHeading}
+
+
+
+
+ Better Auth.
+
+
+
+
+ github.com/better-auth/better-auth
+
+
+
+
+
,
+ {
+ width: 1200,
+ height: 630,
+ fonts: [
+ {
+ name: "Geist",
+ data: geist,
+ weight: 400,
+ style: "normal",
+ },
+ {
+ name: "GeistMono",
+ data: geistMono,
+ weight: 700,
+ style: "normal",
+ },
+ ],
+ },
+ );
+ } catch (err) {
+ console.log({ err });
+ return new Response("Failed to generate the og image", { status: 500 });
+ }
+}
diff --git a/landing/app/blog/[[...slug]]/page.tsx b/landing/app/blog/[[...slug]]/page.tsx
index 89e2f2f17a..e3d99cdb84 100644
--- a/landing/app/blog/[[...slug]]/page.tsx
+++ b/landing/app/blog/[[...slug]]/page.tsx
@@ -277,19 +277,40 @@ export async function generateMetadata({
}
const page = blogs.getPage(slug);
if (!page || page.data.draft) return notFound();
- const { title, description, image } = page.data;
+ 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;
+
return {
title,
description,
- ...(image && {
- openGraph: {
- images: [image],
- },
- twitter: {
- card: "summary_large_image" as const,
- images: [image],
- },
- }),
+ openGraph: {
+ title,
+ description,
+ type: "article",
+ images: [ogImage],
+ },
+ twitter: {
+ card: "summary_large_image" as const,
+ title,
+ description,
+ images: [ogImage],
+ },
};
}
diff --git a/landing/app/blog/layout.tsx b/landing/app/blog/layout.tsx
index def6c6e153..a4aea65ce8 100644
--- a/landing/app/blog/layout.tsx
+++ b/landing/app/blog/layout.tsx
@@ -4,6 +4,17 @@ import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Blog - Better Auth",
description: "Latest updates, articles, and insights about Better Auth",
+ openGraph: {
+ title: "Blog - Better Auth",
+ description: "Latest updates, articles, and insights about Better Auth",
+ images: ["/api/og-release?heading=Better%20Auth%20Blog"],
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: "Blog - Better Auth",
+ description: "Latest updates, articles, and insights about Better Auth",
+ images: ["/api/og-release?heading=Better%20Auth%20Blog"],
+ },
};
export default function BlogLayout({
diff --git a/landing/app/docs/[[...slug]]/page.tsx b/landing/app/docs/[[...slug]]/page.tsx
index 1ba50d19a8..16b653a455 100644
--- a/landing/app/docs/[[...slug]]/page.tsx
+++ b/landing/app/docs/[[...slug]]/page.tsx
@@ -157,8 +157,34 @@ export async function generateMetadata({
const page = source.getPage(slug);
if (!page) return notFound();
+ const ogSearchParams = new URLSearchParams();
+ ogSearchParams.set("heading", page.data.title);
+ ogSearchParams.set("type", "documentation");
+ ogSearchParams.set("mode", "dark");
+
+ const ogUrl = `/api/og?${ogSearchParams.toString()}`;
+
return {
title: page.data.title,
description: page.data.description,
+ openGraph: {
+ title: page.data.title,
+ description: page.data.description,
+ type: "article",
+ images: [
+ {
+ url: ogUrl,
+ width: 1200,
+ height: 630,
+ alt: page.data.title,
+ },
+ ],
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: page.data.title,
+ description: page.data.description,
+ images: [ogUrl],
+ },
};
}
diff --git a/landing/app/layout.tsx b/landing/app/layout.tsx
index 1e0f4abe28..c01431e7e1 100644
--- a/landing/app/layout.tsx
+++ b/landing/app/layout.tsx
@@ -19,7 +19,13 @@ const fontMono = Geist_Mono({
});
export const metadata: Metadata = {
- metadataBase: new URL("https://docs.better-auth.com"),
+ metadataBase: new URL(
+ process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : process.env.NODE_ENV === "production"
+ ? "https://docs.better-auth.com"
+ : (process.env.NEXT_PUBLIC_URL ?? "http://localhost:3000"),
+ ),
title: {
template: "%s | Better Auth",
default: "Better Auth",
@@ -27,26 +33,26 @@ export const metadata: Metadata = {
description: "The Most Comprehensive Authentication Framework",
icons: {
icon: [
- { url: "https://docs.better-auth.com/favicon/favicon.ico", sizes: "any" },
+ { url: "/favicon/favicon.ico", sizes: "any" },
{
- url: "https://docs.better-auth.com/favicon/favicon-32x32.png",
+ url: "/favicon/favicon-32x32.png",
sizes: "32x32",
type: "image/png",
},
{
- url: "https://docs.better-auth.com/favicon/favicon-16x16.png",
+ url: "/favicon/favicon-16x16.png",
sizes: "16x16",
type: "image/png",
},
],
- apple: "https://docs.better-auth.com/favicon/apple-touch-icon.png",
+ apple: "/favicon/apple-touch-icon.png",
},
openGraph: {
- images: ["https://docs.better-auth.com/og.png"],
+ images: ["/og.png"],
},
twitter: {
card: "summary_large_image",
- images: ["https://docs.better-auth.com/og.png"],
+ images: ["/og.png"],
},
};
diff --git a/landing/assets/Geist.ttf b/landing/assets/Geist.ttf
new file mode 100644
index 0000000000..90d2c7179a
Binary files /dev/null and b/landing/assets/Geist.ttf differ
diff --git a/landing/assets/GeistMono.ttf b/landing/assets/GeistMono.ttf
new file mode 100644
index 0000000000..91754e41f6
Binary files /dev/null and b/landing/assets/GeistMono.ttf differ
diff --git a/landing/package.json b/landing/package.json
index 40bc71db0d..fc0b404f7e 100644
--- a/landing/package.json
+++ b/landing/package.json
@@ -36,6 +36,7 @@
"@tsparticles/engine": "^3.9.1",
"@tsparticles/react": "^3.0.0",
"@tsparticles/slim": "^3.9.1",
+ "@vercel/og": "^0.10.1",
"ai": "^6.0.104",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4a50253fe2..67a8844eeb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -826,6 +826,9 @@ importers:
'@tsparticles/slim':
specifier: ^3.9.1
version: 3.9.1
+ '@vercel/og':
+ specifier: ^0.10.1
+ version: 0.10.1
ai:
specifier: ^6.0.104
version: 6.0.104(zod@4.3.6)
@@ -8122,6 +8125,10 @@ packages:
engines: {node: '>=20'}
hasBin: true
+ '@vercel/og@0.10.1':
+ resolution: {integrity: sha512-Ilhy0kQyTkeQsk3kAi4NrKzWxwaItHqIHlROrtZGscUGuqvE1N5gib0bPlr06NJdnxlBsDo7J169N7XKf4M19A==}
+ engines: {node: '>=16'}
+
'@vercel/og@0.8.6':
resolution: {integrity: sha512-hBcWIOppZV14bi+eAmCZj8Elj8hVSUZJTpf1lgGBhVD85pervzQ1poM/qYfFUlPraYSZYP+ASg6To5BwYmUSGQ==}
engines: {node: '>=16'}
@@ -9391,6 +9398,10 @@ packages:
resolution: {integrity: sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==}
engines: {node: '>=16'}
+ css-gradient-parser@0.0.17:
+ resolution: {integrity: sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==}
+ engines: {node: '>=16'}
+
css-in-js-utils@3.1.0:
resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==}
@@ -14858,6 +14869,10 @@ packages:
resolution: {integrity: sha512-ZvHN3ygzZ8FuxjSNB+mKBiF/NIoqHzlBGbD0MJiT+MvSsFOvotnWOhdTjxKzhHRT2wPC1QbhLzx2q/Y83VhfYQ==}
engines: {node: '>=16'}
+ satori@0.21.0:
+ resolution: {integrity: sha512-PV5II6Z+gsFFLq4jp6mslt0ObNRzcTi5Xq+8JtJ3ReqBS9k4nF4ajqITFXygPZgRA6l5+22NYIS1WRTqTfDp5g==}
+ engines: {node: '>=16'}
+
sax@1.4.4:
resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
engines: {node: '>=11.0.0'}
@@ -24382,6 +24397,13 @@ snapshots:
- rollup
- supports-color
+ '@vercel/og@0.10.1':
+ dependencies:
+ '@resvg/resvg-wasm': 2.4.0
+ satori: 0.21.0
+ optionalDependencies:
+ sharp: 0.34.5
+
'@vercel/og@0.8.6':
dependencies:
'@resvg/resvg-wasm': 2.4.0
@@ -25960,6 +25982,8 @@ snapshots:
css-gradient-parser@0.0.16: {}
+ css-gradient-parser@0.0.17: {}
+
css-in-js-utils@3.1.0:
dependencies:
hyphenate-style-name: 1.1.0
@@ -33251,6 +33275,20 @@ snapshots:
postcss-value-parser: 4.2.0
yoga-layout: 3.2.1
+ satori@0.21.0:
+ dependencies:
+ '@shuding/opentype.js': 1.4.0-beta.0
+ css-background-parser: 0.1.0
+ css-box-shadow: 1.0.0-3
+ css-gradient-parser: 0.0.17
+ css-to-react-native: 3.2.0
+ emoji-regex-xs: 2.0.1
+ escape-html: 1.0.3
+ linebreak: 1.1.0
+ parse-css-color: 0.2.1
+ postcss-value-parser: 4.2.0
+ yoga-layout: 3.2.1
+
sax@1.4.4: {}
scheduler@0.27.0: {}