forked from
pds.ls/pdsls
atmosphere explorer
1import { ImageResponse } from "@takumi-rs/image-response/wasm";
2import wasmModule from "./takumi_wasm_bg.wasm";
3
4// Minimal createElement helper — avoids pulling in React
5function h(type, props, ...children) {
6 const flat = children.flat(Infinity).filter((c) => c != null && c !== false);
7 return {
8 type,
9 props: {
10 ...props,
11 children:
12 flat.length === 0 ? undefined
13 : flat.length === 1 ? flat[0]
14 : flat,
15 },
16 };
17}
18
19let fontData = null;
20async function getFonts() {
21 if (!fontData) {
22 const urls = [
23 [
24 "Roboto Mono",
25 "https://fonts.bunny.net/roboto-mono/files/roboto-mono-latin-400-normal.woff2",
26 ],
27 [
28 "Noto Sans JP",
29 "https://fonts.bunny.net/noto-sans-jp/files/noto-sans-jp-japanese-400-normal.woff2",
30 ],
31 [
32 "Noto Sans SC",
33 "https://fonts.bunny.net/noto-sans-sc/files/noto-sans-sc-chinese-simplified-400-normal.woff2",
34 ],
35 [
36 "Noto Sans KR",
37 "https://fonts.bunny.net/noto-sans-kr/files/noto-sans-kr-korean-400-normal.woff2",
38 ],
39 ["Noto Emoji", "https://fonts.bunny.net/noto-emoji/files/noto-emoji-emoji-400-normal.woff2"],
40 ];
41 const results = await Promise.all(
42 urls.map(([name, url]) =>
43 fetch(url)
44 .then((r) => (r.ok ? r.arrayBuffer() : null))
45 .then((data) => (data ? { data, name, weight: 400, style: "normal" } : null))
46 .catch(() => null),
47 ),
48 );
49 fontData = results.filter(Boolean);
50 }
51 return fontData;
52}
53
54async function fetchRecord(pdsUrl, repo, collection, rkey) {
55 const url = `${pdsUrl}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(repo)}&collection=${encodeURIComponent(collection)}&rkey=${encodeURIComponent(rkey)}`;
56 const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
57 if (!res.ok) return null;
58 return res.json();
59}
60
61const LOGO_PATH =
62 "M14 1a3 3 0 0 1 2.348 4.868l2 3.203Q18.665 9 19 9a3 3 0 1 1-2.347 1.132l-2-3.203a3 3 0 0 1-1.304 0l-2.001 3.203c.408.513.652 1.162.652 1.868s-.244 1.356-.653 1.868l2.002 3.203Q13.664 17 14 17a3 3 0 1 1-2.347 1.132L9.65 14.929a3 3 0 0 1-1.302 0l-2.002 3.203a3 3 0 1 1-1.696-1.06l2.002-3.204A3 3 0 0 1 9.65 9.07l2.002-3.202A3 3 0 0 1 14 1";
63
64// Colors matching json.tsx dark mode
65const C = {
66 key: "#818cf8", // indigo-400
67 index: "#a78bfa", // violet-400
68 string: "#f1f5f9", // slate-100
69 quote: "#a3a3a3", // neutral-400
70 number: "#f1f5f9", // slate-100
71 boolean: "#fbbf24", // amber-400
72 null: "#737373", // neutral-500
73 guide: "#737373", // neutral-500
74 colon: "#a3a3a3", // neutral-400
75};
76
77const MAX_STRING_WIDTH = 80;
78
79function truncateToWidth(str, maxWidth) {
80 let w = 0;
81 let i = 0;
82 const chars = [...str];
83 for (; i < chars.length; i++) {
84 const cp = chars[i].codePointAt(0);
85 const cw =
86 (
87 (cp >= 0x1100 && cp <= 0x115f) ||
88 (cp >= 0x2e80 && cp <= 0x9fff) ||
89 (cp >= 0xac00 && cp <= 0xd7af) ||
90 (cp >= 0xf900 && cp <= 0xfaff) ||
91 (cp >= 0xfe10 && cp <= 0xfe6f) ||
92 (cp >= 0xff01 && cp <= 0xff60) ||
93 (cp >= 0xffe0 && cp <= 0xffe6) ||
94 (cp >= 0x20000 && cp <= 0x2fa1f)
95 ) ?
96 2
97 : 1;
98 if (w + cw > maxWidth) break;
99 w += cw;
100 }
101 return i < chars.length ? chars.slice(0, i).join("") + "…" : str;
102}
103const MAX_LINES = 20;
104
105// Flatten JSON into an array of { depth, segments } lines
106// Each segment is { text, color }
107function flattenJson(value, depth, lines, key, isIndex, maxStrWidth) {
108 if (lines.length >= MAX_LINES) return;
109
110 const keySegs = [];
111 if (key !== undefined) {
112 keySegs.push({ text: String(key), color: isIndex ? C.index : C.key });
113 keySegs.push({ text: ": ", color: C.colon });
114 }
115
116 if (value === null) {
117 lines.push({ depth, segments: [...keySegs, { text: "null", color: C.null }] });
118 } else if (typeof value === "boolean") {
119 lines.push({ depth, segments: [...keySegs, { text: String(value), color: C.boolean }] });
120 } else if (typeof value === "number") {
121 lines.push({ depth, segments: [...keySegs, { text: String(value), color: C.number }] });
122 } else if (typeof value === "string") {
123 const display = value.replace(/\n/g, " ");
124 const truncated = truncateToWidth(display, maxStrWidth);
125 lines.push({
126 depth,
127 segments: [
128 ...keySegs,
129 { text: '"', color: C.quote },
130 { text: truncated, color: C.string },
131 { text: '"', color: C.quote },
132 ],
133 });
134 } else if (Array.isArray(value)) {
135 if (value.length === 0) {
136 lines.push({ depth, segments: [...keySegs, { text: "[ ]", color: C.null }] });
137 } else {
138 if (key !== undefined) {
139 lines.push({
140 depth,
141 segments: [
142 { text: String(key), color: isIndex ? C.index : C.key },
143 { text: ":", color: C.colon },
144 ],
145 });
146 }
147 for (let i = 0; i < value.length; i++) {
148 if (lines.length >= MAX_LINES) break;
149 flattenJson(value[i], depth + 1, lines, `#${i}`, true, maxStrWidth);
150 }
151 }
152 } else {
153 const keys = Object.keys(value);
154 if (keys.length === 0) {
155 lines.push({ depth, segments: [...keySegs, { text: "{ }", color: C.null }] });
156 } else {
157 if (key !== undefined) {
158 lines.push({
159 depth,
160 segments: [
161 { text: String(key), color: isIndex ? C.index : C.key },
162 { text: ":", color: C.colon },
163 ],
164 });
165 }
166 for (const k of keys) {
167 if (lines.length >= MAX_LINES) break;
168 flattenJson(value[k], depth + 1, lines, k, false, maxStrWidth);
169 }
170 }
171 }
172}
173
174function renderLine(line, guideMargin) {
175 const guides = [];
176 for (let i = 0; i < line.depth; i++) {
177 guides.push(
178 h("div", {
179 style: {
180 width: 1,
181 backgroundColor: C.guide,
182 marginRight: guideMargin,
183 flexShrink: 0,
184 },
185 }),
186 );
187 }
188 return h(
189 "div",
190 { style: { display: "flex", overflow: "hidden" } },
191 ...guides,
192 ...line.segments.map((seg) => h("div", { style: { color: seg.color } }, seg.text)),
193 );
194}
195
196function OgImage({ record }) {
197 const lines = [];
198 for (const k of Object.keys(record)) {
199 if (lines.length >= MAX_LINES) break;
200 flattenJson(record[k], 0, lines, k, false, MAX_STRING_WIDTH);
201 }
202 if (lines.length >= MAX_LINES) {
203 lines.push({ depth: 0, segments: [{ text: "…", color: C.null }] });
204 }
205
206 const availableHeight = 630 - 100; // height minus vertical padding
207 const fontSize = Math.min(32, Math.max(18, Math.floor(availableHeight / (lines.length * 1.5))));
208 const guideMargin = Math.round((fontSize * 19) / 18);
209
210 // Re-truncate string values if the larger font size means fewer chars fit.
211 // Available width: 1200 canvas - 100 padding - 80 logo area - 200 for key/depth overhead;
212 // Roboto Mono char ≈ 0.6× fontSize.
213 const maxStrWidth = Math.floor((1200 - 100 - 80 - 200) / (fontSize * 0.6));
214 if (maxStrWidth < MAX_STRING_WIDTH) {
215 for (const line of lines) {
216 for (const seg of line.segments) {
217 if (seg.color === C.string) {
218 seg.text = truncateToWidth(seg.text, maxStrWidth);
219 }
220 }
221 }
222 }
223
224 return h(
225 "div",
226 {
227 style: {
228 display: "flex",
229 flexDirection: "column",
230 justifyContent: "center",
231 position: "relative",
232 width: "100%",
233 height: "100%",
234 background: "#1f1f1f",
235 padding: "50px 50px",
236 fontFamily: "Roboto Mono, Noto Sans JP, Noto Sans SC, Noto Sans KR, Noto Emoji",
237 fontSize,
238 lineHeight: 1.5,
239 color: "#e2e8f0",
240 },
241 },
242 h(
243 "div",
244 {
245 style: {
246 position: "absolute",
247 bottom: 24,
248 right: 24,
249 },
250 },
251 h(
252 "svg",
253 { viewBox: "0 0 24 24", width: 48, height: 48 },
254 h("path", { fill: "#76c4e5", d: LOGO_PATH }),
255 ),
256 ),
257 h(
258 "div",
259 { style: { display: "flex", flexDirection: "column", paddingRight: 80 } },
260 ...lines.map((line) => renderLine(line, guideMargin)),
261 ),
262 );
263}
264
265async function handleOgImage(searchParams) {
266 const did = searchParams.get("did");
267 const collection = searchParams.get("collection");
268 const rkey = searchParams.get("rkey");
269
270 if (!did || !collection || !rkey) {
271 return new Response("Missing params", { status: 400 });
272 }
273
274 const doc = await resolveDidDoc(did).catch(() => null);
275 const pdsUrl = doc ? pdsFromDoc(doc) : null;
276 if (!pdsUrl) {
277 return new Response("Could not resolve PDS", { status: 404 });
278 }
279
280 const data = await fetchRecord(pdsUrl, did, collection, rkey).catch(() => null);
281 if (!data?.value) {
282 return new Response("Record not found", { status: 404 });
283 }
284
285 const fonts = await getFonts();
286
287 return new ImageResponse(OgImage({ record: data.value }), {
288 width: 1200,
289 height: 630,
290 module: wasmModule,
291 fonts,
292 format: "png",
293 });
294}
295
296// ---- existing worker logic ----
297
298const BOT_UAS = [
299 "Discordbot",
300 "Twitterbot",
301 "facebookexternalhit",
302 "LinkedInBot",
303 "Slackbot-LinkExpanding",
304 "TelegramBot",
305 "WhatsApp",
306 "Iframely",
307 "Embedly",
308 "redditbot",
309 "Cardyb",
310];
311
312function isBot(ua) {
313 return BOT_UAS.some((b) => ua.includes(b));
314}
315
316function esc(s) {
317 return s
318 .replace(/&/g, "&")
319 .replace(/</g, "<")
320 .replace(/>/g, ">")
321 .replace(/"/g, """);
322}
323
324async function resolveDidDoc(did) {
325 let docUrl;
326 if (did.startsWith("did:plc:")) {
327 docUrl = `https://plc.directory/${did}`;
328 } else if (did.startsWith("did:web:")) {
329 const host = did.slice("did:web:".length);
330 docUrl = `https://${host}/.well-known/did.json`;
331 } else {
332 return null;
333 }
334
335 const res = await fetch(docUrl, { signal: AbortSignal.timeout(3000) });
336 if (!res.ok) return null;
337 return res.json();
338}
339
340function pdsFromDoc(doc) {
341 return doc.service?.find((s) => s.id === "#atproto_pds")?.serviceEndpoint ?? null;
342}
343
344function handleFromDoc(doc) {
345 const aka = doc.alsoKnownAs?.find((a) => a.startsWith("at://"));
346 return aka ? aka.slice("at://".length) : null;
347}
348
349const STATIC_ROUTES = {
350 "/": { title: "PDSls", description: "Browse the public data on atproto" },
351 "/jetstream": {
352 title: "Jetstream",
353 description: "A simplified event stream with support for collection and DID filtering.",
354 },
355 "/firehose": { title: "Firehose", description: "The raw event stream from a relay or PDS." },
356 "/spacedust": {
357 title: "Spacedust",
358 description: "A stream of links showing interactions across the network.",
359 },
360 "/labels": { title: "Labels", description: "Query labels applied to accounts and records." },
361 "/car": {
362 title: "Archive tools",
363 description: "Tools for working with CAR (Content Addressable aRchive) files.",
364 },
365 "/car/explore": {
366 title: "Explore archive",
367 description: "Upload a CAR file to explore its contents.",
368 },
369 "/car/unpack": {
370 title: "Unpack archive",
371 description: "Upload a CAR file to extract all records into a ZIP archive.",
372 },
373 "/settings": { title: "Settings", description: "Browse the public data on atproto" },
374};
375
376async function resolveOgData(pathname) {
377 if (pathname in STATIC_ROUTES) return STATIC_ROUTES[pathname];
378
379 let title = "PDSls";
380 let description = "Browse the public data on atproto";
381
382 const segments = pathname.slice(1).split("/").filter(Boolean);
383 const isAtUrl = segments[0] === "at:";
384
385 if (isAtUrl) {
386 // at://did[/collection[/rkey]]
387 const [, did, collection, rkey] = segments;
388
389 if (!did) {
390 // bare /at: — use defaults
391 } else if (!collection) {
392 const doc = await resolveDidDoc(did).catch(() => null);
393 const handle = doc ? handleFromDoc(doc) : null;
394 const pdsUrl = doc ? pdsFromDoc(doc) : null;
395 const pdsHost = pdsUrl ? pdsUrl.replace("https://", "").replace("http://", "") : null;
396
397 title = handle ? `${handle} (${did})` : did;
398 description = pdsHost ? `Hosted on ${pdsHost}` : `Repository for ${did}`;
399 } else if (!rkey) {
400 const doc = await resolveDidDoc(did).catch(() => null);
401 const handle = doc ? handleFromDoc(doc) : null;
402 title = `at://${handle ?? did}/${collection}`;
403 description = `List of ${collection} records from ${handle ?? did}`;
404 } else {
405 const doc = await resolveDidDoc(did).catch(() => null);
406 const handle = doc ? handleFromDoc(doc) : null;
407 description = "";
408 title = `at://${handle ?? did}/${collection}/${rkey}`;
409 return { title, description, generateImage: true, did, collection, rkey };
410 }
411 } else {
412 // /pds
413 const [pds] = segments;
414 if (pds) {
415 title = pds;
416 description = `Browse the repositories at ${pds}`;
417 }
418 }
419
420 return { title, description };
421}
422
423class OgTagRewriter {
424 constructor(ogData, url) {
425 this.ogData = ogData;
426 this.url = url;
427 }
428
429 element(element) {
430 const property = element.getAttribute("property");
431 const name = element.getAttribute("name");
432
433 if (
434 property === "og:title" ||
435 property === "og:description" ||
436 property === "og:url" ||
437 property === "og:type" ||
438 property === "og:site_name" ||
439 property === "og:image" ||
440 property === "description" ||
441 name === "description" ||
442 name === "twitter:card" ||
443 name === "twitter:title" ||
444 name === "twitter:description" ||
445 name === "twitter:image"
446 ) {
447 element.remove();
448 }
449 }
450}
451
452class HeadEndRewriter {
453 constructor(ogData, imageUrl) {
454 this.ogData = ogData;
455 this.imageUrl = imageUrl;
456 }
457
458 element(element) {
459 const t = esc(this.ogData.title);
460 const d = esc(this.ogData.description);
461 const i = this.imageUrl ? esc(this.imageUrl) : null;
462
463 const imageTags =
464 i ?
465 `\n <meta property="og:image" content="${i}" />
466 <meta name="twitter:card" content="summary_large_image" />
467 <meta name="twitter:image" content="${i}" />`
468 : `\n <meta name="twitter:card" content="summary" />`;
469
470 element.append(
471 `<meta property="og:title" content="${t}" />
472 <meta property="og:type" content="website" />
473 <meta property="og:description" content="${d}" />
474 <meta property="og:site_name" content="PDSls" />
475 <meta name="description" content="${d}" />
476 <meta name="twitter:title" content="${t}" />
477 <meta name="twitter:description" content="${d}" />${imageTags}`,
478 { html: true },
479 );
480 }
481}
482
483export default {
484 async fetch(request, env) {
485 const url = new URL(request.url);
486
487 if (url.pathname === "/og-image") {
488 return handleOgImage(url.searchParams).catch(
489 (err) => new Response(`Failed to generate image: ${err?.message ?? err}`, { status: 500 }),
490 );
491 }
492
493 const ua = request.headers.get("user-agent") ?? "";
494
495 if (!isBot(ua)) {
496 return env.ASSETS.fetch(request);
497 }
498
499 let ogData;
500 try {
501 ogData = await resolveOgData(url.pathname);
502 } catch {
503 return env.ASSETS.fetch(request);
504 }
505
506 const imageUrl =
507 ogData.generateImage ?
508 `${url.origin}/og-image?` +
509 new URLSearchParams({ did: ogData.did, collection: ogData.collection, rkey: ogData.rkey })
510 : null;
511
512 const response = await env.ASSETS.fetch(request);
513 const contentType = response.headers.get("content-type") ?? "";
514 if (!contentType.includes("text/html")) {
515 return response;
516 }
517
518 return new HTMLRewriter()
519 .on("meta", new OgTagRewriter(ogData, request.url))
520 .on("head", new HeadEndRewriter(ogData, imageUrl))
521 .transform(response);
522 },
523};