@taciturnaxolotl/traverse 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taciturnaxolotl/traverse",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Interactive code walkthrough diagrams via MCP",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -10,6 +10,7 @@
10
10
  "files": [
11
11
  "src",
12
12
  "bin",
13
+ "fonts",
13
14
  "icon.svg"
14
15
  ],
15
16
  "scripts": {
@@ -23,7 +24,6 @@
23
24
  "typescript": "^5"
24
25
  },
25
26
  "dependencies": {
26
- "@fontsource/inter": "^5.2.8",
27
27
  "@modelcontextprotocol/sdk": "^1.26.0",
28
28
  "@resvg/resvg-wasm": "^2.6.2",
29
29
  "satori": "^0.19.1"
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ import { z } from "zod/v4";
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
- import { generateOgImage } from "./og.ts";
7
+ import { generateOgImage, generateIndexOgImage } from "./og.ts";
8
8
  import { loadConfig } from "./config.ts";
9
9
 
10
10
  const config = loadConfig();
@@ -153,11 +153,22 @@ try {
153
153
  });
154
154
  }
155
155
 
156
+ // Index OG image
157
+ if (url.pathname === "/og.png") {
158
+ const png = await generateIndexOgImage(MODE, diagrams.size);
159
+ return new Response(png, {
160
+ headers: {
161
+ "Content-Type": "image/png",
162
+ "Cache-Control": "public, max-age=3600",
163
+ },
164
+ });
165
+ }
166
+
156
167
  // List available diagrams
157
168
  if (url.pathname === "/") {
158
169
  const html = MODE === "server"
159
- ? generateServerIndexHTML(diagrams.size, VERSION)
160
- : generateLocalIndexHTML(diagrams, VERSION);
170
+ ? generateServerIndexHTML(diagrams.size, VERSION, url.origin)
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,7 +316,7 @@ function generate404HTML(title: string, message: string): string {
305
316
  </html>`;
306
317
  }
307
318
 
308
- function generateLocalIndexHTML(diagrams: Map<string, WalkthroughDiagram>, gitHash: string): string {
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,6 +357,7 @@ function generateLocalIndexHTML(diagrams: Map<string, WalkthroughDiagram>, gitHa
346
357
  </div>`
347
358
  : `<div class="diagram-list">${items}</div>`;
348
359
 
360
+ const diagramCount = diagrams.size;
349
361
  return `<!DOCTYPE html>
350
362
  <html lang="en">
351
363
  <head>
@@ -353,6 +365,14 @@ function generateLocalIndexHTML(diagrams: Map<string, WalkthroughDiagram>, gitHa
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
+ <meta property="og:type" content="website" />
369
+ <meta property="og:title" content="Traverse" />
370
+ <meta property="og:description" content="Interactive code walkthrough diagrams. ${diagramCount} diagram${diagramCount !== 1 ? "s" : ""}." />
371
+ <meta property="og:image" content="${escapeHTML(baseUrl)}/og.png" />
372
+ <meta name="twitter:card" content="summary_large_image" />
373
+ <meta name="twitter:title" content="Traverse" />
374
+ <meta name="twitter:description" content="Interactive code walkthrough diagrams." />
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,7 +531,7 @@ function generateLocalIndexHTML(diagrams: Map<string, WalkthroughDiagram>, gitHa
511
531
  </html>`;
512
532
  }
513
533
 
514
- function generateServerIndexHTML(diagramCount: number, gitHash: string): string {
534
+ function generateServerIndexHTML(diagramCount: number, gitHash: string, baseUrl: string): string {
515
535
  return `<!DOCTYPE html>
516
536
  <html lang="en">
517
537
  <head>
@@ -519,6 +539,14 @@ function generateServerIndexHTML(diagramCount: number, gitHash: string): string
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
+ <meta property="og:type" content="website" />
543
+ <meta property="og:title" content="Traverse" />
544
+ <meta property="og:description" content="Interactive code walkthrough diagrams, shareable with anyone. ${diagramCount} diagram${diagramCount !== 1 ? "s" : ""} shared." />
545
+ <meta property="og:image" content="${escapeHTML(baseUrl)}/og.png" />
546
+ <meta name="twitter:card" content="summary_large_image" />
547
+ <meta name="twitter:title" content="Traverse" />
548
+ <meta name="twitter:description" content="Interactive code walkthrough diagrams, shareable with anyone." />
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 {
package/src/og.ts CHANGED
@@ -3,19 +3,145 @@ import { initWasm, Resvg } from "@resvg/resvg-wasm";
3
3
  import { join } from "path";
4
4
 
5
5
  // Load Inter font files (woff, not woff2 — satori doesn't support woff2)
6
- const fontsDir = join(import.meta.dir, "../node_modules/@fontsource/inter/files");
6
+ const fontsDir = join(import.meta.dir, "../fonts");
7
7
  const [interRegular, interBold] = await Promise.all([
8
8
  Bun.file(join(fontsDir, "inter-latin-400-normal.woff")).arrayBuffer(),
9
9
  Bun.file(join(fontsDir, "inter-latin-700-normal.woff")).arrayBuffer(),
10
10
  ]);
11
11
 
12
12
  // Initialize resvg-wasm
13
- const wasmPath = join(import.meta.dir, "../node_modules/@resvg/resvg-wasm/index_bg.wasm");
13
+ // In development: ../node_modules/@resvg/resvg-wasm/
14
+ // When installed: ../../@resvg/resvg-wasm/
15
+ const possiblePaths = [
16
+ join(import.meta.dir, "../node_modules/@resvg/resvg-wasm/index_bg.wasm"),
17
+ join(import.meta.dir, "../../@resvg/resvg-wasm/index_bg.wasm"),
18
+ ];
19
+
20
+ let wasmPath: string | undefined;
21
+ for (const path of possiblePaths) {
22
+ if (await Bun.file(path).exists()) {
23
+ wasmPath = path;
24
+ break;
25
+ }
26
+ }
27
+
28
+ if (!wasmPath) {
29
+ throw new Error("Could not find @resvg/resvg-wasm WASM file");
30
+ }
31
+
14
32
  await initWasm(Bun.file(wasmPath).arrayBuffer());
15
33
 
16
34
  // Cache generated images by diagram ID
17
35
  const cache = new Map<string, Buffer>();
18
36
 
37
+ export async function generateIndexOgImage(
38
+ mode: "local" | "server",
39
+ diagramCount: number,
40
+ ): Promise<Buffer> {
41
+ const cacheKey = `__index_${mode}_${diagramCount}`;
42
+ const cached = cache.get(cacheKey);
43
+ if (cached) return cached;
44
+
45
+ const subtitle = mode === "server"
46
+ ? `${diagramCount} diagram${diagramCount !== 1 ? "s" : ""} shared`
47
+ : `${diagramCount} diagram${diagramCount !== 1 ? "s" : ""}`;
48
+
49
+ const tagline = mode === "server"
50
+ ? "Interactive code walkthrough diagrams, shareable with anyone."
51
+ : "Interactive code walkthrough diagrams";
52
+
53
+ const svg = await satori(
54
+ {
55
+ type: "div",
56
+ props: {
57
+ style: {
58
+ width: "100%",
59
+ height: "100%",
60
+ display: "flex",
61
+ flexDirection: "column",
62
+ justifyContent: "center",
63
+ alignItems: "center",
64
+ padding: "60px",
65
+ backgroundColor: "#0a0a0a",
66
+ color: "#e5e5e5",
67
+ fontFamily: "Inter",
68
+ },
69
+ children: [
70
+ {
71
+ type: "div",
72
+ props: {
73
+ style: {
74
+ display: "flex",
75
+ flexDirection: "column",
76
+ alignItems: "center",
77
+ gap: "20px",
78
+ },
79
+ children: [
80
+ {
81
+ type: "div",
82
+ props: {
83
+ style: {
84
+ fontSize: "80px",
85
+ fontWeight: 700,
86
+ color: "#e5e5e5",
87
+ },
88
+ children: "Traverse",
89
+ },
90
+ },
91
+ {
92
+ type: "div",
93
+ props: {
94
+ style: {
95
+ fontSize: "32px",
96
+ color: "#a3a3a3",
97
+ textAlign: "center",
98
+ },
99
+ children: tagline,
100
+ },
101
+ },
102
+ {
103
+ type: "div",
104
+ props: {
105
+ style: {
106
+ display: "flex",
107
+ alignItems: "center",
108
+ gap: "8px",
109
+ marginTop: "16px",
110
+ fontSize: "24px",
111
+ color: "#a3a3a3",
112
+ backgroundColor: "#1c1c1e",
113
+ padding: "10px 24px",
114
+ borderRadius: "8px",
115
+ border: "1px solid #262626",
116
+ },
117
+ children: subtitle,
118
+ },
119
+ },
120
+ ],
121
+ },
122
+ },
123
+ ],
124
+ },
125
+ },
126
+ {
127
+ width: 1200,
128
+ height: 630,
129
+ fonts: [
130
+ { name: "Inter", data: interRegular, weight: 400, style: "normal" as const },
131
+ { name: "Inter", data: interBold, weight: 700, style: "normal" as const },
132
+ ],
133
+ },
134
+ );
135
+
136
+ const resvg = new Resvg(svg, {
137
+ fitTo: { mode: "width", value: 1200 },
138
+ });
139
+ const png = Buffer.from(resvg.render().asPng());
140
+
141
+ cache.set(cacheKey, png);
142
+ return png;
143
+ }
144
+
19
145
  export async function generateOgImage(
20
146
  id: string,
21
147
  summary: string,