(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
99
fork

Configure Feed

Select the types of activity you want to include in your feed.

better og images

scanash00 590ce816 d85eef29

+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 {