Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

better og images

+340 -312
backend/internal/api/fonts/DroidSansFallback.ttf

This is a binary file and will not be displayed.

backend/internal/api/fonts/Inter-Bold.ttf

This is a binary file and will not be displayed.

backend/internal/api/fonts/Inter-Regular.ttf

This is a binary file and will not be displayed.

+2 -2
web/public/logo.svg
··· 1 1 <svg width="265" height="231" viewBox="0 0 265 231" fill="#027bff" xmlns="http://www.w3.org/2000/svg"> 2 2 <path d="M0 230 V0 H199 V65.7156 H149.5 V115.216 H182.5 L199 131.716 V230 Z"/> 3 - <path d="M215 214.224 V230 H264.5 V0 H215.07 V16.2242 H248.5 V214.224 H215 Z"/> 4 - </svg> 3 + <path d="M215 214.224 V230 H264.5 V0 H215 V16.2242 H248.5 V214.224 H215 Z"/> 4 + </svg>
+338 -310
web/src/pages/og-image.ts
··· 27 27 return fontsLoaded; 28 28 } 29 29 30 - let logoDataURI: string | null = null; 31 - 32 - function getLogoDataURI(): string { 33 - if (logoDataURI) return logoDataURI; 34 - try { 35 - const publicDir = getPublicDir(); 36 - const buf = readFileSync(join(publicDir, "logo.svg")); 37 - logoDataURI = `data:image/svg+xml;base64,${buf.toString("base64")}`; 38 - } catch { 39 - logoDataURI = ""; 40 - } 41 - return logoDataURI; 42 - } 43 - 44 30 const API_URL = process.env.API_URL || "http://localhost:8081"; 45 31 46 32 interface RecordData { 47 33 type: "annotation" | "highlight" | "bookmark" | "collection"; 48 34 author: string; 35 + displayName: string; 49 36 avatarURL: string; 50 37 text: string; 51 38 quote: string; ··· 53 40 title: string; 54 41 icon: string; 55 42 description: string; 43 + color: string; 56 44 } 57 45 58 46 async function fetchRecordData(uri: string): Promise<RecordData | null> { ··· 63 51 if (res.ok) { 64 52 const item = await res.json(); 65 53 const author = item.author || item.creator || {}; 66 - const handle = author.handle 67 - ? `@${author.handle}` 68 - : author.did || "someone"; 54 + const handle = author.handle || ""; 55 + const displayName = author.displayName || handle || "someone"; 69 56 const avatarURL = author.avatar || ""; 70 57 const targetSource = item.target?.source || item.url || item.source || ""; 71 58 const domain = targetSource 72 59 ? (() => { 73 60 try { 74 - return new URL(targetSource).host; 61 + return new URL(targetSource).hostname.replace(/^www\./, ""); 75 62 } catch { 76 63 return ""; 77 64 } ··· 90 77 ) { 91 78 return { 92 79 type: "highlight", 93 - author: handle, 80 + author: handle ? `@${handle}` : "someone", 81 + displayName, 94 82 avatarURL, 95 83 text: targetTitle, 96 84 quote: selectorText, ··· 98 86 title: "", 99 87 icon: "", 100 88 description: "", 89 + color: item.color || "", 101 90 }; 102 91 } 103 92 104 93 if (uri.includes("/at.margin.bookmark/")) { 105 94 return { 106 95 type: "bookmark", 107 - author: handle, 96 + author: handle ? `@${handle}` : "someone", 97 + displayName, 108 98 avatarURL, 109 99 text: item.title || targetTitle || "Bookmark", 110 100 quote: item.description || bodyText || "", ··· 112 102 title: "", 113 103 icon: "", 114 104 description: "", 105 + color: "", 115 106 }; 116 107 } 117 108 118 109 return { 119 110 type: "annotation", 120 - author: handle, 111 + author: handle ? `@${handle}` : "someone", 112 + displayName, 121 113 avatarURL, 122 114 text: bodyText, 123 115 quote: selectorText, ··· 125 117 title: "", 126 118 icon: "", 127 119 description: "", 120 + color: "", 128 121 }; 129 122 } 130 123 } catch { ··· 138 131 if (res.ok) { 139 132 const item = await res.json(); 140 133 const author = item.author || item.creator || {}; 141 - const handle = author.handle 142 - ? `@${author.handle}` 143 - : author.did || "someone"; 134 + const handle = author.handle || ""; 135 + const displayName = author.displayName || handle || "someone"; 144 136 const avatarURL = author.avatar || ""; 145 137 146 138 return { 147 139 type: "collection", 148 - author: handle, 140 + author: handle ? `@${handle}` : "someone", 141 + displayName, 149 142 avatarURL, 150 143 text: "", 151 144 quote: "", ··· 153 146 title: item.name || "Collection", 154 147 icon: item.icon || "📁", 155 148 description: item.description || "", 149 + color: "", 156 150 }; 157 151 } 158 152 } catch { ··· 176 170 return ""; 177 171 } 178 172 179 - function avatarElement(url: string, author: string, size: number): unknown { 173 + const C = { 174 + bg: "#f4f4f5", 175 + text: "#18181b", 176 + textSecondary: "#52525b", 177 + textMuted: "#a1a1aa", 178 + textFaint: "#d4d4d8", 179 + primary: "#3b82f6", 180 + primaryDark: "#2563eb", 181 + border: "#e4e4e7", 182 + }; 183 + 184 + const namedColors: Record<string, string> = { 185 + yellow: "#facc15", 186 + green: "#4ade80", 187 + red: "#f87171", 188 + blue: "#60a5fa", 189 + }; 190 + 191 + function resolveHighlightColor(color: string): string { 192 + if (!color) return "#facc15"; 193 + if (color.startsWith("#")) return color; 194 + return namedColors[color] || "#facc15"; 195 + } 196 + 197 + const typeColors: Record<string, string> = { 198 + annotation: "#3b82f6", 199 + highlight: "#facc15", 200 + bookmark: "#22c55e", 201 + collection: "#3b82f6", 202 + }; 203 + 204 + function lightTint(hex: string): string { 205 + const r = parseInt(hex.slice(1, 3), 16); 206 + const g = parseInt(hex.slice(3, 5), 16); 207 + const b = parseInt(hex.slice(5, 7), 16); 208 + const mix = (c: number) => Math.round(c * 0.12 + 255 * 0.88); 209 + return `rgb(${mix(r)}, ${mix(g)}, ${mix(b)})`; 210 + } 211 + 212 + function avatarElement(url: string, name: string, size: number): unknown { 180 213 if (url) { 181 214 return { 182 215 type: "img", ··· 184 217 src: url, 185 218 width: size, 186 219 height: size, 187 - style: { borderRadius: size / 2 }, 220 + style: { borderRadius: size / 2, flexShrink: 0 }, 188 221 }, 189 222 }; 190 223 } 191 224 const letter = 192 - author[0] === "@" 193 - ? author[1]?.toUpperCase() || "?" 194 - : author[0]?.toUpperCase() || "?"; 225 + name[0] === "@" 226 + ? name[1]?.toUpperCase() || "?" 227 + : name[0]?.toUpperCase() || "?"; 195 228 return { 196 229 type: "div", 197 230 props: { ··· 199 232 width: size, 200 233 height: size, 201 234 borderRadius: size / 2, 202 - background: "#3b82f6", 235 + background: "#e4e4e7", 203 236 display: "flex", 204 237 alignItems: "center", 205 238 justifyContent: "center", 206 - color: "white", 239 + color: "#71717a", 207 240 fontSize: Math.round(size * 0.45), 208 241 fontWeight: 700, 242 + flexShrink: 0, 209 243 }, 210 244 children: letter, 211 245 }, 212 246 }; 213 247 } 214 248 215 - const typeColors: Record< 216 - string, 217 - { accent: string; badge: string; badgeText: string; bar: string } 218 - > = { 219 - annotation: { 220 - accent: "#3b82f6", 221 - badge: "#1e3a8a", 222 - badgeText: "#60a5fa", 223 - bar: "#60a5fa", 224 - }, 225 - highlight: { 226 - accent: "#eab308", 227 - badge: "#422006", 228 - badgeText: "#facc15", 229 - bar: "#facc15", 230 - }, 231 - bookmark: { 232 - accent: "#22c55e", 233 - badge: "#052e16", 234 - badgeText: "#4ade80", 235 - bar: "#4ade80", 236 - }, 237 - }; 238 - 239 - function getTypeColor(type: string) { 240 - return typeColors[type] || typeColors.annotation; 249 + function coloredLogoUri(color: string): string { 250 + const publicDir = getPublicDir(); 251 + const svg = readFileSync(join(publicDir, "logo.svg"), "utf-8"); 252 + const recolored = svg.replace(/fill="[^"]*"/, `fill="${color}"`); 253 + return `data:image/svg+xml,${encodeURIComponent(recolored)}`; 241 254 } 242 255 243 - function typeBadge(type: string): unknown { 244 - const labels: Record<string, string> = { 245 - annotation: "Annotation", 246 - highlight: "Highlight", 247 - bookmark: "Bookmark", 248 - }; 249 - const c = getTypeColor(type); 256 + function logoElement(size: number, color: string): unknown { 250 257 return { 251 - type: "div", 258 + type: "img", 252 259 props: { 253 - style: { 254 - padding: "6px 16px", 255 - borderRadius: 99, 256 - background: c.badge, 257 - color: c.badgeText, 258 - fontSize: 16, 259 - fontWeight: 600, 260 - }, 261 - children: labels[type] || type, 260 + src: coloredLogoUri(color), 261 + width: size, 262 + height: size, 263 + style: { flexShrink: 0 }, 262 264 }, 263 265 }; 264 266 } 265 267 266 - function marginBrand(logo: string): unknown { 267 - if (!logo) return null; 268 + const typeLabels: Record<string, string> = { 269 + annotation: "Annotation", 270 + highlight: "Highlight", 271 + bookmark: "Bookmark", 272 + collection: "Collection", 273 + }; 274 + 275 + function headerWithBadge(data: RecordData, accentColor: string): unknown { 268 276 return { 269 277 type: "div", 270 278 props: { 271 - style: { 272 - display: "flex", 273 - alignItems: "center", 274 - marginLeft: "auto", 275 - }, 279 + style: { display: "flex", alignItems: "center" }, 276 280 children: [ 281 + avatarElement(data.avatarURL, data.displayName, 48), 277 282 { 278 - type: "img", 279 - props: { src: logo, width: 28, height: 24 }, 283 + type: "div", 284 + props: { 285 + style: { 286 + display: "flex", 287 + flexDirection: "column", 288 + marginLeft: 14, 289 + flex: 1, 290 + }, 291 + children: [ 292 + { 293 + type: "span", 294 + props: { 295 + style: { 296 + color: C.text, 297 + fontSize: 22, 298 + fontWeight: 600, 299 + }, 300 + children: data.displayName, 301 + }, 302 + }, 303 + { 304 + type: "span", 305 + props: { 306 + style: { 307 + color: C.textMuted, 308 + fontSize: 17, 309 + marginTop: 1, 310 + }, 311 + children: data.author, 312 + }, 313 + }, 314 + ], 315 + }, 316 + }, 317 + { 318 + type: "div", 319 + props: { 320 + style: { 321 + display: "flex", 322 + alignItems: "center", 323 + gap: 10, 324 + }, 325 + children: [ 326 + logoElement(24, accentColor), 327 + { 328 + type: "span", 329 + props: { 330 + style: { 331 + color: C.textFaint, 332 + fontSize: 18, 333 + }, 334 + children: "|", 335 + }, 336 + }, 337 + { 338 + type: "span", 339 + props: { 340 + style: { 341 + color: accentColor, 342 + fontSize: 16, 343 + fontWeight: 600, 344 + textTransform: "uppercase" as const, 345 + letterSpacing: 1, 346 + }, 347 + children: typeLabels[data.type] || data.type, 348 + }, 349 + }, 350 + ], 351 + }, 280 352 }, 281 353 ], 282 354 }, 283 355 }; 284 356 } 285 357 286 - function buildAnnotationImage(data: RecordData, logo: string) { 287 - const children: unknown[] = []; 288 - const tc = getTypeColor(data.type); 289 - 290 - children.push({ 358 + function footerSource(source?: string): unknown | null { 359 + if (!source) return null; 360 + return { 291 361 type: "div", 292 362 props: { 293 - style: { display: "flex", alignItems: "center", width: "100%" }, 363 + style: { 364 + display: "flex", 365 + alignItems: "center", 366 + marginTop: "auto", 367 + paddingTop: 16, 368 + }, 294 369 children: [ 295 - avatarElement(data.avatarURL, data.author, 48), 296 370 { 297 371 type: "span", 298 372 props: { 299 - style: { color: "#a1a1aa", fontSize: 22, marginLeft: 14 }, 300 - children: data.author, 301 - }, 302 - }, 303 - { 304 - type: "div", 305 - props: { 306 - style: { marginLeft: "auto", display: "flex" }, 307 - children: [typeBadge(data.type)], 373 + style: { color: C.textMuted, fontSize: 16 }, 374 + children: source, 308 375 }, 309 376 }, 310 377 ], 311 378 }, 312 - }); 379 + }; 380 + } 381 + 382 + function wrap(children: unknown[], bg?: string): unknown { 383 + return { 384 + type: "div", 385 + props: { 386 + style: { 387 + display: "flex", 388 + flexDirection: "column", 389 + width: "100%", 390 + height: "100%", 391 + background: bg || C.bg, 392 + padding: "48px 64px", 393 + fontFamily: "Inter", 394 + }, 395 + children, 396 + }, 397 + }; 398 + } 399 + 400 + function buildAnnotationImage(data: RecordData) { 401 + const accent = typeColors.annotation; 402 + const children: unknown[] = [headerWithBadge(data, accent)]; 313 403 314 404 if (data.text) { 405 + const len = data.text.length; 315 406 children.push({ 316 407 type: "div", 317 408 props: { 318 409 style: { 319 - color: "#fafafa", 320 - fontSize: data.text.length > 200 ? 26 : 32, 410 + color: C.text, 411 + fontSize: len > 200 ? 26 : len > 100 ? 30 : 36, 412 + fontWeight: 500, 321 413 lineHeight: 1.45, 322 414 marginTop: 32, 323 415 overflow: "hidden", 324 416 }, 325 - children: truncate(data.text, 300), 417 + children: truncate(data.text, 280), 326 418 }, 327 419 }); 328 420 } ··· 339 431 style: { 340 432 width: 4, 341 433 borderRadius: 2, 342 - background: tc.bar, 434 + background: accent, 343 435 flexShrink: 0, 344 436 }, 345 437 }, ··· 348 440 type: "div", 349 441 props: { 350 442 style: { 351 - color: "#a1a1aa", 352 - fontSize: data.quote.length > 150 ? 22 : 26, 353 - lineHeight: 1.5, 354 - paddingLeft: 18, 443 + color: C.textSecondary, 444 + fontSize: 20, 445 + lineHeight: 1.6, 446 + paddingLeft: 20, 355 447 fontStyle: "italic", 356 448 overflow: "hidden", 357 449 }, 358 - children: `"${truncate(data.quote, 250)}"`, 450 + children: truncate(data.quote, 200), 359 451 }, 360 452 }, 361 453 ], ··· 363 455 }); 364 456 } 365 457 366 - const footerChildren: unknown[] = []; 367 - if (data.source) { 368 - footerChildren.push({ 369 - type: "span", 370 - props: { 371 - style: { color: "#71717a", fontSize: 20 }, 372 - children: data.source, 373 - }, 374 - }); 375 - } 376 - footerChildren.push(marginBrand(logo)); 377 - 378 - children.push({ 379 - type: "div", 380 - props: { 381 - style: { 382 - display: "flex", 383 - alignItems: "center", 384 - marginTop: "auto", 385 - paddingTop: 28, 386 - borderTop: "1px solid #27272a", 387 - }, 388 - children: footerChildren, 389 - }, 390 - }); 458 + const footer = footerSource(data.source); 459 + if (footer) children.push(footer); 391 460 392 - return wrapCard(children, tc.accent); 461 + return wrap(children, lightTint(accent)); 393 462 } 394 463 395 - function buildBookmarkImage(data: RecordData, logo: string) { 396 - const children: unknown[] = []; 397 - const tc = getTypeColor("bookmark"); 464 + function buildHighlightImage(data: RecordData) { 465 + const highlightColor = resolveHighlightColor(data.color); 466 + const bgTint = lightTint(highlightColor); 467 + const quoteText = data.quote || data.text || "Highlighted passage"; 468 + const len = quoteText.length; 398 469 399 - children.push({ 470 + return { 400 471 type: "div", 401 472 props: { 402 - style: { display: "flex", alignItems: "center", width: "100%" }, 473 + style: { 474 + display: "flex", 475 + flexDirection: "column", 476 + width: "100%", 477 + height: "100%", 478 + background: bgTint, 479 + padding: "48px 64px", 480 + fontFamily: "Inter", 481 + }, 403 482 children: [ 483 + headerWithBadge(data, highlightColor), 404 484 { 405 485 type: "div", 406 486 props: { 407 - style: { display: "flex", alignItems: "center", gap: 10 }, 408 - children: [ 409 - { 410 - type: "div", 411 - props: { 412 - style: { 413 - width: 36, 414 - height: 36, 415 - borderRadius: 10, 416 - background: "#052e16", 417 - display: "flex", 418 - alignItems: "center", 419 - justifyContent: "center", 420 - }, 421 - children: { 422 - type: "div", 423 - props: { 424 - style: { 425 - fontSize: 18, 426 - color: "#4ade80", 427 - fontWeight: 700, 428 - }, 429 - children: "🔗", 430 - }, 431 - }, 432 - }, 433 - }, 434 - { 435 - type: "span", 436 - props: { 437 - style: { color: "#71717a", fontSize: 20 }, 438 - children: data.source || "Saved page", 439 - }, 440 - }, 441 - ], 487 + style: { 488 + color: highlightColor, 489 + fontSize: 120, 490 + fontWeight: 700, 491 + lineHeight: 1, 492 + marginTop: 28, 493 + }, 494 + children: "\u201C", 442 495 }, 443 496 }, 444 497 { 445 498 type: "div", 446 499 props: { 447 - style: { marginLeft: "auto", display: "flex" }, 448 - children: [typeBadge("bookmark")], 500 + style: { 501 + color: C.text, 502 + fontSize: len > 150 ? 28 : len > 80 ? 34 : 42, 503 + fontWeight: 600, 504 + lineHeight: 1.4, 505 + marginTop: -30, 506 + overflow: "hidden", 507 + }, 508 + children: truncate(quoteText, 240), 509 + }, 510 + }, 511 + data.text && data.quote 512 + ? { 513 + type: "div", 514 + props: { 515 + style: { 516 + color: C.textSecondary, 517 + fontSize: 20, 518 + marginTop: 20, 519 + }, 520 + children: truncate(data.text, 80), 521 + }, 522 + } 523 + : null, 524 + { 525 + type: "div", 526 + props: { 527 + style: { 528 + display: "flex", 529 + alignItems: "center", 530 + marginTop: "auto", 531 + paddingTop: 16, 532 + }, 533 + children: [ 534 + data.source 535 + ? { 536 + type: "span", 537 + props: { 538 + style: { color: C.textMuted, fontSize: 16 }, 539 + children: data.source, 540 + }, 541 + } 542 + : null, 543 + ].filter(Boolean), 449 544 }, 450 545 }, 451 - ], 546 + ].filter(Boolean), 452 547 }, 453 - }); 548 + }; 549 + } 454 550 551 + function buildBookmarkImage(data: RecordData) { 552 + const children: unknown[] = [headerWithBadge(data, typeColors.bookmark)]; 553 + 554 + if (data.source) { 555 + children.push({ 556 + type: "div", 557 + props: { 558 + style: { 559 + color: typeColors.bookmark, 560 + fontSize: 18, 561 + marginTop: 32, 562 + }, 563 + children: data.source, 564 + }, 565 + }); 566 + } 567 + 568 + const titleLen = (data.text || "").length; 455 569 children.push({ 456 570 type: "div", 457 571 props: { 458 572 style: { 459 - color: "#fafafa", 460 - fontSize: (data.text?.length || 0) > 60 ? 36 : 44, 573 + color: C.text, 574 + fontSize: titleLen > 60 ? 34 : 42, 461 575 fontWeight: 700, 462 - lineHeight: 1.3, 463 - marginTop: 36, 576 + lineHeight: 1.25, 577 + marginTop: data.source ? 10 : 32, 464 578 overflow: "hidden", 465 579 }, 466 - children: truncate(data.text || "Untitled Bookmark", 100), 580 + children: truncate(data.text || "Untitled Bookmark", 90), 467 581 }, 468 582 }); 469 583 ··· 472 586 type: "div", 473 587 props: { 474 588 style: { 475 - color: "#a1a1aa", 476 - fontSize: 24, 589 + color: C.textSecondary, 590 + fontSize: 22, 477 591 lineHeight: 1.5, 478 - marginTop: 20, 592 + marginTop: 16, 479 593 overflow: "hidden", 480 594 }, 481 - children: truncate(data.quote, 200), 595 + children: truncate(data.quote, 180), 482 596 }, 483 597 }); 484 598 } 485 599 600 + return wrap(children, lightTint(typeColors.bookmark)); 601 + } 602 + 603 + function buildCollectionImage(data: RecordData) { 604 + const children: unknown[] = [headerWithBadge(data, typeColors.collection)]; 605 + 486 606 children.push({ 487 607 type: "div", 488 608 props: { 489 609 style: { 490 610 display: "flex", 491 611 alignItems: "center", 492 - marginTop: "auto", 493 - paddingTop: 28, 494 - borderTop: "1px solid #27272a", 612 + gap: 20, 613 + marginTop: 36, 495 614 }, 496 615 children: [ 497 616 { 498 - type: "div", 499 - props: { 500 - style: { display: "flex", alignItems: "center" }, 501 - children: [ 502 - avatarElement(data.avatarURL, data.author, 36), 503 - { 504 - type: "span", 505 - props: { 506 - style: { color: "#71717a", fontSize: 20, marginLeft: 12 }, 507 - children: data.author, 508 - }, 509 - }, 510 - ], 511 - }, 512 - }, 513 - marginBrand(logo), 514 - ], 515 - }, 516 - }); 517 - 518 - return wrapCard(children, tc.accent); 519 - } 520 - 521 - function buildCollectionImage(data: RecordData, logo: string) { 522 - const children: unknown[] = []; 523 - 524 - children.push({ 525 - type: "div", 526 - props: { 527 - style: { display: "flex", alignItems: "center", gap: 18 }, 528 - children: [ 529 - { 530 617 type: "span", 531 - props: { style: { fontSize: 64 }, children: data.icon }, 618 + props: { style: { fontSize: 52 }, children: data.icon }, 532 619 }, 533 620 { 534 621 type: "span", 535 622 props: { 536 623 style: { 537 - color: "#fafafa", 538 - fontSize: 48, 624 + color: C.text, 625 + fontSize: 44, 539 626 fontWeight: 700, 540 627 overflow: "hidden", 541 628 }, 542 - children: truncate(data.title, 40), 629 + children: truncate(data.title, 36), 543 630 }, 544 631 }, 545 632 ], ··· 550 637 type: "div", 551 638 props: { 552 639 style: { 553 - color: data.description ? "#a1a1aa" : "#71717a", 554 - fontSize: 26, 640 + color: data.description ? C.textSecondary : C.textMuted, 641 + fontSize: 24, 555 642 lineHeight: 1.5, 556 - marginTop: 24, 643 + marginTop: 20, 557 644 overflow: "hidden", 558 645 }, 559 646 children: data.description 560 - ? truncate(data.description, 200) 647 + ? truncate(data.description, 180) 561 648 : "A collection on Margin", 562 649 }, 563 650 }); 564 651 565 - children.push({ 566 - type: "div", 567 - props: { 568 - style: { 569 - display: "flex", 570 - alignItems: "center", 571 - marginTop: "auto", 572 - paddingTop: 28, 573 - borderTop: "1px solid #27272a", 574 - }, 575 - children: [ 576 - { 577 - type: "div", 578 - props: { 579 - style: { display: "flex", alignItems: "center" }, 580 - children: [ 581 - avatarElement(data.avatarURL, data.author, 36), 582 - { 583 - type: "span", 584 - props: { 585 - style: { color: "#71717a", fontSize: 20, marginLeft: 12 }, 586 - children: data.author, 587 - }, 588 - }, 589 - ], 590 - }, 591 - }, 592 - marginBrand(logo), 593 - ], 594 - }, 595 - }); 596 - 597 - return wrapCard(children); 598 - } 599 - 600 - function wrapCard(children: unknown[], accent: string = "#3b82f6") { 601 - return { 602 - type: "div", 603 - props: { 604 - style: { 605 - display: "flex", 606 - width: "100%", 607 - height: "100%", 608 - background: "#09090b", 609 - padding: 40, 610 - fontFamily: "Inter", 611 - }, 612 - children: [ 613 - { 614 - type: "div", 615 - props: { 616 - style: { 617 - display: "flex", 618 - flexDirection: "column", 619 - width: "100%", 620 - height: "100%", 621 - padding: "52px 56px", 622 - border: "1px solid #27272a", 623 - borderRadius: 24, 624 - borderTop: `3px solid ${accent}`, 625 - background: "#18181b", 626 - overflow: "hidden", 627 - }, 628 - children, 629 - }, 630 - }, 631 - ], 632 - }, 633 - }; 652 + return wrap(children, lightTint(typeColors.collection)); 634 653 } 635 654 636 655 export const GET: APIRoute = async ({ url }) => { ··· 645 664 } 646 665 647 666 const fonts = loadFonts(); 648 - const logo = getLogoDataURI(); 649 667 650 - const element = 651 - data.type === "collection" 652 - ? buildCollectionImage(data, logo) 653 - : data.type === "bookmark" 654 - ? buildBookmarkImage(data, logo) 655 - : buildAnnotationImage(data, logo); 668 + let element: unknown; 669 + switch (data.type) { 670 + case "collection": 671 + element = buildCollectionImage(data); 672 + break; 673 + case "bookmark": 674 + element = buildBookmarkImage(data); 675 + break; 676 + case "highlight": 677 + element = buildHighlightImage(data); 678 + break; 679 + case "annotation": 680 + default: 681 + element = buildAnnotationImage(data); 682 + break; 683 + } 656 684 657 685 const svg = await satori(element as React.ReactNode, { 658 686 width: 1200, ··· 666 694 const codepoints = [...segment] 667 695 .map((c) => c.codePointAt(0)!.toString(16)) 668 696 .join("-"); 669 - const url = `https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/${codepoints}.svg`; 697 + const emojiUrl = `https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/${codepoints}.svg`; 670 698 try { 671 - const res = await fetch(url); 699 + const res = await fetch(emojiUrl); 672 700 if (res.ok) 673 701 return `data:image/svg+xml,${encodeURIComponent(await res.text())}`; 674 702 } catch {