mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
* 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.
62 lines
1.9 KiB
TypeScript
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;
|
|
}
|