Files
clawhub/server/og/formatOgStats.ts
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

45 lines
1.4 KiB
TypeScript

import { formatCompactStat } from "../../src/lib/numberFormat";
function cleanQueryString(value: unknown) {
if (typeof value !== "string") return "";
return value.trim();
}
export function readOgDownloadsQuery(query: { downloads?: unknown; installs?: unknown }) {
const downloads = cleanQueryString(query.downloads);
if (downloads) return downloads;
return cleanQueryString(query.installs);
}
export function formatOgStat(value: number | null | undefined) {
const numericValue = typeof value === "number" && Number.isFinite(value) ? value : 0;
return formatCompactStat(numericValue);
}
function parseRawOgStatValue(raw: string) {
const trimmed = raw.trim();
if (!trimmed) return null;
if (/^-?\d+$/.test(trimmed)) {
const parsed = Number.parseInt(trimmed, 10);
return Number.isFinite(parsed) ? parsed : null;
}
const compact = /^(-?\d+(?:\.\d+)?)([kKmM])$/.exec(trimmed);
if (!compact) return null;
const base = Number.parseFloat(compact[1]);
if (!Number.isFinite(base)) return null;
return compact[2].toLowerCase() === "k" ? Math.round(base * 1_000) : Math.round(base * 1_000_000);
}
export function resolveOgDownloadsDisplay(
query: { downloads?: unknown; installs?: unknown },
fallback?: number | null,
) {
const raw = readOgDownloadsQuery(query);
if (raw) {
const numeric = parseRawOgStatValue(raw);
if (numeric !== null) return formatOgStat(numeric);
return raw;
}
return formatOgStat(fallback);
}