your personal website on atproto - mirror blento.app

Compare changes

Choose any two refs to compare.

+1021 -1180
+17 -3
CLAUDE.md
··· 53 53 **Card System (`src/lib/cards/`):** 54 54 55 55 - `CardDefinition` type in `types.ts` defines the interface for card types 56 - - Each card type exports a definition with: `type`, `contentComponent`, optional `editingContentComponent`, `creationModalComponent`, `sidebarButtonText`, `loadData`, `upload` (see more info and description in `src/lib/cards/types.ts`) 56 + - Each card type exports a definition with: `type`, `contentComponent`, optional `editingContentComponent`, `creationModalComponent`, `sidebarButtonText`, `loadData`, `loadDataServer`, `upload` (see more info and description in `src/lib/cards/types.ts`) 57 + - `loadData` fetches external data on the client (via remote functions). `loadDataServer` is the server-side equivalent used during SSR to avoid self-referential HTTP requests on Cloudflare Workers. 58 + - Cards that need external data use `.remote.ts` files (SvelteKit remote functions) co-located in the card folder (e.g. `GitHubProfileCard/api.remote.ts`, `LastFMCard/api.remote.ts`). These use `query()` from `$app/server` and run server-side, with SvelteKit generating fetch wrappers for client use. 57 59 - Card types include Text, Link, Image, Bluesky, Embed, Map, Livestream, ATProto collections, and special cards (see `src/lib/cards`). 58 60 - `AllCardDefinitions` and `CardDefinitionsByType` in `index.ts` aggregate all card types 59 61 - See e.g. `src/lib/cards/EmbedCard/` and `src/lib/cards/LivestreamCard/` for examples of implementation. ··· 66 68 - Data is stored in user's PDS under collection `app.blento.card` 67 69 - **Important**: ATProto does not allow floating point numbers in records. All numeric values must be integers. 68 70 71 + **Caching (`src/lib/cache.ts`):** 72 + 73 + - `CacheService` class wraps a single Cloudflare KV namespace (`USER_DATA_CACHE`) with namespaced keys 74 + - Keys are stored as `namespace:key` (e.g. `blento:did:plc:abc`, `github:someuser`, `lastfm:method:user:period:limit`) 75 + - Per-namespace default TTLs via KV `expirationTtl`: `blento` (24h), `identity` (7d), `github` (12h), `gh-contrib` (12h), `lastfm` (1h, overridable), `npmx` (12h), `meta` (no expiry) 76 + - Blento data is keyed by DID with bidirectional handleโ†”DID identity mappings (`identity:h:{handle}` โ†’ DID, `identity:d:{did}` โ†’ handle) 77 + - `getBlento(identifier)` accepts either a handle or DID and resolves automatically 78 + - `putBlento(did, handle, data)` writes data + both identity mappings 79 + - Generic `get(namespace, key)` / `put(namespace, key, value, ttl?)` and JSON variants `getJSON` / `putJSON` for all namespaces 80 + - `createCache(platform)` factory function creates a `CacheService` from `platform.env` 81 + - `CUSTOM_DOMAINS` KV namespace is separate and accessed directly for custom domain โ†’ DID resolution 82 + 69 83 **Data Loading (`src/lib/website/`):** 70 84 71 - - `load.ts` - Fetches user data from their PDS, with optional KV caching via `UserCache` 85 + - `load.ts` - Fetches user data from their PDS, with optional caching via `CacheService` 72 86 - `data.ts` - Defines which collections/records to fetch 73 87 - `context.ts` - Svelte contexts for passing DID, handle, and data down the component tree 74 88 ··· 80 94 - `/edit` - Self-hosted edit mode 81 95 - `/api/links` - Link preview API 82 96 - `/api/geocoding` - Geocoding API for map cards 83 - - `/api/instagram`, `/api/reloadRecent`, `/api/update` - Additional data endpoints 97 + - `/api/reloadRecent`, `/api/update` - Additional data endpoints 84 98 85 99 ### Item Type 86 100
+1 -1
docs/Selfhosting.md
··· 35 35 6. some cards need their own additional env keys, if you have these cards in your profile, create your keys and add them to your cloudflare worker 36 36 37 37 - github profile: GITHUB_TOKEN (token with public_repo access) 38 - - map: PUBLIC_MAPBOX_TOKEN 38 + - map: no token needed (uses OpenFreeMap)
+2 -1
package.json
··· 33 33 "prettier-plugin-tailwindcss": "^0.7.2", 34 34 "svelte": "^5.48.0", 35 35 "svelte-check": "^4.3.5", 36 + "svelte-maplibre-gl": "^1.0.3", 36 37 "tailwindcss": "^4.1.18", 37 38 "typescript": "^5.9.3", 38 39 "typescript-eslint": "^8.53.1", ··· 76 77 "hls.js": "^1.6.15", 77 78 "leaflet": "^1.9.4", 78 79 "link-preview-js": "^4.0.0", 79 - "mapbox-gl": "^3.18.1", 80 + "maplibre-gl": "^5.17.0", 80 81 "marked": "^17.0.1", 81 82 "perfect-freehand": "^1.2.2", 82 83 "plyr": "^3.8.4",
+112 -93
pnpm-lock.yaml
··· 119 119 link-preview-js: 120 120 specifier: ^4.0.0 121 121 version: 4.0.0 122 - mapbox-gl: 123 - specifier: ^3.18.1 124 - version: 3.18.1 122 + maplibre-gl: 123 + specifier: ^5.17.0 124 + version: 5.17.0 125 125 marked: 126 126 specifier: ^17.0.1 127 127 version: 17.0.1 ··· 213 213 svelte-check: 214 214 specifier: ^4.3.5 215 215 version: 4.3.5(picomatch@4.0.3)(svelte@5.48.0)(typescript@5.9.3) 216 + svelte-maplibre-gl: 217 + specifier: ^1.0.3 218 + version: 1.0.3(maplibre-gl@5.17.0)(svelte@5.48.0) 216 219 tailwindcss: 217 220 specifier: ^4.1.18 218 221 version: 4.1.18 ··· 947 950 '@jridgewell/trace-mapping@0.3.9': 948 951 resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 949 952 953 + '@mapbox/geojson-rewind@0.5.2': 954 + resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==, tarball: https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz} 955 + hasBin: true 956 + 950 957 '@mapbox/jsonlint-lines-primitives@2.0.2': 951 - resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} 958 + resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==, tarball: https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz} 952 959 engines: {node: '>= 0.6'} 953 960 954 - '@mapbox/mapbox-gl-supported@3.0.0': 955 - resolution: {integrity: sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg==} 956 - 957 961 '@mapbox/point-geometry@1.1.0': 958 - resolution: {integrity: sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==} 962 + resolution: {integrity: sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==, tarball: https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz} 959 963 960 964 '@mapbox/tiny-sdf@2.0.7': 961 - resolution: {integrity: sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==} 965 + resolution: {integrity: sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==, tarball: https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz} 962 966 963 967 '@mapbox/unitbezier@0.0.1': 964 - resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==} 968 + resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==, tarball: https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz} 965 969 966 970 '@mapbox/vector-tile@2.0.4': 967 - resolution: {integrity: sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==} 971 + resolution: {integrity: sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==, tarball: https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz} 968 972 969 973 '@mapbox/whoots-js@3.1.0': 970 - resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==} 974 + resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==, tarball: https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz} 971 975 engines: {node: '>=6.0.0'} 976 + 977 + '@maplibre/geojson-vt@5.0.4': 978 + resolution: {integrity: sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==, tarball: https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-5.0.4.tgz} 979 + 980 + '@maplibre/maplibre-gl-style-spec@24.4.1': 981 + resolution: {integrity: sha512-UKhA4qv1h30XT768ccSv5NjNCX+dgfoq2qlLVmKejspPcSQTYD4SrVucgqegmYcKcmwf06wcNAa/kRd0NHWbUg==, tarball: https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.4.1.tgz} 982 + hasBin: true 983 + 984 + '@maplibre/mlt@1.1.5': 985 + resolution: {integrity: sha512-HjoAIOYAfZo2El/tHBnvWpCRlodWgWLZ/tyO4Wrw0LVY2bkQz1+pwAwDvmgSRvfkSkSzWnqJJd1JwgCrq/60HQ==, tarball: https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.5.tgz} 986 + 987 + '@maplibre/vt-pbf@4.2.1': 988 + resolution: {integrity: sha512-IxZBGq/+9cqf2qdWlFuQ+ZfoMhWpxDUGQZ/poPHOJBvwMUT1GuxLo6HgYTou+xxtsOsjfbcjI8PZaPCtmt97rA==, tarball: https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.2.1.tgz} 972 989 973 990 '@mixmark-io/domino@2.2.0': 974 991 resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} ··· 1511 1528 '@types/estree@1.0.8': 1512 1529 resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 1513 1530 1514 - '@types/geojson-vt@3.2.5': 1515 - resolution: {integrity: sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==} 1516 - 1517 1531 '@types/geojson@7946.0.16': 1518 - resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} 1532 + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==, tarball: https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz} 1519 1533 1520 1534 '@types/json-schema@7.0.15': 1521 1535 resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} ··· 1523 1537 '@types/linkify-it@5.0.0': 1524 1538 resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} 1525 1539 1526 - '@types/mapbox__point-geometry@0.1.4': 1527 - resolution: {integrity: sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==} 1528 - 1529 1540 '@types/markdown-it@14.1.2': 1530 1541 resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} 1531 1542 ··· 1534 1545 1535 1546 '@types/node@25.0.10': 1536 1547 resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==} 1537 - 1538 - '@types/pbf@3.0.5': 1539 - resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==} 1540 1548 1541 1549 '@types/stats.js@0.17.4': 1542 1550 resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} 1543 1551 1544 1552 '@types/supercluster@7.1.3': 1545 - resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==} 1553 + resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==, tarball: https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz} 1546 1554 1547 1555 '@types/three@0.176.0': 1548 1556 resolution: {integrity: sha512-FwfPXxCqOtP7EdYMagCFePNKoG1AGBDUEVKtluv2BTVRpSt7b+X27xNsirPCTCqY1pGYsPUzaM3jgWP7dXSxlw==} ··· 1713 1721 resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 1714 1722 engines: {node: '>=10'} 1715 1723 1716 - cheap-ruler@4.0.0: 1717 - resolution: {integrity: sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==} 1718 - 1719 1724 cheerio-select@2.1.0: 1720 1725 resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} 1721 1726 ··· 1792 1797 css.escape@1.5.1: 1793 1798 resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} 1794 1799 1795 - csscolorparser@1.0.3: 1796 - resolution: {integrity: sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==} 1797 - 1798 1800 cssesc@3.0.0: 1799 1801 resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 1800 1802 engines: {node: '>=4'} ··· 1853 1855 resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} 1854 1856 1855 1857 earcut@3.0.2: 1856 - resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==} 1858 + resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==, tarball: https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz} 1857 1859 1858 1860 emoji-picker-element@1.28.1: 1859 1861 resolution: {integrity: sha512-8c64IPish2PWoV9oYCo2pvuPHwIv+uK9bO0dfpPyMupDAvaWL9ZvYhWNTAR+2sx7BhfRjciImqP6CIUgNX+DMg==} ··· 2014 2016 engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 2015 2017 os: [darwin] 2016 2018 2017 - geojson-vt@4.0.2: 2018 - resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==} 2019 + get-stream@6.0.1: 2020 + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==, tarball: https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz} 2021 + engines: {node: '>=10'} 2019 2022 2020 2023 gl-matrix@3.4.4: 2021 - resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==} 2024 + resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==, tarball: https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz} 2022 2025 2023 2026 glob-parent@6.0.2: 2024 2027 resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} ··· 2038 2041 2039 2042 graceful-fs@4.2.11: 2040 2043 resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 2041 - 2042 - grid-index@1.1.0: 2043 - resolution: {integrity: sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==} 2044 2044 2045 2045 gsap@3.14.2: 2046 2046 resolution: {integrity: sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==} ··· 2125 2125 json-stable-stringify-without-jsonify@1.0.1: 2126 2126 resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 2127 2127 2128 + json-stringify-pretty-compact@4.0.0: 2129 + resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==, tarball: https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz} 2130 + 2128 2131 kdbush@4.0.2: 2129 - resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==} 2132 + resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==, tarball: https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz} 2130 2133 2131 2134 keyv@4.5.4: 2132 2135 resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} ··· 2265 2268 magic-string@0.30.21: 2266 2269 resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 2267 2270 2268 - mapbox-gl@3.18.1: 2269 - resolution: {integrity: sha512-Izc8dee2zkmb6Pn9hXFbVioPRLXJz1OFUcrvri69MhFACPU4bhLyVmhEsD9AyW1qOAP0Yvhzm60v63xdMIHPPw==} 2271 + maplibre-gl@5.17.0: 2272 + resolution: {integrity: sha512-gwS6NpXBfWD406dtT5YfEpl2hmpMm+wcPqf04UAez/TxY1OBjiMdK2ZoMGcNIlGHelKc4+Uet6zhDdDEnlJVHA==, tarball: https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.17.0.tgz} 2273 + engines: {node: '>=16.14.0', npm: '>=8.1.0'} 2270 2274 2271 2275 markdown-it@14.1.0: 2272 2276 resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} ··· 2277 2281 engines: {node: '>= 20'} 2278 2282 hasBin: true 2279 2283 2280 - martinez-polygon-clipping@0.8.1: 2281 - resolution: {integrity: sha512-9PLLMzMPI6ihHox4Ns6LpVBLpRc7sbhULybZ/wyaY8sY3ECNe2+hxm1hA2/9bEEpRrdpjoeduBuZLg2aq1cSIQ==} 2282 - 2283 2284 mdurl@2.0.0: 2284 2285 resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} 2285 2286 ··· 2301 2302 minimatch@9.0.5: 2302 2303 resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 2303 2304 engines: {node: '>=16 || 14 >=14.17'} 2305 + 2306 + minimist@1.2.8: 2307 + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, tarball: https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz} 2304 2308 2305 2309 mitt@3.0.1: 2306 2310 resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} ··· 2328 2332 resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} 2329 2333 2330 2334 murmurhash-js@1.0.0: 2331 - resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} 2335 + resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==, tarball: https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz} 2332 2336 2333 2337 nanoid@3.3.11: 2334 2338 resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} ··· 2409 2413 resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 2410 2414 2411 2415 pbf@4.0.1: 2412 - resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==} 2416 + resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==, tarball: https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz} 2413 2417 hasBin: true 2414 2418 2415 2419 perfect-freehand@1.2.2: ··· 2471 2475 engines: {node: ^10 || ^12 || >=14} 2472 2476 2473 2477 potpack@2.1.0: 2474 - resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==} 2478 + resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==, tarball: https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz} 2475 2479 2476 2480 prelude-ls@1.2.1: 2477 2481 resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} ··· 2605 2609 resolution: {integrity: sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==} 2606 2610 2607 2611 protocol-buffers-schema@3.6.0: 2608 - resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} 2612 + resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==, tarball: https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz} 2609 2613 2610 2614 punycode.js@2.3.1: 2611 2615 resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} ··· 2623 2627 resolution: {integrity: sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==} 2624 2628 2625 2629 quickselect@3.0.0: 2626 - resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} 2630 + resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==, tarball: https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz} 2627 2631 2628 2632 rangetouch@2.0.1: 2629 2633 resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==} ··· 2678 2682 engines: {node: '>=4'} 2679 2683 2680 2684 resolve-protobuf-schema@2.1.0: 2681 - resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} 2682 - 2683 - robust-predicates@2.0.4: 2684 - resolution: {integrity: sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg==} 2685 + resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==, tarball: https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz} 2685 2686 2686 2687 rollup@4.56.0: 2687 2688 resolution: {integrity: sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==} ··· 2715 2716 '@sveltejs/kit': 2716 2717 optional: true 2717 2718 2719 + rw@1.3.3: 2720 + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==, tarball: https://registry.npmjs.org/rw/-/rw-1.3.3.tgz} 2721 + 2718 2722 sade@1.8.1: 2719 2723 resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} 2720 2724 engines: {node: '>=6'} ··· 2764 2768 resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 2765 2769 engines: {node: '>=0.10.0'} 2766 2770 2767 - splaytree@0.1.4: 2768 - resolution: {integrity: sha512-D50hKrjZgBzqD3FT2Ek53f2dcDLAQT8SSGrzj3vidNH5ISRgceeGVJ2dQIthKOuayqFXfFjXheHNo4bbt9LhRQ==} 2769 - 2770 2771 std-env@3.10.0: 2771 2772 resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} 2772 2773 ··· 2781 2782 resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} 2782 2783 2783 2784 supercluster@8.0.1: 2784 - resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==} 2785 + resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==, tarball: https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz} 2785 2786 2786 2787 supports-color@10.2.2: 2787 2788 resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} ··· 2808 2809 svelte: 2809 2810 optional: true 2810 2811 2812 + svelte-maplibre-gl@1.0.3: 2813 + resolution: {integrity: sha512-pGZMLN5EcJoe6lREyaY2lsbkvrnKjIGHJRM2ijN+TUDeJbV9oecjBxbDXQZd8b5X4JJztSG915L731dFdhhSwA==, tarball: https://registry.npmjs.org/svelte-maplibre-gl/-/svelte-maplibre-gl-1.0.3.tgz} 2814 + peerDependencies: 2815 + maplibre-gl: ^5.0.0 || ^4.0.0 2816 + svelte: '>=5.0.0' 2817 + 2811 2818 svelte-sonner@0.3.28: 2812 2819 resolution: {integrity: sha512-K3AmlySeFifF/cKgsYNv5uXqMVNln0NBAacOYgmkQStLa/UoU0LhfAACU6Gr+YYC8bOCHdVmFNoKuDbMEsppJg==} 2813 2820 peerDependencies: ··· 2902 2909 engines: {node: '>=12.0.0'} 2903 2910 2904 2911 tinyqueue@3.0.0: 2905 - resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==} 2912 + resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==, tarball: https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz} 2906 2913 2907 2914 tlds@1.261.0: 2908 2915 resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} ··· 3724 3731 '@jridgewell/resolve-uri': 3.1.2 3725 3732 '@jridgewell/sourcemap-codec': 1.5.5 3726 3733 3727 - '@mapbox/jsonlint-lines-primitives@2.0.2': {} 3734 + '@mapbox/geojson-rewind@0.5.2': 3735 + dependencies: 3736 + get-stream: 6.0.1 3737 + minimist: 1.2.8 3728 3738 3729 - '@mapbox/mapbox-gl-supported@3.0.0': {} 3739 + '@mapbox/jsonlint-lines-primitives@2.0.2': {} 3730 3740 3731 3741 '@mapbox/point-geometry@1.1.0': {} 3732 3742 ··· 3741 3751 pbf: 4.0.1 3742 3752 3743 3753 '@mapbox/whoots-js@3.1.0': {} 3754 + 3755 + '@maplibre/geojson-vt@5.0.4': {} 3756 + 3757 + '@maplibre/maplibre-gl-style-spec@24.4.1': 3758 + dependencies: 3759 + '@mapbox/jsonlint-lines-primitives': 2.0.2 3760 + '@mapbox/unitbezier': 0.0.1 3761 + json-stringify-pretty-compact: 4.0.0 3762 + minimist: 1.2.8 3763 + quickselect: 3.0.0 3764 + rw: 1.3.3 3765 + tinyqueue: 3.0.0 3766 + 3767 + '@maplibre/mlt@1.1.5': 3768 + dependencies: 3769 + '@mapbox/point-geometry': 1.1.0 3770 + 3771 + '@maplibre/vt-pbf@4.2.1': 3772 + dependencies: 3773 + '@mapbox/point-geometry': 1.1.0 3774 + '@mapbox/vector-tile': 2.0.4 3775 + '@maplibre/geojson-vt': 5.0.4 3776 + '@types/geojson': 7946.0.16 3777 + '@types/supercluster': 7.1.3 3778 + pbf: 4.0.1 3779 + supercluster: 8.0.1 3744 3780 3745 3781 '@mixmark-io/domino@2.2.0': {} 3746 3782 ··· 4226 4262 4227 4263 '@types/estree@1.0.8': {} 4228 4264 4229 - '@types/geojson-vt@3.2.5': 4230 - dependencies: 4231 - '@types/geojson': 7946.0.16 4232 - 4233 4265 '@types/geojson@7946.0.16': {} 4234 4266 4235 4267 '@types/json-schema@7.0.15': {} 4236 4268 4237 4269 '@types/linkify-it@5.0.0': {} 4238 - 4239 - '@types/mapbox__point-geometry@0.1.4': {} 4240 4270 4241 4271 '@types/markdown-it@14.1.2': 4242 4272 dependencies: ··· 4248 4278 '@types/node@25.0.10': 4249 4279 dependencies: 4250 4280 undici-types: 7.16.0 4251 - 4252 - '@types/pbf@3.0.5': {} 4253 4281 4254 4282 '@types/stats.js@0.17.4': {} 4255 4283 ··· 4463 4491 ansi-styles: 4.3.0 4464 4492 supports-color: 7.2.0 4465 4493 4466 - cheap-ruler@4.0.0: {} 4467 - 4468 4494 cheerio-select@2.1.0: 4469 4495 dependencies: 4470 4496 boolbase: 1.0.0 ··· 4554 4580 css-what@6.2.2: {} 4555 4581 4556 4582 css.escape@1.5.1: {} 4557 - 4558 - csscolorparser@1.0.3: {} 4559 4583 4560 4584 cssesc@3.0.0: {} 4561 4585 ··· 4820 4844 fsevents@2.3.3: 4821 4845 optional: true 4822 4846 4823 - geojson-vt@4.0.2: {} 4847 + get-stream@6.0.1: {} 4824 4848 4825 4849 gl-matrix@3.4.4: {} 4826 4850 ··· 4836 4860 4837 4861 graceful-fs@4.2.11: {} 4838 4862 4839 - grid-index@1.1.0: {} 4840 - 4841 4863 gsap@3.14.2: {} 4842 4864 4843 4865 has-flag@4.0.0: {} ··· 4906 4928 json-schema-traverse@0.4.1: {} 4907 4929 4908 4930 json-stable-stringify-without-jsonify@1.0.1: {} 4931 + 4932 + json-stringify-pretty-compact@4.0.0: {} 4909 4933 4910 4934 kdbush@4.0.2: {} 4911 4935 ··· 5017 5041 dependencies: 5018 5042 '@jridgewell/sourcemap-codec': 1.5.5 5019 5043 5020 - mapbox-gl@3.18.1: 5044 + maplibre-gl@5.17.0: 5021 5045 dependencies: 5046 + '@mapbox/geojson-rewind': 0.5.2 5022 5047 '@mapbox/jsonlint-lines-primitives': 2.0.2 5023 - '@mapbox/mapbox-gl-supported': 3.0.0 5024 5048 '@mapbox/point-geometry': 1.1.0 5025 5049 '@mapbox/tiny-sdf': 2.0.7 5026 5050 '@mapbox/unitbezier': 0.0.1 5027 5051 '@mapbox/vector-tile': 2.0.4 5028 5052 '@mapbox/whoots-js': 3.1.0 5053 + '@maplibre/geojson-vt': 5.0.4 5054 + '@maplibre/maplibre-gl-style-spec': 24.4.1 5055 + '@maplibre/mlt': 1.1.5 5056 + '@maplibre/vt-pbf': 4.2.1 5029 5057 '@types/geojson': 7946.0.16 5030 - '@types/geojson-vt': 3.2.5 5031 - '@types/mapbox__point-geometry': 0.1.4 5032 - '@types/pbf': 3.0.5 5033 5058 '@types/supercluster': 7.1.3 5034 - cheap-ruler: 4.0.0 5035 - csscolorparser: 1.0.3 5036 5059 earcut: 3.0.2 5037 - geojson-vt: 4.0.2 5038 5060 gl-matrix: 3.4.4 5039 - grid-index: 1.1.0 5040 5061 kdbush: 4.0.2 5041 - martinez-polygon-clipping: 0.8.1 5042 5062 murmurhash-js: 1.0.0 5043 5063 pbf: 4.0.1 5044 5064 potpack: 2.1.0 ··· 5057 5077 5058 5078 marked@17.0.1: {} 5059 5079 5060 - martinez-polygon-clipping@0.8.1: 5061 - dependencies: 5062 - robust-predicates: 2.0.4 5063 - splaytree: 0.1.4 5064 - tinyqueue: 3.0.0 5065 - 5066 5080 mdurl@2.0.0: {} 5067 5081 5068 5082 meshoptimizer@0.18.1: {} ··· 5089 5103 minimatch@9.0.5: 5090 5104 dependencies: 5091 5105 brace-expansion: 2.0.2 5106 + 5107 + minimist@1.2.8: {} 5092 5108 5093 5109 mitt@3.0.1: {} 5094 5110 ··· 5439 5455 dependencies: 5440 5456 protocol-buffers-schema: 3.6.0 5441 5457 5442 - robust-predicates@2.0.4: {} 5443 - 5444 5458 rollup@4.56.0: 5445 5459 dependencies: 5446 5460 '@types/estree': 1.0.8 ··· 5497 5511 svelte: 5.48.0 5498 5512 optionalDependencies: 5499 5513 '@sveltejs/kit': 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)) 5514 + 5515 + rw@1.3.3: {} 5500 5516 5501 5517 sade@1.8.1: 5502 5518 dependencies: ··· 5573 5589 totalist: 3.0.1 5574 5590 5575 5591 source-map-js@1.2.1: {} 5576 - 5577 - splaytree@0.1.4: {} 5578 5592 5579 5593 std-env@3.10.0: {} 5580 5594 ··· 5617 5631 postcss-scss: 4.0.9(postcss@8.5.6) 5618 5632 postcss-selector-parser: 7.1.1 5619 5633 optionalDependencies: 5634 + svelte: 5.48.0 5635 + 5636 + svelte-maplibre-gl@1.0.3(maplibre-gl@5.17.0)(svelte@5.48.0): 5637 + dependencies: 5638 + maplibre-gl: 5.17.0 5620 5639 svelte: 5.48.0 5621 5640 5622 5641 svelte-sonner@0.3.28(svelte@5.48.0):
+98
src/lib/cache.ts
··· 1 + import type { ActorIdentifier, Did } from '@atcute/lexicons'; 2 + import { isDid } from '@atcute/lexicons/syntax'; 3 + import type { KVNamespace } from '@cloudflare/workers-types'; 4 + 5 + /** TTL in seconds for each cache namespace */ 6 + const NAMESPACE_TTL = { 7 + blento: 60 * 60 * 24, // 24 hours 8 + identity: 60 * 60 * 24 * 7, // 7 days 9 + github: 60 * 60 * 12, // 12 hours 10 + 'gh-contrib': 60 * 60 * 12, // 12 hours 11 + lastfm: 60 * 60, // 1 hour (default, overridable per-put) 12 + npmx: 60 * 60 * 12, // 12 hours 13 + meta: 0 // no auto-expiry 14 + } as const; 15 + 16 + export type CacheNamespace = keyof typeof NAMESPACE_TTL; 17 + 18 + export class CacheService { 19 + constructor(private kv: KVNamespace) {} 20 + 21 + // === Generic namespaced operations === 22 + 23 + async get(namespace: CacheNamespace, key: string): Promise<string | null> { 24 + return this.kv.get(`${namespace}:${key}`); 25 + } 26 + 27 + async put( 28 + namespace: CacheNamespace, 29 + key: string, 30 + value: string, 31 + ttlSeconds?: number 32 + ): Promise<void> { 33 + const ttl = ttlSeconds ?? NAMESPACE_TTL[namespace] ?? 0; 34 + await this.kv.put(`${namespace}:${key}`, value, ttl > 0 ? { expirationTtl: ttl } : undefined); 35 + } 36 + 37 + async delete(namespace: CacheNamespace, key: string): Promise<void> { 38 + await this.kv.delete(`${namespace}:${key}`); 39 + } 40 + 41 + async list(namespace: CacheNamespace): Promise<string[]> { 42 + const prefix = `${namespace}:`; 43 + const result = await this.kv.list({ prefix }); 44 + return result.keys.map((k) => k.name.slice(prefix.length)); 45 + } 46 + 47 + // === JSON convenience === 48 + 49 + async getJSON<T = unknown>(namespace: CacheNamespace, key: string): Promise<T | null> { 50 + const raw = await this.get(namespace, key); 51 + if (!raw) return null; 52 + return JSON.parse(raw) as T; 53 + } 54 + 55 + async putJSON( 56 + namespace: CacheNamespace, 57 + key: string, 58 + value: unknown, 59 + ttlSeconds?: number 60 + ): Promise<void> { 61 + await this.put(namespace, key, JSON.stringify(value), ttlSeconds); 62 + } 63 + 64 + // === blento data (keyed by DID, with handleโ†”did resolution) === 65 + async getBlento(identifier: ActorIdentifier): Promise<string | null> { 66 + const did = await this.resolveDid(identifier); 67 + if (!did) return null; 68 + return this.get('blento', did); 69 + } 70 + 71 + async putBlento(did: string, handle: string, data: string): Promise<void> { 72 + await Promise.all([ 73 + this.put('blento', did, data), 74 + this.put('identity', `h:${handle}`, did), 75 + this.put('identity', `d:${did}`, handle) 76 + ]); 77 + } 78 + 79 + async listBlentos(): Promise<string[]> { 80 + return this.list('blento'); 81 + } 82 + 83 + // === Identity resolution === 84 + async resolveDid(identifier: ActorIdentifier): Promise<string | null> { 85 + if (isDid(identifier)) return identifier; 86 + return this.get('identity', `h:${identifier}`); 87 + } 88 + 89 + async resolveHandle(did: Did): Promise<string | null> { 90 + return this.get('identity', `d:${did}`); 91 + } 92 + } 93 + 94 + export function createCache(platform?: App.Platform): CacheService | undefined { 95 + const kv = platform?.env?.USER_DATA_CACHE; 96 + if (!kv) return undefined; 97 + return new CacheService(kv); 98 + }
+29 -154
src/lib/cards/core/MapCard/Map.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from 'svelte'; 3 - import 'mapbox-gl/dist/mapbox-gl.css'; 4 - import mapboxgl from 'mapbox-gl'; 5 - import { env } from '$env/dynamic/public'; 6 2 import type { Item } from '$lib/types'; 7 - import { getHexOfCardColor } from '../../helper'; 3 + import { MapLibre, Projection, Marker } from 'svelte-maplibre-gl'; 8 4 9 5 let { item = $bindable() }: { item: Item } = $props(); 10 6 11 - // $inspect(item); 12 - 13 - let mapContainer: HTMLElement | undefined = $state(); 14 - let map: mapboxgl.Map | undefined = $state(); 15 - 16 - onMount(() => { 17 - if (!mapContainer || !env.PUBLIC_MAPBOX_TOKEN) { 18 - console.error('no map container or no mapbox token'); 19 - return; 20 - } 21 - 22 - try { 23 - mapboxgl.accessToken = env.PUBLIC_MAPBOX_TOKEN; 24 - 25 - const lat = parseFloat(item.cardData.lat); 26 - const lon = parseFloat(item.cardData.lon); 27 - const zoom = item.cardData.zoom ? parseFloat(item.cardData.zoom) : 0; 28 - const lightPreset = item.cardData.lightPreset || 'day'; 29 - 30 - map = new mapboxgl.Map({ 31 - container: mapContainer, 32 - style: 'mapbox://styles/mapbox/standard', 33 - center: [lon, lat], 34 - config: { 35 - basemap: { 36 - lightPreset: lightPreset, 37 - showPointOfInterestLabels: false 38 - } 39 - }, 40 - zoom: zoom, 41 - attributionControl: false, 42 - dragPan: false, 43 - dragRotate: false, 44 - keyboard: false, 45 - doubleClickZoom: true, 46 - touchZoomRotate: true, 47 - scrollZoom: true, 48 - boxZoom: false, 49 - pitchWithRotate: false, 50 - touchPitch: false 51 - }); 7 + let center = $state({ lng: parseFloat(item.cardData.lon), lat: parseFloat(item.cardData.lat) }); 8 + </script> 52 9 53 - // Keep location centered during zoom and save zoom level 54 - map.on('zoom', () => { 55 - if (map) { 56 - map.setCenter([lon, lat]); 57 - } 58 - }); 59 - 60 - map.on('zoomend', () => { 61 - if (map) { 62 - item.cardData.zoom = map.getZoom().toString(); 63 - } 64 - }); 65 - 66 - map.on('load', () => { 67 - if (!map) return; 10 + <div class="absolute inset-0 isolate h-full w-full"> 11 + <MapLibre 12 + class="h-full w-full" 13 + style="https://tiles.openfreemap.org/styles/liberty" 14 + zoom={item.cardData.zoom} 15 + {center} 16 + attributionControl={{ compact: true }} 17 + dragPan={false} 18 + dragRotate={false} 19 + keyboard={false} 20 + touchZoomRotate={true} 21 + scrollZoom={true} 22 + boxZoom={false} 23 + pitchWithRotate={false} 24 + touchPitch={false} 25 + > 26 + <Projection type={'globe'} /> 68 27 69 - map.resize(); 70 - map.setCenter([lon, lat]); 71 - 72 - const accentColor = getHexOfCardColor(item); 73 - 74 - // Add location point source 75 - map.addSource('location-point', { 76 - type: 'geojson', 77 - data: { 78 - type: 'Feature', 79 - geometry: { 80 - type: 'Point', 81 - coordinates: [lon, lat] 82 - }, 83 - properties: { 84 - name: item.cardData.name || '' 85 - } 86 - } 87 - }); 88 - 89 - // Outer glow 90 - map.addLayer({ 91 - id: 'location-glow-outer', 92 - type: 'circle', 93 - source: 'location-point', 94 - paint: { 95 - 'circle-radius': 20, 96 - 'circle-color': accentColor, 97 - 'circle-opacity': 0.15, 98 - 'circle-blur': 1 99 - } 100 - }); 101 - 102 - // Middle glow 103 - map.addLayer({ 104 - id: 'location-glow-middle', 105 - type: 'circle', 106 - source: 'location-point', 107 - paint: { 108 - 'circle-radius': 12, 109 - 'circle-color': accentColor, 110 - 'circle-opacity': 0.3, 111 - 'circle-blur': 0.5 112 - } 113 - }); 114 - 115 - // White border 116 - map.addLayer({ 117 - id: 'location-dot-border', 118 - type: 'circle', 119 - source: 'location-point', 120 - paint: { 121 - 'circle-radius': 8, 122 - 'circle-color': '#ffffff', 123 - 'circle-opacity': 1 124 - } 125 - }); 126 - 127 - // Accent color center dot 128 - map.addLayer({ 129 - id: 'location-dot', 130 - type: 'circle', 131 - source: 'location-point', 132 - paint: { 133 - 'circle-radius': 6, 134 - 'circle-color': accentColor, 135 - 'circle-opacity': 1 136 - } 137 - }); 138 - }); 139 - 140 - // Handle container resize 141 - const resizeObserver = new ResizeObserver(() => { 142 - if (map) { 143 - map.resize(); 144 - map.setCenter([lon, lat]); 145 - } 146 - }); 147 - if (mapContainer) resizeObserver.observe(mapContainer); 148 - 149 - return () => { 150 - resizeObserver.disconnect(); 151 - if (map) { 152 - map.remove(); 153 - } 154 - }; 155 - } catch (err) { 156 - console.error(`Something went wrong trying to initialize the map`, err); 157 - } 158 - }); 159 - </script> 160 - 161 - <div bind:this={mapContainer} class="absolute inset-0 isolate h-full w-full"></div> 28 + <Marker bind:lnglat={center}> 29 + {#snippet content()} 30 + <div class="from-accent-400 size-10 rounded-full bg-radial via-transparent p-3"> 31 + <div class="bg-accent-500 size-4 rounded-full ring-2 ring-white"></div> 32 + </div> 33 + {/snippet} 34 + </Marker> 35 + </MapLibre> 36 + </div>
+1 -1
src/lib/cards/core/MapCard/index.ts
··· 14 14 }, 15 15 16 16 creationModalComponent: CreateMapCardModal, 17 - allowSetColor: false, 17 + allowSetColor: true, 18 18 canHaveLabel: true, 19 19 settingsComponent: MapCardSettings, 20 20
+1 -1
src/lib/cards/core/SectionCard/EditingSectionCard.svelte
··· 9 9 10 10 <div 11 11 class={[ 12 - 'line-clamp-1 inline-flex h-full w-full rounded-md p-1 px-2 font-semibold', 12 + 'line-clamp-1 inline-flex h-full w-full rounded-md py-2 px-4 font-semibold', 13 13 textAlignClasses[item.cardData.textAlign as string], 14 14 verticalAlignClasses[item.cardData.verticalAlign ?? ('center' as string)], 15 15 textSizeClasses[(item.cardData.textSize ?? 1) as number]
+1 -1
src/lib/cards/core/SectionCard/SectionCard.svelte
··· 12 12 13 13 <div 14 14 class={[ 15 - 'line-clamp-1 inline-flex h-full w-full rounded-md p-1 px-2 font-semibold', 15 + 'line-clamp-1 inline-flex h-full w-full rounded-md py-2 px-4 font-semibold', 16 16 textAlignClasses[item.cardData.textAlign as string], 17 17 verticalAlignClasses[item.cardData.verticalAlign ?? ('center' as string)], 18 18 textSizeClasses[(item.cardData.textSize ?? 1) as number]
+1 -1
src/lib/cards/core/TextCard/EditingTextCard.svelte
··· 15 15 <!-- svelte-ignore a11y_click_events_have_key_events --> 16 16 <div 17 17 class={cn( 18 - 'prose dark:prose-invert prose-neutral prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 hover:bg-base-700/5 accent:hover:bg-accent-300/20 prose-p:first:mt-0 prose-p:last:mb-0 inline-flex h-full w-full max-w-none cursor-text overflow-y-scroll rounded-md p-2 text-lg transition-colors duration-150', 18 + 'prose dark:prose-invert prose-neutral prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 hover:bg-base-700/5 accent:hover:bg-accent-300/20 prose-p:first:mt-0 prose-p:last:mb-0 inline-flex h-full w-full max-w-none cursor-text overflow-y-scroll rounded-md px-6 py-4 text-lg transition-colors duration-150', 19 19 textAlignClasses[item.cardData.textAlign as string], 20 20 verticalAlignClasses[item.cardData.verticalAlign as string], 21 21 textSizeClasses[(item.cardData.textSize ?? 0) as number]
+1 -1
src/lib/cards/core/TextCard/TextCard.svelte
··· 14 14 15 15 <div 16 16 class={cn( 17 - 'prose dark:prose-invert prose-neutral prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 prose-p:first:mt-0 prose-p:last:mb-0 prose-headings:first:mt-0 prose-headings:last:mb-0 inline-flex h-full min-h-full w-full max-w-none overflow-x-hidden overflow-y-scroll rounded-md p-3 text-lg', 17 + 'prose dark:prose-invert prose-neutral prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 prose-p:first:mt-0 prose-p:last:mb-0 prose-headings:first:mt-0 prose-headings:last:mb-0 inline-flex h-full min-h-full w-full max-w-none overflow-x-hidden overflow-y-scroll rounded-md px-6 py-4 text-lg', 18 18 textAlignClasses?.[item.cardData.textAlign as string], 19 19 verticalAlignClasses[item.cardData.verticalAlign as string], 20 20 textSizeClasses[(item.cardData.textSize ?? 0) as number]
+6 -5
src/lib/cards/media/LastFMCard/LastFMProfileCard/LastFMProfileCard.svelte
··· 4 4 import { getAdditionalUserData } from '$lib/website/context'; 5 5 import type { ContentComponentProps } from '../../../types'; 6 6 import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; 7 + import { fetchLastFM } from '../api.remote'; 7 8 8 9 interface UserInfo { 9 10 name: string; ··· 27 28 if (!item.cardData.lastfmUsername) return; 28 29 29 30 try { 30 - const response = await fetch( 31 - `/api/lastfm?method=user.getInfo&user=${encodeURIComponent(item.cardData.lastfmUsername)}` 32 - ); 33 - if (response.ok) { 34 - const result = await response.json(); 31 + const result = await fetchLastFM({ 32 + method: 'user.getInfo', 33 + user: item.cardData.lastfmUsername 34 + }); 35 + if (result) { 35 36 userInfo = result?.user; 36 37 data[cacheKey] = userInfo; 37 38 }
+17 -7
src/lib/cards/media/LastFMCard/LastFMProfileCard/index.ts
··· 1 1 import type { CardDefinition } from '../../../types'; 2 2 import CreateLastFMCardModal from '../CreateLastFMCardModal.svelte'; 3 3 import LastFMProfileCard from './LastFMProfileCard.svelte'; 4 + import { fetchLastFM } from '../api.remote'; 4 5 5 6 export const LastFMProfileCardDefinition = { 6 7 type: 'lastfmProfile', ··· 18 19 const username = item.cardData.lastfmUsername; 19 20 if (!username) continue; 20 21 try { 21 - const response = await fetch( 22 - `https://blento.app/api/lastfm?method=user.getInfo&user=${encodeURIComponent(username)}` 23 - ); 24 - if (!response.ok) continue; 25 - const text = await response.text(); 26 - const result = JSON.parse(text); 27 - allData[`lastfmProfile:${username}`] = result?.user; 22 + const data = await fetchLastFM({ method: 'user.getInfo', user: username }); 23 + if (data) allData[`lastfmProfile:${username}`] = data?.user; 24 + } catch (error) { 25 + console.error('Failed to fetch Last.fm profile:', error); 26 + } 27 + } 28 + return allData; 29 + }, 30 + loadDataServer: async (items) => { 31 + const allData: Record<string, unknown> = {}; 32 + for (const item of items) { 33 + const username = item.cardData.lastfmUsername; 34 + if (!username) continue; 35 + try { 36 + const data = await fetchLastFM({ method: 'user.getInfo', user: username }); 37 + if (data) allData[`lastfmProfile:${username}`] = data?.user; 28 38 } catch (error) { 29 39 console.error('Failed to fetch Last.fm profile:', error); 30 40 }
+6 -5
src/lib/cards/media/LastFMCard/LastFMRecentTracksCard/LastFMRecentTracksCard.svelte
··· 4 4 import type { ContentComponentProps } from '../../../types'; 5 5 import LastFMAlbumArt from '../LastFMAlbumArt.svelte'; 6 6 import { RelativeTime } from '@foxui/time'; 7 + import { fetchLastFM } from '../api.remote'; 7 8 8 9 interface Track { 9 10 name: string; ··· 29 30 if (!item.cardData.lastfmUsername) return; 30 31 31 32 try { 32 - const response = await fetch( 33 - `/api/lastfm?method=user.getRecentTracks&user=${encodeURIComponent(item.cardData.lastfmUsername)}&limit=50` 34 - ); 35 - if (response.ok) { 36 - const result = await response.json(); 33 + const result = await fetchLastFM({ 34 + method: 'user.getRecentTracks', 35 + user: item.cardData.lastfmUsername 36 + }); 37 + if (result) { 37 38 tracks = result?.recenttracks?.track ?? []; 38 39 data[cacheKey] = tracks; 39 40 } else {
+17 -7
src/lib/cards/media/LastFMCard/LastFMRecentTracksCard/index.ts
··· 1 1 import type { CardDefinition } from '../../../types'; 2 2 import CreateLastFMCardModal from '../CreateLastFMCardModal.svelte'; 3 3 import LastFMRecentTracksCard from './LastFMRecentTracksCard.svelte'; 4 + import { fetchLastFM } from '../api.remote'; 4 5 5 6 export const LastFMRecentTracksCardDefinition = { 6 7 type: 'lastfmRecentTracks', ··· 18 19 const username = item.cardData.lastfmUsername; 19 20 if (!username) continue; 20 21 try { 21 - const response = await fetch( 22 - `https://blento.app/api/lastfm?method=user.getRecentTracks&user=${encodeURIComponent(username)}&limit=50` 23 - ); 24 - if (!response.ok) continue; 25 - const text = await response.text(); 26 - const result = JSON.parse(text); 27 - allData[`lastfmRecentTracks:${username}`] = result?.recenttracks?.track ?? []; 22 + const data = await fetchLastFM({ method: 'user.getRecentTracks', user: username }); 23 + if (data) allData[`lastfmRecentTracks:${username}`] = data?.recenttracks?.track ?? []; 24 + } catch (error) { 25 + console.error('Failed to fetch Last.fm recent tracks:', error); 26 + } 27 + } 28 + return allData; 29 + }, 30 + loadDataServer: async (items) => { 31 + const allData: Record<string, unknown> = {}; 32 + for (const item of items) { 33 + const username = item.cardData.lastfmUsername; 34 + if (!username) continue; 35 + try { 36 + const data = await fetchLastFM({ method: 'user.getRecentTracks', user: username }); 37 + if (data) allData[`lastfmRecentTracks:${username}`] = data?.recenttracks?.track ?? []; 28 38 } catch (error) { 29 39 console.error('Failed to fetch Last.fm recent tracks:', error); 30 40 }
+7 -5
src/lib/cards/media/LastFMCard/LastFMTopAlbumsCard/LastFMTopAlbumsCard.svelte
··· 3 3 import type { ContentComponentProps } from '../../../types'; 4 4 import { getAdditionalUserData } from '$lib/website/context'; 5 5 import ImageGrid from '$lib/components/ImageGrid.svelte'; 6 + import { fetchLastFM } from '../api.remote'; 6 7 7 8 interface Album { 8 9 name: string; ··· 30 31 loading = true; 31 32 32 33 try { 33 - const response = await fetch( 34 - `/api/lastfm?method=user.getTopAlbums&user=${encodeURIComponent(item.cardData.lastfmUsername)}&period=${period}&limit=50` 35 - ); 36 - if (response.ok) { 37 - const result = await response.json(); 34 + const result = await fetchLastFM({ 35 + method: 'user.getTopAlbums', 36 + user: item.cardData.lastfmUsername, 37 + period 38 + }); 39 + if (result) { 38 40 albums = result?.topalbums?.album ?? []; 39 41 data[cacheKey] = albums; 40 42 } else {
+18 -7
src/lib/cards/media/LastFMCard/LastFMTopAlbumsCard/index.ts
··· 2 2 import CreateLastFMCardModal from '../CreateLastFMCardModal.svelte'; 3 3 import LastFMTopAlbumsCard from './LastFMTopAlbumsCard.svelte'; 4 4 import LastFMTopAlbumsCardSettings from './LastFMTopAlbumsCardSettings.svelte'; 5 + import { fetchLastFM } from '../api.remote'; 5 6 6 7 export const LastFMTopAlbumsCardDefinition = { 7 8 type: 'lastfmTopAlbums', ··· 22 23 const period = item.cardData.period ?? '7day'; 23 24 if (!username) continue; 24 25 try { 25 - const response = await fetch( 26 - `https://blento.app/api/lastfm?method=user.getTopAlbums&user=${encodeURIComponent(username)}&period=${period}&limit=50` 27 - ); 28 - if (!response.ok) continue; 29 - const text = await response.text(); 30 - const result = JSON.parse(text); 31 - allData[`lastfmTopAlbums:${username}:${period}`] = result?.topalbums?.album ?? []; 26 + const data = await fetchLastFM({ method: 'user.getTopAlbums', user: username, period }); 27 + if (data) allData[`lastfmTopAlbums:${username}:${period}`] = data?.topalbums?.album ?? []; 28 + } catch (error) { 29 + console.error('Failed to fetch Last.fm top albums:', error); 30 + } 31 + } 32 + return allData; 33 + }, 34 + loadDataServer: async (items) => { 35 + const allData: Record<string, unknown> = {}; 36 + for (const item of items) { 37 + const username = item.cardData.lastfmUsername; 38 + const period = item.cardData.period ?? '7day'; 39 + if (!username) continue; 40 + try { 41 + const data = await fetchLastFM({ method: 'user.getTopAlbums', user: username, period }); 42 + if (data) allData[`lastfmTopAlbums:${username}:${period}`] = data?.topalbums?.album ?? []; 32 43 } catch (error) { 33 44 console.error('Failed to fetch Last.fm top albums:', error); 34 45 }
+7 -5
src/lib/cards/media/LastFMCard/LastFMTopTracksCard/LastFMTopTracksCard.svelte
··· 3 3 import { getAdditionalUserData } from '$lib/website/context'; 4 4 import type { ContentComponentProps } from '../../../types'; 5 5 import LastFMAlbumArt from '../LastFMAlbumArt.svelte'; 6 + import { fetchLastFM } from '../api.remote'; 6 7 7 8 interface Track { 8 9 name: string; ··· 29 30 loading = true; 30 31 31 32 try { 32 - const response = await fetch( 33 - `/api/lastfm?method=user.getTopTracks&user=${encodeURIComponent(item.cardData.lastfmUsername)}&period=${period}&limit=50` 34 - ); 35 - if (response.ok) { 36 - const result = await response.json(); 33 + const result = await fetchLastFM({ 34 + method: 'user.getTopTracks', 35 + user: item.cardData.lastfmUsername, 36 + period 37 + }); 38 + if (result) { 37 39 tracks = result?.toptracks?.track ?? []; 38 40 data[cacheKey] = tracks; 39 41 } else {
+18 -7
src/lib/cards/media/LastFMCard/LastFMTopTracksCard/index.ts
··· 2 2 import CreateLastFMCardModal from '../CreateLastFMCardModal.svelte'; 3 3 import LastFMPeriodSettings from '../LastFMPeriodSettings.svelte'; 4 4 import LastFMTopTracksCard from './LastFMTopTracksCard.svelte'; 5 + import { fetchLastFM } from '../api.remote'; 5 6 6 7 export const LastFMTopTracksCardDefinition = { 7 8 type: 'lastfmTopTracks', ··· 22 23 const period = item.cardData.period ?? '7day'; 23 24 if (!username) continue; 24 25 try { 25 - const response = await fetch( 26 - `https://blento.app/api/lastfm?method=user.getTopTracks&user=${encodeURIComponent(username)}&period=${period}&limit=50` 27 - ); 28 - if (!response.ok) continue; 29 - const text = await response.text(); 30 - const result = JSON.parse(text); 31 - allData[`lastfmTopTracks:${username}:${period}`] = result?.toptracks?.track ?? []; 26 + const data = await fetchLastFM({ method: 'user.getTopTracks', user: username, period }); 27 + if (data) allData[`lastfmTopTracks:${username}:${period}`] = data?.toptracks?.track ?? []; 28 + } catch (error) { 29 + console.error('Failed to fetch Last.fm top tracks:', error); 30 + } 31 + } 32 + return allData; 33 + }, 34 + loadDataServer: async (items) => { 35 + const allData: Record<string, unknown> = {}; 36 + for (const item of items) { 37 + const username = item.cardData.lastfmUsername; 38 + const period = item.cardData.period ?? '7day'; 39 + if (!username) continue; 40 + try { 41 + const data = await fetchLastFM({ method: 'user.getTopTracks', user: username, period }); 42 + if (data) allData[`lastfmTopTracks:${username}:${period}`] = data?.toptracks?.track ?? []; 32 43 } catch (error) { 33 44 console.error('Failed to fetch Last.fm top tracks:', error); 34 45 }
+59
src/lib/cards/media/LastFMCard/api.remote.ts
··· 1 + import { query, getRequestEvent } from '$app/server'; 2 + import { env } from '$env/dynamic/private'; 3 + import { createCache } from '$lib/cache'; 4 + 5 + const LASTFM_API_URL = 'https://ws.audioscrobbler.com/2.0/'; 6 + 7 + const CACHE_TTL: Record<string, number> = { 8 + 'user.getRecentTracks': 15 * 60, 9 + 'user.getTopTracks': 60 * 60, 10 + 'user.getTopAlbums': 60 * 60, 11 + 'user.getInfo': 12 * 60 * 60 12 + }; 13 + 14 + export const fetchLastFM = query( 15 + 'unchecked', 16 + async ({ 17 + method, 18 + user, 19 + period = '7day', 20 + limit = '50' 21 + }: { 22 + method: string; 23 + user: string; 24 + period?: string; 25 + limit?: string; 26 + }) => { 27 + const apiKey = env?.LASTFM_API_KEY; 28 + if (!apiKey) return undefined; 29 + 30 + const { platform } = getRequestEvent(); 31 + const cache = createCache(platform); 32 + 33 + const cacheKey = `${method}:${user}:${period}:${limit}`; 34 + const cached = await cache?.get('lastfm', cacheKey); 35 + if (cached) return JSON.parse(cached); 36 + 37 + const params = new URLSearchParams({ 38 + method, 39 + user, 40 + api_key: apiKey, 41 + format: 'json', 42 + limit 43 + }); 44 + 45 + if (method === 'user.getTopTracks' || method === 'user.getTopAlbums') { 46 + params.set('period', period); 47 + } 48 + 49 + const response = await fetch(`${LASTFM_API_URL}?${params}`); 50 + if (!response.ok) return undefined; 51 + 52 + const data = await response.json(); 53 + if (data.error) return undefined; 54 + 55 + const ttl = CACHE_TTL[method] || 60 * 60; 56 + await cache?.put('lastfm', cacheKey, JSON.stringify(data), ttl); 57 + return data; 58 + } 59 + );
+3 -7
src/lib/cards/social/GitHubContributorsCard/GitHubContributorsCard.svelte
··· 4 4 import { getAdditionalUserData, getCanEdit } from '$lib/website/context'; 5 5 import type { GitHubContributor, GitHubContributorsLoadedData } from '.'; 6 6 import ImageGrid from '$lib/components/ImageGrid.svelte'; 7 + import { fetchGitHubContributors } from './api.remote'; 7 8 8 9 let { item }: ContentComponentProps = $props(); 9 10 ··· 41 42 async function loadContributors() { 42 43 if (!owner || !repo) return; 43 44 try { 44 - const response = await fetch( 45 - `/api/github/contributors?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}` 46 - ); 47 - if (response.ok) { 48 - const data = await response.json(); 49 - clientContributors = data; 50 - } 45 + const data = await fetchGitHubContributors({ owner, repo }); 46 + if (data) clientContributors = data; 51 47 } catch (error) { 52 48 console.error('Failed to fetch GitHub contributors:', error); 53 49 }
+26
src/lib/cards/social/GitHubContributorsCard/api.remote.ts
··· 1 + import { query, getRequestEvent } from '$app/server'; 2 + import { createCache } from '$lib/cache'; 3 + 4 + const GITHUB_CONTRIBUTORS_API_URL = 5 + 'https://edge-function-github-contribution.vercel.app/api/github-contributors'; 6 + 7 + export const fetchGitHubContributors = query( 8 + 'unchecked', 9 + async ({ owner, repo }: { owner: string; repo: string }) => { 10 + const { platform } = getRequestEvent(); 11 + const cache = createCache(platform); 12 + 13 + const key = `${owner}/${repo}`; 14 + const cached = await cache?.get('gh-contrib', key); 15 + if (cached) return JSON.parse(cached); 16 + 17 + const response = await fetch( 18 + `${GITHUB_CONTRIBUTORS_API_URL}?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}` 19 + ); 20 + if (!response.ok) return undefined; 21 + 22 + const data = await response.json(); 23 + await cache?.put('gh-contrib', key, JSON.stringify(data)); 24 + return data; 25 + } 26 + );
+19 -6
src/lib/cards/social/GitHubContributorsCard/index.ts
··· 2 2 import GitHubContributorsCard from './GitHubContributorsCard.svelte'; 3 3 import CreateGitHubContributorsCardModal from './CreateGitHubContributorsCardModal.svelte'; 4 4 import GitHubContributorsCardSettings from './GitHubContributorsCardSettings.svelte'; 5 + import { fetchGitHubContributors } from './api.remote'; 5 6 6 7 export type GitHubContributor = { 7 8 username: string; ··· 33 34 const key = `${owner}/${repo}`; 34 35 if (contributorsData[key]) continue; 35 36 try { 36 - const response = await fetch( 37 - `https://blento.app/api/github/contributors?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}` 38 - ); 39 - if (response.ok) { 40 - contributorsData[key] = await response.json(); 41 - } 37 + const data = await fetchGitHubContributors({ owner, repo }); 38 + if (data) contributorsData[key] = data; 39 + } catch (error) { 40 + console.error('Failed to fetch GitHub contributors:', error); 41 + } 42 + } 43 + return contributorsData; 44 + }, 45 + loadDataServer: async (items) => { 46 + const contributorsData: GitHubContributorsLoadedData = {}; 47 + for (const item of items) { 48 + const { owner, repo } = item.cardData; 49 + if (!owner || !repo) continue; 50 + const key = `${owner}/${repo}`; 51 + if (contributorsData[key]) continue; 52 + try { 53 + const data = await fetchGitHubContributors({ owner, repo }); 54 + if (data) contributorsData[key] = data; 42 55 } catch (error) { 43 56 console.error('Failed to fetch GitHub contributors:', error); 44 57 }
+3 -3
src/lib/cards/social/GitHubProfileCard/GitHubProfileCard.svelte
··· 8 8 import { Button } from '@foxui/core'; 9 9 import { browser } from '$app/environment'; 10 10 import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; 11 + import { fetchGitHubContributions } from './api.remote'; 11 12 12 13 let { item, isEditing }: ContentComponentProps = $props(); 13 14 ··· 23 24 onMount(async () => { 24 25 if (!contributionsData && item.cardData?.user) { 25 26 try { 26 - const response = await fetch(`/api/github?user=${encodeURIComponent(item.cardData.user)}`); 27 - if (response.ok) { 28 - contributionsData = await response.json(); 27 + contributionsData = await fetchGitHubContributions(item.cardData.user); 28 + if (contributionsData) { 29 29 data[item.cardType] ??= {}; 30 30 (data[item.cardType] as GithubProfileLoadedData)[item.cardData.user] = contributionsData; 31 31 }
+21
src/lib/cards/social/GitHubProfileCard/api.remote.ts
··· 1 + import { query, getRequestEvent } from '$app/server'; 2 + import { createCache } from '$lib/cache'; 3 + 4 + const GITHUB_API_URL = 'https://edge-function-github-contribution.vercel.app/api/github-data?user='; 5 + 6 + export const fetchGitHubContributions = query('unchecked', async (user: string) => { 7 + const { platform } = getRequestEvent(); 8 + const cache = createCache(platform); 9 + 10 + const cached = await cache?.get('github', user); 11 + if (cached) return JSON.parse(cached); 12 + 13 + const response = await fetch(GITHUB_API_URL + encodeURIComponent(user)); 14 + if (!response.ok) return undefined; 15 + 16 + const data = await response.json(); 17 + if (!data?.user) return undefined; 18 + 19 + await cache?.put('github', user, JSON.stringify(data.user)); 20 + return data.user; 21 + });
+20 -8
src/lib/cards/social/GitHubProfileCard/index.ts
··· 1 1 import type { CardDefinition } from '../../types'; 2 2 import CreateGitHubProfileCardModal from './CreateGitHubProfileCardModal.svelte'; 3 - import type GithubContributionsGraph from './GithubContributionsGraph.svelte'; 4 3 import GitHubProfileCard from './GitHubProfileCard.svelte'; 5 4 import type { GitHubContributionsData } from './types'; 5 + import { fetchGitHubContributions } from './api.remote'; 6 6 7 7 export type GithubProfileLoadedData = Record<string, GitHubContributionsData | undefined>; 8 8 ··· 12 12 creationModalComponent: CreateGitHubProfileCardModal, 13 13 14 14 loadData: async (items) => { 15 - const githubData: Record<string, GithubContributionsGraph> = {}; 15 + const githubData: Record<string, GitHubContributionsData> = {}; 16 16 for (const item of items) { 17 + const user = item.cardData.user; 18 + if (!user) continue; 17 19 try { 18 - const response = await fetch( 19 - `https://blento.app/api/github?user=${encodeURIComponent(item.cardData.user)}` 20 - ); 21 - if (response.ok) { 22 - githubData[item.cardData.user] = await response.json(); 23 - } 20 + const data = await fetchGitHubContributions(user); 21 + if (data) githubData[user] = data; 22 + } catch (error) { 23 + console.error('Failed to fetch GitHub contributions:', error); 24 + } 25 + } 26 + return githubData; 27 + }, 28 + loadDataServer: async (items) => { 29 + const githubData: Record<string, GitHubContributionsData> = {}; 30 + for (const item of items) { 31 + const user = item.cardData.user; 32 + if (!user) continue; 33 + try { 34 + const data = await fetchGitHubContributions(user); 35 + if (data) githubData[user] = data; 24 36 } catch (error) { 25 37 console.error('Failed to fetch GitHub contributions:', error); 26 38 }
+20
src/lib/cards/social/NpmxLikesLeaderboardCard/api.remote.ts
··· 1 + import { query, getRequestEvent } from '$app/server'; 2 + import { createCache } from '$lib/cache'; 3 + 4 + const LEADERBOARD_API_URL = 5 + 'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes?limit=20'; 6 + 7 + export const fetchNpmxLeaderboard = query(async () => { 8 + const { platform } = getRequestEvent(); 9 + const cache = createCache(platform); 10 + 11 + const cached = await cache?.get('npmx', 'likes'); 12 + if (cached) return JSON.parse(cached); 13 + 14 + const response = await fetch(LEADERBOARD_API_URL); 15 + if (!response.ok) return undefined; 16 + 17 + const data = await response.json(); 18 + await cache?.put('npmx', 'likes', JSON.stringify(data)); 19 + return data; 20 + });
+5 -3
src/lib/cards/social/NpmxLikesLeaderboardCard/index.ts
··· 1 1 import type { CardDefinition } from '../../types'; 2 2 import NpmxLikesLeaderboardCard from './NpmxLikesLeaderboardCard.svelte'; 3 + import { fetchNpmxLeaderboard } from './api.remote'; 3 4 4 5 export const NpmxLikesLeaderboardCardDefinition = { 5 6 type: 'npmxLikesLeaderboard', ··· 11 12 card.mobileH = 6; 12 13 }, 13 14 loadData: async () => { 14 - const res = await fetch('https://blento.app/api/npmx-leaderboard'); 15 - const data = await res.json(); 16 - return data; 15 + return await fetchNpmxLeaderboard(); 16 + }, 17 + loadDataServer: async () => { 18 + return await fetchNpmxLeaderboard(); 17 19 }, 18 20 minW: 3, 19 21 canHaveLabel: true,
+3 -4
src/lib/cards/special/UpdatedBlentos/index.ts
··· 15 15 'https://ufos-api.microcosm.blue/records?collection=app.blento.card' 16 16 ); 17 17 const recentRecords = await response.json(); 18 - const existingUsers = await cache?.get('updatedBlentos'); 18 + const existingUsers = await cache?.get('meta', 'updatedBlentos'); 19 19 const existingUsersArray: ProfileWithBlentoFlag[] = existingUsers 20 20 ? JSON.parse(existingUsers) 21 21 : []; ··· 50 50 (v) => v && v.handle !== 'handle.invalid' && !v.handle.endsWith('.pds.rip') 51 51 ); 52 52 53 - if (cache) { 54 - await cache?.put('updatedBlentos', JSON.stringify(result)); 55 - } 53 + await cache?.put('meta', 'updatedBlentos', JSON.stringify(result)); 54 + 56 55 return JSON.parse(JSON.stringify(result.slice(0, 20))); 57 56 } catch (error) { 58 57 console.error('error fetching updated blentos', error);
+20 -2
src/lib/cards/types.ts
··· 1 1 import type { Component } from 'svelte'; 2 - import type { Item, UserCache } from '$lib/types'; 2 + import type { Item } from '$lib/types'; 3 + import type { CacheService } from '$lib/cache'; 3 4 import type { Did } from '@atcute/lexicons'; 4 5 5 6 export type CreationModalComponentProps = { ··· 36 37 loadData?: ( 37 38 // all cards of that type 38 39 items: Item[], 39 - { did, handle, cache }: { did: Did; handle: string; cache?: UserCache } 40 + { did, handle, cache }: { did: Did; handle: string; cache?: CacheService } 41 + ) => Promise<unknown>; 42 + 43 + // server-side version of loadData that calls external APIs directly 44 + // instead of going through self-referential /api/ routes (avoids 522 on Cloudflare Workers) 45 + loadDataServer?: ( 46 + items: Item[], 47 + { 48 + did, 49 + handle, 50 + cache, 51 + env 52 + }: { 53 + did: Did; 54 + handle: string; 55 + cache?: CacheService; 56 + env?: Record<string, string | undefined>; 57 + } 40 58 ) => Promise<unknown>; 41 59 42 60 // show color selection popup
+2 -1
src/lib/layout/EditableGrid.svelte
··· 373 373 ondrop={handleFileDrop} 374 374 /> 375 375 376 - <!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events --> 376 + <!-- svelte-ignore a11y_no_static_element_interactions --> 377 + <!-- svelte-ignore a11y_click_events_have_key_events --> 377 378 <div 378 379 bind:this={container} 379 380 onpointerdown={handlePointerDown}
-5
src/lib/types.ts
··· 66 66 updatedAt: number; 67 67 version?: number; 68 68 }; 69 - 70 - export type UserCache = { 71 - get: (key: string) => string; 72 - put: (key: string, value: string) => void; 73 - };
+23 -24
src/lib/website/load.ts
··· 1 1 import { getDetailedProfile, listRecords, resolveHandle, parseUri, getRecord } from '$lib/atproto'; 2 2 import { CardDefinitionsByType } from '$lib/cards'; 3 - import type { Item, UserCache, WebsiteData } from '$lib/types'; 3 + import type { CacheService } from '$lib/cache'; 4 + import type { Item, WebsiteData } from '$lib/types'; 4 5 import { error } from '@sveltejs/kit'; 5 6 import type { ActorIdentifier, Did } from '@atcute/lexicons'; 6 7 ··· 9 10 10 11 const CURRENT_CACHE_VERSION = 1; 11 12 12 - export async function getCache(handle: string, page: string, cache?: UserCache) { 13 + export async function getCache(identifier: ActorIdentifier, page: string, cache?: CacheService) { 13 14 try { 14 - const cachedResult = await cache?.get?.(handle); 15 + const cachedResult = await cache?.getBlento(identifier); 15 16 16 17 if (!cachedResult) return; 17 18 const result = JSON.parse(cachedResult); 18 - const update = result.updatedAt; 19 - const timePassed = (Date.now() - update) / 1000; 20 - 21 - const ONE_DAY = 60 * 60 * 24; 22 19 23 20 if (!result.version || result.version !== CURRENT_CACHE_VERSION) { 24 21 console.log('skipping cache because of version mismatch'); 25 22 return; 26 23 } 27 24 28 - if (timePassed > ONE_DAY) { 29 - console.log('skipping cache because of age'); 30 - return; 31 - } 32 - 33 25 result.page = 'blento.' + page; 34 26 35 27 result.publication = (result.publications as Awaited<ReturnType<typeof listRecords>>).find( ··· 42 34 43 35 delete result['publications']; 44 36 45 - console.log('using cached result for handle', handle, 'last update', timePassed, 'seconds ago'); 46 37 return checkData(result); 47 38 } catch (error) { 48 39 console.log('getting cached result failed', error); ··· 51 42 52 43 export async function loadData( 53 44 handle: ActorIdentifier, 54 - cache: UserCache | undefined, 45 + cache: CacheService | undefined, 55 46 forceUpdate: boolean = false, 56 - page: string = 'self' 47 + page: string = 'self', 48 + env?: Record<string, string | undefined> 57 49 ): Promise<WebsiteData> { 58 50 if (!handle) throw error(404); 59 51 if (handle === 'favicon.ico') throw error(404); ··· 74 66 } 75 67 76 68 const [cards, mainPublication, pages, profile] = await Promise.all([ 77 - listRecords({ did, collection: 'app.blento.card' }).catch(() => { 78 - console.error('error getting records for collection app.blento.card'); 69 + listRecords({ did, collection: 'app.blento.card' }).catch((e) => { 70 + console.error('error getting records for collection app.blento.card', e); 79 71 return [] as Awaited<ReturnType<typeof listRecords>>; 80 72 }), 81 73 getRecord({ ··· 103 95 for (const cardType of cardTypesArray) { 104 96 const cardDef = CardDefinitionsByType[cardType]; 105 97 106 - if (!cardDef?.loadData) continue; 98 + const items = cards.filter((v) => cardType === v.value.cardType).map((v) => v.value) as Item[]; 107 99 108 100 try { 109 - additionDataPromises[cardType] = cardDef.loadData( 110 - cards.filter((v) => cardType === v.value.cardType).map((v) => v.value) as Item[], 111 - loadOptions 112 - ); 101 + if (cardDef?.loadDataServer) { 102 + additionDataPromises[cardType] = cardDef.loadDataServer(items, { 103 + ...loadOptions, 104 + env 105 + }); 106 + } else if (cardDef?.loadData) { 107 + additionDataPromises[cardType] = cardDef.loadData(items, loadOptions); 108 + } 113 109 } catch { 114 110 console.error('error getting additional data for', cardType); 115 111 } ··· 140 136 version: CURRENT_CACHE_VERSION 141 137 }; 142 138 143 - const stringifiedResult = JSON.stringify(result); 144 - await cache?.put?.(handle, stringifiedResult); 139 + // Only cache results that have cards to avoid caching PDS errors 140 + if (result.cards.length > 0) { 141 + const stringifiedResult = JSON.stringify(result); 142 + await cache?.putBlento(did, handle as string, stringifiedResult); 143 + } 145 144 146 145 const parsedResult = structuredClone(result) as any; 147 146
+6
src/params/actor.ts
··· 1 + import { isActorIdentifier } from '@atcute/lexicons/syntax'; 2 + import type { ParamMatcher } from '@sveltejs/kit'; 3 + 4 + export const match = ((param: string) => { 5 + return isActorIdentifier(param); 6 + }) satisfies ParamMatcher;
-6
src/params/handle.ts
··· 1 - import { isActorIdentifier } from '@atcute/lexicons/syntax'; 2 - import type { ParamMatcher } from '@sveltejs/kit'; 3 - 4 - export const match = ((param: string) => { 5 - return isActorIdentifier(param); 6 - }) satisfies ParamMatcher;
-2
src/routes/+layout.server.ts
··· 1 1 export async function load({ request }) { 2 2 const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase(); 3 3 4 - console.log('domain:', customDomain); 5 - 6 4 return { customDomain }; 7 5 }
-1
src/routes/+layout.svelte
··· 17 17 }; 18 18 19 19 onMount(() => { 20 - console.log(data.customDomain); 21 20 initClient({ customDomain: data.customDomain }); 22 21 23 22 const error = page.url.searchParams.get('error');
+5 -4
src/routes/+page.server.ts
··· 1 1 import { loadData } from '$lib/website/load'; 2 2 import { env } from '$env/dynamic/public'; 3 - import type { UserCache } from '$lib/types'; 3 + import { env as privateEnv } from '$env/dynamic/private'; 4 + import { createCache } from '$lib/cache'; 4 5 import type { ActorIdentifier } from '@atcute/lexicons'; 5 6 6 7 export async function load({ platform, request }) { ··· 8 9 9 10 const kv = platform?.env?.CUSTOM_DOMAINS; 10 11 11 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 12 + const cache = createCache(platform); 12 13 const customDomain = request.headers.get('X-Custom-Domain')?.toLocaleLowerCase(); 13 14 14 15 if (kv && customDomain) { 15 16 try { 16 17 const did = await kv.get(customDomain); 17 18 18 - if (did) return await loadData(did as ActorIdentifier, cache as UserCache); 19 + if (did) return await loadData(did as ActorIdentifier, cache, false, 'self', privateEnv); 19 20 } catch (error) { 20 21 console.error('failed to get custom domain kv', error); 21 22 } 22 23 } 23 24 24 - return await loadData(handle as ActorIdentifier, cache as UserCache); 25 + return await loadData(handle as ActorIdentifier, cache, false, 'self', privateEnv); 25 26 }
+18
src/routes/[actor=actor]/(pages)/+layout.server.ts
··· 1 + import { loadData } from '$lib/website/load'; 2 + import { env } from '$env/dynamic/private'; 3 + import { error } from '@sveltejs/kit'; 4 + import { createCache } from '$lib/cache'; 5 + 6 + export async function load({ params, platform, request }) { 7 + if (env.PUBLIC_IS_SELFHOSTED) error(404); 8 + 9 + const cache = createCache(platform); 10 + 11 + const customDomain = request.headers.get('X-Custom-Domain'); 12 + 13 + if (customDomain) { 14 + throw error(404, 'Page not found!'); 15 + } 16 + 17 + return await loadData(params.actor, cache, false, params.page, env); 18 + }
+13
src/routes/[actor=actor]/(pages)/+page.svelte
··· 1 + <script lang="ts"> 2 + import { refreshData } from '$lib/helper.js'; 3 + import Website from '$lib/website/Website.svelte'; 4 + import { onMount } from 'svelte'; 5 + 6 + let { data } = $props(); 7 + 8 + onMount(() => { 9 + refreshData(data); 10 + }); 11 + </script> 12 + 13 + <Website {data} />
+6
src/routes/[actor=actor]/(pages)/edit/+page.svelte
··· 1 + <script lang="ts"> 2 + import EditableWebsite from '$lib/website/EditableWebsite.svelte'; 3 + let { data } = $props(); 4 + </script> 5 + 6 + <EditableWebsite {data} />
+13
src/routes/[actor=actor]/(pages)/p/[[page]]/+page.svelte
··· 1 + <script lang="ts"> 2 + import { refreshData } from '$lib/helper.js'; 3 + import Website from '$lib/website/Website.svelte'; 4 + import { onMount } from 'svelte'; 5 + 6 + let { data } = $props(); 7 + 8 + onMount(() => { 9 + refreshData(data); 10 + }); 11 + </script> 12 + 13 + <Website {data} />
+252
src/routes/[actor=actor]/(pages)/p/[[page]]/copy/+page.svelte
··· 1 + <script lang="ts"> 2 + import { 3 + putRecord, 4 + deleteRecord, 5 + listRecords, 6 + uploadBlob, 7 + getCDNImageBlobUrl 8 + } from '$lib/atproto/methods'; 9 + import { user } from '$lib/atproto/auth.svelte'; 10 + import { goto } from '$app/navigation'; 11 + import * as TID from '@atcute/tid'; 12 + import { Button } from '@foxui/core'; 13 + import { loginModalState } from '$lib/atproto/UI/LoginModal.svelte'; 14 + 15 + let { data } = $props(); 16 + 17 + let destinationPage = $state(''); 18 + let copying = $state(false); 19 + let error = $state(''); 20 + let success = $state(false); 21 + 22 + const sourceHandle = $derived(data.handle); 23 + 24 + const sourcePage = $derived( 25 + data.page === 'blento.self' ? 'main' : data.page.replace('blento.', '') 26 + ); 27 + const sourceCards = $derived(data.cards); 28 + 29 + // Re-upload blobs from source repo to current user's repo 30 + async function reuploadBlobs(obj: any, sourceDid: string): Promise<void> { 31 + if (!obj || typeof obj !== 'object') return; 32 + 33 + for (const key of Object.keys(obj)) { 34 + const value = obj[key]; 35 + 36 + if (value && typeof value === 'object') { 37 + // Check if this is a blob reference 38 + if (value.$type === 'blob' && value.ref?.$link) { 39 + try { 40 + // Get the blob URL from source repo 41 + const blobUrl = getCDNImageBlobUrl({ did: sourceDid, blob: value }); 42 + if (!blobUrl) continue; 43 + 44 + // Fetch the blob via proxy to avoid CORS 45 + const response = await fetch(`/api/image-proxy?url=${encodeURIComponent(blobUrl)}`); 46 + if (!response.ok) { 47 + console.error('Failed to fetch blob:', blobUrl); 48 + continue; 49 + } 50 + 51 + // Upload to current user's repo 52 + const blob = await response.blob(); 53 + const newBlobRef = await uploadBlob({ blob }); 54 + 55 + if (newBlobRef) { 56 + // Replace with new blob reference 57 + obj[key] = newBlobRef; 58 + } 59 + } catch (err) { 60 + console.error('Failed to re-upload blob:', err); 61 + } 62 + } else { 63 + // Recursively check nested objects 64 + await reuploadBlobs(value, sourceDid); 65 + } 66 + } 67 + } 68 + } 69 + 70 + async function copyPage() { 71 + if (!user.isLoggedIn || !user.did) { 72 + error = 'You must be logged in to copy pages'; 73 + return; 74 + } 75 + 76 + copying = true; 77 + error = ''; 78 + 79 + try { 80 + const targetPage = 81 + destinationPage.trim() === '' ? 'blento.self' : `blento.${destinationPage.trim()}`; 82 + 83 + // Fetch existing cards from destination page and delete them 84 + const existingCards = await listRecords({ 85 + did: user.did, 86 + collection: 'app.blento.card' 87 + }); 88 + 89 + const cardsToDelete = existingCards.filter( 90 + (card: { value: { page?: string } }) => card.value.page === targetPage 91 + ); 92 + 93 + // Delete existing cards from destination page 94 + const deletePromises = cardsToDelete.map((card: { uri: string }) => { 95 + const rkey = card.uri.split('/').pop()!; 96 + return deleteRecord({ 97 + collection: 'app.blento.card', 98 + rkey 99 + }); 100 + }); 101 + 102 + await Promise.all(deletePromises); 103 + 104 + // Copy each card with a new ID to the destination page 105 + // Re-upload blobs from source repo to current user's repo 106 + for (const card of sourceCards) { 107 + const newCard = { 108 + ...structuredClone(card), 109 + id: TID.now(), 110 + page: targetPage, 111 + updatedAt: new Date().toISOString(), 112 + version: 2 113 + }; 114 + 115 + // Re-upload any blobs in cardData 116 + await reuploadBlobs(newCard.cardData, data.did); 117 + 118 + await putRecord({ 119 + collection: 'app.blento.card', 120 + rkey: newCard.id, 121 + record: newCard 122 + }); 123 + } 124 + 125 + const userHandle = user.profile?.handle ?? data.handle; 126 + 127 + // Copy publication data if it exists 128 + if (data.publication) { 129 + const publicationCopy = structuredClone(data.publication) as Record<string, unknown>; 130 + 131 + // Re-upload any blobs in publication (e.g., icon) 132 + await reuploadBlobs(publicationCopy, data.did); 133 + 134 + // Update the URL to point to the user's page 135 + publicationCopy.url = `https://blento.app/${userHandle}`; 136 + if (targetPage !== 'blento.self') { 137 + publicationCopy.url += '/' + targetPage.replace('blento.', ''); 138 + } 139 + 140 + // Save to appropriate collection based on destination page type 141 + if (targetPage === 'blento.self') { 142 + await putRecord({ 143 + collection: 'site.standard.publication', 144 + rkey: targetPage, 145 + record: publicationCopy 146 + }); 147 + } else { 148 + await putRecord({ 149 + collection: 'app.blento.page', 150 + rkey: targetPage, 151 + record: publicationCopy 152 + }); 153 + } 154 + } 155 + 156 + // Refresh the logged-in user's cache 157 + await fetch(`/${userHandle}/api/refresh`); 158 + 159 + success = true; 160 + 161 + // Redirect to the logged-in user's destination page edit 162 + const destPath = destinationPage.trim() === '' ? '' : `/${destinationPage.trim()}`; 163 + setTimeout(() => { 164 + goto(`/${userHandle}${destPath}/edit`); 165 + }, 1000); 166 + } catch (e) { 167 + error = e instanceof Error ? e.message : 'Failed to copy page'; 168 + } finally { 169 + copying = false; 170 + } 171 + } 172 + </script> 173 + 174 + <div class="bg-base-50 dark:bg-base-900 flex min-h-screen items-center justify-center p-4"> 175 + <div class="bg-base-100 dark:bg-base-800 w-full max-w-md rounded-2xl p-6 shadow-lg"> 176 + {#if user.isLoggedIn} 177 + <h1 class="text-base-900 dark:text-base-50 mb-6 text-2xl font-bold">Copy Page</h1> 178 + 179 + <div class="mb-4"> 180 + <div class="text-base-700 dark:text-base-300 mb-1 block text-sm font-medium"> 181 + Source Page 182 + </div> 183 + <div 184 + class="bg-base-200 dark:bg-base-700 text-base-900 dark:text-base-100 rounded-lg px-3 py-2" 185 + > 186 + {sourceHandle}/{sourcePage || 'main'} 187 + </div> 188 + <p class="text-base-500 mt-1 text-sm">{sourceCards.length} cards</p> 189 + </div> 190 + 191 + <div class="mb-6"> 192 + <label 193 + for="destination" 194 + class="text-base-700 dark:text-base-300 mb-1 block text-sm font-medium" 195 + > 196 + Destination Page (on your profile: {user.profile?.handle}) 197 + </label> 198 + <input 199 + id="destination" 200 + type="text" 201 + bind:value={destinationPage} 202 + placeholder="Leave empty for main page" 203 + class="bg-base-50 dark:bg-base-700 border-base-300 dark:border-base-600 text-base-900 dark:text-base-100 focus:ring-accent-500 w-full rounded-lg border px-3 py-2 focus:ring-2 focus:outline-none" 204 + /> 205 + </div> 206 + 207 + {#if error} 208 + <div 209 + class="mb-4 rounded-lg bg-red-100 p-3 text-red-700 dark:bg-red-900/30 dark:text-red-400" 210 + > 211 + {error} 212 + </div> 213 + {/if} 214 + 215 + {#if success} 216 + <div 217 + class="mb-4 rounded-lg bg-green-100 p-3 text-green-700 dark:bg-green-900/30 dark:text-green-400" 218 + > 219 + Page copied successfully! Redirecting... 220 + </div> 221 + {/if} 222 + 223 + <div class="flex gap-3"> 224 + <a 225 + href="/{data.handle}/{sourcePage === 'main' ? '' : sourcePage}" 226 + class="bg-base-200 hover:bg-base-300 dark:bg-base-700 dark:hover:bg-base-600 text-base-700 dark:text-base-300 flex-1 rounded-lg px-4 py-2 text-center font-medium transition-colors" 227 + > 228 + Cancel 229 + </a> 230 + <button 231 + onclick={copyPage} 232 + disabled={copying || success} 233 + class="bg-accent-500 hover:bg-accent-600 flex-1 rounded-lg px-4 py-2 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50" 234 + > 235 + {#if copying} 236 + Copying... 237 + {:else} 238 + Copy {sourceCards.length} cards 239 + {/if} 240 + </button> 241 + </div> 242 + {:else} 243 + <h1 class="text-base-900 dark:text-base-50 mb-6 text-2xl font-bold"> 244 + You must be signed in to copy a page! 245 + </h1> 246 + 247 + <div class="flex w-full justify-center"> 248 + <Button size="lg" onclick={() => loginModalState.show()}>Login</Button> 249 + </div> 250 + {/if} 251 + </div> 252 + </div>
+6
src/routes/[actor=actor]/(pages)/p/[[page]]/edit/+page.svelte
··· 1 + <script lang="ts"> 2 + import EditableWebsite from '$lib/website/EditableWebsite.svelte'; 3 + let { data } = $props(); 4 + </script> 5 + 6 + <EditableWebsite {data} />
+16
src/routes/[actor=actor]/.well-known/site.standard.publication/+server.ts
··· 1 + import { loadData } from '$lib/website/load'; 2 + import { createCache } from '$lib/cache'; 3 + import { env } from '$env/dynamic/private'; 4 + 5 + import { error } from '@sveltejs/kit'; 6 + import { text } from '@sveltejs/kit'; 7 + 8 + export async function GET({ params, platform }) { 9 + const cache = createCache(platform); 10 + 11 + const data = await loadData(params.actor, cache, false, params.page, env); 12 + 13 + if (!data.publication) throw error(300); 14 + 15 + return text(data.did + '/site.standard.publication/blento.self'); 16 + }
+14
src/routes/[actor=actor]/api/refresh/+server.ts
··· 1 + import { createCache } from '$lib/cache'; 2 + import { loadData } from '$lib/website/load.js'; 3 + import { env } from '$env/dynamic/private'; 4 + import type { Handle } from '@atcute/lexicons'; 5 + import { json } from '@sveltejs/kit'; 6 + 7 + export async function GET({ params, platform }) { 8 + const cache = createCache(platform); 9 + if (!cache) return json('no cache'); 10 + 11 + await loadData(params.actor, cache, true, 'self', env); 12 + 13 + return json('ok'); 14 + }
+58
src/routes/[actor=actor]/og.png/+server.ts
··· 1 + import { getCDNImageBlobUrl } from '$lib/atproto/methods.js'; 2 + import { createCache } from '$lib/cache'; 3 + import { loadData } from '$lib/website/load'; 4 + import { env } from '$env/dynamic/private'; 5 + import type { Handle } from '@atcute/lexicons'; 6 + import { ImageResponse } from '@ethercorps/sveltekit-og'; 7 + 8 + function escapeHtml(str: string): string { 9 + return str 10 + .replace(/&/g, '&amp;') 11 + .replace(/</g, '&lt;') 12 + .replace(/>/g, '&gt;') 13 + .replace(/"/g, '&quot;') 14 + .replace(/'/g, '&#39;'); 15 + } 16 + 17 + export async function GET({ params, platform }) { 18 + const cache = createCache(platform); 19 + 20 + const data = await loadData(params.actor, cache, false, 'self', env); 21 + 22 + let image: string | undefined = data.profile.avatar; 23 + 24 + if (data.publication.icon) { 25 + image = 26 + getCDNImageBlobUrl({ did: data.did, blob: data.publication.icon }) ?? data.profile.avatar; 27 + } 28 + 29 + const name = data.publication?.name ?? data.profile.displayName ?? data.profile.handle; 30 + 31 + const htmlString = ` 32 + <div class="flex flex-col p-8 w-full h-full bg-neutral-900"> 33 + <div class="flex items-center mb-8 mt-16"> 34 + <img src="${escapeHtml(image ?? '')}" width="128" height="128" class="rounded-full" /> 35 + 36 + <h1 class="text-neutral-50 text-7xl ml-4">${escapeHtml(name)}</h1> 37 + </div> 38 + 39 + <p class="mt-8 text-4xl text-neutral-300">Check out my blento</p> 40 + 41 + <svg class="absolute w-130 h-130 top-50 right-0" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg"> 42 + <rect x="100" y="100" width="160" height="340" rx="23" fill="#EF4444"/> 43 + <rect x="640" y="280" width="160" height="340" rx="23" fill="#22C55E"/> 44 + <rect x="280" y="100" width="340" height="340" rx="23" fill="#F59E0B"/> 45 + <rect x="100" y="460" width="340" height="160" rx="23" fill="#0EA5E9"/> 46 + <rect x="640" y="100" width="160" height="160" rx="23" fill="#EAB308"/> 47 + <rect x="100" y="640" width="160" height="160" rx="23" fill="#6366F1"/> 48 + <rect x="460" y="460" width="160" height="160" rx="23" fill="#14B8A6"/> 49 + <rect x="280" y="640" width="520" height="160" rx="23" fill="#A855F7"/> 50 + </svg> 51 + </div> 52 + `; 53 + 54 + return new ImageResponse(htmlString, { 55 + width: 1200, 56 + height: 630 57 + }); 58 + }
-13
src/routes/[handle=handle]/(pages)/+layout.server.ts
··· 1 - import { loadData } from '$lib/website/load'; 2 - import { env } from '$env/dynamic/private'; 3 - import { error } from '@sveltejs/kit'; 4 - import type { UserCache } from '$lib/types'; 5 - import type { Handle } from '@atcute/lexicons'; 6 - 7 - export async function load({ params, platform }) { 8 - if (env.PUBLIC_IS_SELFHOSTED) error(404); 9 - 10 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 11 - 12 - return await loadData(params.handle as Handle, cache as UserCache, false, params.page); 13 - }
-13
src/routes/[handle=handle]/(pages)/+page.svelte
··· 1 - <script lang="ts"> 2 - import { refreshData } from '$lib/helper.js'; 3 - import Website from '$lib/website/Website.svelte'; 4 - import { onMount } from 'svelte'; 5 - 6 - let { data } = $props(); 7 - 8 - onMount(() => { 9 - refreshData(data); 10 - }); 11 - </script> 12 - 13 - <Website {data} />
-6
src/routes/[handle=handle]/(pages)/edit/+page.svelte
··· 1 - <script lang="ts"> 2 - import EditableWebsite from '$lib/website/EditableWebsite.svelte'; 3 - let { data } = $props(); 4 - </script> 5 - 6 - <EditableWebsite {data} />
-13
src/routes/[handle=handle]/(pages)/p/[[page]]/+page.svelte
··· 1 - <script lang="ts"> 2 - import { refreshData } from '$lib/helper.js'; 3 - import Website from '$lib/website/Website.svelte'; 4 - import { onMount } from 'svelte'; 5 - 6 - let { data } = $props(); 7 - 8 - onMount(() => { 9 - refreshData(data); 10 - }); 11 - </script> 12 - 13 - <Website {data} />
-252
src/routes/[handle=handle]/(pages)/p/[[page]]/copy/+page.svelte
··· 1 - <script lang="ts"> 2 - import { 3 - putRecord, 4 - deleteRecord, 5 - listRecords, 6 - uploadBlob, 7 - getCDNImageBlobUrl 8 - } from '$lib/atproto/methods'; 9 - import { user } from '$lib/atproto/auth.svelte'; 10 - import { goto } from '$app/navigation'; 11 - import * as TID from '@atcute/tid'; 12 - import { Button } from '@foxui/core'; 13 - import { loginModalState } from '$lib/atproto/UI/LoginModal.svelte'; 14 - 15 - let { data } = $props(); 16 - 17 - let destinationPage = $state(''); 18 - let copying = $state(false); 19 - let error = $state(''); 20 - let success = $state(false); 21 - 22 - const sourceHandle = $derived(data.handle); 23 - 24 - const sourcePage = $derived( 25 - data.page === 'blento.self' ? 'main' : data.page.replace('blento.', '') 26 - ); 27 - const sourceCards = $derived(data.cards); 28 - 29 - // Re-upload blobs from source repo to current user's repo 30 - async function reuploadBlobs(obj: any, sourceDid: string): Promise<void> { 31 - if (!obj || typeof obj !== 'object') return; 32 - 33 - for (const key of Object.keys(obj)) { 34 - const value = obj[key]; 35 - 36 - if (value && typeof value === 'object') { 37 - // Check if this is a blob reference 38 - if (value.$type === 'blob' && value.ref?.$link) { 39 - try { 40 - // Get the blob URL from source repo 41 - const blobUrl = getCDNImageBlobUrl({ did: sourceDid, blob: value }); 42 - if (!blobUrl) continue; 43 - 44 - // Fetch the blob via proxy to avoid CORS 45 - const response = await fetch(`/api/image-proxy?url=${encodeURIComponent(blobUrl)}`); 46 - if (!response.ok) { 47 - console.error('Failed to fetch blob:', blobUrl); 48 - continue; 49 - } 50 - 51 - // Upload to current user's repo 52 - const blob = await response.blob(); 53 - const newBlobRef = await uploadBlob({ blob }); 54 - 55 - if (newBlobRef) { 56 - // Replace with new blob reference 57 - obj[key] = newBlobRef; 58 - } 59 - } catch (err) { 60 - console.error('Failed to re-upload blob:', err); 61 - } 62 - } else { 63 - // Recursively check nested objects 64 - await reuploadBlobs(value, sourceDid); 65 - } 66 - } 67 - } 68 - } 69 - 70 - async function copyPage() { 71 - if (!user.isLoggedIn || !user.did) { 72 - error = 'You must be logged in to copy pages'; 73 - return; 74 - } 75 - 76 - copying = true; 77 - error = ''; 78 - 79 - try { 80 - const targetPage = 81 - destinationPage.trim() === '' ? 'blento.self' : `blento.${destinationPage.trim()}`; 82 - 83 - // Fetch existing cards from destination page and delete them 84 - const existingCards = await listRecords({ 85 - did: user.did, 86 - collection: 'app.blento.card' 87 - }); 88 - 89 - const cardsToDelete = existingCards.filter( 90 - (card: { value: { page?: string } }) => card.value.page === targetPage 91 - ); 92 - 93 - // Delete existing cards from destination page 94 - const deletePromises = cardsToDelete.map((card: { uri: string }) => { 95 - const rkey = card.uri.split('/').pop()!; 96 - return deleteRecord({ 97 - collection: 'app.blento.card', 98 - rkey 99 - }); 100 - }); 101 - 102 - await Promise.all(deletePromises); 103 - 104 - // Copy each card with a new ID to the destination page 105 - // Re-upload blobs from source repo to current user's repo 106 - for (const card of sourceCards) { 107 - const newCard = { 108 - ...structuredClone(card), 109 - id: TID.now(), 110 - page: targetPage, 111 - updatedAt: new Date().toISOString(), 112 - version: 2 113 - }; 114 - 115 - // Re-upload any blobs in cardData 116 - await reuploadBlobs(newCard.cardData, data.did); 117 - 118 - await putRecord({ 119 - collection: 'app.blento.card', 120 - rkey: newCard.id, 121 - record: newCard 122 - }); 123 - } 124 - 125 - const userHandle = user.profile?.handle ?? data.handle; 126 - 127 - // Copy publication data if it exists 128 - if (data.publication) { 129 - const publicationCopy = structuredClone(data.publication) as Record<string, unknown>; 130 - 131 - // Re-upload any blobs in publication (e.g., icon) 132 - await reuploadBlobs(publicationCopy, data.did); 133 - 134 - // Update the URL to point to the user's page 135 - publicationCopy.url = `https://blento.app/${userHandle}`; 136 - if (targetPage !== 'blento.self') { 137 - publicationCopy.url += '/' + targetPage.replace('blento.', ''); 138 - } 139 - 140 - // Save to appropriate collection based on destination page type 141 - if (targetPage === 'blento.self') { 142 - await putRecord({ 143 - collection: 'site.standard.publication', 144 - rkey: targetPage, 145 - record: publicationCopy 146 - }); 147 - } else { 148 - await putRecord({ 149 - collection: 'app.blento.page', 150 - rkey: targetPage, 151 - record: publicationCopy 152 - }); 153 - } 154 - } 155 - 156 - // Refresh the logged-in user's cache 157 - await fetch(`/${userHandle}/api/refresh`); 158 - 159 - success = true; 160 - 161 - // Redirect to the logged-in user's destination page edit 162 - const destPath = destinationPage.trim() === '' ? '' : `/${destinationPage.trim()}`; 163 - setTimeout(() => { 164 - goto(`/${userHandle}${destPath}/edit`); 165 - }, 1000); 166 - } catch (e) { 167 - error = e instanceof Error ? e.message : 'Failed to copy page'; 168 - } finally { 169 - copying = false; 170 - } 171 - } 172 - </script> 173 - 174 - <div class="bg-base-50 dark:bg-base-900 flex min-h-screen items-center justify-center p-4"> 175 - <div class="bg-base-100 dark:bg-base-800 w-full max-w-md rounded-2xl p-6 shadow-lg"> 176 - {#if user.isLoggedIn} 177 - <h1 class="text-base-900 dark:text-base-50 mb-6 text-2xl font-bold">Copy Page</h1> 178 - 179 - <div class="mb-4"> 180 - <div class="text-base-700 dark:text-base-300 mb-1 block text-sm font-medium"> 181 - Source Page 182 - </div> 183 - <div 184 - class="bg-base-200 dark:bg-base-700 text-base-900 dark:text-base-100 rounded-lg px-3 py-2" 185 - > 186 - {sourceHandle}/{sourcePage || 'main'} 187 - </div> 188 - <p class="text-base-500 mt-1 text-sm">{sourceCards.length} cards</p> 189 - </div> 190 - 191 - <div class="mb-6"> 192 - <label 193 - for="destination" 194 - class="text-base-700 dark:text-base-300 mb-1 block text-sm font-medium" 195 - > 196 - Destination Page (on your profile: {user.profile?.handle}) 197 - </label> 198 - <input 199 - id="destination" 200 - type="text" 201 - bind:value={destinationPage} 202 - placeholder="Leave empty for main page" 203 - class="bg-base-50 dark:bg-base-700 border-base-300 dark:border-base-600 text-base-900 dark:text-base-100 focus:ring-accent-500 w-full rounded-lg border px-3 py-2 focus:ring-2 focus:outline-none" 204 - /> 205 - </div> 206 - 207 - {#if error} 208 - <div 209 - class="mb-4 rounded-lg bg-red-100 p-3 text-red-700 dark:bg-red-900/30 dark:text-red-400" 210 - > 211 - {error} 212 - </div> 213 - {/if} 214 - 215 - {#if success} 216 - <div 217 - class="mb-4 rounded-lg bg-green-100 p-3 text-green-700 dark:bg-green-900/30 dark:text-green-400" 218 - > 219 - Page copied successfully! Redirecting... 220 - </div> 221 - {/if} 222 - 223 - <div class="flex gap-3"> 224 - <a 225 - href="/{data.handle}/{sourcePage === 'main' ? '' : sourcePage}" 226 - class="bg-base-200 hover:bg-base-300 dark:bg-base-700 dark:hover:bg-base-600 text-base-700 dark:text-base-300 flex-1 rounded-lg px-4 py-2 text-center font-medium transition-colors" 227 - > 228 - Cancel 229 - </a> 230 - <button 231 - onclick={copyPage} 232 - disabled={copying || success} 233 - class="bg-accent-500 hover:bg-accent-600 flex-1 rounded-lg px-4 py-2 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50" 234 - > 235 - {#if copying} 236 - Copying... 237 - {:else} 238 - Copy {sourceCards.length} cards 239 - {/if} 240 - </button> 241 - </div> 242 - {:else} 243 - <h1 class="text-base-900 dark:text-base-50 mb-6 text-2xl font-bold"> 244 - You must be signed in to copy a page! 245 - </h1> 246 - 247 - <div class="flex w-full justify-center"> 248 - <Button size="lg" onclick={() => loginModalState.show()}>Login</Button> 249 - </div> 250 - {/if} 251 - </div> 252 - </div>
-6
src/routes/[handle=handle]/(pages)/p/[[page]]/edit/+page.svelte
··· 1 - <script lang="ts"> 2 - import EditableWebsite from '$lib/website/EditableWebsite.svelte'; 3 - let { data } = $props(); 4 - </script> 5 - 6 - <EditableWebsite {data} />
-14
src/routes/[handle=handle]/.well-known/site.standard.publication/+server.ts
··· 1 - import { loadData } from '$lib/website/load'; 2 - import { error } from '@sveltejs/kit'; 3 - import type { UserCache } from '$lib/types'; 4 - import { text } from '@sveltejs/kit'; 5 - 6 - export async function GET({ params, platform }) { 7 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 8 - 9 - const data = await loadData(params.handle, cache as UserCache, false, params.page); 10 - 11 - if (!data.publication) throw error(300); 12 - 13 - return text(data.did + '/site.standard.publication/blento.self'); 14 - }
-14
src/routes/[handle=handle]/api/refresh/+server.ts
··· 1 - import type { UserCache } from '$lib/types'; 2 - import { loadData } from '$lib/website/load.js'; 3 - import type { Handle } from '@atcute/lexicons'; 4 - import { json } from '@sveltejs/kit'; 5 - 6 - export async function GET({ params, platform }) { 7 - if (!platform?.env?.USER_DATA_CACHE) return json('no cache'); 8 - const handle = params.handle; 9 - 10 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 11 - await loadData(handle as Handle, cache as UserCache, true); 12 - 13 - return json('ok'); 14 - }
-58
src/routes/[handle=handle]/og.png/+server.ts
··· 1 - import { getCDNImageBlobUrl } from '$lib/atproto/methods.js'; 2 - import type { UserCache } from '$lib/types'; 3 - import { loadData } from '$lib/website/load'; 4 - import type { Handle } from '@atcute/lexicons'; 5 - import { isDid } from '@atcute/lexicons/syntax'; 6 - import { ImageResponse } from '@ethercorps/sveltekit-og'; 7 - 8 - function escapeHtml(str: string): string { 9 - return str 10 - .replace(/&/g, '&amp;') 11 - .replace(/</g, '&lt;') 12 - .replace(/>/g, '&gt;') 13 - .replace(/"/g, '&quot;') 14 - .replace(/'/g, '&#39;'); 15 - } 16 - 17 - export async function GET({ params, platform }) { 18 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 19 - 20 - const data = await loadData(params.handle as Handle, cache as UserCache); 21 - 22 - let image: string | undefined = data.profile.avatar; 23 - 24 - if (data.publication.icon) { 25 - image = 26 - getCDNImageBlobUrl({ did: data.did, blob: data.publication.icon }) ?? data.profile.avatar; 27 - } 28 - 29 - const name = data.publication?.name ?? data.profile.displayName ?? data.profile.handle; 30 - 31 - const htmlString = ` 32 - <div class="flex flex-col p-8 w-full h-full bg-neutral-900"> 33 - <div class="flex items-center mb-8 mt-16"> 34 - <img src="${escapeHtml(image ?? '')}" width="128" height="128" class="rounded-full" /> 35 - 36 - <h1 class="text-neutral-50 text-7xl ml-4">${escapeHtml(name)}</h1> 37 - </div> 38 - 39 - <p class="mt-8 text-4xl text-neutral-300">Check out my blento</p> 40 - 41 - <svg class="absolute w-130 h-130 top-50 right-0" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg"> 42 - <rect x="100" y="100" width="160" height="340" rx="23" fill="#EF4444"/> 43 - <rect x="640" y="280" width="160" height="340" rx="23" fill="#22C55E"/> 44 - <rect x="280" y="100" width="340" height="340" rx="23" fill="#F59E0B"/> 45 - <rect x="100" y="460" width="340" height="160" rx="23" fill="#0EA5E9"/> 46 - <rect x="640" y="100" width="160" height="160" rx="23" fill="#EAB308"/> 47 - <rect x="100" y="640" width="160" height="160" rx="23" fill="#6366F1"/> 48 - <rect x="460" y="460" width="160" height="160" rx="23" fill="#14B8A6"/> 49 - <rect x="280" y="640" width="520" height="160" rx="23" fill="#A855F7"/> 50 - </svg> 51 - </div> 52 - `; 53 - 54 - return new ImageResponse(htmlString, { 55 - width: 1200, 56 - height: 630 57 - }); 58 - }
-35
src/routes/all/+page.server.ts
··· 1 - import { env } from '$env/dynamic/public'; 2 - import type { UserCache, WebsiteData } from '$lib/types.js'; 3 - import { loadData } from '$lib/website/load'; 4 - import type { Handle } from '@atcute/lexicons'; 5 - import type { AppBskyActorDefs } from '@atcute/bluesky'; 6 - 7 - export async function load({ platform }) { 8 - const cache = platform?.env?.USER_DATA_CACHE; 9 - 10 - const list = await cache?.list(); 11 - 12 - const profiles: AppBskyActorDefs.ProfileViewDetailed[] = []; 13 - for (const value of list?.keys ?? []) { 14 - // check if at least one card 15 - const result = await cache?.get(value.name); 16 - if (!result) continue; 17 - const parsed = JSON.parse(result) as WebsiteData; 18 - 19 - if (parsed.version !== 1 || !parsed.cards?.length) continue; 20 - 21 - profiles.push(parsed.profile); 22 - } 23 - 24 - profiles.sort((a, b) => a.handle.localeCompare(b.handle)); 25 - 26 - const handle = env.PUBLIC_HANDLE; 27 - 28 - const data = await loadData(handle as Handle, cache as unknown as UserCache); 29 - 30 - data.publication ??= {}; 31 - data.publication.preferences ??= {}; 32 - data.publication.preferences.hideProfileSection = true; 33 - 34 - return { ...data, profiles }; 35 - }
-29
src/routes/all/+page.svelte
··· 1 - <script lang="ts"> 2 - import { createEmptyCard } from '$lib/helper.js'; 3 - import Website from '$lib/website/Website.svelte'; 4 - 5 - let { data } = $props(); 6 - </script> 7 - 8 - <Website 9 - data={{ 10 - ...data, 11 - cards: data.profiles.map((v, i) => { 12 - const card = createEmptyCard(''); 13 - card.cardType = 'blueskyProfile'; 14 - card.cardData = { 15 - avatar: v.avatar, 16 - handle: v.handle, 17 - displayName: v.displayName 18 - }; 19 - 20 - card.x = (i % 4) * 2; 21 - card.y = Math.floor(i / 4) * 2; 22 - 23 - card.mobileX = (i % 2) * 4; 24 - card.mobileY = Math.floor(i / 2) * 4; 25 - 26 - return card; 27 - }) 28 - }} 29 - />
-54
src/routes/api/github/+server.ts
··· 1 - import { json } from '@sveltejs/kit'; 2 - import type { RequestHandler } from './$types'; 3 - import type { GitHubContributionsData } from '$lib/cards/social/GitHubProfileCard/types'; 4 - 5 - const GithubAPIURL = 'https://edge-function-github-contribution.vercel.app/api/github-data?user='; 6 - 7 - export const GET: RequestHandler = async ({ url, platform }) => { 8 - const user = url.searchParams.get('user'); 9 - 10 - if (!user) { 11 - return json({ error: 'No user provided' }, { status: 400 }); 12 - } 13 - 14 - const cachedData = await platform?.env?.USER_DATA_CACHE?.get('#github:' + user); 15 - 16 - if (cachedData) { 17 - const parsedCache = JSON.parse(cachedData); 18 - 19 - const TWELVE_HOURS = 12 * 60 * 60 * 1000; 20 - const now = Date.now(); 21 - 22 - if (now - (parsedCache.updatedAt || 0) < TWELVE_HOURS) { 23 - return json(parsedCache); 24 - } 25 - } 26 - 27 - try { 28 - const response = await fetch(GithubAPIURL + user); 29 - 30 - if (!response.ok) { 31 - console.error('error', response.statusText); 32 - return json( 33 - { error: 'Failed to fetch GitHub data ' + response.statusText }, 34 - { status: response.status } 35 - ); 36 - } 37 - 38 - const data = await response.json(); 39 - 40 - if (!data?.user) { 41 - return json({ error: 'User not found' }, { status: 404 }); 42 - } 43 - 44 - const result = data.user as GitHubContributionsData; 45 - result.updatedAt = Date.now(); 46 - 47 - await platform?.env?.USER_DATA_CACHE?.put('#github:' + user, JSON.stringify(result)); 48 - 49 - return json(result); 50 - } catch (error) { 51 - console.error('Error fetching GitHub contributions:', error); 52 - return json({ error: 'Failed to fetch GitHub data' }, { status: 500 }); 53 - } 54 - };
-53
src/routes/api/github/contributors/+server.ts
··· 1 - import { json } from '@sveltejs/kit'; 2 - import type { RequestHandler } from './$types'; 3 - 4 - const GithubContributorsAPIURL = 5 - 'https://edge-function-github-contribution.vercel.app/api/github-contributors'; 6 - 7 - export const GET: RequestHandler = async ({ url, platform }) => { 8 - const owner = url.searchParams.get('owner'); 9 - const repo = url.searchParams.get('repo'); 10 - 11 - if (!owner || !repo) { 12 - return json({ error: 'Missing owner or repo parameter' }, { status: 400 }); 13 - } 14 - 15 - const cacheKey = `#github-contributors:${owner}/${repo}`; 16 - const cachedData = await platform?.env?.USER_DATA_CACHE?.get(cacheKey); 17 - 18 - if (cachedData) { 19 - const parsedCache = JSON.parse(cachedData); 20 - 21 - const TWELVE_HOURS = 12 * 60 * 60 * 1000; 22 - const now = Date.now(); 23 - 24 - if (now - (parsedCache.updatedAt || 0) < TWELVE_HOURS) { 25 - return json(parsedCache.data); 26 - } 27 - } 28 - 29 - try { 30 - const response = await fetch( 31 - `${GithubContributorsAPIURL}?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}` 32 - ); 33 - 34 - if (!response.ok) { 35 - return json( 36 - { error: 'Failed to fetch GitHub contributors ' + response.statusText }, 37 - { status: response.status } 38 - ); 39 - } 40 - 41 - const data = await response.json(); 42 - 43 - await platform?.env?.USER_DATA_CACHE?.put( 44 - cacheKey, 45 - JSON.stringify({ data, updatedAt: Date.now() }) 46 - ); 47 - 48 - return json(data); 49 - } catch (error) { 50 - console.error('Error fetching GitHub contributors:', error); 51 - return json({ error: 'Failed to fetch GitHub contributors' }, { status: 500 }); 52 - } 53 - };
-89
src/routes/api/lastfm/+server.ts
··· 1 - import { json } from '@sveltejs/kit'; 2 - import type { RequestHandler } from './$types'; 3 - import { env } from '$env/dynamic/private'; 4 - 5 - const LASTFM_API_URL = 'https://ws.audioscrobbler.com/2.0/'; 6 - 7 - const ALLOWED_METHODS = [ 8 - 'user.getRecentTracks', 9 - 'user.getTopTracks', 10 - 'user.getTopAlbums', 11 - 'user.getInfo' 12 - ]; 13 - 14 - const CACHE_TTL: Record<string, number> = { 15 - 'user.getRecentTracks': 15 * 60 * 1000, 16 - 'user.getTopTracks': 60 * 60 * 1000, 17 - 'user.getTopAlbums': 60 * 60 * 1000, 18 - 'user.getInfo': 12 * 60 * 60 * 1000 19 - }; 20 - 21 - export const GET: RequestHandler = async ({ url, platform }) => { 22 - const method = url.searchParams.get('method'); 23 - const user = url.searchParams.get('user'); 24 - const period = url.searchParams.get('period') || '7day'; 25 - const limit = url.searchParams.get('limit') || '50'; 26 - 27 - if (!method || !user) { 28 - return json({ error: 'Missing method or user parameter' }, { status: 400 }); 29 - } 30 - 31 - if (!ALLOWED_METHODS.includes(method)) { 32 - return json({ error: 'Method not allowed' }, { status: 400 }); 33 - } 34 - 35 - const cacheKey = `#lastfm:${method}:${user}:${period}:${limit}`; 36 - const cachedData = await platform?.env?.USER_DATA_CACHE?.get(cacheKey); 37 - 38 - if (cachedData) { 39 - const parsed = JSON.parse(cachedData); 40 - const ttl = CACHE_TTL[method] || 60 * 60 * 1000; 41 - 42 - if (Date.now() - (parsed._cachedAt || 0) < ttl) { 43 - return json(parsed); 44 - } 45 - } 46 - 47 - const apiKey = env?.LASTFM_API_KEY; 48 - if (!apiKey) { 49 - return json({ error: 'Last.fm API key not configured' }, { status: 500 }); 50 - } 51 - 52 - try { 53 - const params = new URLSearchParams({ 54 - method, 55 - user, 56 - api_key: apiKey, 57 - format: 'json', 58 - limit 59 - }); 60 - 61 - if (method === 'user.getTopTracks' || method === 'user.getTopAlbums') { 62 - params.set('period', period); 63 - } 64 - 65 - const response = await fetch(`${LASTFM_API_URL}?${params}`); 66 - 67 - if (!response.ok) { 68 - return json( 69 - { error: 'Failed to fetch Last.fm data: ' + response.statusText }, 70 - { status: response.status } 71 - ); 72 - } 73 - 74 - const data = await response.json(); 75 - 76 - if (data.error) { 77 - return json({ error: data.message || 'Last.fm API error' }, { status: 400 }); 78 - } 79 - 80 - data._cachedAt = Date.now(); 81 - 82 - await platform?.env?.USER_DATA_CACHE?.put(cacheKey, JSON.stringify(data)); 83 - 84 - return json(data); 85 - } catch (error) { 86 - console.error('Error fetching Last.fm data:', error); 87 - return json({ error: 'Failed to fetch Last.fm data' }, { status: 500 }); 88 - } 89 - };
-44
src/routes/api/npmx-leaderboard/+server.ts
··· 1 - import { json } from '@sveltejs/kit'; 2 - import type { RequestHandler } from './$types'; 3 - 4 - const LEADERBOARD_API_URL = 5 - 'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes?limit=20'; 6 - 7 - export const GET: RequestHandler = async ({ platform }) => { 8 - const cacheKey = '#npmx-leaderboard:likes'; 9 - const cachedData = await platform?.env?.USER_DATA_CACHE?.get(cacheKey); 10 - 11 - if (cachedData) { 12 - const parsedCache = JSON.parse(cachedData); 13 - 14 - const TWELVE_HOURS = 12 * 60 * 60 * 1000; 15 - const now = Date.now(); 16 - 17 - if (now - (parsedCache.updatedAt || 0) < TWELVE_HOURS) { 18 - return json(parsedCache.data); 19 - } 20 - } 21 - 22 - try { 23 - const response = await fetch(LEADERBOARD_API_URL); 24 - 25 - if (!response.ok) { 26 - return json( 27 - { error: 'Failed to fetch npmx leaderboard ' + response.statusText }, 28 - { status: response.status } 29 - ); 30 - } 31 - 32 - const data = await response.json(); 33 - 34 - await platform?.env?.USER_DATA_CACHE?.put( 35 - cacheKey, 36 - JSON.stringify({ data, updatedAt: Date.now() }) 37 - ); 38 - 39 - return json(data); 40 - } catch (error) { 41 - console.error('Error fetching npmx leaderboard:', error); 42 - return json({ error: 'Failed to fetch npmx leaderboard' }, { status: 500 }); 43 - } 44 - };
+6 -3
src/routes/api/reloadRecent/+server.ts
··· 1 1 import { getDetailedProfile } from '$lib/atproto'; 2 + import { createCache } from '$lib/cache'; 2 3 import { json } from '@sveltejs/kit'; 3 4 import type { AppBskyActorDefs } from '@atcute/bluesky'; 4 5 5 6 export async function GET({ platform }) { 6 - if (!platform?.env?.USER_DATA_CACHE) return json('no cache'); 7 - const existingUsers = await platform?.env?.USER_DATA_CACHE?.get('updatedBlentos'); 7 + const cache = createCache(platform); 8 + if (!cache) return json('no cache'); 9 + 10 + const existingUsers = await cache.get('meta', 'updatedBlentos'); 8 11 9 12 const existingUsersArray: AppBskyActorDefs.ProfileViewDetailed[] = existingUsers 10 13 ? JSON.parse(existingUsers) ··· 21 24 22 25 const newProfiles = await Promise.all(newProfilesPromises); 23 26 24 - await platform?.env?.USER_DATA_CACHE.put('updatedBlentos', JSON.stringify(newProfiles)); 27 + await cache.put('meta', 'updatedBlentos', JSON.stringify(newProfiles)); 25 28 26 29 return json('ok'); 27 30 }
+9 -8
src/routes/api/update/+server.ts
··· 1 - import type { UserCache } from '$lib/types'; 1 + import { createCache } from '$lib/cache'; 2 2 import { getCache, loadData } from '$lib/website/load'; 3 - import type { AppBskyActorDefs } from '@atcute/bluesky'; 3 + import { env } from '$env/dynamic/private'; 4 4 import { json } from '@sveltejs/kit'; 5 + import type { AppBskyActorDefs } from '@atcute/bluesky'; 5 6 6 7 export async function GET({ platform }) { 7 - if (!platform?.env?.USER_DATA_CACHE) return json('no cache'); 8 - const existingUsers = await platform?.env?.USER_DATA_CACHE?.get('updatedBlentos'); 8 + const cache = createCache(platform); 9 + if (!cache) return json('no cache'); 10 + 11 + const existingUsers = await cache.get('meta', 'updatedBlentos'); 9 12 10 13 const existingUsersArray: AppBskyActorDefs.ProfileViewDetailed[] = existingUsers 11 14 ? JSON.parse(existingUsers) 12 15 : []; 13 16 14 17 const existingUsersHandle = existingUsersArray.map((v) => v.handle); 15 - 16 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 17 18 18 19 for (const handle of existingUsersHandle) { 19 20 if (!handle) continue; 20 21 21 22 try { 22 - const cached = await getCache(handle, 'self', cache as UserCache); 23 - if (!cached) await loadData(handle, cache as UserCache, true); 23 + const cached = await getCache(handle, 'self', cache); 24 + if (!cached) await loadData(handle, cache, true, 'self', env); 24 25 } catch (error) { 25 26 console.error(error); 26 27 return json('error');
+6 -5
src/routes/edit/+page.server.ts
··· 1 1 import { loadData } from '$lib/website/load'; 2 2 import { env } from '$env/dynamic/public'; 3 - import type { UserCache } from '$lib/types'; 3 + import { env as privateEnv } from '$env/dynamic/private'; 4 + import { createCache } from '$lib/cache'; 4 5 import type { ActorIdentifier } from '@atcute/lexicons'; 5 6 6 7 export async function load({ platform, request }) { ··· 8 9 9 10 const kv = platform?.env?.CUSTOM_DOMAINS; 10 11 11 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 12 - const customDomain = request.headers.get('X-Custom-Domain')?.toLocaleLowerCase(); 12 + const cache = createCache(platform); 13 + const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase(); 13 14 14 15 if (kv && customDomain) { 15 16 try { 16 17 const did = await kv.get(customDomain); 17 18 18 - if (did) return await loadData(did as ActorIdentifier, cache as UserCache); 19 + if (did) return await loadData(did as ActorIdentifier, cache, false, 'self', privateEnv); 19 20 } catch (error) { 20 21 console.error('failed to get custom domain kv', error); 21 22 } 22 23 } 23 24 24 - return await loadData(handle as ActorIdentifier, cache as UserCache); 25 + return await loadData(handle as ActorIdentifier, cache, false, 'self', privateEnv); 25 26 }
+5 -4
src/routes/p/[[page]]/+layout.server.ts
··· 1 1 import { loadData } from '$lib/website/load'; 2 2 import { env } from '$env/dynamic/public'; 3 - import type { UserCache } from '$lib/types'; 3 + import { env as privateEnv } from '$env/dynamic/private'; 4 + import { createCache } from '$lib/cache'; 4 5 import type { Did, Handle } from '@atcute/lexicons'; 5 6 6 7 export async function load({ params, platform, request }) { 7 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 8 + const cache = createCache(platform); 8 9 9 10 const handle = env.PUBLIC_HANDLE; 10 11 ··· 15 16 if (kv && customDomain) { 16 17 try { 17 18 const did = await kv.get(customDomain); 18 - return await loadData(did as Did, cache as UserCache, false, params.page); 19 + return await loadData(did as Did, cache, false, params.page, privateEnv); 19 20 } catch { 20 21 console.error('failed'); 21 22 } 22 23 } 23 24 24 - return await loadData(handle as Handle, cache as UserCache, false, params.page); 25 + return await loadData(handle as Handle, cache, false, params.page, privateEnv); 25 26 }
-30
src/routes/random/+page.server.ts
··· 1 - import type { UserCache, WebsiteData } from '$lib/types.js'; 2 - import { getCache } from '$lib/website/load.js'; 3 - import { error } from '@sveltejs/kit'; 4 - 5 - export async function load({ platform }) { 6 - const cache = platform?.env?.USER_DATA_CACHE; 7 - 8 - const list = await cache?.list(); 9 - 10 - if (!list) { 11 - throw error(404); 12 - } 13 - 14 - let foundData: WebsiteData | undefined = undefined; 15 - let i = 0; 16 - 17 - while (!foundData && i < 20) { 18 - const rando = Math.floor(Math.random() * list.keys.length); 19 - console.log(list.keys[rando].name); 20 - 21 - foundData = await getCache(list.keys[rando].name, 'self', cache as unknown as UserCache); 22 - 23 - if (!foundData?.cards.length) foundData = undefined; 24 - i++; 25 - } 26 - 27 - if (!foundData) throw error(404); 28 - 29 - return foundData; 30 - }
-40
src/routes/random/+page.svelte
··· 1 - <script lang="ts"> 2 - import Website from '$lib/website/Website.svelte'; 3 - import { Button } from '@foxui/core'; 4 - 5 - let { data } = $props(); 6 - </script> 7 - 8 - <svelte:body 9 - onkeydown={(e) => { 10 - if (e.key === 'ArrowRight' || e.key === 'r') { 11 - window.location.reload(); 12 - } 13 - }} 14 - /> 15 - 16 - <Website {data} /> 17 - 18 - <Button 19 - onclick={() => { 20 - window.location.reload(); 21 - }} 22 - size="lg" 23 - class="bg-accent-100 hover:bg-accent-200 dark:bg-accent-950/50 dark:hover:bg-accent-900/50 fixed right-4 bottom-4" 24 - ><svg 25 - xmlns="http://www.w3.org/2000/svg" 26 - width="24" 27 - height="24" 28 - viewBox="0 0 24 24" 29 - fill="none" 30 - stroke="currentColor" 31 - stroke-width="2" 32 - stroke-linecap="round" 33 - stroke-linejoin="round" 34 - class="lucide lucide-dices-icon lucide-dices" 35 - ><rect width="12" height="12" x="2" y="10" rx="2" ry="2" /><path 36 - d="m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6" 37 - /><path d="M6 18h.01" /><path d="M10 14h.01" /><path d="M15 6h.01" /><path d="M18 9h.01" /></svg 38 - >Next 39 - <span class="sr-only">Next random profile</span></Button 40 - >
-16
src/routes/test/domains/+server.ts
··· 1 - import { json } from '@sveltejs/kit'; 2 - 3 - export async function GET({ platform }) { 4 - const kv = platform?.env?.CUSTOM_DOMAINS; 5 - if (!kv) return json({ error: 'KV not available' }, { status: 500 }); 6 - 7 - const list = await kv.list(); 8 - const entries: Record<string, string> = {}; 9 - 10 - for (const key of list.keys) { 11 - const value = await kv.get(key.name); 12 - entries[key.name] = value ?? ''; 13 - } 14 - 15 - return json(entries); 16 - }
+4
svelte.config.js
··· 11 11 adapter: adapter(), 12 12 paths: { 13 13 base: '' 14 + }, 15 + experimental: { 16 + remoteFunctions: true 14 17 } 15 18 }, 19 + 16 20 compilerOptions: { 17 21 experimental: { 18 22 async: true