diff --git a/apps/cursor/src/actions/track-install.ts b/apps/cursor/src/actions/track-install.ts index 9abf9223..1197578b 100644 --- a/apps/cursor/src/actions/track-install.ts +++ b/apps/cursor/src/actions/track-install.ts @@ -4,7 +4,11 @@ import { revalidatePath } from "next/cache"; import { z } from "zod"; import { installGlobalLimit, installPerPluginLimit } from "@/lib/rate-limit"; import { createClient as createAdminClient } from "@/utils/supabase/admin-client"; -import { ActionError, actionClient } from "./safe-action"; +import { actionClient } from "./safe-action"; + +export type TrackInstallResult = + | { tracked: true } + | { tracked: false; rateLimited: true }; export const trackInstallAction = actionClient .metadata({ @@ -23,7 +27,7 @@ export const trackInstallAction = actionClient ]); if (!global.success || !perPlugin.success) { - throw new ActionError("Rate limit exceeded. Please try again later."); + return { tracked: false, rateLimited: true } satisfies TrackInstallResult; } const admin = await createAdminClient(); @@ -34,4 +38,6 @@ export const trackInstallAction = actionClient revalidatePath("/"); revalidatePath(`/plugins/${slug}`); + + return { tracked: true } satisfies TrackInstallResult; }); diff --git a/apps/cursor/src/app/c/[slug]/opengraph-image.tsx b/apps/cursor/src/app/c/[slug]/opengraph-image.tsx index c950eb70..99b75c2c 100644 --- a/apps/cursor/src/app/c/[slug]/opengraph-image.tsx +++ b/apps/cursor/src/app/c/[slug]/opengraph-image.tsx @@ -1,9 +1,15 @@ import { getCompanyProfile } from "@/data/queries"; -import { createOGResponse, OG, OGLayout } from "@/lib/og"; +import { + createOGResponse, + OG, + OGLayout, + resolveOgImageUrl, +} from "@/lib/og"; export const alt = "Company Profile"; export const size = { width: OG.width, height: OG.height }; export const contentType = "image/png"; +export const revalidate = 86400; export default async function Image({ params, @@ -30,10 +36,12 @@ export default async function Image({ ); } + const logoUrl = resolveOgImageUrl(data.image); + return createOGResponse(
- {data.image && ( + {logoUrl && (
= {}; for (const c of components) { @@ -62,9 +66,9 @@ export default async function Image({ padding: 6, }} > - {data.logo ? ( + {logoUrl ? (
{data.author_name && ( -
+
by {data.author_name}
)} @@ -110,6 +122,7 @@ export default async function Image({ {data.description && (
- + {formatCount(data.install_count)}
{componentSummary && ( -
+
{componentSummary}
)} @@ -164,6 +179,7 @@ export default async function Image({
- {data.image && ( + {avatarUrl && (
{ + if (data?.tracked) { + setInstallCount((c) => c + 1); + } else if (data?.rateLimited) { + toast("Too many installs right now. Please try again in a bit."); + } + }, + }); const handleInstall = useCallback(() => { - setInstallCount((c) => c + 1); trackInstall({ pluginId: plugin.id, slug: plugin.slug }); }, [plugin.id, plugin.slug, trackInstall]); diff --git a/apps/cursor/src/lib/og.tsx b/apps/cursor/src/lib/og.tsx index 431e3b97..335ab13a 100644 --- a/apps/cursor/src/lib/og.tsx +++ b/apps/cursor/src/lib/og.tsx @@ -17,6 +17,39 @@ export const OG = { cardBg: "#1b1913", }; +const OG_CACHE_CONTROL = + "public, max-age=86400, s-maxage=86400, stale-while-revalidate=604800"; + +function ogSiteOrigin(): string { + const configured = process.env.NEXT_PUBLIC_APP_URL?.replace(/\/$/, ""); + if (configured) return configured; + if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; + return "https://cursor.directory"; +} + +/** + * Satori requires absolute http(s) image URLs. Plugin logos are sometimes stored + * as site-relative paths (e.g. `assets/logo.png`). + */ +export function resolveOgImageUrl( + src: string | null | undefined, +): string | null { + const trimmed = src?.trim(); + if (!trimmed) return null; + + try { + const parsed = new URL(trimmed); + if (parsed.protocol === "http:" || parsed.protocol === "https:") { + return parsed.toString(); + } + } catch { + // Fall through — treat as a path relative to the site origin. + } + + const path = trimmed.startsWith("/") ? trimmed : `/${trimmed}`; + return new URL(path, ogSiteOrigin()).toString(); +} + export async function loadFonts() { const [regular, bold] = await Promise.all([ readFile( @@ -121,6 +154,9 @@ export async function createOGResponse(element: ReactElement) { width: OG.width, height: OG.height, fonts, + headers: { + "Cache-Control": OG_CACHE_CONTROL, + }, }); } @@ -130,6 +166,7 @@ export async function createListingOG(title: string, subtitle: string) {