snatching amp's walkthrough for my own purposes mwhahaha traverse.dunkirk.sh/diagram/6121f05c-a5ef-4ecf-8ffc-02534c5e767c

feat: add index og images

dunkirk.sh b7240893 3015a36e

verified
+141 -5
+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 - 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 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 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 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 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 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 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 {
+108
src/og.ts
··· 16 16 // Cache generated images by diagram ID 17 17 const cache = new Map<string, Buffer>(); 18 18 19 + export async function generateIndexOgImage( 20 + mode: "local" | "server", 21 + diagramCount: number, 22 + ): Promise<Buffer> { 23 + const cacheKey = `__index_${mode}_${diagramCount}`; 24 + const cached = cache.get(cacheKey); 25 + if (cached) return cached; 26 + 27 + const subtitle = mode === "server" 28 + ? `${diagramCount} diagram${diagramCount !== 1 ? "s" : ""} shared` 29 + : `${diagramCount} diagram${diagramCount !== 1 ? "s" : ""}`; 30 + 31 + const tagline = mode === "server" 32 + ? "Interactive code walkthrough diagrams, shareable with anyone." 33 + : "Interactive code walkthrough diagrams"; 34 + 35 + const svg = await satori( 36 + { 37 + type: "div", 38 + props: { 39 + style: { 40 + width: "100%", 41 + height: "100%", 42 + display: "flex", 43 + flexDirection: "column", 44 + justifyContent: "center", 45 + alignItems: "center", 46 + padding: "60px", 47 + backgroundColor: "#0a0a0a", 48 + color: "#e5e5e5", 49 + fontFamily: "Inter", 50 + }, 51 + children: [ 52 + { 53 + type: "div", 54 + props: { 55 + style: { 56 + display: "flex", 57 + flexDirection: "column", 58 + alignItems: "center", 59 + gap: "20px", 60 + }, 61 + children: [ 62 + { 63 + type: "div", 64 + props: { 65 + style: { 66 + fontSize: "80px", 67 + fontWeight: 700, 68 + color: "#e5e5e5", 69 + }, 70 + children: "Traverse", 71 + }, 72 + }, 73 + { 74 + type: "div", 75 + props: { 76 + style: { 77 + fontSize: "32px", 78 + color: "#a3a3a3", 79 + textAlign: "center", 80 + }, 81 + children: tagline, 82 + }, 83 + }, 84 + { 85 + type: "div", 86 + props: { 87 + style: { 88 + display: "flex", 89 + alignItems: "center", 90 + gap: "8px", 91 + marginTop: "16px", 92 + fontSize: "24px", 93 + color: "#a3a3a3", 94 + backgroundColor: "#1c1c1e", 95 + padding: "10px 24px", 96 + borderRadius: "8px", 97 + border: "1px solid #262626", 98 + }, 99 + children: subtitle, 100 + }, 101 + }, 102 + ], 103 + }, 104 + }, 105 + ], 106 + }, 107 + }, 108 + { 109 + width: 1200, 110 + height: 630, 111 + fonts: [ 112 + { name: "Inter", data: interRegular, weight: 400, style: "normal" as const }, 113 + { name: "Inter", data: interBold, weight: 700, style: "normal" as const }, 114 + ], 115 + }, 116 + ); 117 + 118 + const resvg = new Resvg(svg, { 119 + fitTo: { mode: "width", value: 1200 }, 120 + }); 121 + const png = Buffer.from(resvg.render().asPng()); 122 + 123 + cache.set(cacheKey, png); 124 + return png; 125 + } 126 + 19 127 export async function generateOgImage( 20 128 id: string, 21 129 summary: string,