Thumbnail plugin card screenshots through Cloudflare image transformations (#6240)

* perf(website): thumbnail plugin card screenshots

* fix(website): preserve and sandbox plugin screenshot fallbacks
This commit is contained in:
Mohamed Boudra
2026-10-06 17:30:18 +01:00
committed by GitHub
parent 48059aeae3
commit 64c9f263e8
5 changed files with 407 additions and 2 deletions
+47 -1
View File
@@ -1,4 +1,5 @@
import { expect, test, type Page } from "playwright/test";
import { expect, test, type Page, type Locator } from "playwright/test";
import registry from "./registry.fixture.json" with { type: "json" };
import { CATEGORIES } from "../src/plugins/categories";
async function openPlugins(page: Page) {
@@ -306,6 +307,26 @@ test("keeps the directory unlinked until the coordinated announcement", async ({
test.describe("registry fixture layout", () => {
test.skip(Boolean(process.env.WEBSITE_TEST_URL), "Requires the local registry fixture");
test("uses density-aware card thumbnails and keeps detail screenshots original", async ({
page,
}) => {
const plugin = registry.plugins.find((entry) => entry.id === "alhassanaraouf/base2tone")!;
const source = plugin.screenshots[0];
await openPlugins(page);
await expectThumbnailCard(
page.getByRole("region", { name: "What’s new" }).getByRole("link", { name: /Base2Tone/ }),
source,
plugin.id,
);
await page.goto("/plugins/all");
const card = page.getByRole("main").getByRole("link", { name: /Base2Tone/ });
await expectThumbnailCard(card, source, plugin.id);
await card.click();
await expect(
page.getByRole("img", { name: "Base2Tone screenshot 1", exact: true }),
).toHaveAttribute("src", source);
});
test("lists the nine categories in order with counts, and the newest plugins first", async ({
page,
}) => {
@@ -320,3 +341,28 @@ test.describe("registry fixture layout", () => {
).toHaveText([/Base2Tone/, /Sayr/, /PromptKit/, /Defer/]);
});
});
async function expectThumbnailCard(card: Locator, source: string, id: string) {
// Card screenshots are decorative and hidden from the accessibility tree.
const image = card.locator("img").first();
const path = (width: number) =>
`/plugins/thumb/${width}/${encodeURIComponent(source)}?plugin=${encodeURIComponent(id)}`;
await expect(image).toHaveAttribute("src", path(592));
await expect(image).toHaveAttribute("srcset", `${path(592)} 1x, ${path(1184)} 2x`);
await expect(image).toHaveAttribute("loading", "lazy");
await expect(image).toHaveAttribute("decoding", "async");
await image.scrollIntoViewIfNeeded();
await expect
.poll(
() =>
image.evaluate((element: HTMLImageElement) => element.complete && element.naturalWidth > 0),
{ timeout: 15_000 },
)
.toBe(true);
const box = await image.evaluate((element) => {
const rect = element.parentElement!.getBoundingClientRect();
return { width: rect.width, height: rect.height };
});
expect(box.width).toBeGreaterThan(0);
expect(box.width / box.height).toBeCloseTo(1.6, 2);
}
+3 -1
View File
@@ -1,4 +1,5 @@
import { Download } from "lucide-react";
import { pluginCardScreenshot } from "./thumbnails";
import { pluginHref } from "./links";
import { PluginTile } from "./plugin-tile";
import { formatInstalls, getCategory, type Plugin } from "./registry";
@@ -16,9 +17,10 @@ function PluginShot({ plugin }: { plugin: Plugin }) {
>
{url ? (
<img
src={url}
{...pluginCardScreenshot(plugin.id, url)}
alt=""
loading="lazy"
decoding="async"
className="h-full w-full object-cover object-left-top"
/>
) : (
@@ -4,6 +4,7 @@ import {
PublishedPluginDetailSchema,
} from "@getpaseo/protocol/plugin-registry";
import { getBlockingColdCache, type WebsiteCacheContext } from "../github-cache";
import { handlePluginThumbnailRequest } from "./thumbnails";
import { CATEGORIES } from "./categories";
import { type InstallCounts, readInstallCounts, recordClientInstall } from "./installs";
export interface RegistryEnvironment {
@@ -63,6 +64,10 @@ export async function handlePluginRegistryRequest(
const url = new URL(request.url);
if (request.method !== "GET") return null;
const base = env.PLUGINS_REGISTRY_URL ?? "https://getpaseo.github.io/plugins";
if (url.pathname.startsWith("/plugins/thumb/")) {
const index = await loadRegistryIndex(base, context);
return handlePluginThumbnailRequest(request, index.plugins);
}
if (url.hostname === "plugins.paseo.sh" && url.pathname === "/index.json")
return Response.json(await loadRegistryIndex(base, context));
if (url.pathname === "/sitemap-plugins.xml") {
@@ -0,0 +1,234 @@
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { handlePluginThumbnailRequest, pluginCardScreenshot } from "./thumbnails";
import { NewPluginCard, PluginCard } from "./plugin-card";
import type { Plugin } from "./registry";
const source = "https://cdn.jsdelivr.net/npm/paseo-example@1.2.3/preview.png";
const plugin: Plugin = {
id: "acme/example",
name: "Example",
description: "An example",
categories: ["themes"],
author: { github: "acme" },
repository: { url: "https://github.com/acme/example" },
artifact: {
kind: "npm",
package: "paseo-example",
version: "1.2.3",
resolved: "https://registry.npmjs.org/example.tgz",
integrity: "sha512-YWJj",
},
screenshots: [source],
submittedAt: "2026-10-03",
reviewedAt: "2026-10-03",
updatedAt: "2026-10-03",
publishedAt: "2026-10-03",
installs: 42,
};
describe("plugin card thumbnails", () => {
it("keeps a screenshot on an unsupported host visible at its original URL", () => {
const screenshot = "https://example.com/preview.png";
const html = renderToStaticMarkup(
createElement(PluginCard, { plugin: { ...plugin, screenshots: [screenshot] } }),
);
expect(html).toContain(`src="${screenshot}"`);
expect(html).not.toContain("/plugins/thumb/");
});
it.each([PluginCard, NewPluginCard])(
"renders a thumbnail instead of downloading the original screenshot (%s)",
(Card) => {
const html = renderToStaticMarkup(createElement(Card, { plugin, added: "today" }));
expect(html).toContain(
`src="/plugins/thumb/592/${encodeURIComponent(source)}?plugin=acme%2Fexample"`,
);
expect(html).toContain(
`srcSet="/plugins/thumb/592/${encodeURIComponent(source)}?plugin=acme%2Fexample 1x, /plugins/thumb/1184/${encodeURIComponent(source)}?plugin=acme%2Fexample 2x"`,
);
expect(html).toContain('loading="lazy"');
expect(html).toContain('decoding="async"');
},
);
});
function thumbnailRequest(
sourceUrl = source,
width = 592,
id = plugin.id,
accept = "image/avif,image/webp",
) {
return new Request(
`https://paseo.sh/plugins/thumb/${width}/${encodeURIComponent(sourceUrl)}?plugin=${encodeURIComponent(id)}`,
{ headers: { accept } },
);
}
function imageResponse(body: string, headers: Record<string, string> = {}) {
return new Response(body, { headers: { "content-type": "image/png", ...headers } });
}
// A typed fetch dependency, returning real Responses without patching the global fetch.
function imageFetch(...responses: (Response | Error)[]) {
const calls: { input: Parameters<typeof fetch>[0]; init: Parameters<typeof fetch>[1] }[] = [];
const fetchImage: typeof fetch = async (input, init) => {
calls.push({ input, init });
const response = responses.shift();
if (!response) throw new Error("Unexpected fetch");
if (response instanceof Error) throw response;
return response;
};
return { calls, fetchImage };
}
describe("thumbnail route", () => {
it("sandboxes an SVG fallback when opened as a document on the website origin", async () => {
const svg =
'<svg xmlns="http://www.w3.org/2000/svg"><script>alert(document.domain)</script></svg>';
const upstream = imageFetch(
new Response("unsupported", { status: 415 }),
imageResponse(svg, { "content-type": "image/svg+xml" }),
);
const response = await handlePluginThumbnailRequest(
thumbnailRequest(),
[plugin],
upstream.fetchImage,
);
expect(await response.text()).toBe(svg);
expect(response.headers.get("content-security-policy")).toBe("sandbox");
expect(response.headers.get("x-content-type-options")).toBe("nosniff");
});
it.each([
[592, source, "image/avif,image/webp", "avif"],
[592, source, "image/avif;q=0,image/webp", "webp"],
[592, source, "image/avif;q=0,image/webp;q=0", undefined],
[
1184,
"https://raw.githubusercontent.com/acme/example/abc123/preview.png",
"image/webp",
"webp",
],
[592, source, "image/png,image/*;q=0.8", undefined],
] as const)(
"transforms a registered screenshot at %s from %s for %s",
async (width, screenshot, accept, format) => {
const upstream = imageFetch(imageResponse("thumbnail"));
const response = await handlePluginThumbnailRequest(
thumbnailRequest(screenshot, width, plugin.id, accept),
[{ ...plugin, screenshots: [screenshot] }],
upstream.fetchImage,
);
expect(upstream.calls).toEqual([
{
input: screenshot,
init: {
cf: { image: { width, fit: "scale-down", format, quality: 80 } },
},
},
]);
expect(response.status).toBe(200);
expect(await response.text()).toBe("thumbnail");
expect(response.headers.get("content-type")).toBe("image/png");
expect(response.headers.get("cache-control")).toBe("public, max-age=31536000, immutable");
expect(response.headers.get("vary")).toBe("Accept");
},
);
it.each([
["Cloudflare error", () => new Response("error 9401", { status: 403 })],
[
"non-image success",
() => new Response("<html>unavailable</html>", { headers: { "content-type": "text/html" } }),
],
["Cf-Resized error", () => imageResponse("error", { "cf-resized": "err=9401" })],
["network exception", () => new Error("Transform unavailable")],
] as const)("proxies the original with immutable caching on %s", async (_, failure) => {
const upstream = imageFetch(failure(), imageResponse("original"));
const response = await handlePluginThumbnailRequest(
thumbnailRequest(),
[plugin],
upstream.fetchImage,
);
expect(upstream.calls.map((call) => call.input)).toEqual([source, source]);
expect(upstream.calls[1].init).toBeUndefined();
expect(response.status).toBe(200);
expect(await response.text()).toBe("original");
expect(response.headers.get("cache-control")).toBe("public, max-age=31536000, immutable");
});
it.each([
["arbitrary width", thumbnailRequest(source, 593), [plugin], 400],
["unlisted source on allowed host", thumbnailRequest(source + "?different"), [plugin], 404],
["wrong plugin ID", thumbnailRequest(source, 592, "acme/other"), [plugin], 404],
[
"missing plugin ID",
new Request(`https://paseo.sh/plugins/thumb/592/${encodeURIComponent(source)}`),
[plugin],
404,
],
["removed plugin", thumbnailRequest(), [], 404],
["malformed encoding", new Request("https://paseo.sh/plugins/thumb/592/%ZZ"), [plugin], 400],
["malformed URL", thumbnailRequest("not a URL"), [plugin], 400],
...[
"https://example.com/preview.png",
"https://cdn.jsdelivr.net.evil.test/preview.png",
"http://cdn.jsdelivr.net/preview.png",
"https://user:pass@cdn.jsdelivr.net/preview.png",
"https://cdn.jsdelivr.net:8080/preview.png",
].map(
(url) =>
[
"disallowed registered source",
thumbnailRequest(url),
[{ ...plugin, screenshots: [url] }],
400,
] satisfies [string, Request, Plugin[], number],
),
] satisfies [string, Request, Plugin[], number][])(
"rejects %s without fetching an image",
async (_, request, plugins, status) => {
const upstream = imageFetch();
const response = await handlePluginThumbnailRequest(
request,
[...plugins],
upstream.fetchImage,
);
expect(response.status).toBe(status);
expect(upstream.calls).toEqual([]);
expect(response.headers.get("cache-control")).toBeNull();
},
);
it.each([
() => new Response("missing", { status: 404 }),
() => new Response("<html>error</html>", { headers: { "content-type": "text/html" } }),
() => new Error("Origin unavailable"),
])("does not cache a failed original for a year", async (failure) => {
const upstream = imageFetch(new Response("transform error", { status: 502 }), failure());
const response = await handlePluginThumbnailRequest(
thumbnailRequest(),
[plugin],
upstream.fetchImage,
);
expect(response.status).toBe(502);
expect(response.headers.get("cache-control")).toBe("no-store");
});
it("round-trips screenshot URL punctuation through the card URL", async () => {
const screenshot = source + "?name=a&other=b#preview";
const upstream = imageFetch(imageResponse("thumbnail"));
const request = new Request(
"https://paseo.sh" + pluginCardScreenshot(plugin.id, screenshot).src,
);
const response = await handlePluginThumbnailRequest(
request,
[{ ...plugin, screenshots: [screenshot] }],
upstream.fetchImage,
);
expect(response.status).toBe(200);
expect(upstream.calls[0].input).toBe(screenshot);
});
});
+118
View File
@@ -0,0 +1,118 @@
import type { Plugin } from "./registry";
// The single-column grid reaches 591px just below sm (640px minus 48px padding).
const CARD_WIDTH = 592;
const WIDTHS = new Set([CARD_WIDTH, CARD_WIDTH * 2]);
const SOURCE_HOSTS = new Set(["cdn.jsdelivr.net", "raw.githubusercontent.com"]);
export function pluginCardScreenshot(id: string, source: string): { src: string; srcSet?: string } {
if (!parseSource(source)) return { src: source };
return {
src: pluginThumbnailUrl(id, source, 1),
srcSet: `${pluginThumbnailUrl(id, source, 1)} 1x, ${pluginThumbnailUrl(id, source, 2)} 2x`,
};
}
function pluginThumbnailUrl(id: string, source: string, scale: 1 | 2): string {
return `/plugins/thumb/${CARD_WIDTH * scale}/${encodeURIComponent(source)}?plugin=${encodeURIComponent(id)}`;
}
/** Only registry-listed screenshots are eligible for transformation or original fallback. */
export async function handlePluginThumbnailRequest(
request: Request,
plugins: Plugin[],
fetchImage: typeof fetch = fetch,
): Promise<Response> {
const url = new URL(request.url);
const match = /^\/plugins\/thumb\/(\d+)\/([^/]+)$/.exec(url.pathname);
if (!match || !WIDTHS.has(Number(match[1])))
return new Response("Invalid thumbnail width or source", { status: 400 });
let decoded: string;
try {
decoded = decodeURIComponent(match[2]);
} catch {
return new Response("Invalid thumbnail source", { status: 400 });
}
const source = parseSource(decoded);
if (!source) return new Response("Invalid thumbnail source", { status: 400 });
const plugin = plugins.find((entry) => entry.id === url.searchParams.get("plugin"));
if (!plugin?.screenshots.includes(source))
return new Response("Screenshot not found", { status: 404 });
const format = negotiatedFormat(request.headers.get("accept") ?? "");
let response: Response | undefined;
try {
response = await fetchImage(source, {
cf: { image: { width: Number(match[1]), fit: "scale-down", format, quality: 80 } },
});
} catch {
// An unavailable transformation must not prevent fetching the original.
}
if (!response || !isImage(response) || /err=/i.test(response.headers.get("cf-resized") ?? "")) {
await response?.body?.cancel();
try {
response = await fetchImage(source);
} catch {
return unavailableImage();
}
}
if (!isImage(response)) {
await response.body?.cancel();
return unavailableImage();
}
return new Response(response.body, {
headers: {
"content-type": response.headers.get("content-type")!,
"cache-control": "public, max-age=31536000, immutable",
vary: "Accept",
// Originals can be SVGs. Keep direct navigation from executing them on our origin.
"content-security-policy": "sandbox",
"x-content-type-options": "nosniff",
},
});
}
function isImage(response: Response): boolean {
return response.ok && /^image\//i.test(response.headers.get("content-type") ?? "");
}
function unavailableImage(): Response {
return new Response("Screenshot unavailable", {
status: 502,
headers: { "cache-control": "no-store" },
});
}
function parseSource(source: string): string | null {
try {
const url = new URL(source);
if (
url.protocol !== "https:" ||
!SOURCE_HOSTS.has(url.hostname) ||
url.username ||
url.password ||
url.port
)
return null;
return source;
} catch {
return null;
}
}
function negotiatedFormat(accept: string): "avif" | "webp" | undefined {
// The Worker API requires explicit negotiation; only the URL API accepts format=auto.
const accepted = new Map(
accept
.toLowerCase()
.split(",")
.map((range) => {
const [type, ...parameters] = range.split(";").map((part) => part.trim());
const quality = parameters.find((parameter) => parameter.startsWith("q="));
return [type, quality === undefined ? 1 : Number(quality.slice(2))] as const;
}),
);
return (["avif", "webp"] as const).find((format) => (accepted.get(`image/${format}`) ?? 0) > 0);
}