this repo has no description atmosphereconf-vods.wisp.place/
4
fork

Configure Feed

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

og images

+1391 -9
+5 -1
package.json
··· 7 7 }, 8 8 "scripts": { 9 9 "dev": "vite dev --port 3000", 10 - "build": "vite build", 10 + "build": "node --experimental-strip-types ./scripts/generate-og-images.ts && vite build", 11 11 "preview": "vite preview", 12 12 "test": "vitest run", 13 13 "lint": "eslint", 14 14 "format": "oxfmt", 15 15 "format:check": "oxfmt --check", 16 16 "check": "oxfmt && eslint --fix", 17 + "generate:og-images": "node --experimental-strip-types ./scripts/generate-og-images.ts", 17 18 "generate:room-images": "node ./scripts/generate-room-images.mjs", 18 19 "generate:transcript-search-index": "node --experimental-strip-types ./scripts/generate-transcript-search-index.mjs", 19 20 "generate:video-subtitles": "node --experimental-strip-types ./scripts/generate-video-subtitles.mjs", ··· 25 26 "@react-aria/utils": "^3.33.1", 26 27 "@react-stately/utils": "^3.11.0", 27 28 "@react-types/overlays": "^3.9.4", 29 + "@resvg/resvg-js": "^2.6.2", 28 30 "@stylexjs/stylex": "^0.18.2", 29 31 "@tailwindcss/vite": "^4.1.18", 30 32 "@tanstack/react-devtools": "latest", ··· 47 49 "react-stately": "^3.45.0", 48 50 "rehype-sanitize": "^6.0.0", 49 51 "remark-gfm": "^4.0.1", 52 + "satori": "^0.26.0", 50 53 "tailwindcss": "^4.1.18", 54 + "typeface-ibm-plex-sans": "^1.1.13", 51 55 "web-haptics": "^0.0.6" 52 56 }, 53 57 "devDependencies": {
+318
pnpm-lock.yaml
··· 23 23 '@react-types/overlays': 24 24 specifier: ^3.9.4 25 25 version: 3.9.4(react@19.2.4) 26 + '@resvg/resvg-js': 27 + specifier: ^2.6.2 28 + version: 2.6.2 26 29 '@stylexjs/stylex': 27 30 specifier: ^0.18.2 28 31 version: 0.18.2 ··· 89 92 remark-gfm: 90 93 specifier: ^4.0.1 91 94 version: 4.0.1 95 + satori: 96 + specifier: ^0.26.0 97 + version: 0.26.0 92 98 tailwindcss: 93 99 specifier: ^4.1.18 94 100 version: 4.2.2 101 + typeface-ibm-plex-sans: 102 + specifier: ^1.1.13 103 + version: 1.1.13 95 104 web-haptics: 96 105 specifier: ^0.0.6 97 106 version: 0.0.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ··· 675 684 engines: {node: ^20.19.0 || >=22.12.0} 676 685 cpu: [arm64] 677 686 os: [linux] 687 + libc: [glibc] 678 688 679 689 '@oxfmt/binding-linux-arm64-musl@0.42.0': 680 690 resolution: {integrity: sha512-+JA0YMlSdDqmacygGi2REp57c3fN+tzARD8nwsukx9pkCHK+6DkbAA9ojS4lNKsiBjIW8WWa0pBrBWhdZEqfuw==} 681 691 engines: {node: ^20.19.0 || >=22.12.0} 682 692 cpu: [arm64] 683 693 os: [linux] 694 + libc: [musl] 684 695 685 696 '@oxfmt/binding-linux-ppc64-gnu@0.42.0': 686 697 resolution: {integrity: sha512-VfnET0j4Y5mdfCzh5gBt0NK28lgn5DKx+8WgSMLYYeSooHhohdbzwAStLki9pNuGy51y4I7IoW8bqwAaCMiJQg==} 687 698 engines: {node: ^20.19.0 || >=22.12.0} 688 699 cpu: [ppc64] 689 700 os: [linux] 701 + libc: [glibc] 690 702 691 703 '@oxfmt/binding-linux-riscv64-gnu@0.42.0': 692 704 resolution: {integrity: sha512-gVlCbmBkB0fxBWbhBj9rcxezPydsQHf4MFKeHoTSPicOQ+8oGeTQgQ8EeesSybWeiFPVRx3bgdt4IJnH6nOjAA==} 693 705 engines: {node: ^20.19.0 || >=22.12.0} 694 706 cpu: [riscv64] 695 707 os: [linux] 708 + libc: [glibc] 696 709 697 710 '@oxfmt/binding-linux-riscv64-musl@0.42.0': 698 711 resolution: {integrity: sha512-zN5OfstL0avgt/IgvRu0zjQzVh/EPkcLzs33E9LMAzpqlLWiPWeMDZyMGFlSRGOdDjuNmlZBCgj0pFnK5u32TQ==} 699 712 engines: {node: ^20.19.0 || >=22.12.0} 700 713 cpu: [riscv64] 701 714 os: [linux] 715 + libc: [musl] 702 716 703 717 '@oxfmt/binding-linux-s390x-gnu@0.42.0': 704 718 resolution: {integrity: sha512-9X6+H2L0qMc2sCAgO9HS03bkGLMKvOFjmEdchaFlany3vNZOjnVui//D8k/xZAtQv2vaCs1reD5KAgPoIU4msA==} 705 719 engines: {node: ^20.19.0 || >=22.12.0} 706 720 cpu: [s390x] 707 721 os: [linux] 722 + libc: [glibc] 708 723 709 724 '@oxfmt/binding-linux-x64-gnu@0.42.0': 710 725 resolution: {integrity: sha512-BajxJ6KQvMMdpXGPWhBGyjb2Jvx4uec0w+wi6TJZ6Tv7+MzPwe0pO8g5h1U0jyFgoaF7mDl6yKPW3ykWcbUJRw==} 711 726 engines: {node: ^20.19.0 || >=22.12.0} 712 727 cpu: [x64] 713 728 os: [linux] 729 + libc: [glibc] 714 730 715 731 '@oxfmt/binding-linux-x64-musl@0.42.0': 716 732 resolution: {integrity: sha512-0wV284I6vc5f0AqAhgAbHU2935B4bVpncPoe5n/WzVZY/KnHgqxC8iSFGeSyLWEgstFboIcWkOPck7tqbdHkzA==} 717 733 engines: {node: ^20.19.0 || >=22.12.0} 718 734 cpu: [x64] 719 735 os: [linux] 736 + libc: [musl] 720 737 721 738 '@oxfmt/binding-openharmony-arm64@0.42.0': 722 739 resolution: {integrity: sha512-p4BG6HpGnhfgHk1rzZfyR6zcWkE7iLrWxyehHfXUy4Qa5j3e0roglFOdP/Nj5cJJ58MA3isQ5dlfkW2nNEpolw==} ··· 1331 1348 peerDependencies: 1332 1349 react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 1333 1350 1351 + '@resvg/resvg-js-android-arm-eabi@2.6.2': 1352 + resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==} 1353 + engines: {node: '>= 10'} 1354 + cpu: [arm] 1355 + os: [android] 1356 + 1357 + '@resvg/resvg-js-android-arm64@2.6.2': 1358 + resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==} 1359 + engines: {node: '>= 10'} 1360 + cpu: [arm64] 1361 + os: [android] 1362 + 1363 + '@resvg/resvg-js-darwin-arm64@2.6.2': 1364 + resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==} 1365 + engines: {node: '>= 10'} 1366 + cpu: [arm64] 1367 + os: [darwin] 1368 + 1369 + '@resvg/resvg-js-darwin-x64@2.6.2': 1370 + resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==} 1371 + engines: {node: '>= 10'} 1372 + cpu: [x64] 1373 + os: [darwin] 1374 + 1375 + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': 1376 + resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==} 1377 + engines: {node: '>= 10'} 1378 + cpu: [arm] 1379 + os: [linux] 1380 + 1381 + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': 1382 + resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==} 1383 + engines: {node: '>= 10'} 1384 + cpu: [arm64] 1385 + os: [linux] 1386 + libc: [glibc] 1387 + 1388 + '@resvg/resvg-js-linux-arm64-musl@2.6.2': 1389 + resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==} 1390 + engines: {node: '>= 10'} 1391 + cpu: [arm64] 1392 + os: [linux] 1393 + libc: [musl] 1394 + 1395 + '@resvg/resvg-js-linux-x64-gnu@2.6.2': 1396 + resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==} 1397 + engines: {node: '>= 10'} 1398 + cpu: [x64] 1399 + os: [linux] 1400 + libc: [glibc] 1401 + 1402 + '@resvg/resvg-js-linux-x64-musl@2.6.2': 1403 + resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==} 1404 + engines: {node: '>= 10'} 1405 + cpu: [x64] 1406 + os: [linux] 1407 + libc: [musl] 1408 + 1409 + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': 1410 + resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==} 1411 + engines: {node: '>= 10'} 1412 + cpu: [arm64] 1413 + os: [win32] 1414 + 1415 + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': 1416 + resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==} 1417 + engines: {node: '>= 10'} 1418 + cpu: [ia32] 1419 + os: [win32] 1420 + 1421 + '@resvg/resvg-js-win32-x64-msvc@2.6.2': 1422 + resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==} 1423 + engines: {node: '>= 10'} 1424 + cpu: [x64] 1425 + os: [win32] 1426 + 1427 + '@resvg/resvg-js@2.6.2': 1428 + resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} 1429 + engines: {node: '>= 10'} 1430 + 1334 1431 '@rolldown/pluginutils@1.0.0-beta.40': 1335 1432 resolution: {integrity: sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w==} 1336 1433 ··· 1371 1468 resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} 1372 1469 cpu: [arm] 1373 1470 os: [linux] 1471 + libc: [glibc] 1374 1472 1375 1473 '@rollup/rollup-linux-arm-musleabihf@4.60.1': 1376 1474 resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} 1377 1475 cpu: [arm] 1378 1476 os: [linux] 1477 + libc: [musl] 1379 1478 1380 1479 '@rollup/rollup-linux-arm64-gnu@4.60.1': 1381 1480 resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} 1382 1481 cpu: [arm64] 1383 1482 os: [linux] 1483 + libc: [glibc] 1384 1484 1385 1485 '@rollup/rollup-linux-arm64-musl@4.60.1': 1386 1486 resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} 1387 1487 cpu: [arm64] 1388 1488 os: [linux] 1489 + libc: [musl] 1389 1490 1390 1491 '@rollup/rollup-linux-loong64-gnu@4.60.1': 1391 1492 resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} 1392 1493 cpu: [loong64] 1393 1494 os: [linux] 1495 + libc: [glibc] 1394 1496 1395 1497 '@rollup/rollup-linux-loong64-musl@4.60.1': 1396 1498 resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} 1397 1499 cpu: [loong64] 1398 1500 os: [linux] 1501 + libc: [musl] 1399 1502 1400 1503 '@rollup/rollup-linux-ppc64-gnu@4.60.1': 1401 1504 resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} 1402 1505 cpu: [ppc64] 1403 1506 os: [linux] 1507 + libc: [glibc] 1404 1508 1405 1509 '@rollup/rollup-linux-ppc64-musl@4.60.1': 1406 1510 resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} 1407 1511 cpu: [ppc64] 1408 1512 os: [linux] 1513 + libc: [musl] 1409 1514 1410 1515 '@rollup/rollup-linux-riscv64-gnu@4.60.1': 1411 1516 resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} 1412 1517 cpu: [riscv64] 1413 1518 os: [linux] 1519 + libc: [glibc] 1414 1520 1415 1521 '@rollup/rollup-linux-riscv64-musl@4.60.1': 1416 1522 resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} 1417 1523 cpu: [riscv64] 1418 1524 os: [linux] 1525 + libc: [musl] 1419 1526 1420 1527 '@rollup/rollup-linux-s390x-gnu@4.60.1': 1421 1528 resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} 1422 1529 cpu: [s390x] 1423 1530 os: [linux] 1531 + libc: [glibc] 1424 1532 1425 1533 '@rollup/rollup-linux-x64-gnu@4.60.1': 1426 1534 resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} 1427 1535 cpu: [x64] 1428 1536 os: [linux] 1537 + libc: [glibc] 1429 1538 1430 1539 '@rollup/rollup-linux-x64-musl@4.60.1': 1431 1540 resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} 1432 1541 cpu: [x64] 1433 1542 os: [linux] 1543 + libc: [musl] 1434 1544 1435 1545 '@rollup/rollup-openbsd-x64@4.60.1': 1436 1546 resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} ··· 1462 1572 cpu: [x64] 1463 1573 os: [win32] 1464 1574 1575 + '@shuding/opentype.js@1.4.0-beta.0': 1576 + resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} 1577 + engines: {node: '>= 8.0.0'} 1578 + hasBin: true 1579 + 1465 1580 '@solid-primitives/event-listener@2.4.5': 1466 1581 resolution: {integrity: sha512-nwRV558mIabl4yVAhZKY8cb6G+O1F0M6Z75ttTu5hk+SxdOnKSGj+eetDIu7Oax1P138ZdUU01qnBPR8rnxaEA==} 1467 1582 peerDependencies: ··· 1556 1671 engines: {node: '>= 20'} 1557 1672 cpu: [arm64] 1558 1673 os: [linux] 1674 + libc: [glibc] 1559 1675 1560 1676 '@tailwindcss/oxide-linux-arm64-musl@4.2.2': 1561 1677 resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} 1562 1678 engines: {node: '>= 20'} 1563 1679 cpu: [arm64] 1564 1680 os: [linux] 1681 + libc: [musl] 1565 1682 1566 1683 '@tailwindcss/oxide-linux-x64-gnu@4.2.2': 1567 1684 resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} 1568 1685 engines: {node: '>= 20'} 1569 1686 cpu: [x64] 1570 1687 os: [linux] 1688 + libc: [glibc] 1571 1689 1572 1690 '@tailwindcss/oxide-linux-x64-musl@4.2.2': 1573 1691 resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} 1574 1692 engines: {node: '>= 20'} 1575 1693 cpu: [x64] 1576 1694 os: [linux] 1695 + libc: [musl] 1577 1696 1578 1697 '@tailwindcss/oxide-wasm32-wasi@4.2.2': 1579 1698 resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} ··· 2026 2145 resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} 2027 2146 cpu: [arm64] 2028 2147 os: [linux] 2148 + libc: [glibc] 2029 2149 2030 2150 '@unrs/resolver-binding-linux-arm64-musl@1.11.1': 2031 2151 resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} 2032 2152 cpu: [arm64] 2033 2153 os: [linux] 2154 + libc: [musl] 2034 2155 2035 2156 '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': 2036 2157 resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} 2037 2158 cpu: [ppc64] 2038 2159 os: [linux] 2160 + libc: [glibc] 2039 2161 2040 2162 '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': 2041 2163 resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} 2042 2164 cpu: [riscv64] 2043 2165 os: [linux] 2166 + libc: [glibc] 2044 2167 2045 2168 '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': 2046 2169 resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} 2047 2170 cpu: [riscv64] 2048 2171 os: [linux] 2172 + libc: [musl] 2049 2173 2050 2174 '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': 2051 2175 resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} 2052 2176 cpu: [s390x] 2053 2177 os: [linux] 2178 + libc: [glibc] 2054 2179 2055 2180 '@unrs/resolver-binding-linux-x64-gnu@1.11.1': 2056 2181 resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} 2057 2182 cpu: [x64] 2058 2183 os: [linux] 2184 + libc: [glibc] 2059 2185 2060 2186 '@unrs/resolver-binding-linux-x64-musl@1.11.1': 2061 2187 resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} 2062 2188 cpu: [x64] 2063 2189 os: [linux] 2190 + libc: [musl] 2064 2191 2065 2192 '@unrs/resolver-binding-wasm32-wasi@1.11.1': 2066 2193 resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} ··· 2222 2349 resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} 2223 2350 engines: {node: 18 || 20 || >=22} 2224 2351 2352 + base64-js@0.0.8: 2353 + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} 2354 + engines: {node: '>= 0.4'} 2355 + 2225 2356 baseline-browser-mapping@2.10.12: 2226 2357 resolution: {integrity: sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==} 2227 2358 engines: {node: '>=6.0.0'} ··· 2256 2387 cac@6.7.14: 2257 2388 resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 2258 2389 engines: {node: '>=8'} 2390 + 2391 + camelize@1.0.1: 2392 + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} 2259 2393 2260 2394 caniuse-lite@1.0.30001782: 2261 2395 resolution: {integrity: sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==} ··· 2340 2474 color-name@1.1.3: 2341 2475 resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} 2342 2476 2477 + color-name@1.1.4: 2478 + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 2479 + 2343 2480 comma-separated-tokens@2.0.3: 2344 2481 resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} 2345 2482 ··· 2372 2509 resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 2373 2510 engines: {node: '>= 8'} 2374 2511 2512 + css-background-parser@0.1.0: 2513 + resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==} 2514 + 2515 + css-box-shadow@1.0.0-3: 2516 + resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==} 2517 + 2518 + css-color-keywords@1.0.0: 2519 + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} 2520 + engines: {node: '>=4'} 2521 + 2522 + css-gradient-parser@0.0.17: 2523 + resolution: {integrity: sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==} 2524 + engines: {node: '>=16'} 2525 + 2375 2526 css-mediaquery@0.1.2: 2376 2527 resolution: {integrity: sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==} 2377 2528 2378 2529 css-select@5.2.2: 2379 2530 resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} 2531 + 2532 + css-to-react-native@3.2.0: 2533 + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} 2380 2534 2381 2535 css-tree@3.2.1: 2382 2536 resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} ··· 2472 2626 electron-to-chromium@1.5.329: 2473 2627 resolution: {integrity: sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ==} 2474 2628 2629 + emoji-regex-xs@2.0.1: 2630 + resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==} 2631 + engines: {node: '>=10.0.0'} 2632 + 2475 2633 emoji-regex@10.6.0: 2476 2634 resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} 2477 2635 ··· 2513 2671 resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 2514 2672 engines: {node: '>=6'} 2515 2673 2674 + escape-html@1.0.3: 2675 + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} 2676 + 2516 2677 escape-string-regexp@1.0.5: 2517 2678 resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} 2518 2679 engines: {node: '>=0.8.0'} ··· 2657 2818 peerDependenciesMeta: 2658 2819 picomatch: 2659 2820 optional: true 2821 + 2822 + fflate@0.7.4: 2823 + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} 2660 2824 2661 2825 figures@6.1.0: 2662 2826 resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} ··· 2751 2915 hast-util-whitespace@3.0.0: 2752 2916 resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} 2753 2917 2918 + hex-rgb@4.3.0: 2919 + resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} 2920 + engines: {node: '>=6'} 2921 + 2754 2922 hip-ui@0.3.0: 2755 2923 resolution: {integrity: sha512-aJQG0t3vAnQtDb5Ny9a/ZpHl+nF4Dm62iF/Zfpk7DWDlvK8vYP6xVMP6xKQZBlbrPB9akalqmaUVM1s5POfvcg==} 2756 2924 hasBin: true ··· 2966 3134 engines: {node: '>= 12.0.0'} 2967 3135 cpu: [arm64] 2968 3136 os: [linux] 3137 + libc: [glibc] 2969 3138 2970 3139 lightningcss-linux-arm64-musl@1.32.0: 2971 3140 resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} 2972 3141 engines: {node: '>= 12.0.0'} 2973 3142 cpu: [arm64] 2974 3143 os: [linux] 3144 + libc: [musl] 2975 3145 2976 3146 lightningcss-linux-x64-gnu@1.32.0: 2977 3147 resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} 2978 3148 engines: {node: '>= 12.0.0'} 2979 3149 cpu: [x64] 2980 3150 os: [linux] 3151 + libc: [glibc] 2981 3152 2982 3153 lightningcss-linux-x64-musl@1.32.0: 2983 3154 resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} 2984 3155 engines: {node: '>= 12.0.0'} 2985 3156 cpu: [x64] 2986 3157 os: [linux] 3158 + libc: [musl] 2987 3159 2988 3160 lightningcss-win32-arm64-msvc@1.32.0: 2989 3161 resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} ··· 3004 3176 lilconfig@3.1.3: 3005 3177 resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} 3006 3178 engines: {node: '>=14'} 3179 + 3180 + linebreak@1.1.0: 3181 + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} 3007 3182 3008 3183 locate-path@6.0.0: 3009 3184 resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} ··· 3240 3415 resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 3241 3416 engines: {node: '>=10'} 3242 3417 3418 + pako@0.2.9: 3419 + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} 3420 + 3421 + parse-css-color@0.2.1: 3422 + resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} 3423 + 3243 3424 parse-entities@4.0.2: 3244 3425 resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} 3245 3426 ··· 3413 3594 safer-buffer@2.1.2: 3414 3595 resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 3415 3596 3597 + satori@0.26.0: 3598 + resolution: {integrity: sha512-tkMFrfIs3l2mQ2JEcyW0ADTy3zGggFRFzi6Ef8YozQSFsFKEqaSO1Y8F9wJg4//PJGQauMalHGTUEkPrFwhVPA==} 3599 + engines: {node: '>=16'} 3600 + 3416 3601 saxes@6.0.0: 3417 3602 resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} 3418 3603 engines: {node: '>=v12.22.7'} ··· 3508 3693 string-width@8.2.0: 3509 3694 resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} 3510 3695 engines: {node: '>=20'} 3696 + 3697 + string.prototype.codepointat@0.2.1: 3698 + resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} 3511 3699 3512 3700 stringify-entities@4.0.4: 3513 3701 resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} ··· 3554 3742 resolution: {integrity: sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==} 3555 3743 engines: {node: '>=18'} 3556 3744 3745 + tiny-inflate@1.0.3: 3746 + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} 3747 + 3557 3748 tiny-invariant@1.3.3: 3558 3749 resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} 3559 3750 ··· 3651 3842 resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==} 3652 3843 engines: {node: '>=20'} 3653 3844 3845 + typeface-ibm-plex-sans@1.1.13: 3846 + resolution: {integrity: sha512-WLe7IEjz4m3lDU5xXHMpOyVxnhGmCnuee5A9GJExHpYJcto+JzFg6uRNBSlsW6KDmb8mh8WJSGLSJJgjzj6iMg==} 3847 + 3654 3848 typescript-eslint@8.58.0: 3655 3849 resolution: {integrity: sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==} 3656 3850 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} ··· 3680 3874 undici@7.24.6: 3681 3875 resolution: {integrity: sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==} 3682 3876 engines: {node: '>=20.18.1'} 3877 + 3878 + unicode-trie@2.0.0: 3879 + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} 3683 3880 3684 3881 unified@11.0.5: 3685 3882 resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} ··· 5481 5678 '@react-types/shared': 3.33.1(react@19.2.4) 5482 5679 react: 19.2.4 5483 5680 5681 + '@resvg/resvg-js-android-arm-eabi@2.6.2': 5682 + optional: true 5683 + 5684 + '@resvg/resvg-js-android-arm64@2.6.2': 5685 + optional: true 5686 + 5687 + '@resvg/resvg-js-darwin-arm64@2.6.2': 5688 + optional: true 5689 + 5690 + '@resvg/resvg-js-darwin-x64@2.6.2': 5691 + optional: true 5692 + 5693 + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': 5694 + optional: true 5695 + 5696 + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': 5697 + optional: true 5698 + 5699 + '@resvg/resvg-js-linux-arm64-musl@2.6.2': 5700 + optional: true 5701 + 5702 + '@resvg/resvg-js-linux-x64-gnu@2.6.2': 5703 + optional: true 5704 + 5705 + '@resvg/resvg-js-linux-x64-musl@2.6.2': 5706 + optional: true 5707 + 5708 + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': 5709 + optional: true 5710 + 5711 + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': 5712 + optional: true 5713 + 5714 + '@resvg/resvg-js-win32-x64-msvc@2.6.2': 5715 + optional: true 5716 + 5717 + '@resvg/resvg-js@2.6.2': 5718 + optionalDependencies: 5719 + '@resvg/resvg-js-android-arm-eabi': 2.6.2 5720 + '@resvg/resvg-js-android-arm64': 2.6.2 5721 + '@resvg/resvg-js-darwin-arm64': 2.6.2 5722 + '@resvg/resvg-js-darwin-x64': 2.6.2 5723 + '@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2 5724 + '@resvg/resvg-js-linux-arm64-gnu': 2.6.2 5725 + '@resvg/resvg-js-linux-arm64-musl': 2.6.2 5726 + '@resvg/resvg-js-linux-x64-gnu': 2.6.2 5727 + '@resvg/resvg-js-linux-x64-musl': 2.6.2 5728 + '@resvg/resvg-js-win32-arm64-msvc': 2.6.2 5729 + '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 5730 + '@resvg/resvg-js-win32-x64-msvc': 2.6.2 5731 + 5484 5732 '@rolldown/pluginutils@1.0.0-beta.40': {} 5485 5733 5486 5734 '@rolldown/pluginutils@1.0.0-rc.3': {} ··· 5559 5807 5560 5808 '@rollup/rollup-win32-x64-msvc@4.60.1': 5561 5809 optional: true 5810 + 5811 + '@shuding/opentype.js@1.4.0-beta.0': 5812 + dependencies: 5813 + fflate: 0.7.4 5814 + string.prototype.codepointat: 0.2.1 5562 5815 5563 5816 '@solid-primitives/event-listener@2.4.5(solid-js@1.9.12)': 5564 5817 dependencies: ··· 6431 6684 6432 6685 balanced-match@4.0.4: {} 6433 6686 6687 + base64-js@0.0.8: {} 6688 + 6434 6689 baseline-browser-mapping@2.10.12: {} 6435 6690 6436 6691 bidi-js@1.0.3: ··· 6460 6715 update-browserslist-db: 1.2.3(browserslist@4.28.1) 6461 6716 6462 6717 cac@6.7.14: {} 6718 + 6719 + camelize@1.0.1: {} 6463 6720 6464 6721 caniuse-lite@1.0.30001782: {} 6465 6722 ··· 6557 6814 6558 6815 color-name@1.1.3: {} 6559 6816 6817 + color-name@1.1.4: {} 6818 + 6560 6819 comma-separated-tokens@2.0.3: {} 6561 6820 6562 6821 command-line-application@0.10.1: ··· 6598 6857 shebang-command: 2.0.0 6599 6858 which: 2.0.2 6600 6859 6860 + css-background-parser@0.1.0: {} 6861 + 6862 + css-box-shadow@1.0.0-3: {} 6863 + 6864 + css-color-keywords@1.0.0: {} 6865 + 6866 + css-gradient-parser@0.0.17: {} 6867 + 6601 6868 css-mediaquery@0.1.2: {} 6602 6869 6603 6870 css-select@5.2.2: ··· 6607 6874 domhandler: 5.0.3 6608 6875 domutils: 3.2.2 6609 6876 nth-check: 2.1.1 6877 + 6878 + css-to-react-native@3.2.0: 6879 + dependencies: 6880 + camelize: 1.0.1 6881 + css-color-keywords: 1.0.0 6882 + postcss-value-parser: 4.2.0 6610 6883 6611 6884 css-tree@3.2.1: 6612 6885 dependencies: ··· 6687 6960 6688 6961 electron-to-chromium@1.5.329: {} 6689 6962 6963 + emoji-regex-xs@2.0.1: {} 6964 + 6690 6965 emoji-regex@10.6.0: {} 6691 6966 6692 6967 encoding-sniffer@0.2.1: ··· 6741 7016 '@esbuild/win32-x64': 0.27.4 6742 7017 6743 7018 escalade@3.2.0: {} 7019 + 7020 + escape-html@1.0.3: {} 6744 7021 6745 7022 escape-string-regexp@1.0.5: {} 6746 7023 ··· 6899 7176 fdir@6.5.0(picomatch@4.0.4): 6900 7177 optionalDependencies: 6901 7178 picomatch: 4.0.4 7179 + 7180 + fflate@0.7.4: {} 6902 7181 6903 7182 figures@6.1.0: 6904 7183 dependencies: ··· 6996 7275 dependencies: 6997 7276 '@types/hast': 3.0.4 6998 7277 7278 + hex-rgb@4.3.0: {} 7279 + 6999 7280 hip-ui@0.3.0(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3): 7000 7281 dependencies: 7001 7282 '@inkjs/ui': 2.0.0(ink@6.8.0(@types/react@19.2.14)(react@19.2.4)) ··· 7263 7544 lightningcss-win32-x64-msvc: 1.32.0 7264 7545 7265 7546 lilconfig@3.1.3: {} 7547 + 7548 + linebreak@1.1.0: 7549 + dependencies: 7550 + base64-js: 0.0.8 7551 + unicode-trie: 2.0.0 7266 7552 7267 7553 locate-path@6.0.0: 7268 7554 dependencies: ··· 7719 8005 dependencies: 7720 8006 p-limit: 3.1.0 7721 8007 8008 + pako@0.2.9: {} 8009 + 8010 + parse-css-color@0.2.1: 8011 + dependencies: 8012 + color-name: 1.1.4 8013 + hex-rgb: 4.3.0 8014 + 7722 8015 parse-entities@4.0.2: 7723 8016 dependencies: 7724 8017 '@types/unist': 2.0.11 ··· 8033 8326 8034 8327 safer-buffer@2.1.2: {} 8035 8328 8329 + satori@0.26.0: 8330 + dependencies: 8331 + '@shuding/opentype.js': 1.4.0-beta.0 8332 + css-background-parser: 0.1.0 8333 + css-box-shadow: 1.0.0-3 8334 + css-gradient-parser: 0.0.17 8335 + css-to-react-native: 3.2.0 8336 + emoji-regex-xs: 2.0.1 8337 + escape-html: 1.0.3 8338 + linebreak: 1.1.0 8339 + parse-css-color: 0.2.1 8340 + postcss-value-parser: 4.2.0 8341 + yoga-layout: 3.2.1 8342 + 8036 8343 saxes@6.0.0: 8037 8344 dependencies: 8038 8345 xmlchars: 2.2.0 ··· 8104 8411 dependencies: 8105 8412 get-east-asian-width: 1.5.0 8106 8413 strip-ansi: 7.2.0 8414 + 8415 + string.prototype.codepointat@0.2.1: {} 8107 8416 8108 8417 stringify-entities@4.0.4: 8109 8418 dependencies: ··· 8149 8458 8150 8459 terminal-size@4.0.1: {} 8151 8460 8461 + tiny-inflate@1.0.3: {} 8462 + 8152 8463 tiny-invariant@1.3.3: {} 8153 8464 8154 8465 tinybench@2.9.0: {} ··· 8232 8543 dependencies: 8233 8544 tagged-tag: 1.0.0 8234 8545 8546 + typeface-ibm-plex-sans@1.1.13: {} 8547 + 8235 8548 typescript-eslint@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): 8236 8549 dependencies: 8237 8550 '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) ··· 8254 8567 undici-types@6.21.0: {} 8255 8568 8256 8569 undici@7.24.6: {} 8570 + 8571 + unicode-trie@2.0.0: 8572 + dependencies: 8573 + pako: 0.2.9 8574 + tiny-inflate: 1.0.3 8257 8575 8258 8576 unified@11.0.5: 8259 8577 dependencies:
public/og/home.png

This is a binary file and will not be displayed.

public/og/schedule.png

This is a binary file and will not be displayed.

public/og/search.png

This is a binary file and will not be displayed.

public/og/tracks/2301-classroom.png

This is a binary file and will not be displayed.

public/og/tracks/2311-classroom.png

This is a binary file and will not be displayed.

public/og/tracks/bukhman-lounge.png

This is a binary file and will not be displayed.

public/og/tracks/great-hall-south.png

This is a binary file and will not be displayed.

public/og/tracks/performance-theatre.png

This is a binary file and will not be displayed.

public/og/videos/2026-atmosphere-report.png

This is a binary file and will not be displayed.

public/og/videos/a-discussion-with-news-creators.png

This is a binary file and will not be displayed.

public/og/videos/a-fireside-chat-on-resonant-computing-why-we-wrote-the-manifesto-and-where-we-go-from-here.png

This is a binary file and will not be displayed.

public/og/videos/a-free-press-needs-free-protocols.png

This is a binary file and will not be displayed.

public/og/videos/abstracting-the-appview-workshop.png

This is a binary file and will not be displayed.

public/og/videos/abstracting-the-appview.png

This is a binary file and will not be displayed.

public/og/videos/account-logic-tee.png

This is a binary file and will not be displayed.

public/og/videos/advocating-for-digital-sovereignty-european-experiences-and-global-lessons.png

This is a binary file and will not be displayed.

public/og/videos/affordances-of-the-atmosphere.png

This is a binary file and will not be displayed.

public/og/videos/artist-dreaming-atmosphere.png

This is a binary file and will not be displayed.

public/og/videos/at-advent-an-atproto-adventure.png

This is a binary file and will not be displayed.

public/og/videos/at-transparency-logs-accountable-record-collections.png

This is a binary file and will not be displayed.

public/og/videos/atdata-distributed-datasets-over-atproto.png

This is a binary file and will not be displayed.

public/og/videos/atmospheric-publishing-discussion.png

This is a binary file and will not be displayed.

public/og/videos/atscience-unconference.png

This is a binary file and will not be displayed.

public/og/videos/automated-science-coordination-with-atproto.png

This is a binary file and will not be displayed.

public/og/videos/beyond-bluesky-community-infrastructure.png

This is a binary file and will not be displayed.

public/og/videos/blousques-case-study-on-the-challenges-in-translating-bluesky-s-ui.png

This is a binary file and will not be displayed.

public/og/videos/bluenotes-community-notes.png

This is a binary file and will not be displayed.

public/og/videos/bookhive-design-philosophy.png

This is a binary file and will not be displayed.

public/og/videos/bridging-social-graphs-how-sky-follower-bridge-helps-people-move-to-bluesky.png

This is a binary file and will not be displayed.

public/og/videos/bringing-self-sovereign-identities-to-the-masses-via-atproto-and-how-to-maximize-coherence-between-upcoming-did-plc-forks.png

This is a binary file and will not be displayed.

public/og/videos/build-and-share-personal-apps.png

This is a binary file and will not be displayed.

public/og/videos/building-bridgy-not-walls.png

This is a binary file and will not be displayed.

public/og/videos/building-cirrus.png

This is a binary file and will not be displayed.

public/og/videos/building-collective-intelligence-to-reduce-division-at-viewsift.png

This is a binary file and will not be displayed.

public/og/videos/building-decentralized-ai.png

This is a binary file and will not be displayed.

public/og/videos/building-future-of-ai-on-atproto.png

This is a binary file and will not be displayed.

public/og/videos/building-public-interest-infrastructure-on-atproto.png

This is a binary file and will not be displayed.

public/og/videos/burning-down-data-walls-in-the-us-fire-service-and-beyond.png

This is a binary file and will not be displayed.

public/og/videos/can-decentralists-cooperate-rethinking-commons-and-collective-action-in-the-age-of-platforms-and-ai.png

This is a binary file and will not be displayed.

public/og/videos/chive-decentralized-preprints-with-atproto.png

This is a binary file and will not be displayed.

public/og/videos/community-privacy-decentralized-network.png

This is a binary file and will not be displayed.

public/og/videos/compete-or-kill-cooperate-and-succeed.png

This is a binary file and will not be displayed.

public/og/videos/computational-education-commons-on-the-atmosphere.png

This is a binary file and will not be displayed.

public/og/videos/consent-before-cryptography.png

This is a binary file and will not be displayed.

public/og/videos/consuming-the-atmosphere.png

This is a binary file and will not be displayed.

public/og/videos/content-moderation-futures.png

This is a binary file and will not be displayed.

public/og/videos/coop-open-source-trust-and-safety-infrastructure-for-all.png

This is a binary file and will not be displayed.

public/og/videos/creating-a-safer-web-blacksky-s-moderation-tool.png

This is a binary file and will not be displayed.

public/og/videos/creating-the-atmosphere.png

This is a binary file and will not be displayed.

public/og/videos/creators-first-video-and-media-as-the-foundation-of-a-thriving-creator-economy-on-atproto.png

This is a binary file and will not be displayed.

public/og/videos/crowdsourced-research-synthesis-on-atproto-envisioning-an-inclusive-future.png

This is a binary file and will not be displayed.

public/og/videos/custom-feeds-landscape.png

This is a binary file and will not be displayed.

public/og/videos/data-sovereignty-for-games.png

This is a binary file and will not be displayed.

public/og/videos/day-3-closing-remarks.png

This is a binary file and will not be displayed.

public/og/videos/decentralized-is-bluesky.png

This is a binary file and will not be displayed.

public/og/videos/designing-for-social-web.png

This is a binary file and will not be displayed.

public/og/videos/did-lexicon-enterprise-data-problem.png

This is a binary file and will not be displayed.

public/og/videos/did-plc-war-games.png

This is a binary file and will not be displayed.

public/og/videos/dive-into-user-research-with-fellow-atproto-builders-and-users.png

This is a binary file and will not be displayed.

public/og/videos/e2ee-dms.png

This is a binary file and will not be displayed.

public/og/videos/feature-product-business-a-framework-for-sustainable-atproto-projects.png

This is a binary file and will not be displayed.

public/og/videos/feeds-are-the-new-websites.png

This is a binary file and will not be displayed.

public/og/videos/founders-and-funders.png

This is a binary file and will not be displayed.

public/og/videos/from-protocol-to-product-how-expo-powers-the-next-wave-of-at-proto-applications.png

This is a binary file and will not be displayed.

public/og/videos/from-toilets-to-moths.png

This is a binary file and will not be displayed.

public/og/videos/furryli-st-building-communities-without-landlords-from-the-protocol-up.png

This is a binary file and will not be displayed.

public/og/videos/future-of-science-social-media.png

This is a binary file and will not be displayed.

public/og/videos/groundings-with-my-siblings-lessons-learned-building-for-community.png

This is a binary file and will not be displayed.

public/og/videos/hospicing-social-media-personal-archival-practices-for-transition.png

This is a binary file and will not be displayed.

public/og/videos/how-and-why-news-on-atprotocol.png

This is a binary file and will not be displayed.

public/og/videos/how-de-centralized-is-bluesky-really.png

This is a binary file and will not be displayed.

public/og/videos/how-streamplace-works-vods.png

This is a binary file and will not be displayed.

public/og/videos/how-to-have-more-non-english-speaking-users.png

This is a binary file and will not be displayed.

public/og/videos/how-to-use-bluesky-to-easily-and-securely-preview-a-software-product-to-users.png

This is a binary file and will not be displayed.

public/og/videos/hypercerts-on-atproto.png

This is a binary file and will not be displayed.

public/og/videos/jacquard-magic-rust.png

This is a binary file and will not be displayed.

public/og/videos/journalism-must-create-its-own-algorithms.png

This is a binary file and will not be displayed.

public/og/videos/keynote-towards-modular-open-science.png

This is a binary file and will not be displayed.

public/og/videos/keywords-vs-embeddings.png

This is a binary file and will not be displayed.

public/og/videos/landslide.png

This is a binary file and will not be displayed.

public/og/videos/lea-a-social-app-for-researchers.png

This is a binary file and will not be displayed.

public/og/videos/making-wisdom-together.png

This is a binary file and will not be displayed.

public/og/videos/matadata-publishing-scientific-data-straight-to-at.png

This is a binary file and will not be displayed.

public/og/videos/meet-and-greet-the-team-from-new-public.png

This is a binary file and will not be displayed.

public/og/videos/narrative-strands-and-memetic-lineages-in-community-social-data-using-community-archive.png

This is a binary file and will not be displayed.

public/og/videos/new-directions.png

This is a binary file and will not be displayed.

public/og/videos/npmx-modern-browser-for-npm.png

This is a binary file and will not be displayed.

public/og/videos/oaklog-building-a-community-calendar-in-the-oakland-bay-area.png

This is a binary file and will not be displayed.

public/og/videos/oauth-masterclass.png

This is a binary file and will not be displayed.

public/og/videos/office-hours-lounge.png

This is a binary file and will not be displayed.

public/og/videos/one-year-of-graze.png

This is a binary file and will not be displayed.

public/og/videos/open-social-tech-and-geopolitical-risk.png

This is a binary file and will not be displayed.

public/og/videos/opening-remarks-day-3.png

This is a binary file and will not be displayed.

public/og/videos/opening-remarks-day-4.png

This is a binary file and will not be displayed.

public/og/videos/opening-remarks.png

This is a binary file and will not be displayed.

public/og/videos/pollen-toolkit.png

This is a binary file and will not be displayed.

public/og/videos/protocol-governance-hard-decentralization.png

This is a binary file and will not be displayed.

public/og/videos/reigniting-the-party-lessons-from-a-stalled-migration-to-bluesky.png

This is a binary file and will not be displayed.

public/og/videos/reproducible-citation-aware-automated-paper-reviews.png

This is a binary file and will not be displayed.

public/og/videos/rethinking-the-client.png

This is a binary file and will not be displayed.

public/og/videos/rewilding-internet-atproto.png

This is a binary file and will not be displayed.

public/og/videos/roomy-and-community-organizing-for-system-change.png

This is a binary file and will not be displayed.

public/og/videos/sattestations.png

This is a binary file and will not be displayed.

public/og/videos/scaling-the-atmosphere.png

This is a binary file and will not be displayed.

public/og/videos/semble-a-social-knowledge-network-for-research-on-atproto.png

This is a binary file and will not be displayed.

public/og/videos/semble-rediscovering-the-magic-of-trails.png

This is a binary file and will not be displayed.

public/og/videos/sensemaking-systems-ai-for-science.png

This is a binary file and will not be displayed.

public/og/videos/skylimit-a-curating-web-client-with-fine-grained-controls-to-mimic-the-newspaper-experience.png

This is a binary file and will not be displayed.

public/og/videos/skysquare-is-context-as-a-service.png

This is a binary file and will not be displayed.

public/og/videos/social-components.png

This is a binary file and will not be displayed.

public/og/videos/stop-hallucinating-the-protocol.png

This is a binary file and will not be displayed.

public/og/videos/studying-social-media-through-the-atmosphere.png

This is a binary file and will not be displayed.

public/og/videos/tangled-the-lewis-end.png

This is a binary file and will not be displayed.

public/og/videos/the-aggregation-era-burned-journalism-institutions-to-the-ground-the-federated-era-is-emerging-from-those-embers.png

This is a binary file and will not be displayed.

public/og/videos/the-astrosky-ecosystem-an-independent-online-home-for-astronomy.png

This is a binary file and will not be displayed.

public/og/videos/the-economics-of-sovereign-media-a-roadmap-for-at-protocol.png

This is a binary file and will not be displayed.

public/og/videos/the-future-of-open-source-is-social.png

This is a binary file and will not be displayed.

public/og/videos/the-phoenix-architecture.png

This is a binary file and will not be displayed.

public/og/videos/this-isn-t-over-until-we-all-listen-to-kpop.png

This is a binary file and will not be displayed.

public/og/videos/this-title-left-intentionally-blank.png

This is a binary file and will not be displayed.

public/og/videos/two-years-of-skywatch-lessons-learned-for-community-moderation.png

This is a binary file and will not be displayed.

public/og/videos/unconf-classroom-am.png

This is a binary file and will not be displayed.

public/og/videos/unconf-classroom.png

This is a binary file and will not be displayed.

public/og/videos/unconf-discussions.png

This is a binary file and will not be displayed.

public/og/videos/unconf-social-media-tools.png

This is a binary file and will not be displayed.

public/og/videos/using-graphql-to-build-with-atproto.png

This is a binary file and will not be displayed.

public/og/videos/verified-human-users-game-changer-in-the-atmosphere.png

This is a binary file and will not be displayed.

public/og/videos/waiting-for-the-future-to-load.png

This is a binary file and will not be displayed.

public/og/videos/webtiles-showcase.png

This is a binary file and will not be displayed.

public/og/videos/what-350-000-users-taught-me-about-growing-on-open-social.png

This is a binary file and will not be displayed.

public/og/videos/what-futures-can-we-build-together.png

This is a binary file and will not be displayed.

public/og/videos/wherever-you-get-your-podcasts-interoperability-in-the-atmosphere.png

This is a binary file and will not be displayed.

public/og/videos/who-owns-the-group-chat-building-collaborative-spaces-on-atproto.png

This is a binary file and will not be displayed.

public/og/videos/who-where-why-what-about-w-social.png

This is a binary file and will not be displayed.

public/og/videos/why-gander-social.png

This is a binary file and will not be displayed.

public/og/videos/your-research-institution-in-the-atmosphere.png

This is a binary file and will not be displayed.

+223
scripts/generate-og-images.ts
··· 1 + import { mkdir, writeFile } from "node:fs/promises"; 2 + import path from "node:path"; 3 + import process from "node:process"; 4 + import { fileURLToPath } from "node:url"; 5 + import type { ReactNode } from "react"; 6 + 7 + import { Resvg } from "@resvg/resvg-js"; 8 + import satori from "satori"; 9 + 10 + import { 11 + conferenceDays, 12 + sessions, 13 + speakerProfiles, 14 + tracks, 15 + } from "../src/data/conference.ts"; 16 + import { 17 + fetchImageAsDataUrl, 18 + GOOSE_IMAGE_URL, 19 + loadOgFonts, 20 + readPublicImageAsDataUrl, 21 + } from "../src/og/assets.ts"; 22 + import { OG_HEIGHT, OG_WIDTH } from "../src/og/palette.ts"; 23 + import { 24 + type OgSpeaker, 25 + renderHomeOg, 26 + renderScheduleOg, 27 + renderSearchOg, 28 + renderTrackOg, 29 + renderVideoOg, 30 + } from "../src/og/templates.ts"; 31 + 32 + function getProjectRoot(): string { 33 + const __dirname = path.dirname(fileURLToPath(import.meta.url)); 34 + return path.resolve(__dirname, ".."); 35 + } 36 + 37 + function formatLocalTime(isoDateTime: string): string { 38 + return new Intl.DateTimeFormat("en-US", { 39 + hour: "numeric", 40 + minute: "2-digit", 41 + timeZone: "America/Vancouver", 42 + }).format(new Date(isoDateTime)); 43 + } 44 + 45 + function getConferenceDay(dayId: string) { 46 + return conferenceDays.find((day) => day.id === dayId); 47 + } 48 + 49 + function getSpeakerAvatarUrl(name: string): string | null { 50 + const profile = speakerProfiles.find((candidate) => candidate.name === name); 51 + const avatarUrl = profile?.avatarUrl ?? null; 52 + if (!avatarUrl) { 53 + return null; 54 + } 55 + 56 + // Bluesky avatar endpoints default to WebP, which we intentionally skip in 57 + // fetchImageAsDataUrl to avoid Satori decode failures. Request JPEG instead. 58 + if ( 59 + avatarUrl.includes("cdn.bsky.app/img/avatar/plain/") && 60 + !avatarUrl.includes("@jpeg") && 61 + !avatarUrl.includes("@jpg") 62 + ) { 63 + return `${avatarUrl}@jpeg`; 64 + } 65 + 66 + return avatarUrl; 67 + } 68 + 69 + async function renderPng( 70 + node: ReactNode, 71 + fonts: Awaited<ReturnType<typeof loadOgFonts>>, 72 + ): Promise<Buffer> { 73 + const svg = await satori(node, { 74 + width: OG_WIDTH, 75 + height: OG_HEIGHT, 76 + fonts, 77 + }); 78 + 79 + const image = new Resvg(svg, { 80 + fitTo: { 81 + mode: "width", 82 + value: OG_WIDTH, 83 + }, 84 + }).render(); 85 + 86 + return image.asPng(); 87 + } 88 + 89 + async function writeOgPng( 90 + rootDir: string, 91 + relativeOutputPath: string, 92 + node: ReactNode, 93 + fonts: Awaited<ReturnType<typeof loadOgFonts>>, 94 + ) { 95 + const absoluteOutputPath = path.join(rootDir, "public", relativeOutputPath); 96 + await mkdir(path.dirname(absoluteOutputPath), { recursive: true }); 97 + const png = await renderPng(node, fonts); 98 + await writeFile(absoluteOutputPath, png); 99 + } 100 + 101 + async function buildStaticPages( 102 + rootDir: string, 103 + fonts: Awaited<ReturnType<typeof loadOgFonts>>, 104 + gooseImageDataUrl: string | null, 105 + ) { 106 + await writeOgPng( 107 + rootDir, 108 + "og/home.png", 109 + renderHomeOg({ 110 + gooseImageDataUrl, 111 + trackNames: tracks.slice(0, 4).map((track) => track.name), 112 + }), 113 + fonts, 114 + ); 115 + await writeOgPng( 116 + rootDir, 117 + "og/schedule.png", 118 + renderScheduleOg({ gooseImageDataUrl }), 119 + fonts, 120 + ); 121 + await writeOgPng( 122 + rootDir, 123 + "og/search.png", 124 + renderSearchOg({ gooseImageDataUrl }), 125 + fonts, 126 + ); 127 + } 128 + 129 + async function buildTrackPages( 130 + rootDir: string, 131 + fonts: Awaited<ReturnType<typeof loadOgFonts>>, 132 + gooseImageDataUrl: string | null, 133 + ) { 134 + for (const track of tracks) { 135 + const trackImageDataUrl = 136 + (await readPublicImageAsDataUrl(rootDir, track.imagePath)) ?? 137 + gooseImageDataUrl; 138 + 139 + await writeOgPng( 140 + rootDir, 141 + `og/tracks/${track.slug}.png`, 142 + renderTrackOg({ 143 + title: track.name, 144 + stageLabel: track.stageLabel, 145 + description: track.description, 146 + imageDataUrl: trackImageDataUrl, 147 + }), 148 + fonts, 149 + ); 150 + } 151 + } 152 + 153 + async function buildVideoPages( 154 + rootDir: string, 155 + fonts: Awaited<ReturnType<typeof loadOgFonts>>, 156 + gooseImageDataUrl: string | null, 157 + ) { 158 + const speakerAvatarCache = new Map<string, Promise<string | null>>(); 159 + 160 + for (const session of sessions) { 161 + const track = tracks.find((candidate) => candidate.slug === session.trackSlug); 162 + const fallbackImageDataUrl = 163 + (track ? await readPublicImageAsDataUrl(rootDir, track.imagePath) : null) ?? 164 + gooseImageDataUrl; 165 + 166 + const speakers: OgSpeaker[] = await Promise.all( 167 + session.speakers.slice(0, 4).map(async (name) => { 168 + const avatarUrl = getSpeakerAvatarUrl(name); 169 + 170 + if (!avatarUrl) { 171 + return { name, avatarDataUrl: null }; 172 + } 173 + 174 + let pending = speakerAvatarCache.get(avatarUrl); 175 + if (!pending) { 176 + pending = fetchImageAsDataUrl(avatarUrl); 177 + speakerAvatarCache.set(avatarUrl, pending); 178 + } 179 + 180 + return { name, avatarDataUrl: await pending }; 181 + }), 182 + ); 183 + 184 + const day = getConferenceDay(session.dayId); 185 + const primarySpeakerImageDataUrl = 186 + speakers.find((speaker) => speaker.avatarDataUrl)?.avatarDataUrl ?? null; 187 + 188 + await writeOgPng( 189 + rootDir, 190 + `og/videos/${session.slug}.png`, 191 + renderVideoOg({ 192 + title: session.title, 193 + trackName: track?.name ?? "ATmosphereConf", 194 + dayLabel: day?.label, 195 + timeLabel: `${formatLocalTime(session.startsAt)}–${formatLocalTime(session.endsAt)} PDT`, 196 + speakers, 197 + imageDataUrl: primarySpeakerImageDataUrl ?? fallbackImageDataUrl, 198 + }), 199 + fonts, 200 + ); 201 + } 202 + } 203 + 204 + async function main() { 205 + const rootDir = getProjectRoot(); 206 + const [fonts, gooseImageDataUrl] = await Promise.all([ 207 + loadOgFonts(), 208 + fetchImageAsDataUrl(GOOSE_IMAGE_URL), 209 + ]); 210 + 211 + await buildStaticPages(rootDir, fonts, gooseImageDataUrl); 212 + await buildTrackPages(rootDir, fonts, gooseImageDataUrl); 213 + await buildVideoPages(rootDir, fonts, gooseImageDataUrl); 214 + 215 + process.stdout.write( 216 + `Generated OG images for 3 static routes, ${tracks.length} track routes, and ${sessions.length} video routes.\n`, 217 + ); 218 + } 219 + 220 + main().catch((error) => { 221 + console.error(error); 222 + process.exitCode = 1; 223 + });
+139
src/og/assets.ts
··· 1 + import { readFile } from "node:fs/promises"; 2 + import { fileURLToPath } from "node:url"; 3 + import path from "node:path"; 4 + 5 + export const GOOSE_IMAGE_URL = 6 + "https://atmosphereconf.org/_image?href=%2F_astro%2Fgoodstuff-goose.DKPXDrcQ.png&w=792&h=990&f=png"; 7 + 8 + interface OgFont { 9 + name: string; 10 + data: ArrayBuffer; 11 + weight: number; 12 + style: "normal"; 13 + } 14 + 15 + const mimeByExtension: Record<string, string> = { 16 + ".avif": "image/avif", 17 + ".gif": "image/gif", 18 + ".jpeg": "image/jpeg", 19 + ".jpg": "image/jpeg", 20 + ".png": "image/png", 21 + ".svg": "image/svg+xml", 22 + ".webp": "image/webp", 23 + }; 24 + 25 + const imageCache = new Map<string, Promise<string | null>>(); 26 + let fontCache: Promise<OgFont[]> | null = null; 27 + 28 + function getMimeTypeFromPath(filePath: string): string { 29 + const extension = path.extname(filePath).toLowerCase(); 30 + return mimeByExtension[extension] ?? "application/octet-stream"; 31 + } 32 + 33 + async function readBufferAsDataUrl( 34 + key: string, 35 + read: () => Promise<Buffer>, 36 + mimeType: string, 37 + ): Promise<string | null> { 38 + let pending = imageCache.get(key); 39 + if (!pending) { 40 + pending = read() 41 + .then((buffer) => `data:${mimeType};base64,${buffer.toString("base64")}`) 42 + .catch(() => null); 43 + imageCache.set(key, pending); 44 + } 45 + return pending; 46 + } 47 + 48 + function getProjectRootFromModule(): string { 49 + const moduleDir = path.dirname(fileURLToPath(import.meta.url)); 50 + return path.resolve(moduleDir, "..", ".."); 51 + } 52 + 53 + async function loadFontFile( 54 + relativePath: string, 55 + name: string, 56 + weight: number, 57 + ): Promise<OgFont> { 58 + const fontBuffer = await readFile( 59 + path.join(getProjectRootFromModule(), relativePath), 60 + ); 61 + 62 + return { 63 + name, 64 + data: fontBuffer.buffer.slice( 65 + fontBuffer.byteOffset, 66 + fontBuffer.byteOffset + fontBuffer.byteLength, 67 + ), 68 + weight, 69 + style: "normal", 70 + }; 71 + } 72 + 73 + export async function loadOgFonts(): Promise<OgFont[]> { 74 + if (!fontCache) { 75 + fontCache = (async () => { 76 + return Promise.all([ 77 + loadFontFile( 78 + "node_modules/typeface-ibm-plex-sans/files/ibm-plex-sans-latin-400.woff", 79 + "IBM Plex Sans", 80 + 400, 81 + ), 82 + loadFontFile( 83 + "node_modules/typeface-ibm-plex-sans/files/ibm-plex-sans-latin-600.woff", 84 + "IBM Plex Sans", 85 + 600, 86 + ), 87 + loadFontFile( 88 + "node_modules/typeface-ibm-plex-sans/files/ibm-plex-sans-latin-700.woff", 89 + "IBM Plex Sans", 90 + 700, 91 + ), 92 + ]); 93 + })(); 94 + } 95 + 96 + return fontCache; 97 + } 98 + 99 + export async function fetchImageAsDataUrl(url: string): Promise<string | null> { 100 + let pending = imageCache.get(url); 101 + if (!pending) { 102 + pending = (async () => { 103 + const response = await fetch(url); 104 + if (!response.ok) { 105 + throw new Error( 106 + `Unable to fetch image ${url}: ${response.status} ${response.statusText}`, 107 + ); 108 + } 109 + 110 + const mimeType = response.headers.get("content-type") ?? "image/webp"; 111 + if (!mimeType.includes("image/png") && !mimeType.includes("image/jpeg")) { 112 + return null; 113 + } 114 + const arrayBuffer = await response.arrayBuffer(); 115 + const buffer = Buffer.from(arrayBuffer); 116 + return `data:${mimeType};base64,${buffer.toString("base64")}`; 117 + })().catch(() => null); 118 + 119 + imageCache.set(url, pending); 120 + } 121 + 122 + return pending; 123 + } 124 + 125 + export async function readPublicImageAsDataUrl( 126 + rootDir: string, 127 + publicPath: string, 128 + ): Promise<string | null> { 129 + const normalizedPath = publicPath.startsWith("/") 130 + ? publicPath.slice(1) 131 + : publicPath; 132 + const absolutePath = path.join(rootDir, "public", normalizedPath); 133 + 134 + return readBufferAsDataUrl( 135 + absolutePath, 136 + () => readFile(absolutePath), 137 + getMimeTypeFromPath(absolutePath), 138 + ); 139 + }
+52
src/og/meta.ts
··· 1 + const DEFAULT_SITE_URL = "http://localhost:3000"; 2 + 3 + interface OgMetaInput { 4 + title: string; 5 + description: string; 6 + imagePath: string; 7 + type?: "website" | "article"; 8 + } 9 + 10 + function getSiteUrl(): string { 11 + // Set VITE_SITE_URL in CI/deploy so prerendered OG image URLs are absolute in production. 12 + const configured = import.meta.env.VITE_SITE_URL as string | undefined; 13 + const url = configured?.trim() || DEFAULT_SITE_URL; 14 + return url.endsWith("/") ? url.slice(0, -1) : url; 15 + } 16 + 17 + function toAbsoluteUrl(pathname: string): string { 18 + const normalizedPath = pathname.startsWith("/") ? pathname : `/${pathname}`; 19 + return `${getSiteUrl()}${normalizedPath}`; 20 + } 21 + 22 + export function getCanonicalUrl(pathname: string): string { 23 + return toAbsoluteUrl(pathname); 24 + } 25 + 26 + export function getOgImageUrl(pathname: string): string { 27 + return toAbsoluteUrl(pathname); 28 + } 29 + 30 + export function buildOgMeta({ 31 + title, 32 + description, 33 + imagePath, 34 + type = "website", 35 + }: OgMetaInput) { 36 + const imageUrl = getOgImageUrl(imagePath); 37 + 38 + return [ 39 + { title }, 40 + { name: "description", content: description }, 41 + { property: "og:title", content: title }, 42 + { property: "og:description", content: description }, 43 + { property: "og:type", content: type }, 44 + { property: "og:image", content: imageUrl }, 45 + { property: "og:image:width", content: "1200" }, 46 + { property: "og:image:height", content: "630" }, 47 + { name: "twitter:card", content: "summary_large_image" }, 48 + { name: "twitter:title", content: title }, 49 + { name: "twitter:description", content: description }, 50 + { name: "twitter:image", content: imageUrl }, 51 + ]; 52 + }
+21
src/og/palette.ts
··· 1 + export const OG_WIDTH = 1200; 2 + export const OG_HEIGHT = 630; 3 + 4 + export const ogPalette = { 5 + bg: "#0d1520", 6 + bgSubtle: "#111927", 7 + border: "#205d9e", 8 + accent: "#0090ff", 9 + textPrimary: "#70b8ff", 10 + textSecondary: "#c2e6ff", 11 + textContrast: "#ffffff", 12 + } as const; 13 + 14 + export const ogSpacing = { 15 + xs: 8, 16 + sm: 12, 17 + md: 18, 18 + lg: 24, 19 + xl: 32, 20 + xxl: 44, 21 + } as const;
+463
src/og/templates.ts
··· 1 + import React from "react"; 2 + import type { ReactNode } from "react"; 3 + import { OG_HEIGHT, OG_WIDTH, ogPalette } from "./palette.ts"; 4 + 5 + const h = React.createElement; 6 + 7 + export interface OgSpeaker { 8 + name: string; 9 + avatarDataUrl?: string | null; 10 + } 11 + 12 + export interface TrackOgInput { 13 + title: string; 14 + stageLabel: string; 15 + description: string; 16 + imageDataUrl?: string | null; 17 + } 18 + 19 + export interface VideoOgInput { 20 + title: string; 21 + trackName: string; 22 + dayLabel?: string; 23 + timeLabel?: string; 24 + speakers: OgSpeaker[]; 25 + imageDataUrl?: string | null; 26 + } 27 + 28 + interface SharedCardInput { 29 + title: string; 30 + subtitle: string; 31 + description?: string; 32 + badge?: string; 33 + imageDataUrl?: string | null; 34 + imageDataUrls?: string[]; 35 + speakers?: OgSpeaker[]; 36 + imagePaneWidth?: number; 37 + imageObjectPosition?: string; 38 + } 39 + 40 + function truncate(inputText: string, maxLength: number): string { 41 + return inputText.length > maxLength 42 + ? `${inputText.slice(0, maxLength - 1).trimEnd()}…` 43 + : inputText; 44 + } 45 + 46 + function getTitleTypography( 47 + title: string, 48 + options?: { isNarrow?: boolean }, 49 + ): { 50 + fontSize: number; 51 + lineHeight: number; 52 + } { 53 + const length = title.trim().length; 54 + const isNarrow = options?.isNarrow ?? false; 55 + 56 + if (length <= (isNarrow ? 38 : 48)) { 57 + return { fontSize: 62, lineHeight: 1.1 }; 58 + } 59 + 60 + if (length <= (isNarrow ? 58 : 72)) { 61 + return { fontSize: isNarrow ? 50 : 54, lineHeight: 1.12 }; 62 + } 63 + 64 + if (length <= (isNarrow ? 74 : 88)) { 65 + return { fontSize: isNarrow ? 42 : 46, lineHeight: 1.14 }; 66 + } 67 + 68 + if (length <= (isNarrow ? 92 : 104)) { 69 + return { fontSize: isNarrow ? 33 : 36, lineHeight: 1.18 }; 70 + } 71 + 72 + return { fontSize: isNarrow ? 29 : 32, lineHeight: 1.2 }; 73 + } 74 + 75 + function getSubtitleTypography( 76 + title: string, 77 + options?: { isNarrow?: boolean }, 78 + ): { 79 + fontSize: number; 80 + lineHeight: number; 81 + marginBottom: number; 82 + } { 83 + const length = title.trim().length; 84 + const isNarrow = options?.isNarrow ?? false; 85 + 86 + if (length <= (isNarrow ? 74 : 88)) { 87 + return { 88 + fontSize: isNarrow ? 26 : 30, 89 + lineHeight: 1.3, 90 + marginBottom: isNarrow ? 12 : 16, 91 + }; 92 + } 93 + 94 + if (length <= (isNarrow ? 92 : 104)) { 95 + return { 96 + fontSize: isNarrow ? 23 : 26, 97 + lineHeight: 1.28, 98 + marginBottom: isNarrow ? 10 : 12, 99 + }; 100 + } 101 + 102 + return { 103 + fontSize: isNarrow ? 20 : 23, 104 + lineHeight: 1.25, 105 + marginBottom: isNarrow ? 8 : 10, 106 + }; 107 + } 108 + 109 + function box(style: Record<string, string | number>, children?: ReactNode) { 110 + return h("div", { style }, children ?? null); 111 + } 112 + 113 + function text( 114 + value: string, 115 + style: Record<string, string | number>, 116 + key?: string, 117 + ) { 118 + return h("div", { key, style }, value); 119 + } 120 + 121 + function renderImagePane(input: SharedCardInput, imagePaneWidth: number) { 122 + const collageUrls = (input.imageDataUrls ?? []).filter(Boolean).slice(0, 4); 123 + const objectPosition = input.imageObjectPosition ?? "center top"; 124 + 125 + if (collageUrls.length > 1) { 126 + if (collageUrls.length === 2) { 127 + return box( 128 + { 129 + width: imagePaneWidth, 130 + height: "100%", 131 + display: "flex", 132 + backgroundColor: "#08101a", 133 + }, 134 + collageUrls.map((url, index) => 135 + box( 136 + { 137 + key: `pair-${index}`, 138 + width: "100%", 139 + height: "100%", 140 + display: "flex", 141 + backgroundColor: "#08101a", 142 + }, 143 + h("img", { 144 + src: url, 145 + alt: input.title, 146 + width: "100%", 147 + height: "100%", 148 + style: { 149 + width: "100%", 150 + height: "100%", 151 + objectFit: "cover", 152 + objectPosition, 153 + display: "flex", 154 + }, 155 + }), 156 + ), 157 + ), 158 + ); 159 + } 160 + 161 + if (collageUrls.length === 3) { 162 + return box( 163 + { 164 + width: imagePaneWidth, 165 + height: "100%", 166 + display: "flex", 167 + backgroundColor: "#08101a", 168 + }, 169 + [ 170 + box( 171 + { 172 + key: "trio-left-full-column", 173 + width: "100%", 174 + height: "100%", 175 + display: "flex", 176 + backgroundColor: "#08101a", 177 + alignItems: "center", 178 + justifyContent: "center", 179 + flex: 1, 180 + }, 181 + h("img", { 182 + src: collageUrls[0], 183 + alt: input.title, 184 + width: "100%", 185 + height: "100%", 186 + style: { 187 + width: "100%", 188 + height: "100%", 189 + objectFit: "cover", 190 + objectPosition, 191 + display: "flex", 192 + }, 193 + }), 194 + ), 195 + box( 196 + { 197 + width: "100%", 198 + height: "100%", 199 + display: "flex", 200 + flexDirection: "column", 201 + flex: 1, 202 + }, 203 + [ 204 + box( 205 + { 206 + key: "trio-top-right", 207 + width: "100%", 208 + height: "50%", 209 + display: "flex", 210 + }, 211 + h("img", { 212 + src: collageUrls[1], 213 + alt: input.title, 214 + width: "100%", 215 + height: "100%", 216 + style: { 217 + width: "100%", 218 + height: "100%", 219 + objectFit: "cover", 220 + objectPosition, 221 + display: "flex", 222 + }, 223 + }), 224 + ), 225 + box( 226 + { 227 + key: "trio-bottom-right", 228 + width: "100%", 229 + height: "50%", 230 + display: "flex", 231 + }, 232 + h("img", { 233 + src: collageUrls[2], 234 + alt: input.title, 235 + width: "100%", 236 + height: "100%", 237 + style: { 238 + width: "100%", 239 + height: "100%", 240 + objectFit: "cover", 241 + objectPosition, 242 + display: "flex", 243 + }, 244 + }), 245 + ), 246 + ], 247 + ), 248 + ], 249 + ); 250 + } 251 + 252 + return box( 253 + { 254 + width: imagePaneWidth, 255 + height: "100%", 256 + display: "flex", 257 + flexWrap: "wrap", 258 + backgroundColor: "#08101a", 259 + }, 260 + collageUrls.map((url, index) => 261 + box( 262 + { 263 + key: `grid-2x2-${index}`, 264 + width: "50%", 265 + height: "50%", 266 + display: "flex", 267 + }, 268 + h("img", { 269 + src: url, 270 + alt: input.title, 271 + width: "100%", 272 + height: "100%", 273 + style: { 274 + width: "100%", 275 + height: "100%", 276 + objectFit: "cover", 277 + objectPosition, 278 + display: "flex", 279 + }, 280 + }), 281 + ), 282 + ), 283 + ); 284 + } 285 + 286 + if (input.imageDataUrl) { 287 + return h("img", { 288 + src: input.imageDataUrl, 289 + alt: input.title, 290 + width: imagePaneWidth, 291 + height: OG_HEIGHT, 292 + style: { 293 + width: imagePaneWidth, 294 + height: OG_HEIGHT, 295 + objectFit: "cover", 296 + objectPosition: input.imageObjectPosition ?? "center center", 297 + display: "flex", 298 + }, 299 + }); 300 + } 301 + 302 + return box({ 303 + width: imagePaneWidth, 304 + height: OG_HEIGHT, 305 + backgroundColor: "#08101a", 306 + display: "flex", 307 + }); 308 + } 309 + 310 + function sharedCard(input: SharedCardInput) { 311 + const speakerNames = (input.speakers ?? []).map((speaker) => speaker.name).join(" · "); 312 + const contentWidth = OG_WIDTH - 80; 313 + const imagePaneWidth = input.imagePaneWidth ?? 440; 314 + const textPaneWidth = contentWidth - imagePaneWidth; 315 + const isNarrowTextPane = textPaneWidth <= 620; 316 + const titleTypography = getTitleTypography(input.title, { 317 + isNarrow: isNarrowTextPane, 318 + }); 319 + const subtitleTypography = getSubtitleTypography(input.title, { 320 + isNarrow: isNarrowTextPane, 321 + }); 322 + 323 + return box( 324 + { 325 + width: OG_WIDTH, 326 + height: OG_HEIGHT, 327 + display: "flex", 328 + padding: 40, 329 + backgroundColor: ogPalette.bg, 330 + fontFamily: "IBM Plex Sans", 331 + }, 332 + box( 333 + { 334 + width: "100%", 335 + height: "100%", 336 + display: "flex", 337 + borderWidth: 1, 338 + borderStyle: "solid", 339 + borderColor: ogPalette.border, 340 + borderRadius: 24, 341 + overflow: "hidden", 342 + backgroundColor: ogPalette.bgSubtle, 343 + }, 344 + [ 345 + box( 346 + { 347 + width: textPaneWidth, 348 + height: "100%", 349 + display: "flex", 350 + flexDirection: "column", 351 + justifyContent: "center", 352 + padding: 42, 353 + }, 354 + [ 355 + input.badge 356 + ? text(input.badge, { 357 + fontSize: 24, 358 + fontWeight: 600, 359 + color: ogPalette.textPrimary, 360 + marginBottom: 16, 361 + }) 362 + : null, 363 + text(truncate(input.title, 110), { 364 + fontSize: titleTypography.fontSize, 365 + lineHeight: titleTypography.lineHeight, 366 + fontWeight: 700, 367 + color: ogPalette.textContrast, 368 + marginBottom: 14, 369 + }), 370 + text(truncate(input.subtitle, 100), { 371 + fontSize: subtitleTypography.fontSize, 372 + lineHeight: subtitleTypography.lineHeight, 373 + fontWeight: 600, 374 + color: ogPalette.textPrimary, 375 + marginBottom: subtitleTypography.marginBottom, 376 + }), 377 + input.description 378 + ? text(truncate(input.description, 220), { 379 + fontSize: 25, 380 + lineHeight: 1.4, 381 + color: ogPalette.textSecondary, 382 + }) 383 + : null, 384 + speakerNames 385 + ? text(truncate(`Speakers: ${speakerNames}`, 140), { 386 + marginTop: 18, 387 + fontSize: 22, 388 + lineHeight: 1.4, 389 + color: ogPalette.textSecondary, 390 + }) 391 + : null, 392 + ], 393 + ), 394 + renderImagePane(input, imagePaneWidth), 395 + ], 396 + ), 397 + ); 398 + } 399 + 400 + export function renderHomeOg(input: { 401 + gooseImageDataUrl?: string | null; 402 + trackNames: string[]; 403 + }) { 404 + return sharedCard({ 405 + title: "ATmosphereConf VODs", 406 + subtitle: "The global AT Protocol community conference", 407 + description: `Browse talks by room, session, and speaker across March 26–29, 2026. Rooms: ${input.trackNames.join(" · ")}`, 408 + badge: "Live + Remote", 409 + imageDataUrl: input.gooseImageDataUrl ?? null, 410 + }); 411 + } 412 + 413 + export function renderScheduleOg(input: { gooseImageDataUrl?: string | null }) { 414 + return sharedCard({ 415 + title: "Schedule", 416 + subtitle: "ATmosphereConf 2026 VOD timeline", 417 + description: 418 + "Compare sessions across rooms in Vancouver time and jump directly into recordings.", 419 + badge: "March 26–29, 2026", 420 + imageDataUrl: input.gooseImageDataUrl ?? null, 421 + }); 422 + } 423 + 424 + export function renderSearchOg(input: { gooseImageDataUrl?: string | null }) { 425 + return sharedCard({ 426 + title: "Search Transcripts", 427 + subtitle: "Find exact moments in talks", 428 + description: 429 + "Search subtitle text across all conference videos and jump to matching timestamps.", 430 + badge: "Transcript Search", 431 + imageDataUrl: input.gooseImageDataUrl ?? null, 432 + }); 433 + } 434 + 435 + export function renderTrackOg(input: TrackOgInput) { 436 + return sharedCard({ 437 + title: input.title, 438 + subtitle: input.stageLabel, 439 + description: input.description, 440 + badge: "Room", 441 + imageDataUrl: input.imageDataUrl ?? null, 442 + }); 443 + } 444 + 445 + export function renderVideoOg(input: VideoOgInput) { 446 + const subtitleParts = [input.timeLabel].filter( 447 + (value): value is string => Boolean(value), 448 + ); 449 + const speakerImageDataUrls = input.speakers 450 + .map((speaker) => speaker.avatarDataUrl) 451 + .filter((value): value is string => Boolean(value)); 452 + 453 + return sharedCard({ 454 + title: input.title, 455 + subtitle: subtitleParts.join(" · "), 456 + badge: input.dayLabel, 457 + imageDataUrl: input.imageDataUrl ?? null, 458 + imageDataUrls: speakerImageDataUrls, 459 + speakers: input.speakers, 460 + imagePaneWidth: 520, 461 + imageObjectPosition: "center top", 462 + }); 463 + }
+16 -4
src/routes/__root.tsx
··· 28 28 import { Footer } from "#/components/footer"; 29 29 import { Link } from "#/components/link"; 30 30 import { SmallBody } from "#/components/typography"; 31 + import { buildOgMeta, getCanonicalUrl } from "#/og/meta"; 31 32 import { Search } from "lucide-react"; 32 33 33 34 const IconButtonLink = createLink(IconButton); ··· 52 53 name: "viewport", 53 54 content: "width=device-width, initial-scale=1", 54 55 }, 55 - { 56 + ...buildOgMeta({ 56 57 title: "ATmosphereConf 2026 VOD Rooms", 58 + description: 59 + "Browse AtmosphereConf 2026 VODs by room, with schedule-first navigation and Streamplace playback.", 60 + imagePath: "/og/home.png", 61 + type: "website", 62 + }), 63 + { 64 + property: "og:site_name", 65 + content: "ATmosphereConf VODs", 57 66 }, 58 67 { 59 - name: "description", 60 - content: 61 - "Browse AtmosphereConf 2026 VODs by room, with schedule-first navigation and Streamplace playback.", 68 + property: "og:url", 69 + content: getCanonicalUrl("/"), 62 70 }, 63 71 ], 64 72 links: [ 73 + { 74 + rel: "canonical", 75 + href: getCanonicalUrl("/"), 76 + }, 65 77 { 66 78 rel: "preconnect", 67 79 href: "https://fonts.googleapis.com",
+20 -1
src/routes/index.tsx
··· 8 8 import { Blockquote } from "#/components/typography"; 9 9 import { Text } from "#/components/typography/text"; 10 10 import { getSessionsForTrack, getTracks } from "#/lib/conference"; 11 + import { buildOgMeta, getCanonicalUrl } from "#/og/meta"; 11 12 import { primaryColor, uiColor } from "../components/theme/color.stylex"; 12 13 import { breakpoints } from "../components/theme/media-queries.stylex"; 13 14 import { ui } from "../components/theme/semantic-color.stylex"; ··· 17 18 verticalSpace, 18 19 } from "../components/theme/semantic-spacing.stylex"; 19 20 20 - export const Route = createFileRoute("/")({ component: App }); 21 + const HOME_TITLE = "ATmosphereConf 2026 VOD Rooms"; 22 + const HOME_DESCRIPTION = 23 + "Browse AtmosphereConf 2026 VODs by room, with schedule-first navigation and Streamplace playback."; 24 + 25 + export const Route = createFileRoute("/")({ 26 + head: () => ({ 27 + meta: [ 28 + ...buildOgMeta({ 29 + title: HOME_TITLE, 30 + description: HOME_DESCRIPTION, 31 + imagePath: "/og/home.png", 32 + type: "website", 33 + }), 34 + { property: "og:url", content: getCanonicalUrl("/") }, 35 + ], 36 + links: [{ rel: "canonical", href: getCanonicalUrl("/") }], 37 + }), 38 + component: App, 39 + }); 21 40 22 41 const styles = stylex.create({ 23 42 introContainer: {
+30 -3
src/routes/schedule.tsx
··· 5 5 import { Page } from "#/components/page"; 6 6 import { Text } from "#/components/typography/text"; 7 7 import { formatLocalTime, getScheduleByDay } from "#/lib/conference"; 8 + import { buildOgMeta, getCanonicalUrl } from "#/og/meta"; 8 9 import { uiColor, warningColor } from "../components/theme/color.stylex"; 9 10 import { breakpoints } from "../components/theme/media-queries.stylex"; 10 11 import { ··· 13 14 verticalSpace, 14 15 } from "../components/theme/semantic-spacing.stylex"; 15 16 16 - export const Route = createFileRoute("/schedule")({ component: SchedulePage }); 17 + const SCHEDULE_TITLE = "Schedule | ATmosphereConf VODs"; 18 + const SCHEDULE_DESCRIPTION = 19 + "Compare sessions across rooms in Vancouver time and jump directly into recordings."; 20 + 21 + export const Route = createFileRoute("/schedule")({ 22 + head: () => ({ 23 + meta: [ 24 + ...buildOgMeta({ 25 + title: SCHEDULE_TITLE, 26 + description: SCHEDULE_DESCRIPTION, 27 + imagePath: "/og/schedule.png", 28 + type: "website", 29 + }), 30 + { property: "og:url", content: getCanonicalUrl("/schedule") }, 31 + ], 32 + links: [{ rel: "canonical", href: getCanonicalUrl("/schedule") }], 33 + }), 34 + component: SchedulePage, 35 + }); 17 36 18 37 const CALENDAR_PIXELS_PER_MINUTE = 5; 19 38 const CALENDAR_SLOT_MINUTES = 30; ··· 428 447 <Link 429 448 to="/videos/$videoSlug" 430 449 params={{ videoSlug: session.slug }} 431 - search={{ autoplay: false }} 450 + search={{ 451 + autoplay: false, 452 + q: "", 453 + t: undefined, 454 + }} 432 455 title={`${session.title} · ${formatLocalTime(session.startsAt)} - ${formatLocalTime(session.endsAt)}`} 433 456 {...stylex.props(styles.calendarEventLink)} 434 457 > ··· 605 628 <Link 606 629 to="/videos/$videoSlug" 607 630 params={{ videoSlug: session.slug }} 608 - search={{ autoplay: false }} 631 + search={{ 632 + autoplay: false, 633 + q: "", 634 + t: undefined, 635 + }} 609 636 title={`${session.title} · ${formatLocalTime(session.startsAt)} - ${formatLocalTime(session.endsAt)}`} 610 637 {...stylex.props(styles.calendarEventLink)} 611 638 >
+14
src/routes/search.tsx
··· 8 8 import { SpeakerList } from "#/components/speaker-list"; 9 9 import { Text } from "#/components/typography/text"; 10 10 import { getSpeakerProfile } from "#/lib/conference"; 11 + import { buildOgMeta, getCanonicalUrl } from "#/og/meta"; 11 12 import type { TranscriptSearchDocument } from "#/lib/video-subtitles"; 12 13 import { uiColor } from "../components/theme/color.stylex"; 13 14 import { ··· 44 45 export const Route = createFileRoute("/search")({ 45 46 validateSearch: (search: Record<string, unknown>) => ({ 46 47 q: typeof search.q === "string" ? search.q : "", 48 + }), 49 + head: () => ({ 50 + meta: [ 51 + ...buildOgMeta({ 52 + title: "Search Transcripts | ATmosphereConf VODs", 53 + description: 54 + "Search subtitle text across all conference videos and jump to matching timestamps.", 55 + imagePath: "/og/search.png", 56 + type: "website", 57 + }), 58 + { property: "og:url", content: getCanonicalUrl("/search") }, 59 + ], 60 + links: [{ rel: "canonical", href: getCanonicalUrl("/search") }], 47 61 }), 48 62 component: SearchPage, 49 63 });
+31
src/routes/tracks.$trackSlug.tsx
··· 33 33 import { Body } from "#/components/typography"; 34 34 import { mergeProps, useHover } from "react-aria"; 35 35 import { animationDuration } from "../components/theme/animations.stylex"; 36 + import { buildOgMeta, getCanonicalUrl } from "#/og/meta"; 36 37 37 38 const CardLink = createLink(Card); 38 39 ··· 54 55 } 55 56 56 57 export const Route = createFileRoute("/tracks/$trackSlug")({ 58 + head: ({ params }) => { 59 + const track = getTrackBySlug(params.trackSlug); 60 + const title = track 61 + ? `${track.name} | ATmosphereConf VODs` 62 + : "Room not found | ATmosphereConf VODs"; 63 + const description = track 64 + ? track.description 65 + : "This room could not be found in the conference catalog."; 66 + 67 + return { 68 + meta: [ 69 + ...buildOgMeta({ 70 + title, 71 + description, 72 + imagePath: track ? `/og/tracks/${track.slug}.png` : "/og/home.png", 73 + type: "website", 74 + }), 75 + { 76 + property: "og:url", 77 + content: getCanonicalUrl(`/tracks/${params.trackSlug}`), 78 + }, 79 + ], 80 + links: [ 81 + { 82 + rel: "canonical", 83 + href: getCanonicalUrl(`/tracks/${params.trackSlug}`), 84 + }, 85 + ], 86 + }; 87 + }, 57 88 component: TrackRouteComponent, 58 89 }); 59 90
+59
src/routes/videos.$videoSlug.tsx
··· 40 40 import { animationDuration } from "../components/theme/animations.stylex"; 41 41 import { Tooltip } from "#/components/tooltip"; 42 42 import { mergeProps, useHover } from "react-aria"; 43 + import { buildOgMeta, getCanonicalUrl } from "#/og/meta"; 43 44 44 45 const RouterLink = createLink(DSLink); 45 46 ··· 54 55 ? Number.parseFloat(search.t) 55 56 : undefined, 56 57 }), 58 + head: ({ params }) => { 59 + const session = getSessionBySlug(params.videoSlug); 60 + 61 + if (!session) { 62 + return { 63 + meta: [ 64 + ...buildOgMeta({ 65 + title: "Video not found | ATmosphereConf VODs", 66 + description: "This conference session could not be found.", 67 + imagePath: "/og/home.png", 68 + type: "website", 69 + }), 70 + { 71 + property: "og:url", 72 + content: getCanonicalUrl(`/videos/${params.videoSlug}`), 73 + }, 74 + ], 75 + links: [ 76 + { 77 + rel: "canonical", 78 + href: getCanonicalUrl(`/videos/${params.videoSlug}`), 79 + }, 80 + ], 81 + }; 82 + } 83 + 84 + const track = getTrackBySlug(session.trackSlug); 85 + const day = getConferenceDay(session.dayId); 86 + const descriptionParts = [ 87 + track?.name, 88 + day?.label, 89 + `${formatLocalTime(session.startsAt)}-${formatLocalTime(session.endsAt)} PDT`, 90 + session.speakers.length > 0 91 + ? `Speakers: ${session.speakers.join(", ")}` 92 + : null, 93 + ].filter((value): value is string => Boolean(value)); 94 + 95 + return { 96 + meta: [ 97 + ...buildOgMeta({ 98 + title: `${session.title} | ATmosphereConf VODs`, 99 + description: descriptionParts.join(" · "), 100 + imagePath: `/og/videos/${session.slug}.png`, 101 + type: "article", 102 + }), 103 + { 104 + property: "og:url", 105 + content: getCanonicalUrl(`/videos/${params.videoSlug}`), 106 + }, 107 + ], 108 + links: [ 109 + { 110 + rel: "canonical", 111 + href: getCanonicalUrl(`/videos/${params.videoSlug}`), 112 + }, 113 + ], 114 + }; 115 + }, 57 116 component: VideoRouteComponent, 58 117 }); 59 118