tangled
alpha
login
or
join now
dunkirk.sh
/
traverse
1
fork
atom
snatching amp's walkthrough for my own purposes mwhahaha
traverse.dunkirk.sh/diagram/6121f05c-a5ef-4ecf-8ffc-02534c5e767c
1
fork
atom
overview
issues
pulls
pipelines
feat: add index og images
dunkirk.sh
1 month ago
b7240893
3015a36e
verified
This commit was signed with the committer's
known signature
.
dunkirk.sh
SSH Key Fingerprint:
SHA256:DqcG0RXYExE26KiWo3VxJnsxswN1QNfTBvB+bdSpk80=
+141
-5
2 changed files
expand all
collapse all
unified
split
src
index.ts
og.ts
+33
-5
src/index.ts
···
4
4
import { generateViewerHTML } from "./template.ts";
5
5
import type { WalkthroughDiagram } from "./types.ts";
6
6
import { initDb, loadAllDiagrams, saveDiagram, deleteDiagramFromDb, generateId, getSharedUrl, saveSharedUrl } from "./storage.ts";
7
7
-
import { generateOgImage } from "./og.ts";
7
7
+
import { generateOgImage, generateIndexOgImage } from "./og.ts";
8
8
import { loadConfig } from "./config.ts";
9
9
10
10
const config = loadConfig();
···
153
153
});
154
154
}
155
155
156
156
+
// Index OG image
157
157
+
if (url.pathname === "/og.png") {
158
158
+
const png = await generateIndexOgImage(MODE, diagrams.size);
159
159
+
return new Response(png, {
160
160
+
headers: {
161
161
+
"Content-Type": "image/png",
162
162
+
"Cache-Control": "public, max-age=3600",
163
163
+
},
164
164
+
});
165
165
+
}
166
166
+
156
167
// List available diagrams
157
168
if (url.pathname === "/") {
158
169
const html = MODE === "server"
159
159
-
? generateServerIndexHTML(diagrams.size, VERSION)
160
160
-
: generateLocalIndexHTML(diagrams, VERSION);
170
170
+
? generateServerIndexHTML(diagrams.size, VERSION, url.origin)
171
171
+
: generateLocalIndexHTML(diagrams, VERSION, url.origin);
161
172
return new Response(html, {
162
173
headers: { "Content-Type": "text/html; charset=utf-8" },
163
174
});
···
305
316
</html>`;
306
317
}
307
318
308
308
-
function generateLocalIndexHTML(diagrams: Map<string, WalkthroughDiagram>, gitHash: string): string {
319
319
+
function generateLocalIndexHTML(diagrams: Map<string, WalkthroughDiagram>, gitHash: string, baseUrl: string): string {
309
320
const items = [...diagrams.entries()]
310
321
.map(
311
322
([id, d]) => {
···
346
357
</div>`
347
358
: `<div class="diagram-list">${items}</div>`;
348
359
360
360
+
const diagramCount = diagrams.size;
349
361
return `<!DOCTYPE html>
350
362
<html lang="en">
351
363
<head>
···
353
365
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
354
366
<title>Traverse</title>
355
367
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
368
368
+
<meta property="og:type" content="website" />
369
369
+
<meta property="og:title" content="Traverse" />
370
370
+
<meta property="og:description" content="Interactive code walkthrough diagrams. ${diagramCount} diagram${diagramCount !== 1 ? "s" : ""}." />
371
371
+
<meta property="og:image" content="${escapeHTML(baseUrl)}/og.png" />
372
372
+
<meta name="twitter:card" content="summary_large_image" />
373
373
+
<meta name="twitter:title" content="Traverse" />
374
374
+
<meta name="twitter:description" content="Interactive code walkthrough diagrams." />
375
375
+
<meta name="twitter:image" content="${escapeHTML(baseUrl)}/og.png" />
356
376
<style>
357
377
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
358
378
:root {
···
511
531
</html>`;
512
532
}
513
533
514
514
-
function generateServerIndexHTML(diagramCount: number, gitHash: string): string {
534
534
+
function generateServerIndexHTML(diagramCount: number, gitHash: string, baseUrl: string): string {
515
535
return `<!DOCTYPE html>
516
536
<html lang="en">
517
537
<head>
···
519
539
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
520
540
<title>Traverse</title>
521
541
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
542
542
+
<meta property="og:type" content="website" />
543
543
+
<meta property="og:title" content="Traverse" />
544
544
+
<meta property="og:description" content="Interactive code walkthrough diagrams, shareable with anyone. ${diagramCount} diagram${diagramCount !== 1 ? "s" : ""} shared." />
545
545
+
<meta property="og:image" content="${escapeHTML(baseUrl)}/og.png" />
546
546
+
<meta name="twitter:card" content="summary_large_image" />
547
547
+
<meta name="twitter:title" content="Traverse" />
548
548
+
<meta name="twitter:description" content="Interactive code walkthrough diagrams, shareable with anyone." />
549
549
+
<meta name="twitter:image" content="${escapeHTML(baseUrl)}/og.png" />
522
550
<style>
523
551
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
524
552
:root {
+108
src/og.ts
···
16
16
// Cache generated images by diagram ID
17
17
const cache = new Map<string, Buffer>();
18
18
19
19
+
export async function generateIndexOgImage(
20
20
+
mode: "local" | "server",
21
21
+
diagramCount: number,
22
22
+
): Promise<Buffer> {
23
23
+
const cacheKey = `__index_${mode}_${diagramCount}`;
24
24
+
const cached = cache.get(cacheKey);
25
25
+
if (cached) return cached;
26
26
+
27
27
+
const subtitle = mode === "server"
28
28
+
? `${diagramCount} diagram${diagramCount !== 1 ? "s" : ""} shared`
29
29
+
: `${diagramCount} diagram${diagramCount !== 1 ? "s" : ""}`;
30
30
+
31
31
+
const tagline = mode === "server"
32
32
+
? "Interactive code walkthrough diagrams, shareable with anyone."
33
33
+
: "Interactive code walkthrough diagrams";
34
34
+
35
35
+
const svg = await satori(
36
36
+
{
37
37
+
type: "div",
38
38
+
props: {
39
39
+
style: {
40
40
+
width: "100%",
41
41
+
height: "100%",
42
42
+
display: "flex",
43
43
+
flexDirection: "column",
44
44
+
justifyContent: "center",
45
45
+
alignItems: "center",
46
46
+
padding: "60px",
47
47
+
backgroundColor: "#0a0a0a",
48
48
+
color: "#e5e5e5",
49
49
+
fontFamily: "Inter",
50
50
+
},
51
51
+
children: [
52
52
+
{
53
53
+
type: "div",
54
54
+
props: {
55
55
+
style: {
56
56
+
display: "flex",
57
57
+
flexDirection: "column",
58
58
+
alignItems: "center",
59
59
+
gap: "20px",
60
60
+
},
61
61
+
children: [
62
62
+
{
63
63
+
type: "div",
64
64
+
props: {
65
65
+
style: {
66
66
+
fontSize: "80px",
67
67
+
fontWeight: 700,
68
68
+
color: "#e5e5e5",
69
69
+
},
70
70
+
children: "Traverse",
71
71
+
},
72
72
+
},
73
73
+
{
74
74
+
type: "div",
75
75
+
props: {
76
76
+
style: {
77
77
+
fontSize: "32px",
78
78
+
color: "#a3a3a3",
79
79
+
textAlign: "center",
80
80
+
},
81
81
+
children: tagline,
82
82
+
},
83
83
+
},
84
84
+
{
85
85
+
type: "div",
86
86
+
props: {
87
87
+
style: {
88
88
+
display: "flex",
89
89
+
alignItems: "center",
90
90
+
gap: "8px",
91
91
+
marginTop: "16px",
92
92
+
fontSize: "24px",
93
93
+
color: "#a3a3a3",
94
94
+
backgroundColor: "#1c1c1e",
95
95
+
padding: "10px 24px",
96
96
+
borderRadius: "8px",
97
97
+
border: "1px solid #262626",
98
98
+
},
99
99
+
children: subtitle,
100
100
+
},
101
101
+
},
102
102
+
],
103
103
+
},
104
104
+
},
105
105
+
],
106
106
+
},
107
107
+
},
108
108
+
{
109
109
+
width: 1200,
110
110
+
height: 630,
111
111
+
fonts: [
112
112
+
{ name: "Inter", data: interRegular, weight: 400, style: "normal" as const },
113
113
+
{ name: "Inter", data: interBold, weight: 700, style: "normal" as const },
114
114
+
],
115
115
+
},
116
116
+
);
117
117
+
118
118
+
const resvg = new Resvg(svg, {
119
119
+
fitTo: { mode: "width", value: 1200 },
120
120
+
});
121
121
+
const png = Buffer.from(resvg.render().asPng());
122
122
+
123
123
+
cache.set(cacheKey, png);
124
124
+
return png;
125
125
+
}
126
126
+
19
127
export async function generateOgImage(
20
128
id: string,
21
129
summary: string,