tangled
alpha
login
or
join now
pds.ls
/
pdsls
398
fork
atom
atmosphere explorer
pds.ls
tool
typescript
atproto
398
fork
atom
overview
issues
1
pulls
pipelines
better opengraph support
handle.invalid
1 week ago
d7a041b1
bbba69b1
verified
This commit was signed with the committer's
known signature
.
handle.invalid
SSH Key Fingerprint:
SHA256:mBrT4x0JdzLpbVR95g1hjI1aaErfC02kmLRkPXwsYCk=
+208
2 changed files
expand all
collapse all
unified
split
index.html
public
_worker.js
+1
index.html
···
4
4
<meta charset="utf-8" />
5
5
<meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
<link rel="icon" href="/favicon.ico" />
7
7
+
<meta name="theme-color" content="#76c4e5" />
7
8
<meta property="og:title" content="PDSls" />
8
9
<meta property="og:type" content="website" />
9
10
<meta property="og:url" content="https://pds.ls" />
+207
public/_worker.js
···
1
1
+
const BOT_UAS = [
2
2
+
"Discordbot",
3
3
+
"Twitterbot",
4
4
+
"facebookexternalhit",
5
5
+
"LinkedInBot",
6
6
+
"Slackbot-LinkExpanding",
7
7
+
"TelegramBot",
8
8
+
"WhatsApp",
9
9
+
"Iframely",
10
10
+
"Embedly",
11
11
+
"redditbot",
12
12
+
"Cardyb",
13
13
+
];
14
14
+
15
15
+
function isBot(ua) {
16
16
+
return BOT_UAS.some((b) => ua.includes(b));
17
17
+
}
18
18
+
19
19
+
function esc(s) {
20
20
+
return s
21
21
+
.replace(/&/g, "&")
22
22
+
.replace(/</g, "<")
23
23
+
.replace(/>/g, ">")
24
24
+
.replace(/"/g, """);
25
25
+
}
26
26
+
27
27
+
async function resolveDidDoc(did) {
28
28
+
let docUrl;
29
29
+
if (did.startsWith("did:plc:")) {
30
30
+
docUrl = `https://plc.directory/${did}`;
31
31
+
} else if (did.startsWith("did:web:")) {
32
32
+
const host = did.slice("did:web:".length);
33
33
+
docUrl = `https://${host}/.well-known/did.json`;
34
34
+
} else {
35
35
+
return null;
36
36
+
}
37
37
+
38
38
+
const res = await fetch(docUrl, { signal: AbortSignal.timeout(3000) });
39
39
+
if (!res.ok) return null;
40
40
+
return res.json();
41
41
+
}
42
42
+
43
43
+
function pdsFromDoc(doc) {
44
44
+
return doc.service?.find((s) => s.id === "#atproto_pds")?.serviceEndpoint ?? null;
45
45
+
}
46
46
+
47
47
+
function handleFromDoc(doc) {
48
48
+
const aka = doc.alsoKnownAs?.find((a) => a.startsWith("at://"));
49
49
+
return aka ? aka.slice("at://".length) : null;
50
50
+
}
51
51
+
52
52
+
const STATIC_ROUTES = {
53
53
+
"/": { title: "PDSls", description: "Browse the public data on atproto" },
54
54
+
"/jetstream": {
55
55
+
title: "Jetstream",
56
56
+
description: "A simplified event stream with support for collection and DID filtering.",
57
57
+
},
58
58
+
"/firehose": { title: "Firehose", description: "The raw event stream from a relay or PDS." },
59
59
+
"/spacedust": {
60
60
+
title: "Spacedust",
61
61
+
description: "A stream of links showing interactions across the network.",
62
62
+
},
63
63
+
"/labels": { title: "Labels", description: "Query labels applied to accounts and records." },
64
64
+
"/car": {
65
65
+
title: "Archive tools",
66
66
+
description: "Tools for working with CAR (Content Addressable aRchive) files.",
67
67
+
},
68
68
+
"/car/explore": {
69
69
+
title: "Explore archive",
70
70
+
description: "Upload a CAR file to explore its contents.",
71
71
+
},
72
72
+
"/car/unpack": {
73
73
+
title: "Unpack archive",
74
74
+
description: "Upload a CAR file to extract all records into a ZIP archive.",
75
75
+
},
76
76
+
"/settings": { title: "Settings", description: "Browse the public data on atproto" },
77
77
+
};
78
78
+
79
79
+
async function resolveOgData(pathname) {
80
80
+
if (pathname in STATIC_ROUTES) return STATIC_ROUTES[pathname];
81
81
+
82
82
+
let title = "PDSls";
83
83
+
let description = "Browse the public data on atproto";
84
84
+
85
85
+
const segments = pathname.slice(1).split("/").filter(Boolean);
86
86
+
const isAtUrl = segments[0] === "at:";
87
87
+
88
88
+
if (isAtUrl) {
89
89
+
// at://did[/collection[/rkey]]
90
90
+
const [, did, collection, rkey] = segments;
91
91
+
92
92
+
if (!did) {
93
93
+
// bare /at: — use defaults
94
94
+
} else if (!collection) {
95
95
+
const doc = await resolveDidDoc(did).catch(() => null);
96
96
+
const handle = doc ? handleFromDoc(doc) : null;
97
97
+
const pdsUrl = doc ? pdsFromDoc(doc) : null;
98
98
+
const pdsHost = pdsUrl ? pdsUrl.replace("https://", "").replace("http://", "") : null;
99
99
+
100
100
+
title = handle ? `${handle} (${did})` : did;
101
101
+
description = pdsHost ? `Hosted on ${pdsHost}` : `Repository for ${did}`;
102
102
+
} else if (!rkey) {
103
103
+
const doc = await resolveDidDoc(did).catch(() => null);
104
104
+
const handle = doc ? handleFromDoc(doc) : null;
105
105
+
title = `at://${handle ?? did}/${collection}`;
106
106
+
description = `List of ${collection} records for ${handle ?? did}`;
107
107
+
} else {
108
108
+
description = `View the ${rkey} record in ${collection} from ${did}`;
109
109
+
110
110
+
const doc = await resolveDidDoc(did).catch(() => null);
111
111
+
const handle = doc ? handleFromDoc(doc) : null;
112
112
+
title = `at://${handle ?? did}/${collection}/${rkey}`;
113
113
+
}
114
114
+
} else {
115
115
+
// /pds
116
116
+
const [pds] = segments;
117
117
+
if (pds) {
118
118
+
title = pds;
119
119
+
description = `Browse the repositories at ${pds}`;
120
120
+
}
121
121
+
}
122
122
+
123
123
+
return { title, description };
124
124
+
}
125
125
+
126
126
+
class OgTagRewriter {
127
127
+
constructor(ogData, url) {
128
128
+
this.ogData = ogData;
129
129
+
this.url = url;
130
130
+
}
131
131
+
132
132
+
element(element) {
133
133
+
const property = element.getAttribute("property");
134
134
+
const name = element.getAttribute("name");
135
135
+
136
136
+
if (
137
137
+
property === "og:title" ||
138
138
+
property === "og:description" ||
139
139
+
property === "og:url" ||
140
140
+
property === "og:type" ||
141
141
+
property === "og:site_name" ||
142
142
+
property === "description" ||
143
143
+
name === "description" ||
144
144
+
name === "twitter:card" ||
145
145
+
name === "twitter:title" ||
146
146
+
name === "twitter:description"
147
147
+
) {
148
148
+
element.remove();
149
149
+
}
150
150
+
}
151
151
+
}
152
152
+
153
153
+
class HeadEndRewriter {
154
154
+
constructor(ogData, url) {
155
155
+
this.ogData = ogData;
156
156
+
this.url = url;
157
157
+
}
158
158
+
159
159
+
element(element) {
160
160
+
const t = esc(this.ogData.title);
161
161
+
const d = esc(this.ogData.description);
162
162
+
const u = esc(this.url);
163
163
+
164
164
+
element.append(
165
165
+
`<meta property="og:title" content="${t}" />
166
166
+
<meta property="og:type" content="website" />
167
167
+
<meta property="og:url" content="${u}" />
168
168
+
<meta property="og:description" content="${d}" />
169
169
+
<meta property="og:site_name" content="PDSls" />
170
170
+
<meta name="description" content="${d}" />
171
171
+
<meta name="twitter:card" content="summary" />
172
172
+
<meta name="twitter:title" content="${t}" />
173
173
+
<meta name="twitter:description" content="${d}" />`,
174
174
+
{ html: true },
175
175
+
);
176
176
+
}
177
177
+
}
178
178
+
179
179
+
export default {
180
180
+
async fetch(request, env) {
181
181
+
const ua = request.headers.get("user-agent") ?? "";
182
182
+
183
183
+
if (!isBot(ua)) {
184
184
+
return env.ASSETS.fetch(request);
185
185
+
}
186
186
+
187
187
+
const url = new URL(request.url);
188
188
+
189
189
+
let ogData;
190
190
+
try {
191
191
+
ogData = await resolveOgData(url.pathname);
192
192
+
} catch {
193
193
+
return env.ASSETS.fetch(request);
194
194
+
}
195
195
+
196
196
+
const response = await env.ASSETS.fetch(request);
197
197
+
const contentType = response.headers.get("content-type") ?? "";
198
198
+
if (!contentType.includes("text/html")) {
199
199
+
return response;
200
200
+
}
201
201
+
202
202
+
return new HTMLRewriter()
203
203
+
.on("meta", new OgTagRewriter(ogData, request.url))
204
204
+
.on("head", new HeadEndRewriter(ogData, request.url))
205
205
+
.transform(response);
206
206
+
},
207
207
+
};