g1t/apps/web/app/routes/repo/production-screenshot.ts

37 lines1,758 bytesCodeBlame
1/**
2 * A screenshot of a project's production, for its overview. Members only,
3 * like the deployments it shows. The overview asks with `?v=<commit>`, so a
4 * screenshot of that commit is kept by the browser for good; one of an
5 * older deploy, while the new one is being taken, only for a minute.
6 */
7import { env } from "cloudflare:workers";
8
9import type { Route } from "./+types/production-screenshot";
10import { deployments } from "../../lib/services.server";
11import { getViewer } from "../../lib/session.server";
12
13const LONG = "private, max-age=31536000, immutable";
14const BRIEF = "private, max-age=60";
15
16export async function loader({ params, context, request }: Route.LoaderArgs) {
17 const viewer = getViewer(context);
18 const none = (status: number) => new Response(null, { status, headers: { "cache-control": BRIEF } });
19 if (!viewer || !env.SCREENSHOTS) return none(404);
20 const list = await deployments.list({ workspace: params.owner, slug: params.repo }, viewer).catch(() => null);
21 const production = list?.ok ? list.value.live.find((app) => app.kind === "production") : undefined;
22 if (!production) return none(404);
23 const shot = await env.SCREENSHOTS.image({ host: new URL(production.url).host, commit: production.commit }).catch(
24 (error: unknown) => (console.warn("screenshot:", error), null),
25 );
26 if (!shot) return none(404);
27 const asked = new URL(request.url).searchParams.get("v");
28 const current = shot.commit === production.commit && asked === production.commit;
29 return new Response(shot.body, {
30 headers: {
31 "content-type": shot.contentType,
32 "cache-control": current ? LONG : BRIEF,
33 "x-content-type-options": "nosniff",
34 "last-modified": new Date(shot.capturedAt).toUTCString(),
35 },
36 });
37}