Files
clawhub/server/og/fetchPluginOgMeta.ts
T
Vyctor H. Brzezowski 6f537bf7ad fix(og): org profile images and Downloads metric in OG cards (#2840)
* fix(og): render org profile images in publisher OG cards

Pass publisher avatar, kind, and installs into OG meta URLs and allow
safely fetching public HTTPS org logos when generating profile images.

* fix: render org profile images in publisher OG cards

Org logos use public HTTPS URLs outside the GitHub/gravatar allowlist, so OG
generation fell back to the default mark. Allow SSRF-safe public fetches for
publisher profile avatars and embed avatar/kind metadata in OG URLs.

* fix(og): show Downloads instead of Installs on OG cards

Switch skill, plugin, and publisher OG image generators to read and
label download counts, with legacy installs query param fallback.

* fix(og): type skill API payload for canonical stat reads

Export SkillStatReadable so fetchSkillOgMeta can pass API skill stats
through readCanonicalStat without a TypeScript error.

* fix(og): format compact downloads in OG cards

Query-param download counts were rendered as raw integers on publisher
OG images. Reuse formatCompactStat, add download icon + lowercase label,
bump layout versions, and refresh org profile visual proof.

* fix(og): use Downloads label without icon on OG cards

Remove the download SVG from OG stat blocks and show only a muted
"Downloads" label above compact values. Bump skill/plugin/publisher
layout versions to bust cached social previews.
2026-06-24 14:08:49 -07:00

62 lines
1.9 KiB
TypeScript

export type PluginOgMeta = {
name: string | null;
displayName: string | null;
summary: string | null;
owner: string | null;
ownerImage: string | null;
latestVersion: string | null;
stats: {
downloads: number;
};
verification: {
scanStatus: string | null;
} | null;
};
export async function fetchPluginOgMeta(
packageName: string,
apiBase: string,
): Promise<PluginOgMeta | null> {
try {
const url = new URL(`/api/v1/packages/${encodeURIComponent(packageName)}`, apiBase);
const response = await fetch(url.toString(), { headers: { Accept: "application/json" } });
if (!response.ok) return null;
const payload = (await response.json()) as {
package?: {
name?: string;
displayName?: string;
summary?: string | null;
latestVersion?: string | null;
stats?: unknown;
verification?: { scanStatus?: string | null } | null;
} | null;
owner?: { handle?: string | null; image?: string | null } | null;
};
const stats = readStats(payload.package?.stats);
return {
name: payload.package?.name ?? null,
displayName: payload.package?.displayName ?? null,
summary: payload.package?.summary ?? null,
owner: payload.owner?.handle ?? null,
ownerImage: payload.owner?.image ?? null,
latestVersion: payload.package?.latestVersion ?? null,
stats: {
downloads: readNumber(stats.downloads),
},
verification: payload.package?.verification
? { scanStatus: payload.package.verification.scanStatus ?? null }
: null,
};
} catch {
return null;
}
}
function readStats(value: unknown): Record<string, unknown> {
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
}
function readNumber(value: unknown) {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
}