my website at ewancroft.uk
6
fork

Configure Feed

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

feat(og): integrate @ewanc26/og for dynamic OG images

Replace static OG images with on-demand generation via /api/og endpoint.
Uses createOgEndpoint from the package with noise backgrounds enabled.

- Add /api/og dynamic endpoint using createOgEndpoint
- Enable seeded noise backgrounds for unique per-page visuals
- Remove static OG images from static/og/
- Update defaultSiteMeta to generate dynamic OG URLs
- Add createDynamicSiteMeta helper for page-specific OG images
- Update archive, github, and site/meta pages to use dynamic OG

👾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta Code <noreply@letta.com>

+413 -28
+1
package.json
··· 40 40 "@atproto/api": "^0.18.21", 41 41 "@ewanc26/atproto": "^0.2.8", 42 42 "@ewanc26/noise-avatar": "^0.2.3", 43 + "@ewanc26/og": "^0.1.2", 43 44 "@ewanc26/supporters": "^0.3.0", 44 45 "@ewanc26/tid": "^1.1.3", 45 46 "@ewanc26/ui": "^0.3.8",
+290
pnpm-lock.yaml
··· 17 17 '@ewanc26/noise-avatar': 18 18 specifier: ^0.2.3 19 19 version: 0.2.3 20 + '@ewanc26/og': 21 + specifier: ^0.1.2 22 + version: 0.1.2 20 23 '@ewanc26/supporters': 21 24 specifier: ^0.3.0 22 25 version: 0.3.0(@atproto/api@0.18.21)(svelte@5.54.1) ··· 619 622 '@ewanc26/noise@0.1.1': 620 623 resolution: {integrity: sha512-XeAc0vFrcDHQA7K8xoVVCTYhB2opeBnvGj4s+6SqDS/E3IAP6V32mGf+H2U0JcQHsH4hvpVehYEFt0i1Blnrfg==} 621 624 625 + '@ewanc26/noise@0.1.3': 626 + resolution: {integrity: sha512-iffC4uo/6gtAdHcmJ2VY2+o278nECydJ8OAPmr6rkPO9eUKq9t/42A2KHO5gxPlttjWPQjj3kCQz1aCE7IlAMg==} 627 + 628 + '@ewanc26/og@0.1.2': 629 + resolution: {integrity: sha512-OUy1KTfCBPBc7Y5NOKMbJL8MDYcpuUzy+hNZiUqwbcmTsrol7Gg6JrlvSpyK+b3oghaaqXeNpPYdq63JpO/Ngg==} 630 + 622 631 '@ewanc26/supporters@0.3.0': 623 632 resolution: {integrity: sha512-T+E7U0uhi1KX4mKbjprRi3BzsADPqileMQHo9oFOc5n9NIIwsddxLYDg0jGzW5XCr39voQkfRGj7g1vyfggKYA==} 624 633 peerDependencies: ··· 832 841 resolution: {integrity: sha512-2FK1hF93Fuf1laSdfiEmJvSJPVIDHEUTz68D3Fi9s0IZrrpaEcj6pTFBTbYvsgC5du4ogrtf5re7yMMvrKNgkw==} 833 842 engines: {node: ^20.9.0 || ^22.11.0 || ^24, pnpm: ^10.0.0} 834 843 844 + '@resvg/resvg-js-android-arm-eabi@2.6.2': 845 + resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==} 846 + engines: {node: '>= 10'} 847 + cpu: [arm] 848 + os: [android] 849 + 850 + '@resvg/resvg-js-android-arm64@2.6.2': 851 + resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==} 852 + engines: {node: '>= 10'} 853 + cpu: [arm64] 854 + os: [android] 855 + 856 + '@resvg/resvg-js-darwin-arm64@2.6.2': 857 + resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==} 858 + engines: {node: '>= 10'} 859 + cpu: [arm64] 860 + os: [darwin] 861 + 862 + '@resvg/resvg-js-darwin-x64@2.6.2': 863 + resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==} 864 + engines: {node: '>= 10'} 865 + cpu: [x64] 866 + os: [darwin] 867 + 868 + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': 869 + resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==} 870 + engines: {node: '>= 10'} 871 + cpu: [arm] 872 + os: [linux] 873 + 874 + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': 875 + resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==} 876 + engines: {node: '>= 10'} 877 + cpu: [arm64] 878 + os: [linux] 879 + 880 + '@resvg/resvg-js-linux-arm64-musl@2.6.2': 881 + resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==} 882 + engines: {node: '>= 10'} 883 + cpu: [arm64] 884 + os: [linux] 885 + 886 + '@resvg/resvg-js-linux-x64-gnu@2.6.2': 887 + resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==} 888 + engines: {node: '>= 10'} 889 + cpu: [x64] 890 + os: [linux] 891 + 892 + '@resvg/resvg-js-linux-x64-musl@2.6.2': 893 + resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==} 894 + engines: {node: '>= 10'} 895 + cpu: [x64] 896 + os: [linux] 897 + 898 + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': 899 + resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==} 900 + engines: {node: '>= 10'} 901 + cpu: [arm64] 902 + os: [win32] 903 + 904 + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': 905 + resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==} 906 + engines: {node: '>= 10'} 907 + cpu: [ia32] 908 + os: [win32] 909 + 910 + '@resvg/resvg-js-win32-x64-msvc@2.6.2': 911 + resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==} 912 + engines: {node: '>= 10'} 913 + cpu: [x64] 914 + os: [win32] 915 + 916 + '@resvg/resvg-js@2.6.2': 917 + resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} 918 + engines: {node: '>= 10'} 919 + 835 920 '@rolldown/binding-android-arm64@1.0.0-rc.1': 836 921 resolution: {integrity: sha512-He6ZoCfv5D7dlRbrhNBkuMVIHd0GDnjJwbICE1OWpG7G3S2gmJ+eXkcNLJjzjNDpeI2aRy56ou39AJM9AD8YFA==} 837 922 engines: {node: ^20.19.0 || >=22.12.0} ··· 1045 1130 resolution: {integrity: sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==} 1046 1131 cpu: [x64] 1047 1132 os: [win32] 1133 + 1134 + '@shuding/opentype.js@1.4.0-beta.0': 1135 + resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} 1136 + engines: {node: '>= 8.0.0'} 1137 + hasBin: true 1048 1138 1049 1139 '@sinclair/typebox@0.25.24': 1050 1140 resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==} ··· 1419 1509 bare-abort-controller: 1420 1510 optional: true 1421 1511 1512 + base64-js@0.0.8: 1513 + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} 1514 + engines: {node: '>= 0.4'} 1515 + 1422 1516 basic-ftp@5.2.1: 1423 1517 resolution: {integrity: sha512-0yaL8JdxTknKDILitVpfYfV2Ob6yb3udX/hK97M7I3jOeznBNxQPtVvTUtnhUkyHlxFWyr5Lvknmgzoc7jf+1Q==} 1424 1518 engines: {node: '>=10.0.0'} ··· 1448 1542 resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} 1449 1543 engines: {node: '>= 0.4'} 1450 1544 1545 + camelize@1.0.1: 1546 + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} 1547 + 1451 1548 chokidar@4.0.0: 1452 1549 resolution: {integrity: sha512-mxIojEAQcuEvT/lyXq+jf/3cO/KoA6z4CeNDGGevTybECPOMFCnQy3OPahluUkbqgPNGw5Bi78UC7Po6Lhy+NA==} 1453 1550 engines: {node: '>= 14.16.0'} ··· 1469 1566 1470 1567 code-block-writer@10.1.1: 1471 1568 resolution: {integrity: sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==} 1569 + 1570 + color-name@1.1.4: 1571 + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 1472 1572 1473 1573 combined-stream@1.0.8: 1474 1574 resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} ··· 1500 1600 resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 1501 1601 engines: {node: '>= 8'} 1502 1602 1603 + css-background-parser@0.1.0: 1604 + resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==} 1605 + 1606 + css-box-shadow@1.0.0-3: 1607 + resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==} 1608 + 1609 + css-color-keywords@1.0.0: 1610 + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} 1611 + engines: {node: '>=4'} 1612 + 1613 + css-gradient-parser@0.0.16: 1614 + resolution: {integrity: sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==} 1615 + engines: {node: '>=16'} 1616 + 1617 + css-to-react-native@3.2.0: 1618 + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} 1619 + 1503 1620 cssesc@3.0.0: 1504 1621 resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 1505 1622 engines: {node: '>=4'} ··· 1559 1676 engines: {node: '>=16'} 1560 1677 hasBin: true 1561 1678 1679 + emoji-regex-xs@2.0.1: 1680 + resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==} 1681 + engines: {node: '>=10.0.0'} 1682 + 1562 1683 end-of-stream@1.1.0: 1563 1684 resolution: {integrity: sha512-EoulkdKF/1xa92q25PbjuDcgJ9RDHYU2Rs3SCIvs2/dSQ3BpmxneNHmA/M7fe60M3PrV7nNGTTNbkK62l6vXiQ==} 1564 1685 ··· 1605 1726 resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} 1606 1727 engines: {node: '>=18'} 1607 1728 hasBin: true 1729 + 1730 + escape-html@1.0.3: 1731 + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} 1608 1732 1609 1733 escodegen@2.1.0: 1610 1734 resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} ··· 1675 1799 peerDependenciesMeta: 1676 1800 picomatch: 1677 1801 optional: true 1802 + 1803 + fflate@0.7.4: 1804 + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} 1678 1805 1679 1806 file-uri-to-path@1.0.0: 1680 1807 resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} ··· 1757 1884 resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 1758 1885 engines: {node: '>= 0.4'} 1759 1886 1887 + hex-rgb@4.3.0: 1888 + resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} 1889 + engines: {node: '>=6'} 1890 + 1760 1891 hls.js@1.6.15: 1761 1892 resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==} 1762 1893 ··· 1919 2050 lightningcss@1.32.0: 1920 2051 resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} 1921 2052 engines: {node: '>= 12.0.0'} 2053 + 2054 + linebreak@1.1.0: 2055 + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} 1922 2056 1923 2057 locate-character@3.0.0: 1924 2058 resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} ··· 2100 2234 resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} 2101 2235 engines: {node: '>= 14'} 2102 2236 2237 + pako@0.2.9: 2238 + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} 2239 + 2240 + parse-css-color@0.2.1: 2241 + resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} 2242 + 2103 2243 parse-ms@2.1.0: 2104 2244 resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} 2105 2245 engines: {node: '>=6'} ··· 2148 2288 postcss-selector-parser@6.0.10: 2149 2289 resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} 2150 2290 engines: {node: '>=4'} 2291 + 2292 + postcss-value-parser@4.2.0: 2293 + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} 2151 2294 2152 2295 postcss@8.5.8: 2153 2296 resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} ··· 2298 2441 resolution: {integrity: sha512-tnFr7nyiuEhsAGb+xy60SDbij0790X+FgDljh3J/2HaRM6yQgNJkQKHbDH8ld7mR+PozXGgEfJ2Dc/5OyFnwsg==} 2299 2442 hasBin: true 2300 2443 2444 + satori@0.15.2: 2445 + resolution: {integrity: sha512-vu/49vdc8MzV5jUchs3TIRDCOkOvMc1iJ11MrZvhg9tE4ziKIEIBjBZvies6a9sfM2vQ2gc3dXeu6rCK7AztHA==} 2446 + engines: {node: '>=16'} 2447 + 2301 2448 semver@6.3.1: 2302 2449 resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 2303 2450 hasBin: true ··· 2382 2529 streamx@2.25.0: 2383 2530 resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==} 2384 2531 2532 + string.prototype.codepointat@0.2.1: 2533 + resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} 2534 + 2385 2535 strip-final-newline@2.0.0: 2386 2536 resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} 2387 2537 engines: {node: '>=6'} ··· 2428 2578 resolution: {integrity: sha512-MyqZCTGLDZ77u4k+jqg4UlrzPTPZ49NDlaekU6uuFaJLzPIN1woaRXCbGeqOfxwc3Y37ZROGAJ614Rdv7Olt+g==} 2429 2579 engines: {node: '>=10'} 2430 2580 2581 + tiny-inflate@1.0.3: 2582 + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} 2583 + 2431 2584 tinyexec@0.3.2: 2432 2585 resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} 2433 2586 ··· 2503 2656 2504 2657 unicode-segmenter@0.14.5: 2505 2658 resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} 2659 + 2660 + unicode-trie@2.0.0: 2661 + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} 2506 2662 2507 2663 universalify@2.0.1: 2508 2664 resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} ··· 2613 2769 2614 2770 yauzl@2.10.0: 2615 2771 resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} 2772 + 2773 + yoga-wasm-web@0.3.3: 2774 + resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} 2616 2775 2617 2776 zimmerframe@1.1.4: 2618 2777 resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} ··· 2956 3115 2957 3116 '@ewanc26/noise@0.1.1': {} 2958 3117 3118 + '@ewanc26/noise@0.1.3': {} 3119 + 3120 + '@ewanc26/og@0.1.2': 3121 + dependencies: 3122 + '@ewanc26/noise': 0.1.3 3123 + '@resvg/resvg-js': 2.6.2 3124 + satori: 0.15.2 3125 + 2959 3126 '@ewanc26/supporters@0.3.0(@atproto/api@0.18.21)(svelte@5.54.1)': 2960 3127 dependencies: 2961 3128 '@atproto/api': 0.18.21 ··· 3119 3286 '@polka/url@1.0.0-next.29': {} 3120 3287 3121 3288 '@renovatebot/pep440@4.2.1': {} 3289 + 3290 + '@resvg/resvg-js-android-arm-eabi@2.6.2': 3291 + optional: true 3292 + 3293 + '@resvg/resvg-js-android-arm64@2.6.2': 3294 + optional: true 3295 + 3296 + '@resvg/resvg-js-darwin-arm64@2.6.2': 3297 + optional: true 3298 + 3299 + '@resvg/resvg-js-darwin-x64@2.6.2': 3300 + optional: true 3301 + 3302 + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': 3303 + optional: true 3304 + 3305 + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': 3306 + optional: true 3307 + 3308 + '@resvg/resvg-js-linux-arm64-musl@2.6.2': 3309 + optional: true 3310 + 3311 + '@resvg/resvg-js-linux-x64-gnu@2.6.2': 3312 + optional: true 3313 + 3314 + '@resvg/resvg-js-linux-x64-musl@2.6.2': 3315 + optional: true 3316 + 3317 + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': 3318 + optional: true 3319 + 3320 + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': 3321 + optional: true 3322 + 3323 + '@resvg/resvg-js-win32-x64-msvc@2.6.2': 3324 + optional: true 3325 + 3326 + '@resvg/resvg-js@2.6.2': 3327 + optionalDependencies: 3328 + '@resvg/resvg-js-android-arm-eabi': 2.6.2 3329 + '@resvg/resvg-js-android-arm64': 2.6.2 3330 + '@resvg/resvg-js-darwin-arm64': 2.6.2 3331 + '@resvg/resvg-js-darwin-x64': 2.6.2 3332 + '@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2 3333 + '@resvg/resvg-js-linux-arm64-gnu': 2.6.2 3334 + '@resvg/resvg-js-linux-arm64-musl': 2.6.2 3335 + '@resvg/resvg-js-linux-x64-gnu': 2.6.2 3336 + '@resvg/resvg-js-linux-x64-musl': 2.6.2 3337 + '@resvg/resvg-js-win32-arm64-msvc': 2.6.2 3338 + '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 3339 + '@resvg/resvg-js-win32-x64-msvc': 2.6.2 3122 3340 3123 3341 '@rolldown/binding-android-arm64@1.0.0-rc.1': 3124 3342 optional: true ··· 3249 3467 '@rollup/rollup-win32-x64-msvc@4.60.0': 3250 3468 optional: true 3251 3469 3470 + '@shuding/opentype.js@1.4.0-beta.0': 3471 + dependencies: 3472 + fflate: 0.7.4 3473 + string.prototype.codepointat: 0.2.1 3474 + 3252 3475 '@sinclair/typebox@0.25.24': {} 3253 3476 3254 3477 '@standard-schema/spec@1.1.0': {} ··· 3769 3992 3770 3993 bare-events@2.8.2: {} 3771 3994 3995 + base64-js@0.0.8: {} 3996 + 3772 3997 basic-ftp@5.2.1: {} 3773 3998 3774 3999 bindings@1.5.0: ··· 3796 4021 dependencies: 3797 4022 es-errors: 1.3.0 3798 4023 function-bind: 1.1.2 4024 + 4025 + camelize@1.0.1: {} 3799 4026 3800 4027 chokidar@4.0.0: 3801 4028 dependencies: ··· 3813 4040 3814 4041 code-block-writer@10.1.1: {} 3815 4042 4043 + color-name@1.1.4: {} 4044 + 3816 4045 combined-stream@1.0.8: 3817 4046 dependencies: 3818 4047 delayed-stream: 1.0.0 ··· 3835 4064 shebang-command: 2.0.0 3836 4065 which: 2.0.2 3837 4066 4067 + css-background-parser@0.1.0: {} 4068 + 4069 + css-box-shadow@1.0.0-3: {} 4070 + 4071 + css-color-keywords@1.0.0: {} 4072 + 4073 + css-gradient-parser@0.0.16: {} 4074 + 4075 + css-to-react-native@3.2.0: 4076 + dependencies: 4077 + camelize: 1.0.1 4078 + css-color-keywords: 1.0.0 4079 + postcss-value-parser: 4.2.0 4080 + 3838 4081 cssesc@3.0.0: {} 3839 4082 3840 4083 data-uri-to-buffer@6.0.2: {} ··· 3880 4123 pretty-ms: 7.0.1 3881 4124 signal-exit: 4.0.2 3882 4125 time-span: 4.0.0 4126 + 4127 + emoji-regex-xs@2.0.1: {} 3883 4128 3884 4129 end-of-stream@1.1.0: 3885 4130 dependencies: ··· 4000 4245 '@esbuild/win32-ia32': 0.27.4 4001 4246 '@esbuild/win32-x64': 0.27.4 4002 4247 4248 + escape-html@1.0.3: {} 4249 + 4003 4250 escodegen@2.1.0: 4004 4251 dependencies: 4005 4252 esprima: 4.0.1 ··· 4081 4328 fdir@6.5.0(picomatch@4.0.3): 4082 4329 optionalDependencies: 4083 4330 picomatch: 4.0.3 4331 + 4332 + fflate@0.7.4: {} 4084 4333 4085 4334 file-uri-to-path@1.0.0: {} 4086 4335 ··· 4175 4424 dependencies: 4176 4425 function-bind: 1.1.2 4177 4426 4427 + hex-rgb@4.3.0: {} 4428 + 4178 4429 hls.js@1.6.15: {} 4179 4430 4180 4431 http-errors@1.7.3: ··· 4307 4558 lightningcss-win32-arm64-msvc: 1.32.0 4308 4559 lightningcss-win32-x64-msvc: 1.32.0 4309 4560 4561 + linebreak@1.1.0: 4562 + dependencies: 4563 + base64-js: 0.0.8 4564 + unicode-trie: 2.0.0 4565 + 4310 4566 locate-character@3.0.0: {} 4311 4567 4312 4568 lru-cache@11.2.7: {} ··· 4467 4723 dependencies: 4468 4724 degenerator: 5.0.1 4469 4725 netmask: 2.1.1 4726 + 4727 + pako@0.2.9: {} 4728 + 4729 + parse-css-color@0.2.1: 4730 + dependencies: 4731 + color-name: 1.1.4 4732 + hex-rgb: 4.3.0 4470 4733 4471 4734 parse-ms@2.1.0: {} 4472 4735 ··· 4502 4765 cssesc: 3.0.0 4503 4766 util-deprecate: 1.0.2 4504 4767 4768 + postcss-value-parser@4.2.0: {} 4769 + 4505 4770 postcss@8.5.8: 4506 4771 dependencies: 4507 4772 nanoid: 3.3.11 ··· 4645 4910 - react-native-b4a 4646 4911 - supports-color 4647 4912 4913 + satori@0.15.2: 4914 + dependencies: 4915 + '@shuding/opentype.js': 1.4.0-beta.0 4916 + css-background-parser: 0.1.0 4917 + css-box-shadow: 1.0.0-3 4918 + css-gradient-parser: 0.0.16 4919 + css-to-react-native: 3.2.0 4920 + emoji-regex-xs: 2.0.1 4921 + escape-html: 1.0.3 4922 + linebreak: 1.1.0 4923 + parse-css-color: 0.2.1 4924 + postcss-value-parser: 4.2.0 4925 + yoga-wasm-web: 0.3.3 4926 + 4648 4927 semver@6.3.1: {} 4649 4928 4650 4929 semver@7.5.4: ··· 4721 5000 transitivePeerDependencies: 4722 5001 - bare-abort-controller 4723 5002 - react-native-b4a 5003 + 5004 + string.prototype.codepointat@0.2.1: {} 4724 5005 4725 5006 strip-final-newline@2.0.0: {} 4726 5007 ··· 4796 5077 dependencies: 4797 5078 convert-hrtime: 3.0.0 4798 5079 5080 + tiny-inflate@1.0.3: {} 5081 + 4799 5082 tinyexec@0.3.2: {} 4800 5083 4801 5084 tinyglobby@0.2.15: ··· 4854 5137 undici@7.24.7: {} 4855 5138 4856 5139 unicode-segmenter@0.14.5: {} 5140 + 5141 + unicode-trie@2.0.0: 5142 + dependencies: 5143 + pako: 0.2.9 5144 + tiny-inflate: 1.0.3 4857 5145 4858 5146 universalify@2.0.1: {} 4859 5147 ··· 4967 5255 dependencies: 4968 5256 buffer-crc32: 0.2.13 4969 5257 fd-slicer: 1.1.0 5258 + 5259 + yoga-wasm-web@0.3.3: {} 4970 5260 4971 5261 zimmerframe@1.1.4: {} 4972 5262
+2 -2
src/lib/assets/index.ts
··· 2 2 export { default as profileFallback } from './fallback/profile.svg'; 3 3 export { default as bannerFallback } from './fallback/banner.svg'; 4 4 5 - // Note: OG images are now served from ./static/og/ 6 - // Use the ogImages object from $lib/helper/ogImages for OG image paths 5 + // Note: OG images are generated dynamically via /api/og endpoint 6 + // Use ogUrl() from $lib/helper/ogImages to generate OG image URLs
+24 -11
src/lib/helper/ogImages.ts
··· 1 1 /** 2 - * OG (Open Graph) image paths 3 - * 4 - * These images are served from the ./static/og/ directory. 5 - * They are accessible at /og/ URLs in the application. 2 + * OG (Open Graph) image URL generation. 3 + * Dynamic images are generated via /api/og endpoint. 4 + */ 5 + 6 + /** 7 + * Generate a dynamic OG image URL. 6 8 * 7 - * To add new OG images: 8 - * 1. Place the image in ./static/og/ 9 - * 2. Add the path here as /og/filename.png 9 + * @example 10 + * ```ts 11 + * ogUrl({ title: 'My Post', description: 'A great post' }) 12 + * // Returns: "/api/og?title=My+Post&description=A+great+post&template=default" 13 + * ``` 10 14 */ 11 - export const ogImages: Record<string, string> = { 12 - main: '/og/main.png', 13 - siteMeta: '/og/site-meta.png' 14 - }; 15 + export interface OgUrlOptions { 16 + title: string 17 + description?: string 18 + template?: 'default' | 'blog' | 'profile' 19 + } 20 + 21 + export function ogUrl(options: OgUrlOptions): string { 22 + const params = new URLSearchParams() 23 + params.set('title', options.title) 24 + if (options.description) params.set('description', options.description) 25 + if (options.template && options.template !== 'default') params.set('template', options.template) 26 + return `/api/og?${params.toString()}` 27 + }
+47 -3
src/lib/helper/siteMeta.ts
··· 1 - import { ogImages } from '$lib/helper/ogImages'; 1 + import { ogUrl } from '$lib/helper/ogImages'; 2 2 import { 3 3 PUBLIC_SITE_TITLE, 4 4 PUBLIC_SITE_DESCRIPTION, ··· 12 12 13 13 /** 14 14 * Default metadata that applies site-wide. 15 - * Can be overridden per-page via createSiteMeta. 15 + * Generates dynamic OG images via /api/og endpoint. 16 16 */ 17 17 export const defaultSiteMeta: SiteMetadata = { 18 18 title: PUBLIC_SITE_TITLE, 19 19 description: PUBLIC_SITE_DESCRIPTION, 20 20 keywords: PUBLIC_SITE_KEYWORDS, 21 21 url: PUBLIC_SITE_URL, 22 - image: ogImages.main, 22 + image: `${PUBLIC_SITE_URL}${ogUrl({ title: PUBLIC_SITE_TITLE })}`, 23 23 imageWidth: 1200, 24 24 imageHeight: 630 25 25 }; 26 + 27 + /** 28 + * Create site meta with dynamic OG image. 29 + * 30 + * @example 31 + * ```ts 32 + * // Home page 33 + * createDynamicSiteMeta({ 34 + * title: "Ewan's Corner", 35 + * description: 'personal site, blog, and digital garden' 36 + * }) 37 + * 38 + * // Blog post 39 + * createDynamicSiteMeta({ 40 + * title: post.title, 41 + * description: post.description, 42 + * template: 'blog' 43 + * }) 44 + * ``` 45 + */ 46 + export interface DynamicSiteMetaOptions { 47 + title: string 48 + description?: string 49 + template?: 'default' | 'blog' | 'profile' 50 + url?: string 51 + } 52 + 53 + export function createDynamicSiteMeta(options: DynamicSiteMetaOptions): SiteMetadata { 54 + const siteUrl = options.url || PUBLIC_SITE_URL 55 + 56 + return { 57 + title: options.title, 58 + description: options.description || PUBLIC_SITE_DESCRIPTION, 59 + keywords: PUBLIC_SITE_KEYWORDS, 60 + url: siteUrl, 61 + image: `${PUBLIC_SITE_URL}${ogUrl({ 62 + title: options.title, 63 + description: options.description, 64 + template: options.template 65 + })}`, 66 + imageWidth: 1200, 67 + imageHeight: 630 68 + } 69 + }
+8
src/routes/+page.ts
··· 1 1 import type { PageLoad } from './$types'; 2 + import { createDynamicSiteMeta } from '$lib/helper/siteMeta'; 2 3 import { 3 4 fetchMusicStatus, 4 5 fetchKibunStatus, ··· 22 23 fetchRecentPopfeedReviews(fetch) 23 24 ]); 24 25 26 + // Create page metadata with dynamic OG 27 + const meta = createDynamicSiteMeta({ 28 + title: "Ewan's Corner", 29 + description: 'personal site, blog, and digital garden' 30 + }); 31 + 25 32 return { 26 33 profile, 34 + meta, 27 35 musicStatus: musicStatus.status === 'fulfilled' ? musicStatus.value : null, 28 36 kibunStatus: kibunStatus.status === 'fulfilled' ? kibunStatus.value : null, 29 37 latestPost: latestPost.status === 'fulfilled' ? latestPost.value : null,
+24
src/routes/api/og/+server.ts
··· 1 + /** 2 + * Dynamic OG image endpoint. 3 + * Generates OpenGraph images on demand using @ewanc26/og. 4 + * 5 + * Query params: 6 + * - title: Page title (required) 7 + * - description: Page description (optional) 8 + * - template: 'default' | 'blog' | 'profile' (default: 'default') 9 + */ 10 + 11 + import { createOgEndpoint } from '@ewanc26/og' 12 + import { PUBLIC_SITE_URL } from '$env/static/public' 13 + 14 + export const GET = createOgEndpoint({ 15 + siteName: new URL(PUBLIC_SITE_URL).hostname, 16 + defaultTemplate: 'default', 17 + colors: { 18 + background: '#0f1a15', 19 + text: '#e8f5e9', 20 + accent: '#86efac', 21 + }, 22 + noise: { enabled: true, opacity: 0.4 }, 23 + cacheMaxAge: 86400, // 24 hours 24 + })
+4 -4
src/routes/archive/+page.ts
··· 1 1 import type { PageLoad } from './$types'; 2 - import { createSiteMeta, type SiteMetadata } from '$lib/helper/siteMeta'; 2 + import { createDynamicSiteMeta } from '$lib/helper/siteMeta'; 3 3 import { fetchDocuments } from '$lib/services/atproto'; 4 4 5 5 export const load: PageLoad = async ({ fetch }) => { ··· 13 13 console.warn('Archive page: failed to fetch documents', err); 14 14 } 15 15 16 - // Create page metadata 17 - const meta: Partial<SiteMetadata> = { 16 + // Create page metadata with dynamic OG 17 + const meta = createDynamicSiteMeta({ 18 18 title: 'Archive', 19 19 description: `Browse all ${documents.length} documents from Standard.site` 20 - }; 20 + }); 21 21 22 22 return { 23 23 meta,
+9 -1
src/routes/github/+page.ts
··· 1 1 import type { PageLoad } from './$types'; 2 + import { createDynamicSiteMeta } from '$lib/helper/siteMeta'; 2 3 import { fetchGitHubData, fetchContributions } from '$lib/services/github'; 3 4 4 5 const GITHUB_USERNAME = 'ewanc26'; ··· 8 9 fetchGitHubData(GITHUB_USERNAME, fetch), 9 10 fetchContributions(GITHUB_USERNAME, fetch, 90) 10 11 ]); 11 - return { ...profileData, contributions }; 12 + 13 + // Create page metadata with dynamic OG 14 + const meta = createDynamicSiteMeta({ 15 + title: 'GitHub', 16 + description: `Ewan's GitHub profile and contributions` 17 + }); 18 + 19 + return { ...profileData, contributions, meta }; 12 20 };
+4 -7
src/routes/site/meta/+page.ts
··· 1 1 import type { PageLoad } from './$types'; 2 2 import { fetchSiteInfo, type SiteInfoData } from '$lib/services/atproto'; 3 - import { createSiteMeta, type SiteMetadata, defaultSiteMeta } from '$lib/helper/siteMeta'; 4 - import { ogImages } from '$lib/helper/ogImages'; 3 + import { createDynamicSiteMeta } from '$lib/helper/siteMeta'; 5 4 6 5 export const load: PageLoad = async ({ parent, fetch }) => { 7 6 const { siteMeta } = await parent(); ··· 15 14 error = err instanceof Error ? err.message : 'Failed to load site information'; 16 15 } 17 16 18 - const meta: SiteMetadata = createSiteMeta({ 19 - ...siteMeta, 20 - title: `Site Meta - ${defaultSiteMeta.title}`, 21 - description: 'Information about this website, its technology stack, and credits.', 22 - image: ogImages.siteMeta 17 + const meta = createDynamicSiteMeta({ 18 + title: 'Site Meta', 19 + description: 'Information about this website, its technology stack, and credits.' 23 20 }); 24 21 25 22 return { siteInfo, error, meta };
static/og/main.png

This is a binary file and will not be displayed.

static/og/site-meta.png

This is a binary file and will not be displayed.