diff --git a/apps/dashboard/src/@/components/blocks/Avatars/GradientAvatar.tsx b/apps/dashboard/src/@/components/blocks/Avatars/GradientAvatar.tsx
index 1d70a27b13f..e0c411d6070 100644
--- a/apps/dashboard/src/@/components/blocks/Avatars/GradientAvatar.tsx
+++ b/apps/dashboard/src/@/components/blocks/Avatars/GradientAvatar.tsx
@@ -1,34 +1,8 @@
import { Img } from "@/components/blocks/Img";
-import { useMemo } from "react";
import type { ThirdwebClient } from "thirdweb";
import { resolveSchemeWithErrorHandler } from "../../../lib/resolveSchemeWithErrorHandler";
import { cn } from "../../../lib/utils";
-
-const gradients = [
- ["#fca5a5", "#b91c1c"],
- ["#fdba74", "#c2410c"],
- ["#fcd34d", "#b45309"],
- ["#fde047", "#a16207"],
- ["#a3e635", "#4d7c0f"],
- ["#86efac", "#15803d"],
- ["#67e8f9", "#0e7490"],
- ["#7dd3fc", "#0369a1"],
- ["#93c5fd", "#1d4ed8"],
- ["#a5b4fc", "#4338ca"],
- ["#c4b5fd", "#6d28d9"],
- ["#d8b4fe", "#7e22ce"],
- ["#f0abfc", "#a21caf"],
- ["#f9a8d4", "#be185d"],
- ["#fda4af", "#be123c"],
-];
-
-function getGradientForString(str: string) {
- const number = Math.abs(
- str.split("").reduce((acc, b, i) => acc + b.charCodeAt(0) * (i + 1), 0),
- );
- const index = number % gradients.length;
- return gradients[index];
-}
+import { GradientBlobbie } from "./GradientBlobbie";
export function GradientAvatar(props: {
src: string | undefined;
@@ -36,13 +10,6 @@ export function GradientAvatar(props: {
className: string;
client: ThirdwebClient;
}) {
- const gradient = useMemo(() => {
- if (!props.id) {
- return undefined;
- }
- return getGradientForString(props.id);
- }, [props.id]);
-
const resolvedSrc = props.src
? resolveSchemeWithErrorHandler({
client: props.client,
@@ -54,15 +21,7 @@ export function GradientAvatar(props: {
- ) : undefined
- }
+ fallback={props.id ? : undefined}
/>
);
}
diff --git a/apps/dashboard/src/@/components/blocks/Avatars/GradientBlobbie.tsx b/apps/dashboard/src/@/components/blocks/Avatars/GradientBlobbie.tsx
new file mode 100644
index 00000000000..d6446c6b147
--- /dev/null
+++ b/apps/dashboard/src/@/components/blocks/Avatars/GradientBlobbie.tsx
@@ -0,0 +1,45 @@
+// Note: This is also used in opengraph-image.tsx
+// don't use any hooks or client side stuff here
+
+const gradients = [
+ ["#fca5a5", "#b91c1c"],
+ ["#fdba74", "#c2410c"],
+ ["#fcd34d", "#b45309"],
+ ["#fde047", "#a16207"],
+ ["#a3e635", "#4d7c0f"],
+ ["#86efac", "#15803d"],
+ ["#67e8f9", "#0e7490"],
+ ["#7dd3fc", "#0369a1"],
+ ["#93c5fd", "#1d4ed8"],
+ ["#a5b4fc", "#4338ca"],
+ ["#c4b5fd", "#6d28d9"],
+ ["#d8b4fe", "#7e22ce"],
+ ["#f0abfc", "#a21caf"],
+ ["#f9a8d4", "#be185d"],
+ ["#fda4af", "#be123c"],
+];
+
+function getGradientForString(str: string) {
+ const number = Math.abs(
+ str.split("").reduce((acc, b, i) => acc + b.charCodeAt(0) * (i + 1), 0),
+ );
+ const index = number % gradients.length;
+ return gradients[index];
+}
+
+export function GradientBlobbie(props: {
+ id: string;
+ style?: React.CSSProperties;
+}) {
+ const gradient = getGradientForString(props.id);
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(dashboard)/profile/[addressOrEns]/opengraph-image.tsx b/apps/dashboard/src/app/(dashboard)/profile/[addressOrEns]/opengraph-image.tsx
new file mode 100644
index 00000000000..8cc4ea9c115
--- /dev/null
+++ b/apps/dashboard/src/app/(dashboard)/profile/[addressOrEns]/opengraph-image.tsx
@@ -0,0 +1,129 @@
+import { GradientBlobbie } from "@/components/blocks/Avatars/GradientBlobbie";
+/* eslint-disable @next/next/no-img-element */
+import { getThirdwebClient } from "@/constants/thirdweb.server";
+import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
+import { notFound } from "next/navigation";
+import { ImageResponse } from "next/og";
+import { resolveAvatar } from "thirdweb/extensions/ens";
+import { shortenIfAddress } from "../../../../utils/usedapp-external";
+import { resolveAddressAndEns } from "./resolveAddressAndEns";
+
+export const runtime = "edge";
+
+export const size = {
+ width: 1200,
+ height: 630,
+};
+
+type PageProps = {
+ params: Promise<{
+ addressOrEns: string;
+ }>;
+};
+
+export default async function Image(props: PageProps) {
+ const client = getThirdwebClient();
+ const params = await props.params;
+ const resolvedInfo = await resolveAddressAndEns(params.addressOrEns);
+
+ if (!resolvedInfo) {
+ notFound();
+ }
+
+ const [inter700, profileBackground] = await Promise.all([
+ fetch(new URL("og-lib/fonts/inter/700.ttf", import.meta.url)).then((res) =>
+ res.arrayBuffer(),
+ ),
+ fetch(
+ new URL("og-lib/assets/profile/background.png", import.meta.url),
+ ).then((res) => res.arrayBuffer()),
+ ]);
+
+ const ensImage = resolvedInfo.ensName
+ ? await resolveAvatar({
+ client: client,
+ name: resolvedInfo.ensName,
+ })
+ : null;
+
+ const resolvedENSImageSrc = ensImage
+ ? resolveSchemeWithErrorHandler({
+ client: client,
+ uri: ensImage,
+ })
+ : null;
+
+ return new ImageResponse(
+
+
+
+
+
+ {resolvedENSImageSrc ? (
+
+ ) : (
+
+ )}
+
+
+ {resolvedInfo.ensName || shortenIfAddress(resolvedInfo.address)}
+
+
+
+
+
,
+ {
+ ...size,
+ fonts: [
+ {
+ data: inter700,
+ name: "Inter",
+ weight: 700,
+ },
+ ],
+ },
+ );
+}
+
+const OgBrandIcon: React.FC = () => (
+ // biome-ignore lint/a11y/noSvgWithoutTitle: not needed
+
+);
diff --git a/apps/dashboard/src/app/(dashboard)/profile/[addressOrEns]/page.tsx b/apps/dashboard/src/app/(dashboard)/profile/[addressOrEns]/page.tsx
index 173bdc95b93..3cdcd5ff828 100644
--- a/apps/dashboard/src/app/(dashboard)/profile/[addressOrEns]/page.tsx
+++ b/apps/dashboard/src/app/(dashboard)/profile/[addressOrEns]/page.tsx
@@ -39,8 +39,15 @@ export async function generateMetadata(props: PageProps): Promise {
replaceDeployerAddress(resolvedInfo.ensName || resolvedInfo.address),
);
+ const title = displayName;
+ const description = `Visit ${displayName}'s profile. See their published contracts and deploy them in one click.`;
+
return {
- title: displayName,
- description: `Visit ${displayName}'s profile. See their published contracts and deploy them in one click.`,
+ title,
+ description,
+ openGraph: {
+ title,
+ description,
+ },
};
}
diff --git a/apps/dashboard/src/og-lib/url-utils.ts b/apps/dashboard/src/og-lib/url-utils.ts
deleted file mode 100644
index 8d000d70f37..00000000000
--- a/apps/dashboard/src/og-lib/url-utils.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { getAbsoluteUrl } from "lib/vercel-utils";
-
-interface OgImageProfile {
- displayName: string;
- bio?: string;
- avatar?: string;
- publishedCnt?: string;
-}
-
-const OG_VERSION = "0.1.0";
-
-type OgProps = {
- profile: OgImageProfile;
-};
-
-function toUrl(
- type: TOgType,
- props: OgProps[TOgType],
-): URL {
- const url = new URL(`${getAbsoluteUrl()}/api/og/${type}`);
- // biome-ignore lint/complexity/noForEach: FIXME
- Object.entries(props).forEach(([key, value]) => {
- if (value !== undefined) {
- if (Array.isArray(value)) {
- // biome-ignore lint/complexity/noForEach: FIXME
- value.forEach((item) =>
- url.searchParams.append(key.toLowerCase(), item),
- );
- } else {
- url.searchParams.append(key.toLowerCase(), value);
- }
- }
- });
- url.searchParams.sort();
- url.searchParams.append("og_version", OG_VERSION);
- return url;
-}
-
-function fromUrl(type: keyof OgProps, url: URL): OgProps[typeof type] {
- switch (type) {
- case "profile":
- return {
- displayName: url.searchParams.get("displayname") || "",
- bio: url.searchParams.get("bio") || undefined,
- avatar: url.searchParams.get("avatar") || undefined,
- publishedCnt: url.searchParams.get("publishedcnt") || undefined,
- } as OgProps["profile"];
- default:
- throw new Error(`Unknown OG type: ${type}`);
- }
-}
-
-export const ProfileOG = {
- toUrl: (props: OgProps["profile"]) => toUrl("profile", props),
- fromUrl: (url: URL) => fromUrl("profile", url) as OgProps["profile"],
-};
diff --git a/apps/dashboard/src/pages/api/og/profile.tsx b/apps/dashboard/src/pages/api/og/profile.tsx
deleted file mode 100644
index 525bb451446..00000000000
--- a/apps/dashboard/src/pages/api/og/profile.tsx
+++ /dev/null
@@ -1,239 +0,0 @@
-/* eslint-disable @next/next/no-img-element */
-import { isProd } from "@/constants/env";
-import { ImageResponse } from "@vercel/og";
-import type { NextRequest } from "next/server";
-import { ProfileOG } from "og-lib/url-utils";
-
-// Make sure the font exists in the specified path:
-export const config = {
- runtime: "edge",
-};
-
-const image = fetch(
- new URL("og-lib/assets/profile/background.png", import.meta.url),
-).then((res) => res.arrayBuffer());
-
-const inter400_ = fetch(
- new URL("og-lib/fonts/inter/400.ttf", import.meta.url),
-).then((res) => res.arrayBuffer());
-const inter500_ = fetch(
- new URL("og-lib/fonts/inter/500.ttf", import.meta.url),
-).then((res) => res.arrayBuffer());
-const inter700_ = fetch(
- new URL("og-lib/fonts/inter/700.ttf", import.meta.url),
-).then((res) => res.arrayBuffer());
-
-const ibmPlexMono400_ = fetch(
- new URL("og-lib/fonts/ibm-plex-mono/400.ttf", import.meta.url),
-).then((res) => res.arrayBuffer());
-const ibmPlexMono500_ = fetch(
- new URL("og-lib/fonts/ibm-plex-mono/500.ttf", import.meta.url),
-).then((res) => res.arrayBuffer());
-const ibmPlexMono700_ = fetch(
- new URL("og-lib/fonts/ibm-plex-mono/700.ttf", import.meta.url),
-).then((res) => res.arrayBuffer());
-
-const OgBrandIcon: React.FC = () => (
- // biome-ignore lint/a11y/noSvgWithoutTitle: not needed
-
-);
-
-const PackageIcon: React.FC = () => (
- // biome-ignore lint/a11y/noSvgWithoutTitle: not needed
-
-);
-
-const MAX_LENGTH = 320;
-
-function descriptionShortener(description: string) {
- let words = [];
- let currentLength = 0;
- let shortened = false;
- for (const word of description.split(" ")) {
- // +1 for the space
- if (currentLength + word.length + 1 > MAX_LENGTH) {
- shortened = true;
- break;
- }
- words.push(word);
- currentLength += word.length + 1;
- }
-
- const lastWord = words[words.length - 1];
- if (lastWord && lastWord.length < 4) {
- words = words.slice(0, -1);
- }
- if (lastWord?.endsWith(".")) {
- return words.join(" ");
- }
- if (!shortened) {
- return words.join(" ");
- }
- return `${words.join(" ")} ...`;
-}
-
-const IPFS_GATEWAY = process.env.API_ROUTES_CLIENT_ID
- ? `https://${process.env.API_ROUTES_CLIENT_ID}.${isProd ? "ipfscdn.io/ipfs/" : "thirdwebstorage-dev.com/ipfs/"}`
- : "https://ipfs.io/ipfs/";
-
-function replaceAnyIpfsUrlWithGateway(url: string) {
- if (url.startsWith("ipfs://")) {
- return `${IPFS_GATEWAY}${url.slice(7)}`;
- }
- if (url.includes("/ipfs/")) {
- const [, after] = url.split("/ipfs/");
- return `${IPFS_GATEWAY}${after}`;
- }
- return url;
-}
-
-export default async function handler(req: NextRequest) {
- if (req.method !== "GET") {
- return new Response("Method not allowed", { status: 405 });
- }
-
- const profileData = ProfileOG.fromUrl(new URL(req.url));
-
- const [
- inter400,
- inter500,
- inter700,
- ibmPlexMono400,
- ibmPlexMono500,
- ibmPlexMono700,
- imageData,
- ] = await Promise.all([
- inter400_,
- inter500_,
- inter700_,
- ibmPlexMono400_,
- ibmPlexMono500_,
- ibmPlexMono700_,
- image,
- ]);
-
- return new ImageResponse(
-
-
- {/* the actual component starts here */}
-
-
- {/* title description and profile image */}
-
-
-
- {profileData.displayName}
-
- {profileData.bio && (
-
- {descriptionShortener(profileData.bio)}
-
- )}
-
-
-
-
-
- {profileData.publishedCnt || 0} published contract
- {profileData.publishedCnt === "1" ? "" : "s"}
-
-
-
-
-
-
-
-
,
- {
- width: 1200,
- height: 630,
- fonts: [
- {
- data: inter400,
- name: "Inter",
- weight: 400,
- },
- {
- data: inter500,
- name: "Inter",
- weight: 500,
- },
- {
- data: inter700,
- name: "Inter",
- weight: 700,
- },
- {
- data: ibmPlexMono400,
- name: "IBM Plex Mono",
- weight: 400,
- },
- {
- data: ibmPlexMono500,
- name: "IBM Plex Mono",
- weight: 500,
- },
- {
- data: ibmPlexMono700,
- name: "IBM Plex Mono",
- weight: 700,
- },
- ],
- },
- );
-}