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.
This commit is contained in:
Vyctor H. Brzezowski
2026-06-24 14:08:49 -07:00
committed by GitHub
parent cfb21978dd
commit 6f537bf7ad
34 changed files with 389 additions and 99 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ type SkillStatDeltas = {
installsAllTime?: number;
};
type SkillStatReadable = {
export type SkillStatReadable = {
stats: Partial<
Pick<Doc<"skills">["stats"], "downloads" | "stars" | "installsCurrent" | "installsAllTime">
>;
Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

@@ -0,0 +1,17 @@
## Visual proof
Publisher OG images captured from local ClawHub at `http://localhost:3000/og/profile` against production Convex data.
Each card shows the org logo (or default lobster mark) and compact downloads (e.g. `43.5k`) with a muted **Downloads** label — not Installs.
### @nvidia (org with custom profile image + compact downloads)
![NVIDIA publisher OG image](nvidia-og.png)
### @openclaw (org with GitHub avatar + compact downloads)
![OpenClaw publisher OG image](openclaw-og.png)
### @expediagroup (org without profile image — default mark + compact downloads)
![Expedia Group publisher OG image](expediagroup-og.png)
+33 -1
View File
@@ -1,7 +1,11 @@
/* @vitest-environment node */
import { afterEach, describe, expect, it, vi } from "vitest";
import { fetchImageDataUrl, isTrustedOgImageUrl } from "./fetchImageDataUrl";
import {
fetchImageDataUrl,
isSafePublicHttpsOgImageUrl,
isTrustedOgImageUrl,
} from "./fetchImageDataUrl";
describe("fetchImageDataUrl", () => {
afterEach(() => {
@@ -17,6 +21,18 @@ describe("fetchImageDataUrl", () => {
expect(isTrustedOgImageUrl("https://example.com/avatar.png")).toBe(false);
});
it("allows public https org profile images on domain names", () => {
expect(
isSafePublicHttpsOgImageUrl("https://iprsoftwaremedia.com/219/files/202512/nvidia-logo.png"),
).toBe(true);
expect(isSafePublicHttpsOgImageUrl("https://avatars.githubusercontent.com/u/1?v=4")).toBe(true);
expect(isSafePublicHttpsOgImageUrl("http://example.com/logo.png")).toBe(false);
expect(isSafePublicHttpsOgImageUrl("https://127.0.0.1/logo.png")).toBe(false);
expect(isSafePublicHttpsOgImageUrl("https://localhost/logo.png")).toBe(false);
expect(isSafePublicHttpsOgImageUrl("https://metadata.google.internal/logo.png")).toBe(false);
expect(isSafePublicHttpsOgImageUrl("https://192.168.0.10/logo.png")).toBe(false);
});
it("does not fetch untrusted image URLs", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
@@ -26,6 +42,22 @@ describe("fetchImageDataUrl", () => {
expect(fetchMock).not.toHaveBeenCalled();
});
it("fetches public https org profile images", async () => {
const fetchMock = vi.fn(async () => {
return new Response(new Uint8Array([1, 2]), {
status: 200,
headers: { "content-type": "image/png" },
});
});
vi.stubGlobal("fetch", fetchMock);
await expect(
fetchImageDataUrl("https://cdn.example.com/org-logo.png", {
allowPublicHttps: true,
}),
).resolves.toBe("data:image/png;base64,AQI=");
});
it("converts trusted image responses to data URLs", async () => {
const fetchMock = vi.fn(async () => {
return new Response(new Uint8Array([1, 2]), {
+112 -8
View File
@@ -1,5 +1,6 @@
const MAX_IMAGE_BYTES = 1_500_000;
const IMAGE_FETCH_TIMEOUT_MS = 1_500;
const MAX_IMAGE_REDIRECTS = 3;
const TRUSTED_IMAGE_HOSTS = new Set([
"avatars.githubusercontent.com",
"camo.githubusercontent.com",
@@ -11,6 +12,17 @@ const TRUSTED_IMAGE_HOSTS = new Set([
"www.gravatar.com",
]);
const BLOCKED_IMAGE_HOSTNAMES = new Set([
"localhost",
"metadata.google.internal",
"metadata.google",
]);
type FetchImageDataUrlOptions = {
allowPublicHttps?: boolean;
followRedirects?: boolean;
};
export function isTrustedOgImageUrl(url: string | null | undefined) {
if (!url) return false;
try {
@@ -22,20 +34,46 @@ export function isTrustedOgImageUrl(url: string | null | undefined) {
}
}
export async function fetchImageDataUrl(url: string | null | undefined) {
export function isSafePublicHttpsOgImageUrl(url: string | null | undefined) {
if (!url) return false;
if (isTrustedOgImageUrl(url)) return true;
try {
const parsed = new URL(url);
if (parsed.protocol !== "https:") return false;
if (parsed.username || parsed.password) return false;
const hostname = parsed.hostname.toLowerCase().replace(/\.$/, "");
if (BLOCKED_IMAGE_HOSTNAMES.has(hostname)) return false;
if (
hostname.endsWith(".localhost") ||
hostname.endsWith(".local") ||
hostname.endsWith(".internal")
) {
return false;
}
if (isPrivateIpv4(hostname) || isPrivateIpv6(hostname)) return false;
return !isIpv4Hostname(hostname) && !hostname.includes(":");
} catch {
return false;
}
}
function isAllowedOgImageUrl(url: string, options: FetchImageDataUrlOptions) {
return options.allowPublicHttps ? isSafePublicHttpsOgImageUrl(url) : isTrustedOgImageUrl(url);
}
export async function fetchImageDataUrl(
url: string | null | undefined,
options: FetchImageDataUrlOptions = {},
) {
if (!url) return null;
try {
const parsed = new URL(url);
if (!isTrustedOgImageUrl(parsed.toString())) return null;
if (!isAllowedOgImageUrl(parsed.toString(), options)) return null;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), IMAGE_FETCH_TIMEOUT_MS);
try {
const response = await fetch(parsed, {
headers: { Accept: "image/avif,image/webp,image/png,image/jpeg,image/*" },
redirect: "manual",
signal: controller.signal,
});
if (!response.ok) return null;
const response = await fetchOgImageResponse(parsed, controller.signal, options);
if (!response?.ok) return null;
const contentType = response.headers.get("content-type")?.split(";")[0]?.trim();
if (!contentType?.startsWith("image/")) return null;
const buffer = await readLimitedImageBody(response);
@@ -49,6 +87,72 @@ export async function fetchImageDataUrl(url: string | null | undefined) {
}
}
export async function fetchPublisherProfileImageDataUrl(url: string | null | undefined) {
return fetchImageDataUrl(url, { allowPublicHttps: true, followRedirects: true });
}
async function fetchOgImageResponse(
initialUrl: URL,
signal: AbortSignal,
options: FetchImageDataUrlOptions,
) {
let currentUrl = initialUrl;
for (let hop = 0; hop <= MAX_IMAGE_REDIRECTS; hop += 1) {
const response = await fetch(currentUrl, {
headers: { Accept: "image/avif,image/webp,image/png,image/jpeg,image/*" },
redirect: "manual",
signal,
});
if (
options.followRedirects &&
response.status >= 300 &&
response.status < 400 &&
hop < MAX_IMAGE_REDIRECTS
) {
const location = response.headers.get("location")?.trim();
if (!location) return null;
const nextUrl = new URL(location, currentUrl);
if (!isAllowedOgImageUrl(nextUrl.toString(), options)) return null;
currentUrl = nextUrl;
continue;
}
return response;
}
return null;
}
function isIpv4Hostname(hostname: string) {
return parseIpv4(hostname) !== null;
}
function parseIpv4(hostname: string) {
const parts = hostname.split(".");
if (parts.length !== 4) return null;
const nums = parts.map((part) => Number.parseInt(part, 10));
if (nums.some((value) => !Number.isFinite(value) || value < 0 || value > 255)) return null;
return nums;
}
function isPrivateIpv4(hostname: string) {
const nums = parseIpv4(hostname);
if (!nums) return false;
const [a, b] = nums;
if (a === 10 || a === 127 || a === 0) return true;
if (a === 169 && b === 254) return true;
if (a === 172 && b >= 16 && b <= 31) return true;
if (a === 192 && b === 168) return true;
if (a === 100 && b >= 64 && b <= 127) return true;
return false;
}
function isPrivateIpv6(hostname: string) {
const lower = hostname.toLowerCase();
if (lower === "::1") return true;
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
if (lower.startsWith("fe80")) return true;
return false;
}
async function readLimitedImageBody(response: Response) {
const contentLength = response.headers.get("content-length");
if (contentLength) {
+2 -2
View File
@@ -8,7 +8,7 @@ describe("fetchPluginOgMeta", () => {
vi.unstubAllGlobals();
});
it("reads installs from package API stats", async () => {
it("reads downloads from package API stats", async () => {
const fetchMock = vi.fn(async () => ({
ok: true,
json: async () => ({
@@ -31,6 +31,6 @@ describe("fetchPluginOgMeta", () => {
"https://clawhub.ai/api/v1/packages/%40openclaw%2Fcodex",
{ headers: { Accept: "application/json" } },
);
expect(meta?.stats.installs).toBe(1200);
expect(meta?.stats.downloads).toBe(99);
});
});
+2 -2
View File
@@ -6,7 +6,7 @@ export type PluginOgMeta = {
ownerImage: string | null;
latestVersion: string | null;
stats: {
installs: number;
downloads: number;
};
verification: {
scanStatus: string | null;
@@ -41,7 +41,7 @@ export async function fetchPluginOgMeta(
ownerImage: payload.owner?.image ?? null,
latestVersion: payload.package?.latestVersion ?? null,
stats: {
installs: readNumber(stats.installs),
downloads: readNumber(stats.downloads),
},
verification: payload.package?.verification
? { scanStatus: payload.package.verification.scanStatus ?? null }
+2 -2
View File
@@ -29,7 +29,7 @@ describe("fetchPublisherOgMeta", () => {
vi.resetModules();
});
it("reads installs from publisher profile stats", async () => {
it("reads downloads from publisher profile stats", async () => {
queryMock.mockResolvedValue({
handle: "openclaw",
kind: "org",
@@ -46,6 +46,6 @@ describe("fetchPublisherOgMeta", () => {
expect(queryMock).toHaveBeenCalledWith("publishers.getProfileByHandle", {
handle: "openclaw",
});
expect(meta?.stats.installs).toBe(1200);
expect(meta?.stats.downloads).toBe(99);
});
});
+3 -2
View File
@@ -8,7 +8,7 @@ export type PublisherOgMeta = {
bio: string | null;
image: string | null;
stats: {
installs: number;
downloads: number;
};
};
@@ -19,6 +19,7 @@ type PublisherProfileResult = {
bio?: string | null;
image?: string | null;
stats?: {
downloads?: number;
installs?: number;
};
} | null;
@@ -40,7 +41,7 @@ export async function fetchPublisherOgMeta(
bio: profile.bio ?? null,
image: profile.image ?? null,
stats: {
installs: readNumber(profile.stats?.installs),
downloads: readNumber(profile.stats?.downloads),
},
};
} catch {
+3 -2
View File
@@ -8,7 +8,7 @@ describe("fetchSkillOgMeta", () => {
vi.unstubAllGlobals();
});
it("reads all-time installs from the public skill API stats", async () => {
it("reads downloads from the public skill API stats", async () => {
const fetchMock = vi.fn(async () => ({
ok: true,
json: async () => ({
@@ -16,6 +16,7 @@ describe("fetchSkillOgMeta", () => {
displayName: "Gifgrep",
summary: "Search GIFs fast",
stats: { downloads: 99, installsAllTime: 1200 },
statsDownloads: 1200,
},
owner: { handle: "steipete", image: "https://avatars.githubusercontent.com/u/1?v=4" },
latestVersion: { version: "1.0.1" },
@@ -32,6 +33,6 @@ describe("fetchSkillOgMeta", () => {
headers: { Accept: "application/json" },
},
);
expect(meta?.stats.installsAllTime).toBe(1200);
expect(meta?.stats.downloads).toBe(1200);
});
});
+10 -12
View File
@@ -1,3 +1,10 @@
import { readCanonicalStat, type SkillStatReadable } from "../../convex/lib/skillStats";
type SkillApiPayload = SkillStatReadable & {
displayName?: string;
summary?: string | null;
};
export type SkillOgMeta = {
displayName: string | null;
summary: string | null;
@@ -5,7 +12,7 @@ export type SkillOgMeta = {
ownerImage: string | null;
version: string | null;
stats: {
installsAllTime: number;
downloads: number;
};
moderation: {
verdict: "clean" | "suspicious" | "malicious" | null;
@@ -26,7 +33,7 @@ export async function fetchSkillOgMeta(
const response = await fetch(url.toString(), { headers: { Accept: "application/json" } });
if (!response.ok) return null;
const payload = (await response.json()) as {
skill?: { displayName?: string; summary?: string | null; stats?: unknown } | null;
skill?: SkillApiPayload | null;
owner?: { handle?: string | null; image?: string | null } | null;
latestVersion?: { version?: string | null } | null;
moderation?: {
@@ -35,7 +42,6 @@ export async function fetchSkillOgMeta(
isMalwareBlocked?: boolean;
} | null;
};
const stats = readStats(payload.skill?.stats);
return {
displayName: payload.skill?.displayName ?? null,
summary: payload.skill?.summary ?? null,
@@ -43,7 +49,7 @@ export async function fetchSkillOgMeta(
ownerImage: payload.owner?.image ?? null,
version: payload.latestVersion?.version ?? null,
stats: {
installsAllTime: readNumber(stats.installsAllTime),
downloads: payload.skill ? readCanonicalStat(payload.skill, "downloads") : 0,
},
moderation: payload.moderation
? {
@@ -57,11 +63,3 @@ export async function fetchSkillOgMeta(
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;
}
+38
View File
@@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import { formatOgStat, readOgDownloadsQuery, resolveOgDownloadsDisplay } from "./formatOgStats";
describe("formatOgStat", () => {
it("formats large counts with compact k suffix", () => {
expect(formatOgStat(43_456)).toBe("43.5k");
expect(formatOgStat(282_345)).toBe("282k");
});
it("formats millions with compact M suffix", () => {
expect(formatOgStat(2_360_000)).toBe("2.4M");
});
});
describe("readOgDownloadsQuery", () => {
it("prefers downloads over legacy installs", () => {
expect(readOgDownloadsQuery({ downloads: "1200", installs: "9.9k" })).toBe("1200");
});
it("falls back to installs when downloads is missing", () => {
expect(readOgDownloadsQuery({ installs: "9.9k" })).toBe("9.9k");
});
});
describe("resolveOgDownloadsDisplay", () => {
it("formats raw integer query params", () => {
expect(resolveOgDownloadsDisplay({ downloads: "43456" })).toBe("43.5k");
expect(resolveOgDownloadsDisplay({ downloads: "282345" })).toBe("282k");
});
it("formats metadata fallback values", () => {
expect(resolveOgDownloadsDisplay({}, 1200)).toBe("1.2k");
});
it("passes through already-compact query values", () => {
expect(resolveOgDownloadsDisplay({ downloads: "43.5k" })).toBe("43.5k");
});
});
+41 -4
View File
@@ -1,7 +1,44 @@
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;
const abs = Math.abs(numericValue);
if (abs >= 1_000_000) return `${(numericValue / 1_000_000).toFixed(abs >= 10_000_000 ? 0 : 1)}m`;
if (abs >= 1_000) return `${(numericValue / 1_000).toFixed(abs >= 10_000 ? 0 : 1)}k`;
return String(numericValue);
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);
}
+8 -6
View File
@@ -1,5 +1,11 @@
import { FONT_SANS } from "./ogAssets";
import { escapeXml, OPENCLAW_RED, type RegistryOgStat, wrapText } from "./registryOgSvg";
import {
escapeXml,
OPENCLAW_RED,
type RegistryOgStat,
statLabelMarkup,
wrapText,
} from "./registryOgSvg";
export type PublisherOgSvgParams = {
markDataUrl: string;
@@ -15,11 +21,7 @@ export type PublisherOgSvgParams = {
function statBlock(stats: RegistryOgStat[] | undefined, x: number, y: number) {
const stat = stats?.[0] ?? { value: "ClawHub", label: "Publisher" };
return `<g>
<text x="${x}" y="${y}"
fill="#9D9692"
font-size="22"
font-weight="700"
font-family="${FONT_SANS}, sans-serif">${escapeXml(stat.label)}</text>
${statLabelMarkup(x, y, stat.label, { fontSize: 22 })}
<text x="${x}" y="${y + 44}"
fill="#F7F1EA"
font-size="44"
+19 -5
View File
@@ -7,6 +7,24 @@ export type RegistryOgStat = {
label: string;
};
export function buildOgDownloadsStat(value: string): RegistryOgStat {
return { value, label: "Downloads" };
}
export function statLabelMarkup(
x: number,
y: number,
label: string,
options?: { fontSize?: number },
) {
const fontSize = options?.fontSize ?? 21;
return `<text x="${x}" y="${y}"
fill="#9D9692"
font-size="${fontSize}"
font-weight="700"
font-family="${FONT_SANS}, sans-serif">${escapeXml(label)}</text>`;
}
export type RegistryOgCommand = {
subject: string;
action: string;
@@ -152,11 +170,7 @@ function statColumns(stats: RegistryOgStat[], contentX: number) {
.map((stat, index) => {
const x = contentX + index * 190;
return `<g>
<text x="${x}" y="424"
fill="#9D9692"
font-size="21"
font-weight="700"
font-family="${FONT_SANS}, sans-serif">${escapeXml(stat.label)}</text>
${statLabelMarkup(x, 424, stat.label)}
<text x="${x}" y="464"
fill="#F7F1EA"
font-size="34"
+1 -1
View File
@@ -16,7 +16,7 @@ describe("skill OG SVG", () => {
target: "discord-doctor",
},
stats: [
{ value: "1.2k", label: "Installs" },
{ value: "1.2k", label: "Downloads" },
{ value: "PASS", label: "Audit" },
],
});
+10 -10
View File
@@ -111,7 +111,7 @@ describe("plugin og route", () => {
latestVersion: null,
displayName: "Codex",
summary: "OpenClaw Codex harness.",
stats: { installs: 1200 },
stats: { downloads: 1200 },
verification: { scanStatus: "pending" },
});
@@ -126,7 +126,7 @@ describe("plugin og route", () => {
expect(buildPluginOgSvgMock).toHaveBeenCalledWith(
expect.objectContaining({
stats: [
{ value: "1.2k", label: "Installs" },
{ value: "1.2k", label: "Downloads" },
{ value: "PENDING", label: "Audit" },
],
}),
@@ -142,7 +142,7 @@ describe("plugin og route", () => {
latestVersion: "1.0.0",
displayName: "Codex",
summary: "OpenClaw Codex harness.",
stats: { installs: 1200 },
stats: { downloads: 1200 },
verification: { scanStatus: "clean" },
});
@@ -153,7 +153,7 @@ describe("plugin og route", () => {
expect(buildPluginOgSvgMock).toHaveBeenCalledWith(
expect.objectContaining({
stats: [
{ value: "1.2k", label: "Installs" },
{ value: "1.2k", label: "Downloads" },
{ value: "PASS", label: "Audit" },
],
}),
@@ -166,7 +166,7 @@ describe("plugin og route", () => {
owner: "openclaw",
title: "Codex",
description: "OpenClaw Codex harness.",
installs: "0",
downloads: "0",
});
const handler = (await import("./plugin.png")).default;
@@ -176,21 +176,21 @@ describe("plugin og route", () => {
expect(buildPluginOgSvgMock).toHaveBeenCalledWith(
expect.objectContaining({
stats: [
{ value: "0", label: "Installs" },
{ value: "0", label: "Downloads" },
{ value: "UNKNOWN", label: "Audit" },
],
}),
);
});
it("uses explicit installs over legacy downloads query params", async () => {
it("uses explicit downloads over legacy installs query params", async () => {
getQueryMock.mockReturnValue({
name: "@openclaw/codex",
owner: "openclaw",
title: "Codex",
description: "OpenClaw Codex harness.",
installs: "0",
downloads: "9.9k",
downloads: "0",
installs: "9.9k",
});
const handler = (await import("./plugin.png")).default;
@@ -200,7 +200,7 @@ describe("plugin og route", () => {
expect(buildPluginOgSvgMock).toHaveBeenCalledWith(
expect.objectContaining({
stats: [
{ value: "0", label: "Installs" },
{ value: "0", label: "Downloads" },
{ value: "UNKNOWN", label: "Audit" },
],
}),
+4 -6
View File
@@ -2,7 +2,7 @@ import { Resvg } from "@resvg/resvg-wasm";
import { defineEventHandler, getQuery, getRequestHost, setHeader } from "h3";
import { fetchImageDataUrl } from "../../og/fetchImageDataUrl";
import { fetchPluginOgMeta } from "../../og/fetchPluginOgMeta";
import { formatOgStat } from "../../og/formatOgStats";
import { resolveOgDownloadsDisplay } from "../../og/formatOgStats";
import {
ensureResvgWasm,
FONT_MONO,
@@ -13,12 +13,14 @@ import {
} from "../../og/ogAssets";
import { buildPluginOgSvg } from "../../og/pluginOgSvg";
import { pngResponse } from "../../og/pngResponse";
import { buildOgDownloadsStat } from "../../og/registryOgSvg";
type OgQuery = {
name?: string;
owner?: string;
title?: string;
description?: string;
downloads?: string;
installs?: string;
audit?: string;
avatar?: string;
@@ -63,7 +65,6 @@ export default defineEventHandler(async (event) => {
const ownerFromQuery = cleanString(query.owner);
const titleFromQuery = cleanString(query.title);
const descriptionFromQuery = cleanString(query.description);
const installsFromQuery = cleanString(query.installs);
const auditFromQuery = cleanString(query.audit);
const avatarFromQuery = cleanString(query.avatar);
const needFetch = !ownerFromQuery || !titleFromQuery || !descriptionFromQuery;
@@ -99,10 +100,7 @@ export default defineEventHandler(async (event) => {
target: `clawhub:${packageName}`,
},
stats: [
{
value: installsFromQuery || formatOgStat(meta?.stats.installs),
label: "Installs",
},
buildOgDownloadsStat(resolveOgDownloadsDisplay(query, meta?.stats.downloads)),
{
value: (auditFromQuery || getAuditLabel(meta?.verification?.scanStatus)).replace(
/^Audit\s+/i,
+8 -11
View File
@@ -1,8 +1,8 @@
import { Resvg } from "@resvg/resvg-wasm";
import { defineEventHandler, getQuery, setHeader } from "h3";
import { fetchImageDataUrl } from "../../og/fetchImageDataUrl";
import { fetchPublisherProfileImageDataUrl } from "../../og/fetchImageDataUrl";
import { fetchPublisherOgMeta } from "../../og/fetchPublisherOgMeta";
import { formatOgStat } from "../../og/formatOgStats";
import { readOgDownloadsQuery, resolveOgDownloadsDisplay } from "../../og/formatOgStats";
import {
ensureResvgWasm,
FONT_MONO,
@@ -13,11 +13,13 @@ import {
} from "../../og/ogAssets";
import { pngResponse } from "../../og/pngResponse";
import { buildPublisherOgSvg } from "../../og/publisherOgSvg";
import { buildOgDownloadsStat } from "../../og/registryOgSvg";
type OgQuery = {
handle?: string;
title?: string;
description?: string;
downloads?: string;
installs?: string;
kind?: string;
avatar?: string;
@@ -43,11 +45,11 @@ export default defineEventHandler(async (event) => {
const titleFromQuery = cleanString(query.title);
const descriptionFromQuery = cleanString(query.description);
const installsFromQuery = cleanString(query.installs);
const kindFromQuery = cleanString(query.kind);
const avatarFromQuery = cleanString(query.avatar);
const convexUrl = getConvexUrl();
const needFetch = !titleFromQuery || !descriptionFromQuery || !installsFromQuery;
const needFetch =
!titleFromQuery || !descriptionFromQuery || !readOgDownloadsQuery(query) || !avatarFromQuery;
const meta = needFetch && convexUrl ? await fetchPublisherOgMeta(handle, convexUrl) : null;
const handleLabel = `@${meta?.handle || handle}`;
const title = titleFromQuery || meta?.displayName || handleLabel;
@@ -58,7 +60,7 @@ export default defineEventHandler(async (event) => {
getWatermarkDataUrl(),
ensureResvgWasm().then(() => getFontBuffers()),
]);
const avatarDataUrl = await fetchImageDataUrl(avatarFromQuery || meta?.image);
const avatarDataUrl = await fetchPublisherProfileImageDataUrl(avatarFromQuery || meta?.image);
const svg = buildPublisherOgSvg({
markDataUrl,
@@ -68,12 +70,7 @@ export default defineEventHandler(async (event) => {
title,
description,
handleLabel,
stats: [
{
value: installsFromQuery || formatOgStat(meta?.stats.installs),
label: "Installs",
},
],
stats: [buildOgDownloadsStat(resolveOgDownloadsDisplay(query, meta?.stats.downloads))],
});
const resvg = new Resvg(svg, {
+31 -8
View File
@@ -108,7 +108,7 @@ describe("skill og route", () => {
version: "1.0.1",
title: "Gifgrep",
description: "Search GIFs fast",
installs: "0",
downloads: "0",
});
const handler = (await import("./skill.png")).default;
@@ -132,7 +132,7 @@ describe("skill og route", () => {
target: "gifgrep",
},
stats: [
{ value: "0", label: "Installs" },
{ value: "0", label: "Downloads" },
{ value: "PASS", label: "Audit" },
],
});
@@ -157,7 +157,7 @@ describe("skill og route", () => {
displayName: "Gifgrep",
summary: "Search GIFs fast",
ownerImage: null,
stats: { installsAllTime: 1200 },
stats: { downloads: 1200 },
moderation: { verdict: "clean", isSuspicious: false, isMalwareBlocked: false },
});
@@ -177,22 +177,22 @@ describe("skill og route", () => {
ownerLabel: "@steipete",
versionLabel: "latest",
stats: [
{ value: "1.2k", label: "Installs" },
{ value: "1.2k", label: "Downloads" },
{ value: "PASS", label: "Audit" },
],
}),
);
});
it("uses explicit installs over legacy downloads query params", async () => {
it("uses explicit downloads over legacy installs query params", async () => {
getQueryMock.mockReturnValue({
slug: "gifgrep",
owner: "steipete",
version: "1.0.1",
title: "Gifgrep",
description: "Search GIFs fast",
installs: "0",
downloads: "9.9k",
downloads: "0",
installs: "9.9k",
});
const handler = (await import("./skill.png")).default;
@@ -202,7 +202,30 @@ describe("skill og route", () => {
expect(buildSkillOgSvgMock).toHaveBeenCalledWith(
expect.objectContaining({
stats: [
{ value: "0", label: "Installs" },
{ value: "0", label: "Downloads" },
{ value: "PASS", label: "Audit" },
],
}),
);
});
it("formats raw integer downloads query params", async () => {
getQueryMock.mockReturnValue({
slug: "gifgrep",
owner: "steipete",
version: "1.0.1",
title: "Gifgrep",
description: "Search GIFs fast",
downloads: "43456",
});
const handler = (await import("./skill.png")).default;
await handler({} as never);
expect(buildSkillOgSvgMock).toHaveBeenCalledWith(
expect.objectContaining({
stats: [
{ value: "43.5k", label: "Downloads" },
{ value: "PASS", label: "Audit" },
],
}),
+4 -6
View File
@@ -2,7 +2,7 @@ import { Resvg } from "@resvg/resvg-wasm";
import { defineEventHandler, getQuery, getRequestHost, setHeader } from "h3";
import { fetchImageDataUrl } from "../../og/fetchImageDataUrl";
import { fetchSkillOgMeta } from "../../og/fetchSkillOgMeta";
import { formatOgStat } from "../../og/formatOgStats";
import { resolveOgDownloadsDisplay } from "../../og/formatOgStats";
import {
ensureResvgWasm,
FONT_MONO,
@@ -12,6 +12,7 @@ import {
getWatermarkDataUrl,
} from "../../og/ogAssets";
import { pngResponse } from "../../og/pngResponse";
import { buildOgDownloadsStat } from "../../og/registryOgSvg";
import { buildSkillOgSvg } from "../../og/skillOgSvg";
type OgQuery = {
@@ -20,6 +21,7 @@ type OgQuery = {
version?: string;
title?: string;
description?: string;
downloads?: string;
installs?: string;
audit?: string;
avatar?: string;
@@ -54,7 +56,6 @@ export default defineEventHandler(async (event) => {
const versionFromQuery = cleanString(query.version);
const titleFromQuery = cleanString(query.title);
const descriptionFromQuery = cleanString(query.description);
const installsFromQuery = cleanString(query.installs);
const auditFromQuery = cleanString(query.audit);
const avatarFromQuery = cleanString(query.avatar);
@@ -101,10 +102,7 @@ export default defineEventHandler(async (event) => {
target: slug,
},
stats: [
{
value: installsFromQuery || formatOgStat(meta?.stats.installsAllTime),
label: "Installs",
},
buildOgDownloadsStat(resolveOgDownloadsDisplay(query, meta?.stats.downloads)),
{ value: auditLabel.replace(/^Audit\s+/i, ""), label: "Audit" },
],
});
+2 -2
View File
@@ -337,11 +337,11 @@ describe("skill route loader", () => {
{ property: "og:url", content: "https://clawhub.ai/steipete/skills/weather" },
{
property: "og:image",
content: "https://clawhub.ai/og/skill?v=8&slug=weather&owner=steipete&version=1.0.0",
content: "https://clawhub.ai/og/skill?v=10&slug=weather&owner=steipete&version=1.0.0",
},
{
name: "twitter:image",
content: "https://clawhub.ai/og/skill?v=8&slug=weather&owner=steipete&version=1.0.0",
content: "https://clawhub.ai/og/skill?v=10&slug=weather&owner=steipete&version=1.0.0",
},
]),
);
+11 -3
View File
@@ -19,7 +19,7 @@ describe("og helpers", () => {
expect(meta.url).toContain("/steipete/skills/weather");
expect(meta.owner).toBe("steipete");
expect(meta.image).toContain("/og/skill?");
expect(meta.image).toContain("v=8");
expect(meta.image).toContain("v=10");
expect(meta.image).toContain("slug=weather");
expect(meta.image).toContain("owner=steipete");
expect(meta.image).toContain("version=1.2.3");
@@ -39,7 +39,7 @@ describe("og helpers", () => {
expect(meta.description).toBe("OpenClaw Codex harness.");
expect(meta.url).toBe("https://clawhub.ai/openclaw/plugins/codex");
expect(meta.image).toContain("/og/plugin?");
expect(meta.image).toContain("v=3");
expect(meta.image).toContain("v=5");
expect(meta.image).toContain("name=%40openclaw%2Fcodex");
expect(meta.image).toContain("version=1.0.0");
});
@@ -49,13 +49,21 @@ describe("og helpers", () => {
handle: "@byungkyu",
displayName: "byungkyu",
bio: "maton.ai",
image: "https://example.com/logo.png",
kind: "org",
downloads: 1200,
});
expect(meta.title).toBe("byungkyu — ClawHub");
expect(meta.description).toBe("maton.ai");
expect(meta.url).toBe("https://clawhub.ai/byungkyu");
expect(meta.image).toContain("/og/profile?");
expect(meta.image).toContain("v=3");
expect(meta.image).toContain("v=7");
expect(meta.image).toContain("handle=byungkyu");
expect(meta.image).toContain("title=byungkyu");
expect(meta.image).toContain("description=maton.ai");
expect(meta.image).toContain("kind=org");
expect(meta.image).toContain("avatar=https%3A%2F%2Fexample.com%2Flogo.png");
expect(meta.image).toContain("downloads=1200");
});
it("uses defaults when owner and summary are missing", () => {
+14 -3
View File
@@ -32,6 +32,9 @@ type PublisherMetaSource = {
handle: string;
displayName?: string | null;
bio?: string | null;
image?: string | null;
kind?: "user" | "org";
downloads?: number | null;
};
type BasicMeta = {
@@ -41,9 +44,9 @@ type BasicMeta = {
url: string;
};
const OG_SKILL_IMAGE_LAYOUT_VERSION = "8";
const OG_PLUGIN_IMAGE_LAYOUT_VERSION = "3";
const OG_PUBLISHER_IMAGE_LAYOUT_VERSION = "3";
const OG_SKILL_IMAGE_LAYOUT_VERSION = "10";
const OG_PLUGIN_IMAGE_LAYOUT_VERSION = "5";
const OG_PUBLISHER_IMAGE_LAYOUT_VERSION = "7";
function getSiteUrl() {
return getClawHubSiteUrl();
@@ -129,11 +132,19 @@ export function buildPublisherMeta(source: PublisherMetaSource): BasicMeta {
const handle = clean(source.handle).replace(/^@+/, "");
const displayName = clean(source.displayName) || `@${handle}`;
const bio = clean(source.bio);
const image = clean(source.image);
const title = `${displayName} — ClawHub`;
const description = bio || `Publisher @${handle} on ClawHub.`;
const imageParams = new URLSearchParams();
imageParams.set("v", OG_PUBLISHER_IMAGE_LAYOUT_VERSION);
imageParams.set("handle", handle);
imageParams.set("title", displayName);
imageParams.set("description", truncate(description, 200));
if (source.kind === "org") imageParams.set("kind", "org");
if (image) imageParams.set("avatar", image);
if (typeof source.downloads === "number" && Number.isFinite(source.downloads)) {
imageParams.set("downloads", String(Math.max(0, Math.trunc(source.downloads))));
}
return {
title,
description: truncate(description, 200),
+3
View File
@@ -19,6 +19,9 @@ export const Route = createFileRoute("/$slug")({
handle: publisher.handle ?? params.slug,
displayName: publisher.displayName,
bio: publisher.bio,
image: publisher.image,
kind: publisher.kind,
downloads: publisher.stats.downloads,
});
return {
meta: [
+10 -2
View File
@@ -94,8 +94,16 @@ export const Route = createFileRoute("/user/$handle")({
if (!publisher) throw notFound();
return { publisher };
},
head: ({ params }) => {
const meta = buildPublisherMeta({ handle: params.handle });
head: ({ params, loaderData }) => {
const publisher = loaderData?.publisher;
const meta = buildPublisherMeta({
handle: publisher?.handle ?? params.handle,
displayName: publisher?.displayName,
bio: publisher?.bio,
image: publisher?.image,
kind: publisher?.kind,
downloads: publisher?.stats.downloads,
});
return {
meta: [
{ title: meta.title },