Fixing article errors by adding fetch retry with backoff and layout level fetches.

This commit is contained in:
Bradley Shellnut 2025-08-11 22:38:38 -07:00
parent ce78870162
commit a5e48e4d63
19 changed files with 639 additions and 424 deletions

View file

@ -1,4 +1,3 @@
version: "3.8"
services: services:
redis: redis:
image: redis:latest image: redis:latest
@ -7,5 +6,6 @@ services:
- "6379:6379" - "6379:6379"
volumes: volumes:
- redis_data:/data - redis_data:/data
network_mode: host
volumes: volumes:
redis_data: redis_data:

View file

@ -18,9 +18,10 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.4", "@biomejs/biome": "^1.9.4",
"@playwright/test": "^1.54.1", "@internationalized/date": "^3.8.2",
"@playwright/test": "^1.54.2",
"@sveltejs/enhanced-img": "^0.5.1", "@sveltejs/enhanced-img": "^0.5.1",
"@sveltejs/kit": "^2.26.1", "@sveltejs/kit": "^2.27.3",
"@sveltejs/vite-plugin-svelte": "^5.1.1", "@sveltejs/vite-plugin-svelte": "^5.1.1",
"@unpic/svelte": "^1.0.0", "@unpic/svelte": "^1.0.0",
"@zerodevx/svelte-img": "^2.1.2", "@zerodevx/svelte-img": "^2.1.2",
@ -33,13 +34,13 @@
"postcss-preset-env": "^10.2.4", "postcss-preset-env": "^10.2.4",
"satori": "^0.12.2", "satori": "^0.12.2",
"satori-html": "^0.3.2", "satori-html": "^0.3.2",
"svelte": "^5.37.0", "svelte": "^5.38.0",
"svelte-check": "^4.3.0", "svelte-check": "^4.3.1",
"svelte-meta-tags": "^4.4.0", "svelte-meta-tags": "^4.4.0",
"svelte-preprocess": "^6.0.3", "svelte-preprocess": "^6.0.3",
"svelte-sequential-preprocessor": "^2.0.2", "svelte-sequential-preprocessor": "^2.0.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.8.3", "typescript": "^5.9.2",
"vanilla-lazyload": "^19.1.3", "vanilla-lazyload": "^19.1.3",
"vite": "^6.3.5", "vite": "^6.3.5",
"vite-imagetools": "^7.1.0", "vite-imagetools": "^7.1.0",
@ -47,12 +48,12 @@
}, },
"dependencies": { "dependencies": {
"@resvg/resvg-js": "^2.6.2", "@resvg/resvg-js": "^2.6.2",
"@sveltejs/adapter-node": "^5.2.13", "@sveltejs/adapter-node": "^5.2.14",
"@vercel/og": "^0.6.8", "@vercel/og": "^0.6.8",
"bits-ui": "1.4.7", "bits-ui": "2.9.2",
"flexsearch": "^0.8.205", "flexsearch": "^0.8.205",
"ioredis": "^5.6.1", "ioredis": "^5.7.0",
"lucide-svelte": "^0.509.0", "lucide-svelte": "^0.539.0",
"scrape-it": "^6.1.11", "scrape-it": "^6.1.11",
"sharp": "^0.34.3", "sharp": "^0.34.3",
"svelte-local-storage-store": "^0.6.4" "svelte-local-storage-store": "^0.6.4"

View file

@ -12,23 +12,23 @@ importers:
specifier: ^2.6.2 specifier: ^2.6.2
version: 2.6.2 version: 2.6.2
'@sveltejs/adapter-node': '@sveltejs/adapter-node':
specifier: ^5.2.13 specifier: ^5.2.14
version: 5.2.13(@sveltejs/kit@2.26.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)))(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0))) version: 5.2.14(@sveltejs/kit@2.27.3(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0)))(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0)))
'@vercel/og': '@vercel/og':
specifier: ^0.6.8 specifier: ^0.6.8
version: 0.6.8 version: 0.6.8
bits-ui: bits-ui:
specifier: 1.4.7 specifier: 2.9.2
version: 1.4.7(svelte@5.37.0) version: 2.9.2(@internationalized/date@3.8.2)(svelte@5.38.0)
flexsearch: flexsearch:
specifier: ^0.8.205 specifier: ^0.8.205
version: 0.8.205 version: 0.8.205
ioredis: ioredis:
specifier: ^5.6.1 specifier: ^5.7.0
version: 5.6.1 version: 5.7.0
lucide-svelte: lucide-svelte:
specifier: ^0.509.0 specifier: ^0.539.0
version: 0.509.0(svelte@5.37.0) version: 0.539.0(svelte@5.38.0)
scrape-it: scrape-it:
specifier: ^6.1.11 specifier: ^6.1.11
version: 6.1.11 version: 6.1.11
@ -37,29 +37,32 @@ importers:
version: 0.34.3 version: 0.34.3
svelte-local-storage-store: svelte-local-storage-store:
specifier: ^0.6.4 specifier: ^0.6.4
version: 0.6.4(svelte@5.37.0) version: 0.6.4(svelte@5.38.0)
devDependencies: devDependencies:
'@biomejs/biome': '@biomejs/biome':
specifier: ^1.9.4 specifier: ^1.9.4
version: 1.9.4 version: 1.9.4
'@internationalized/date':
specifier: ^3.8.2
version: 3.8.2
'@playwright/test': '@playwright/test':
specifier: ^1.54.1 specifier: ^1.54.2
version: 1.54.1 version: 1.54.2
'@sveltejs/enhanced-img': '@sveltejs/enhanced-img':
specifier: ^0.5.1 specifier: ^0.5.1
version: 0.5.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)))(rollup@4.34.8)(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)) version: 0.5.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0)))(rollup@4.34.8)(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0))
'@sveltejs/kit': '@sveltejs/kit':
specifier: ^2.26.1 specifier: ^2.27.3
version: 2.26.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)))(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)) version: 2.27.3(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0)))(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0))
'@sveltejs/vite-plugin-svelte': '@sveltejs/vite-plugin-svelte':
specifier: ^5.1.1 specifier: ^5.1.1
version: 5.1.1(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)) version: 5.1.1(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0))
'@unpic/svelte': '@unpic/svelte':
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0(svelte@5.37.0) version: 1.0.0(svelte@5.38.0)
'@zerodevx/svelte-img': '@zerodevx/svelte-img':
specifier: ^2.1.2 specifier: ^2.1.2
version: 2.1.2(rollup@4.34.8)(svelte@5.37.0) version: 2.1.2(rollup@4.34.8)(svelte@5.38.0)
autoprefixer: autoprefixer:
specifier: ^10.4.21 specifier: ^10.4.21
version: 10.4.21(postcss@8.5.6) version: 10.4.21(postcss@8.5.6)
@ -88,17 +91,17 @@ importers:
specifier: ^0.3.2 specifier: ^0.3.2
version: 0.3.2 version: 0.3.2
svelte: svelte:
specifier: ^5.37.0 specifier: ^5.38.0
version: 5.37.0 version: 5.38.0
svelte-check: svelte-check:
specifier: ^4.3.0 specifier: ^4.3.1
version: 4.3.0(picomatch@4.0.2)(svelte@5.37.0)(typescript@5.8.3) version: 4.3.1(picomatch@4.0.2)(svelte@5.38.0)(typescript@5.9.2)
svelte-meta-tags: svelte-meta-tags:
specifier: ^4.4.0 specifier: ^4.4.0
version: 4.4.0(svelte@5.37.0) version: 4.4.0(svelte@5.38.0)
svelte-preprocess: svelte-preprocess:
specifier: ^6.0.3 specifier: ^6.0.3
version: 6.0.3(postcss-load-config@6.0.1(postcss@8.5.6)(yaml@2.7.0))(postcss@8.5.6)(svelte@5.37.0)(typescript@5.8.3) version: 6.0.3(postcss-load-config@6.0.1(postcss@8.5.6)(yaml@2.7.0))(postcss@8.5.6)(svelte@5.38.0)(typescript@5.9.2)
svelte-sequential-preprocessor: svelte-sequential-preprocessor:
specifier: ^2.0.2 specifier: ^2.0.2
version: 2.0.2 version: 2.0.2
@ -106,8 +109,8 @@ importers:
specifier: ^2.8.1 specifier: ^2.8.1
version: 2.8.1 version: 2.8.1
typescript: typescript:
specifier: ^5.8.3 specifier: ^5.9.2
version: 5.8.3 version: 5.9.2
vanilla-lazyload: vanilla-lazyload:
specifier: ^19.1.3 specifier: ^19.1.3
version: 19.1.3 version: 19.1.3
@ -594,14 +597,14 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@floating-ui/core@1.7.0': '@floating-ui/core@1.7.3':
resolution: {integrity: sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==} resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
'@floating-ui/dom@1.7.0': '@floating-ui/dom@1.7.3':
resolution: {integrity: sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==} resolution: {integrity: sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==}
'@floating-ui/utils@0.2.9': '@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@img/sharp-darwin-arm64@0.33.5': '@img/sharp-darwin-arm64@0.33.5':
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
@ -830,11 +833,11 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@internationalized/date@3.8.0': '@internationalized/date@3.8.2':
resolution: {integrity: sha512-J51AJ0fEL68hE4CwGPa6E0PO6JDaVLd8aln48xFCSy7CZkZc96dGEGmLs2OEEbBxcsVZtfrqkXJwI2/MSG8yKw==} resolution: {integrity: sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==}
'@ioredis/commands@1.2.0': '@ioredis/commands@1.3.0':
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} resolution: {integrity: sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==}
'@jridgewell/gen-mapping@0.3.8': '@jridgewell/gen-mapping@0.3.8':
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
@ -854,8 +857,8 @@ packages:
'@jridgewell/trace-mapping@0.3.25': '@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@playwright/test@1.54.1': '@playwright/test@1.54.2':
resolution: {integrity: sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==} resolution: {integrity: sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==}
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
@ -1178,13 +1181,16 @@ packages:
engines: {node: '>= 8.0.0'} engines: {node: '>= 8.0.0'}
hasBin: true hasBin: true
'@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
'@sveltejs/acorn-typescript@1.0.5': '@sveltejs/acorn-typescript@1.0.5':
resolution: {integrity: sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==} resolution: {integrity: sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==}
peerDependencies: peerDependencies:
acorn: ^8.9.0 acorn: ^8.9.0
'@sveltejs/adapter-node@5.2.13': '@sveltejs/adapter-node@5.2.14':
resolution: {integrity: sha512-yS2TVFmIrxjGhYaV5/iIUrJ3mJl6zjaYn0lBD70vTLnYvJeqf3cjvLXeXCUCuYinhSBoyF4DpfGla49BnIy7sQ==} resolution: {integrity: sha512-TjJvfw0HZlbBGGAW2vFtdGjdKhqpGW3ZDIz0nzy8Zx6Ki6oFmYTjV5Kwn3LWTsyjbsUSXhfFPCuYop3z1iS9qQ==}
peerDependencies: peerDependencies:
'@sveltejs/kit': ^2.4.0 '@sveltejs/kit': ^2.4.0
@ -1195,8 +1201,8 @@ packages:
svelte: ^5.0.0 svelte: ^5.0.0
vite: '>= 5.0.0' vite: '>= 5.0.0'
'@sveltejs/kit@2.26.1': '@sveltejs/kit@2.27.3':
resolution: {integrity: sha512-FwDhHAoXYUGnYndrrEzEYcKdYWpSoRKq4kli29oMe83hLri4/DOGQk3xUgwjDo0LKpSmj5M/Sj29/Ug3wO0Cbg==} resolution: {integrity: sha512-jiG3NGZ8RRpi+ncjVnX+oR7uWEgzy//3YLGcTU5mHtjGraeGyNDr7GJFHlk7z0vi8bMXpXIUkEXj6p70FJmHvw==}
engines: {node: '>=18.13'} engines: {node: '>=18.13'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -1331,11 +1337,12 @@ packages:
resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
bits-ui@1.4.7: bits-ui@2.9.2:
resolution: {integrity: sha512-oqfSbgB/2Nc3qwOvohkRzw0nQcUKsNPwthD4uzy9E21wSbhc00RDcZqCJmFrrcW336J+aStM1sITsVGQFjT+iw==} resolution: {integrity: sha512-GGbyr4oVKtHin//Q0AhlygkasmfWt328VjsnmB3sP+h8Sh+Eyghm+1AQ8o+xQMDCYbdL35JZ9UZGTZYTMar4Uw==}
engines: {node: '>=18', pnpm: '>=8.7.0'} engines: {node: '>=20'}
peerDependencies: peerDependencies:
svelte: ^5.11.0 '@internationalized/date': ^3.8.1
svelte: ^5.33.0
boolbase@1.0.0: boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
@ -1361,11 +1368,8 @@ packages:
camelize@1.0.1: camelize@1.0.1:
resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==}
caniuse-lite@1.0.30001706: caniuse-lite@1.0.30001731:
resolution: {integrity: sha512-3ZczoTApMAZwPKYWmwVbQMFpXBDds3/0VciVoUwPUbldlYyVLmRVuRs/PcUZtHpbLRpzzDvrvnFuREsGt6lUug==} resolution: {integrity: sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==}
caniuse-lite@1.0.30001727:
resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==}
chai@5.2.0: chai@5.2.0:
resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==}
@ -1722,8 +1726,8 @@ packages:
inline-style-parser@0.2.4: inline-style-parser@0.2.4:
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
ioredis@5.6.1: ioredis@5.7.0:
resolution: {integrity: sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==} resolution: {integrity: sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==}
engines: {node: '>=12.22.0'} engines: {node: '>=12.22.0'}
is-arrayish@0.3.2: is-arrayish@0.3.2:
@ -1780,8 +1784,8 @@ packages:
loupe@3.2.0: loupe@3.2.0:
resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==}
lucide-svelte@0.509.0: lucide-svelte@0.539.0:
resolution: {integrity: sha512-6U83jZ0RKvLYLGdx/hTqZyWcquwApQ2Q1E5bKFELXtOw7g8dk1P0qwbAQqs1fqWAtpNevtXTpgShHv/yWAcbjQ==} resolution: {integrity: sha512-p4k3GOje/9Si1eIkg1W1OQUhozeja5Ka5shjVpfyP5X2ye+B7sfyMnX3d5D2et+MYJwUFGrMna5MIYgq6bLfqw==}
peerDependencies: peerDependencies:
svelte: ^3 || ^4 || ^5.0.0-next.42 svelte: ^3 || ^4 || ^5.0.0-next.42
@ -1878,13 +1882,13 @@ packages:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
playwright-core@1.54.1: playwright-core@1.54.2:
resolution: {integrity: sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==} resolution: {integrity: sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==}
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
playwright@1.54.1: playwright@1.54.2:
resolution: {integrity: sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==} resolution: {integrity: sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==}
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
@ -2112,8 +2116,8 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
runed@0.23.4: runed@0.29.2:
resolution: {integrity: sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==} resolution: {integrity: sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==}
peerDependencies: peerDependencies:
svelte: ^5.7.0 svelte: ^5.7.0
@ -2191,15 +2195,15 @@ packages:
style-object-to-css-string@1.1.3: style-object-to-css-string@1.1.3:
resolution: {integrity: sha512-bISQoUsir/qGfo7vY8rw00ia9nnyE1jvYt3zZ2jhdkcXZ6dAEi74inMzQ6On57vFI+I4Fck6wOv5UI9BEwJDgw==} resolution: {integrity: sha512-bISQoUsir/qGfo7vY8rw00ia9nnyE1jvYt3zZ2jhdkcXZ6dAEi74inMzQ6On57vFI+I4Fck6wOv5UI9BEwJDgw==}
style-to-object@1.0.8: style-to-object@1.0.9:
resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==}
supports-preserve-symlinks-flag@1.0.0: supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
svelte-check@4.3.0: svelte-check@4.3.1:
resolution: {integrity: sha512-Iz8dFXzBNAM7XlEIsUjUGQhbEE+Pvv9odb9+0+ITTgFWZBGeJRRYqHUUglwe2EkLD5LIsQaAc4IUJyvtKuOO5w==} resolution: {integrity: sha512-lkh8gff5gpHLjxIV+IaApMxQhTGnir2pNUAqcNgeKkvK5bT/30Ey/nzBxNLDlkztCH4dP7PixkMt9SWEKFPBWg==}
engines: {node: '>= 18.0.0'} engines: {node: '>= 18.0.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -2263,18 +2267,18 @@ packages:
resolution: {integrity: sha512-DIFm0kSNscVxtBmKkBiygAHB5otoqN1aVmJ3t57jZhJfCB7Np/lUSoTtSrvPFjmlBbMeOsb1VQ06cut1+rBYOg==} resolution: {integrity: sha512-DIFm0kSNscVxtBmKkBiygAHB5otoqN1aVmJ3t57jZhJfCB7Np/lUSoTtSrvPFjmlBbMeOsb1VQ06cut1+rBYOg==}
engines: {node: '>=16'} engines: {node: '>=16'}
svelte-toolbelt@0.7.1: svelte-toolbelt@0.9.3:
resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==} resolution: {integrity: sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw==}
engines: {node: '>=18', pnpm: '>=8.7.0'} engines: {node: '>=18', pnpm: '>=8.7.0'}
peerDependencies: peerDependencies:
svelte: ^5.0.0 svelte: ^5.30.2
svelte@4.2.20: svelte@4.2.20:
resolution: {integrity: sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==} resolution: {integrity: sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==}
engines: {node: '>=16'} engines: {node: '>=16'}
svelte@5.37.0: svelte@5.38.0:
resolution: {integrity: sha512-BAHgWdKncZ4F1DVBrkKAvelx2Nv3mR032ca8/yj9Gxf5s9zzK1uGXiZTjCFDvmO2e9KQfcR2lEkVjw+ZxExJow==} resolution: {integrity: sha512-cWF1Oc2IM/QbktdK89u5lt9MdKxRtQnRKnf2tq6KOhYuhLOd2hbMuTiJ+vWMzAeMDe81AzbCgLd4GVtOJ4fDRg==}
engines: {node: '>=18'} engines: {node: '>=18'}
tabbable@6.2.0: tabbable@6.2.0:
@ -2319,8 +2323,8 @@ packages:
tslib@2.8.1: tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
typescript@5.8.3: typescript@5.9.2:
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
@ -2853,16 +2857,16 @@ snapshots:
'@esbuild/win32-x64@0.25.4': '@esbuild/win32-x64@0.25.4':
optional: true optional: true
'@floating-ui/core@1.7.0': '@floating-ui/core@1.7.3':
dependencies: dependencies:
'@floating-ui/utils': 0.2.9 '@floating-ui/utils': 0.2.10
'@floating-ui/dom@1.7.0': '@floating-ui/dom@1.7.3':
dependencies: dependencies:
'@floating-ui/core': 1.7.0 '@floating-ui/core': 1.7.3
'@floating-ui/utils': 0.2.9 '@floating-ui/utils': 0.2.10
'@floating-ui/utils@0.2.9': {} '@floating-ui/utils@0.2.10': {}
'@img/sharp-darwin-arm64@0.33.5': '@img/sharp-darwin-arm64@0.33.5':
optionalDependencies: optionalDependencies:
@ -3025,11 +3029,11 @@ snapshots:
'@img/sharp-win32-x64@0.34.3': '@img/sharp-win32-x64@0.34.3':
optional: true optional: true
'@internationalized/date@3.8.0': '@internationalized/date@3.8.2':
dependencies: dependencies:
'@swc/helpers': 0.5.17 '@swc/helpers': 0.5.17
'@ioredis/commands@1.2.0': {} '@ioredis/commands@1.3.0': {}
'@jridgewell/gen-mapping@0.3.8': '@jridgewell/gen-mapping@0.3.8':
dependencies: dependencies:
@ -3048,9 +3052,9 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
'@playwright/test@1.54.1': '@playwright/test@1.54.2':
dependencies: dependencies:
playwright: 1.54.1 playwright: 1.54.2
'@polka/url@1.0.0-next.29': {} '@polka/url@1.0.0-next.29': {}
@ -3265,6 +3269,8 @@ snapshots:
fflate: 0.7.4 fflate: 0.7.4
string.prototype.codepointat: 0.2.1 string.prototype.codepointat: 0.2.1
'@standard-schema/spec@1.0.0': {}
'@sveltejs/acorn-typescript@1.0.5(acorn@8.14.1)': '@sveltejs/acorn-typescript@1.0.5(acorn@8.14.1)':
dependencies: dependencies:
acorn: 8.14.1 acorn: 8.14.1
@ -3273,31 +3279,32 @@ snapshots:
dependencies: dependencies:
acorn: 8.15.0 acorn: 8.15.0
'@sveltejs/adapter-node@5.2.13(@sveltejs/kit@2.26.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)))(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)))': '@sveltejs/adapter-node@5.2.14(@sveltejs/kit@2.27.3(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0)))(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0)))':
dependencies: dependencies:
'@rollup/plugin-commonjs': 28.0.2(rollup@4.34.8) '@rollup/plugin-commonjs': 28.0.2(rollup@4.34.8)
'@rollup/plugin-json': 6.1.0(rollup@4.34.8) '@rollup/plugin-json': 6.1.0(rollup@4.34.8)
'@rollup/plugin-node-resolve': 16.0.0(rollup@4.34.8) '@rollup/plugin-node-resolve': 16.0.0(rollup@4.34.8)
'@sveltejs/kit': 2.26.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)))(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)) '@sveltejs/kit': 2.27.3(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0)))(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0))
rollup: 4.34.8 rollup: 4.34.8
'@sveltejs/enhanced-img@0.5.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)))(rollup@4.34.8)(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0))': '@sveltejs/enhanced-img@0.5.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0)))(rollup@4.34.8)(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)) '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0))
magic-string: 0.30.17 magic-string: 0.30.17
sharp: 0.34.3 sharp: 0.34.3
svelte: 5.37.0 svelte: 5.38.0
svelte-parse-markup: 0.1.5(svelte@5.37.0) svelte-parse-markup: 0.1.5(svelte@5.38.0)
vite: 6.3.5(yaml@2.7.0) vite: 6.3.5(yaml@2.7.0)
vite-imagetools: 7.1.0(rollup@4.34.8) vite-imagetools: 7.1.0(rollup@4.34.8)
zimmerframe: 1.1.2 zimmerframe: 1.1.2
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
'@sveltejs/kit@2.26.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)))(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0))': '@sveltejs/kit@2.27.3(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0)))(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0))':
dependencies: dependencies:
'@standard-schema/spec': 1.0.0
'@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0)
'@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)) '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0))
'@types/cookie': 0.6.0 '@types/cookie': 0.6.0
acorn: 8.15.0 acorn: 8.15.0
cookie: 0.6.0 cookie: 0.6.0
@ -3309,26 +3316,26 @@ snapshots:
sade: 1.8.1 sade: 1.8.1
set-cookie-parser: 2.7.1 set-cookie-parser: 2.7.1
sirv: 3.0.1 sirv: 3.0.1
svelte: 5.37.0 svelte: 5.38.0
vite: 6.3.5(yaml@2.7.0) vite: 6.3.5(yaml@2.7.0)
'@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)))(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0))': '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0)))(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)) '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0))
debug: 4.4.1 debug: 4.4.1
svelte: 5.37.0 svelte: 5.38.0
vite: 6.3.5(yaml@2.7.0) vite: 6.3.5(yaml@2.7.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0))': '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)))(svelte@5.37.0)(vite@6.3.5(yaml@2.7.0)) '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0)))(svelte@5.38.0)(vite@6.3.5(yaml@2.7.0))
debug: 4.4.1 debug: 4.4.1
deepmerge: 4.3.1 deepmerge: 4.3.1
kleur: 4.1.5 kleur: 4.1.5
magic-string: 0.30.17 magic-string: 0.30.17
svelte: 5.37.0 svelte: 5.38.0
vite: 6.3.5(yaml@2.7.0) vite: 6.3.5(yaml@2.7.0)
vitefu: 1.1.1(vite@6.3.5(yaml@2.7.0)) vitefu: 1.1.1(vite@6.3.5(yaml@2.7.0))
transitivePeerDependencies: transitivePeerDependencies:
@ -3356,11 +3363,11 @@ snapshots:
dependencies: dependencies:
unpic: 4.1.2 unpic: 4.1.2
'@unpic/svelte@1.0.0(svelte@5.37.0)': '@unpic/svelte@1.0.0(svelte@5.38.0)':
dependencies: dependencies:
'@unpic/core': 1.0.1 '@unpic/core': 1.0.1
style-object-to-css-string: 1.1.3 style-object-to-css-string: 1.1.3
svelte: 5.37.0 svelte: 5.38.0
'@vercel/og@0.6.8': '@vercel/og@0.6.8':
dependencies: dependencies:
@ -3410,9 +3417,9 @@ snapshots:
loupe: 3.2.0 loupe: 3.2.0
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@zerodevx/svelte-img@2.1.2(rollup@4.34.8)(svelte@5.37.0)': '@zerodevx/svelte-img@2.1.2(rollup@4.34.8)(svelte@5.38.0)':
dependencies: dependencies:
svelte: 5.37.0 svelte: 5.38.0
vite-imagetools: 6.2.9(rollup@4.34.8) vite-imagetools: 6.2.9(rollup@4.34.8)
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
@ -3435,7 +3442,7 @@ snapshots:
autoprefixer@10.4.21(postcss@8.5.6): autoprefixer@10.4.21(postcss@8.5.6):
dependencies: dependencies:
browserslist: 4.24.4 browserslist: 4.24.4
caniuse-lite: 1.0.30001706 caniuse-lite: 1.0.30001731
fraction.js: 4.3.7 fraction.js: 4.3.7
normalize-range: 0.1.2 normalize-range: 0.1.2
picocolors: 1.1.1 picocolors: 1.1.1
@ -3460,29 +3467,29 @@ snapshots:
base64-js@0.0.8: {} base64-js@0.0.8: {}
bits-ui@1.4.7(svelte@5.37.0): bits-ui@2.9.2(@internationalized/date@3.8.2)(svelte@5.38.0):
dependencies: dependencies:
'@floating-ui/core': 1.7.0 '@floating-ui/core': 1.7.3
'@floating-ui/dom': 1.7.0 '@floating-ui/dom': 1.7.3
'@internationalized/date': 3.8.0 '@internationalized/date': 3.8.2
esm-env: 1.2.2 esm-env: 1.2.2
runed: 0.23.4(svelte@5.37.0) runed: 0.29.2(svelte@5.38.0)
svelte: 5.37.0 svelte: 5.38.0
svelte-toolbelt: 0.7.1(svelte@5.37.0) svelte-toolbelt: 0.9.3(svelte@5.38.0)
tabbable: 6.2.0 tabbable: 6.2.0
boolbase@1.0.0: {} boolbase@1.0.0: {}
browserslist@4.24.4: browserslist@4.24.4:
dependencies: dependencies:
caniuse-lite: 1.0.30001706 caniuse-lite: 1.0.30001731
electron-to-chromium: 1.5.120 electron-to-chromium: 1.5.120
node-releases: 2.0.19 node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.24.4) update-browserslist-db: 1.1.3(browserslist@4.24.4)
browserslist@4.25.1: browserslist@4.25.1:
dependencies: dependencies:
caniuse-lite: 1.0.30001727 caniuse-lite: 1.0.30001731
electron-to-chromium: 1.5.191 electron-to-chromium: 1.5.191
node-releases: 2.0.19 node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.1) update-browserslist-db: 1.1.3(browserslist@4.25.1)
@ -3496,9 +3503,7 @@ snapshots:
camelize@1.0.1: {} camelize@1.0.1: {}
caniuse-lite@1.0.30001706: {} caniuse-lite@1.0.30001731: {}
caniuse-lite@1.0.30001727: {}
chai@5.2.0: chai@5.2.0:
dependencies: dependencies:
@ -3853,9 +3858,9 @@ snapshots:
inline-style-parser@0.2.4: {} inline-style-parser@0.2.4: {}
ioredis@5.6.1: ioredis@5.7.0:
dependencies: dependencies:
'@ioredis/commands': 1.2.0 '@ioredis/commands': 1.3.0
cluster-key-slot: 1.1.2 cluster-key-slot: 1.1.2
debug: 4.4.0 debug: 4.4.0
denque: 2.1.0 denque: 2.1.0
@ -3910,9 +3915,9 @@ snapshots:
loupe@3.2.0: {} loupe@3.2.0: {}
lucide-svelte@0.509.0(svelte@5.37.0): lucide-svelte@0.539.0(svelte@5.38.0):
dependencies: dependencies:
svelte: 5.37.0 svelte: 5.38.0
magic-string@0.30.17: magic-string@0.30.17:
dependencies: dependencies:
@ -3990,11 +3995,11 @@ snapshots:
pify@2.3.0: {} pify@2.3.0: {}
playwright-core@1.54.1: {} playwright-core@1.54.2: {}
playwright@1.54.1: playwright@1.54.2:
dependencies: dependencies:
playwright-core: 1.54.1 playwright-core: 1.54.2
optionalDependencies: optionalDependencies:
fsevents: 2.3.2 fsevents: 2.3.2
@ -4311,10 +4316,10 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.40.2 '@rollup/rollup-win32-x64-msvc': 4.40.2
fsevents: 2.3.3 fsevents: 2.3.3
runed@0.23.4(svelte@5.37.0): runed@0.29.2(svelte@5.38.0):
dependencies: dependencies:
esm-env: 1.2.2 esm-env: 1.2.2
svelte: 5.37.0 svelte: 5.38.0
sade@1.8.1: sade@1.8.1:
dependencies: dependencies:
@ -4450,56 +4455,56 @@ snapshots:
style-object-to-css-string@1.1.3: {} style-object-to-css-string@1.1.3: {}
style-to-object@1.0.8: style-to-object@1.0.9:
dependencies: dependencies:
inline-style-parser: 0.2.4 inline-style-parser: 0.2.4
supports-preserve-symlinks-flag@1.0.0: {} supports-preserve-symlinks-flag@1.0.0: {}
svelte-check@4.3.0(picomatch@4.0.2)(svelte@5.37.0)(typescript@5.8.3): svelte-check@4.3.1(picomatch@4.0.2)(svelte@5.38.0)(typescript@5.9.2):
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.25
chokidar: 4.0.3 chokidar: 4.0.3
fdir: 6.4.4(picomatch@4.0.2) fdir: 6.4.4(picomatch@4.0.2)
picocolors: 1.1.1 picocolors: 1.1.1
sade: 1.8.1 sade: 1.8.1
svelte: 5.37.0 svelte: 5.38.0
typescript: 5.8.3 typescript: 5.9.2
transitivePeerDependencies: transitivePeerDependencies:
- picomatch - picomatch
svelte-local-storage-store@0.6.4(svelte@5.37.0): svelte-local-storage-store@0.6.4(svelte@5.38.0):
dependencies: dependencies:
svelte: 5.37.0 svelte: 5.38.0
svelte-meta-tags@4.4.0(svelte@5.37.0): svelte-meta-tags@4.4.0(svelte@5.38.0):
dependencies: dependencies:
schema-dts: 1.1.5 schema-dts: 1.1.5
svelte: 5.37.0 svelte: 5.38.0
svelte-parse-markup@0.1.5(svelte@5.37.0): svelte-parse-markup@0.1.5(svelte@5.38.0):
dependencies: dependencies:
svelte: 5.37.0 svelte: 5.38.0
svelte-preprocess@6.0.3(postcss-load-config@6.0.1(postcss@8.5.6)(yaml@2.7.0))(postcss@8.5.6)(svelte@5.37.0)(typescript@5.8.3): svelte-preprocess@6.0.3(postcss-load-config@6.0.1(postcss@8.5.6)(yaml@2.7.0))(postcss@8.5.6)(svelte@5.38.0)(typescript@5.9.2):
dependencies: dependencies:
svelte: 5.37.0 svelte: 5.38.0
optionalDependencies: optionalDependencies:
postcss: 8.5.6 postcss: 8.5.6
postcss-load-config: 6.0.1(postcss@8.5.6)(yaml@2.7.0) postcss-load-config: 6.0.1(postcss@8.5.6)(yaml@2.7.0)
typescript: 5.8.3 typescript: 5.9.2
svelte-sequential-preprocessor@2.0.2: svelte-sequential-preprocessor@2.0.2:
dependencies: dependencies:
svelte: 4.2.20 svelte: 4.2.20
tslib: 2.7.0 tslib: 2.7.0
svelte-toolbelt@0.7.1(svelte@5.37.0): svelte-toolbelt@0.9.3(svelte@5.38.0):
dependencies: dependencies:
clsx: 2.1.1 clsx: 2.1.1
runed: 0.23.4(svelte@5.37.0) runed: 0.29.2(svelte@5.38.0)
style-to-object: 1.0.8 style-to-object: 1.0.9
svelte: 5.37.0 svelte: 5.38.0
svelte@4.2.20: svelte@4.2.20:
dependencies: dependencies:
@ -4518,7 +4523,7 @@ snapshots:
magic-string: 0.30.17 magic-string: 0.30.17
periscopic: 3.1.0 periscopic: 3.1.0
svelte@5.37.0: svelte@5.38.0:
dependencies: dependencies:
'@ampproject/remapping': 2.3.0 '@ampproject/remapping': 2.3.0
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
@ -4565,7 +4570,7 @@ snapshots:
tslib@2.8.1: {} tslib@2.8.1: {}
typescript@5.8.3: {} typescript@5.9.2: {}
typpy@2.4.0: typpy@2.4.0:
dependencies: dependencies:

View file

@ -1,144 +1,222 @@
import { import {
WALLABAG_CLIENT_ID,
WALLABAG_CLIENT_SECRET,
WALLABAG_USERNAME,
WALLABAG_PASSWORD,
WALLABAG_URL,
PAGE_SIZE, PAGE_SIZE,
USE_REDIS_CACHE, USE_REDIS_CACHE,
WALLABAG_CLIENT_ID,
WALLABAG_CLIENT_SECRET,
WALLABAG_PASSWORD,
WALLABAG_URL,
WALLABAG_USERNAME,
} from "$env/static/private"; } from "$env/static/private";
import intersect from "just-intersect"; import { redis } from "$lib/server/redis";
import type { import type {
Article, Article,
ArticlePageLoad, ArticlePageLoad,
WallabagArticle, WallabagArticle,
} from "$lib/types/article"; } from "$lib/types/article";
import { ArticleTag } from "$lib/types/articleTag"; import { ArticleTag } from "$lib/types/articleTag";
import intersect from "just-intersect";
import type { PageQuery } from "./types/pageQuery"; import type { PageQuery } from "./types/pageQuery";
import { URLSearchParams } from "node:url";
import { redis } from "$lib/server/redis";
const base: string = WALLABAG_URL; const base: string = WALLABAG_URL;
// Retry helper with exponential backoff
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 500
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (attempt === maxRetries) {
throw lastError;
}
// Exponential backoff: 500ms, 1s, 2s
const delay = baseDelay * (2 ** attempt);
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
export async function fetchArticlesApi( export async function fetchArticlesApi(
method: string, method: string,
resource: string, resource: string,
queryParams: Record<string, string> queryParams: Record<string, string>
) { ) {
let perPage = Number(queryParams?.limit); try {
if (perPage > 30) { let perPage = Number(queryParams?.limit);
perPage = Number(PAGE_SIZE); if (perPage === undefined || perPage > 30 || perPage < 1) {
} else { perPage = Number(PAGE_SIZE);
perPage = Number(queryParams?.limit); } else {
} perPage = Number(queryParams?.limit);
const pageQuery: PageQuery = {
sort: "updated",
perPage,
since: 0,
page: Number(queryParams?.page) || 1,
tags: "programming",
content: "metadata",
};
const entriesQueryParams = new URLSearchParams({
...pageQuery,
perPage: `${pageQuery.perPage}`,
since: `${pageQuery.since}`,
page: `${pageQuery.page}`,
});
if (USE_REDIS_CACHE) {
console.log('Using redis cache');
const cached = await redis.get(entriesQueryParams.toString());
if (cached) {
console.log("Cache hit!");
const response = JSON.parse(cached);
const ttl = await redis.ttl(entriesQueryParams.toString());
console.log(`Response ${JSON.stringify(response)}`);
console.log(`Returning cached response with ttl of ${ttl} seconds`);
return { ...response, cacheControl: `max-age=${ttl}` };
} }
}
const authBody = { const pageQuery: PageQuery = {
grant_type: "password", sort: "updated",
client_id: WALLABAG_CLIENT_ID, perPage,
client_secret: WALLABAG_CLIENT_SECRET, since: 0,
username: WALLABAG_USERNAME, page: Number(queryParams?.page) || 1,
password: WALLABAG_PASSWORD, tags: "programming",
}; content: "metadata",
};
const entriesQueryParams = new URLSearchParams({
sort: pageQuery.sort,
perPage: `${pageQuery.perPage}`,
since: `${pageQuery.since}`,
page: `${pageQuery.page}`,
tags: pageQuery.tags,
content: pageQuery.content,
});
const authResponse = await fetch(`${base}/oauth/v2/token`, { if (USE_REDIS_CACHE === 'true') {
method: "POST", console.log('Using redis cache');
headers: { "Content-Type": "application/x-www-form-urlencoded" }, const cacheKey = entriesQueryParams.toString();
body: new URLSearchParams(authBody), console.log(`Cache key: ${cacheKey}`);
}); const cached = await redis.get(cacheKey);
const auth = await authResponse.json(); if (cached) {
console.log("Cache hit!");
const response = JSON.parse(cached);
const ttl = await redis.ttl(cacheKey);
const pageResponse = await fetch( console.log(`Returning cached response for page ${pageQuery.page} with ttl of ${ttl} seconds`);
`${WALLABAG_URL}/api/entries.json?${entriesQueryParams}`, console.log(`Response: ${JSON.stringify(response)}`);
{ return { ...response, cacheControl: `max-age=${ttl}` };
method: "GET", }
headers: { console.log(`Cache miss for page ${pageQuery.page}, fetching from API`);
Authorization: `Bearer ${auth.access_token}`,
},
} }
);
if (!pageResponse.ok) { const authBody = {
throw new Error(pageResponse.statusText); grant_type: "password",
} client_id: WALLABAG_CLIENT_ID,
client_secret: WALLABAG_CLIENT_SECRET,
username: WALLABAG_USERNAME,
password: WALLABAG_PASSWORD,
};
const cacheControl = pageResponse.headers.get("cache-control") || "no-cache"; console.log(`Auth body: ${JSON.stringify(authBody)}`);
const { const auth = await retryWithBackoff(async () => {
_embedded: favoriteArticles, const authResponse = await fetch(`${base}/oauth/v2/token`, {
page, method: "POST",
pages, headers: { "Content-Type": "application/x-www-form-urlencoded" },
total, body: new URLSearchParams(authBody),
limit, signal: AbortSignal.timeout(10000), // 10 second timeout
} = await pageResponse.json();
const articles: Article[] = [];
for (const article of favoriteArticles.items as WallabagArticle[]) {
const rawTags = article?.tags?.map((tag) => tag.slug);
if (intersect(rawTags, Object.values(ArticleTag))?.length > 0) {
const tags = rawTags.map((rawTag) => rawTag as unknown as ArticleTag);
articles.push({
tags,
title: article.title,
url: new URL(article.url),
domain_name: article.domain_name?.replace("www.", "") ?? "",
hashed_url: article.hashed_url,
reading_time: article.reading_time,
preview_picture: article.preview_picture,
created_at: new Date(article.created_at),
updated_at: new Date(article.updated_at),
archived_at: article.archived_at ? new Date(article.archived_at) : null,
}); });
if (!authResponse.ok) {
throw new Error(`Auth failed: ${authResponse.status} ${authResponse.statusText}`);
}
return await authResponse.json();
});
console.log(`Got auth response: ${JSON.stringify(auth)}`);
const { wallabagResponse, cacheControl } = await retryWithBackoff(async () => {
const pageResponse = await fetch(
`${WALLABAG_URL}/api/entries.json?${entriesQueryParams}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${auth.access_token}`,
},
signal: AbortSignal.timeout(15000), // 15 second timeout
}
);
console.log('pageResponse', pageResponse);
if (!pageResponse.ok) {
console.log('pageResponse not ok', pageResponse);
throw new Error(`API request failed: ${pageResponse.status} ${pageResponse.statusText}`);
}
const cacheControl = pageResponse.headers.get("cache-control") || "no-cache";
const wallabagResponse = await pageResponse.json();
return { wallabagResponse, cacheControl };
});
console.log('wallabagResponse', JSON.stringify(wallabagResponse));
const {
_embedded: favoriteArticles,
page,
pages,
total,
limit,
} = wallabagResponse;
const articles: Article[] = [];
console.log('favoriteArticles', JSON.stringify(favoriteArticles));
console.log('pages', pages);
console.log('page', page);
console.log('total', total);
console.log('limit', limit);
for (const article of favoriteArticles.items as WallabagArticle[]) {
const rawTags = article?.tags?.map((tag) => tag.slug);
if (intersect(rawTags, Object.values(ArticleTag))?.length > 0) {
const tags = rawTags.map((rawTag) => rawTag as unknown as ArticleTag);
articles.push({
tags,
title: article.title,
url: new URL(article.url),
domain_name: article.domain_name?.replace("www.", "") ?? "",
hashed_url: article.hashed_url,
reading_time: article.reading_time,
preview_picture: article.preview_picture,
created_at: new Date(article.created_at),
updated_at: new Date(article.updated_at),
archived_at: article.archived_at ? new Date(article.archived_at) : null,
});
}
} }
const responseData: ArticlePageLoad = {
articles,
currentPage: page,
totalPages: pages,
limit,
totalArticles: total,
cacheControl,
};
console.log('Response data from API: ', JSON.stringify(responseData))
if (USE_REDIS_CACHE === 'true' && responseData?.articles?.length > 0) {
const cacheKey = entriesQueryParams.toString();
console.log(`Storing in cache with key: ${cacheKey} for page ${page}`);
redis.set(
cacheKey,
JSON.stringify(responseData),
"EX",
43200
);
}
return responseData;
} catch (error) {
console.error(`Error fetching articles for page ${queryParams?.page}:`, error);
// Return empty response on error to prevent app crash
const fallbackResponse: ArticlePageLoad = {
articles: [],
currentPage: Number(queryParams?.page) || 1,
totalPages: 0,
limit: Number(queryParams?.limit) || Number(PAGE_SIZE),
totalArticles: 0,
cacheControl: 'no-cache',
};
return fallbackResponse;
} }
const responseData: ArticlePageLoad = {
articles,
currentPage: page,
totalPages: pages,
limit,
totalArticles: total,
cacheControl,
};
if (USE_REDIS_CACHE) {
redis.set(
entriesQueryParams.toString(),
JSON.stringify(responseData),
"EX",
43200
);
}
return responseData;
} }

View file

@ -1,51 +1,78 @@
<script lang="ts"> <script lang="ts">
import type { Article } from '$lib/types/article'; import type { Article } from '$lib/types/article';
import { ArrowRight } from 'lucide-svelte'; import { ArrowRight } from 'lucide-svelte';
import ExternalLink from './ExternalLink.svelte'; import ExternalLink from './ExternalLink.svelte';
const { type LoadData = {
articles, articles: Article[];
totalArticles, totalArticles: number;
compact = false, classes?: string[];
classes = [], compact?: boolean;
}: { articles: Article[]; totalArticles: number; compact: boolean; classes?: string[] } = $props(); };
const { data }: { data: LoadData } = $props();
// Use $derived to maintain reactivity when data prop changes
const articles = $derived(data.articles || []);
const totalArticles = $derived(data.totalArticles || 0);
const compact = $derived(data.compact);
const classes = $derived(data.classes || []);
const articlesData = $derived(articles);
</script> </script>
<section class="articles"> <section class="articles">
<h2>Favorite Articles</h2> <h2>Favorite Articles</h2>
<div class={classes.join(' ')}> <div class={classes.join(' ')}>
{#each articles as article (article.hashed_url)} <!-- {#await data.articles}
<article class="card"> {#each Array(6) as _, i (i)}
<section> <article class="card skeleton">
<h3> <section>
<ExternalLink <h3><span class="skeleton-text skeleton-title" aria-hidden="true">Loading article title...</span></h3>
textData={{ <span class="skeleton-text skeleton-domain" aria-hidden="true">Loading domain...</span>
text: compact ? article.title.substring(0, 50).trim() : article.title, </section>
location: 'left', <section>
showIcon: true, <span class="skeleton-text skeleton-reading" aria-hidden="true">Loading reading time...</span>
}} <span class="skeleton-text skeleton-tags" aria-hidden="true">Loading tags...</span>
linkData={{ </section>
href: article.url.toString(), </article>
ariaLabel: `Link to ${article.title}`, {/each}
title: `Link to ${article.title}`, {:then articles} -->
target: '_blank', {#each articlesData as article (article.hashed_url)}
}} <article class="card">
iconData={{ iconClass: 'center' }} <section>
/> <h3>
</h3> <ExternalLink
<p>{article.domain_name}</p> textData={{
</section> text: compact ? article.title.substring(0, 50).trim() : article.title,
<section> location: 'left',
<p>Reading time: {article.reading_time} minutes</p> showIcon: true,
<div class="tagsStyles"> }}
<p>Tags:</p> linkData={{
{#each article.tags as tag} href: article.url.toString(),
<p>{tag}</p> ariaLabel: `Link to ${article.title}`,
{/each} title: `Link to ${article.title}`,
</div> target: '_blank',
</section> }}
</article> iconData={{ iconClass: 'center' }}
{/each} />
</h3>
<p>{article.domain_name}</p>
</section>
<section>
<p>Reading time: {article.reading_time} minutes</p>
<div class="tagsStyles">
<p>Tags:</p>
{#each article.tags as tag}
<p>{tag}</p>
{/each}
</div>
</section>
</article>
{/each}
<!-- {:catch error}
<p>There was an error loading the articles.</p>
{/await} -->
</div> </div>
<a class="moreArticles" href="/articles">{`${totalArticles} more articles`} <ArrowRight /></a> <a class="moreArticles" href="/articles">{`${totalArticles} more articles`} <ArrowRight /></a>
</section> </section>

View file

@ -0,0 +1,80 @@
<script lang="ts">
interface Props {
count?: number;
classes?: string[];
}
let { count = 6, classes = ['columns'] }: Props = $props();
const placeholders = Array.from({ length: count });
</script>
<div class={classes.join(' ')} role="status" aria-live="polite" aria-busy="true">
{#each placeholders as _, i (i)}
<article class="card skeleton">
<section>
<h3><span class="skeleton-text skeleton-title" aria-hidden="true">Loading article title...</span></h3>
<span class="skeleton-text skeleton-domain" aria-hidden="true">Loading domain...</span>
</section>
<section>
<span class="skeleton-text skeleton-reading" aria-hidden="true">Loading reading time...</span>
<span class="skeleton-text skeleton-tags" aria-hidden="true">Loading tags...</span>
</section>
</article>
{/each}
</div>
<style lang="postcss">
.columns {
display: grid;
grid-template-columns: repeat(2, minmax(250px, 1fr));
min-height: 800px;
@media (max-width: 1000px) {
grid-template-columns: repeat(2, minmax(250px, 1fr));
}
@media (max-width: 650px) {
grid-template-columns: minmax(250px, 1fr);
}
gap: 2.5rem;
}
.skeleton {
position: relative;
overflow: hidden;
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--grey, #e5e7eb);
background: var(--surface, #0f172a0d);
}
.skeleton-text {
display: block;
height: 1rem;
margin: 0.5rem 0;
border-radius: 6px;
background: linear-gradient(
90deg,
rgba(148, 163, 184, 0.18) 0%,
rgba(148, 163, 184, 0.35) 50%,
rgba(148, 163, 184, 0.18) 100%
);
background-size: 200% 100%;
animation: shimmer 1.2s ease-in-out infinite;
}
.skeleton-title {
height: 1.25rem;
width: 80%;
}
.skeleton-domain { width: 40%; }
.skeleton-reading { width: 55%; }
.skeleton-tags { width: 65%; }
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>

View file

@ -1,31 +1,25 @@
<script lang="ts"> <script lang="ts">
import { ExternalLink } from "lucide-svelte"; import type { ExternalLinkType, LinkIconType } from '$lib/types/externalLinkTypes';
import type { import { ExternalLink } from 'lucide-svelte';
ExternalLinkType,
LinkIconType,
} from "$lib/types/externalLinkTypes";
const { iconData, linkData, textData }: ExternalLinkType = $props(); const { iconData, linkData, textData }: ExternalLinkType = $props();
let textLocationClass = ""; let textLocationClass = '';
if (textData?.location === "top") { if (textData?.location === 'top') {
textLocationClass = "text-top"; textLocationClass = 'text-top';
} else if (textData?.location === "bottom") { } else if (textData?.location === 'bottom') {
textLocationClass = "text-bottom"; textLocationClass = 'text-bottom';
} else if (textData?.location === "left") { } else if (textData?.location === 'left') {
textLocationClass = "text-left"; textLocationClass = 'text-left';
} else if (textData?.location === "right") { } else if (textData?.location === 'right') {
textLocationClass = "text-right"; textLocationClass = 'text-right';
} else { } else {
textLocationClass = "text-left"; textLocationClass = 'text-left';
} }
const linkDecoration = const linkDecoration =
linkData?.textDecoration && linkData?.textDecoration === "none" linkData?.textDecoration && linkData?.textDecoration === 'none' ? `text-decoration-${linkData?.textDecoration}` : 'text-decoration-underline';
? `text-decoration-${linkData?.textDecoration}` const linkClass = `${linkData?.clazz || ''} ${textLocationClass} ${linkDecoration}`.trim();
: "text-decoration-underline";
const linkClass =
`${linkData?.clazz} ${textLocationClass} ${linkDecoration}`.trim();
</script> </script>
{#snippet externalLink({ iconData, linkData, textData }: ExternalLinkType)} {#snippet externalLink({ iconData, linkData, textData }: ExternalLinkType)}

View file

@ -8,6 +8,7 @@
pageSize: number; pageSize: number;
totalCount: number; totalCount: number;
currentPage: number; currentPage: number;
totalPages: number;
base: string; base: string;
} }
@ -20,7 +21,7 @@
}: Props = $props(); }: Props = $props();
</script> </script>
<Pagination.Root count={totalCount} perPage={pageSize} page={currentPage || 1} class={`${additionalClasses}`} <Pagination.Root count={totalCount} perPage={pageSize} page={currentPage || 1} class={`${additionalClasses}`}
onPageChange={(page) => goto(`${base}/${page}`)}> onPageChange={(page) => goto(`${base}/${page}`)}>
{#snippet children({ pages })} {#snippet children({ pages })}
<Pagination.PrevButton> <Pagination.PrevButton>
@ -31,7 +32,7 @@
<div class="ellipsis text-[15px] font-medium text-foreground-alt">...</div> <div class="ellipsis text-[15px] font-medium text-foreground-alt">...</div>
{:else} {:else}
<Pagination.Page {page}> <Pagination.Page {page}>
<a href={`${base}/${page.value}`}> <a href={`${base}/${page.value}`} data-sveltekit-preload-data="hover">
{page.value} {page.value}
</a> </a>
</Pagination.Page> </Pagination.Page>

View file

@ -1,42 +0,0 @@
import { writable } from 'svelte/store';
import type { Article } from '$lib/types/article';
// Custom store
const state = () => {
const { subscribe, set, update } = writable<Article[]>([]);
function addAll(articles: Article[]) {
// console.log(typeof articles);
for (const article of articles) {
add(article);
}
}
function add(article: Article) {
update((store) => [...store, article]);
}
function addSorted(article: Article, index: number) {
update((store) => {
store.splice(index, 0, article);
return store;
});
}
function remove(hashed_url: string) {
update((store) => {
const newStore = store.filter((item: Article) => item.hashed_url !== hashed_url);
return [...newStore];
});
}
function removeAll() {
update(() => {
return [];
});
}
return { subscribe, set, update, add, addSorted, addAll, remove, removeAll };
};
export const articleStore = state();

View file

@ -6,12 +6,12 @@ import type { Album, BandCampResults } from '../types/album';
export async function fetchBandcampAlbums() { export async function fetchBandcampAlbums() {
try { try {
if (USE_REDIS_CACHE) { if (USE_REDIS_CACHE === 'true') {
const cached: string | null = await redis.get('bandcampAlbums'); const cached: string | null = await redis.get('bandcampAlbums');
if (cached) { if (cached) {
const response: Album[] = JSON.parse(cached); const response: Album[] = JSON.parse(cached);
console.log(`Cache hit!`); console.log('Cache hit!');
const ttl = await redis.ttl('bandcampAlbums'); const ttl = await redis.ttl('bandcampAlbums');
return { ...response, cacheControl: `max-age=${ttl}` }; return { ...response, cacheControl: `max-age=${ttl}` };
@ -46,7 +46,7 @@ export async function fetchBandcampAlbums() {
const albums: Album[] = data?.collectionItems || []; const albums: Album[] = data?.collectionItems || [];
if (albums && albums?.length > 0) { if (albums && albums?.length > 0) {
if (USE_REDIS_CACHE) { if (USE_REDIS_CACHE === 'true') {
redis.set('bandcampAlbums', JSON.stringify(albums), 'EX', 43200); redis.set('bandcampAlbums', JSON.stringify(albums), 'EX', 43200);
} }
return albums; return albums;

View file

@ -63,7 +63,7 @@ const userNames = {
</p> </p>
<div class="social-info"> <div class="social-info">
<Bandcamp {albums} /> <Bandcamp {albums} />
<Articles {articles} {totalArticles} compact /> <Articles data={{ articles, totalArticles, compact: true }} />
</div> </div>
</div> </div>

View file

@ -1,6 +1,7 @@
import { json, error } from '@sveltejs/kit'; import { json, error } from '@sveltejs/kit';
import { PAGE_SIZE } from '$env/static/private'; import { PAGE_SIZE } from '$env/static/private';
import { fetchArticlesApi } from '$lib/api'; import { fetchArticlesApi } from '$lib/api';
import type { ArticlePageLoad } from '@/lib/types/article.js';
export async function GET({ setHeaders, url }) { export async function GET({ setHeaders, url }) {
const page = url?.searchParams?.get('page') || '1'; const page = url?.searchParams?.get('page') || '1';
@ -10,13 +11,11 @@ export async function GET({ setHeaders, url }) {
} }
try { try {
const response = await fetchArticlesApi('get', 'fetchArticles', { const response: ArticlePageLoad = await fetchArticlesApi('get', 'fetchArticles', {
page, page,
limit limit
}); });
console.log(`JSON articles response: ${JSON.stringify(response)}`);
if (response?.articles) { if (response?.articles) {
if (response?.cacheControl) { if (response?.cacheControl) {
if (!response.cacheControl.includes('no-cache')) { if (!response.cacheControl.includes('no-cache')) {

View file

@ -1,13 +1,12 @@
import { json, error } from '@sveltejs/kit'; import { json, error } from '@sveltejs/kit';
import { BANDCAMP_USERNAME, PAGE_SIZE, USE_REDIS_CACHE } from '$env/static/private'; import { BANDCAMP_USERNAME, USE_REDIS_CACHE } from '$env/static/private';
import { fetchArticlesApi } from '$lib/api';
import { redis } from '$lib/server/redis'; import { redis } from '$lib/server/redis';
import type { Album, BandCampResults } from '$lib/types/album'; import type { Album, BandCampResults } from '$lib/types/album';
import scrapeIt, { type ScrapeResult } from 'scrape-it'; import scrapeIt, { type ScrapeResult } from 'scrape-it';
export async function GET({ setHeaders, url }) { export async function GET({ setHeaders, url }) {
try { try {
if (USE_REDIS_CACHE) { if (USE_REDIS_CACHE === 'true') {
const cached: string | null = await redis.get('bandcampAlbums'); const cached: string | null = await redis.get('bandcampAlbums');
if (cached) { if (cached) {
@ -51,7 +50,7 @@ export async function GET({ setHeaders, url }) {
const albums: Album[] = data?.collectionItems || []; const albums: Album[] = data?.collectionItems || [];
if (albums && albums?.length > 0) { if (albums && albums?.length > 0) {
if (USE_REDIS_CACHE) { if (USE_REDIS_CACHE === 'true') {
redis.set('bandcampAlbums', JSON.stringify(albums), 'EX', 43200); redis.set('bandcampAlbums', JSON.stringify(albums), 'EX', 43200);
} }
setHeaders({ setHeaders({

View file

@ -0,0 +1,17 @@
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ fetch }) => {
// Fetch the first page to get common metadata (total articles, total pages, etc.)
const resp = await fetch('/api/articles?page=1');
const data = await resp.json();
console.log('Data: ', JSON.stringify(data));
return {
// Common metadata available to all child routes
totalArticles: data.totalArticles,
totalPages: data.totalPages,
limit: data.limit,
cacheControl: data.cacheControl
};
};

View file

@ -0,0 +1,43 @@
<script lang="ts">
import type { LayoutData } from './$types';
interface Props {
data: LayoutData;
children: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<div class="articles-layout">
<h1 style:margin-bottom="2rem">Favorite Tech Articles</h1>
{@render children()}
</div>
<style>
.articles-layout {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
font-weight: 700;
color: var(--color-theme-1);
text-align: center;
margin-bottom: 2rem;
}
h1 {
font-size: 2.5rem;
font-weight: 700;
color: var(--color-theme-1);
text-align: center;
margin-bottom: 2rem;
}
:global(.top-pagination) {
margin-bottom: 2rem;
}
:global(.bottom-pagination) {
margin-top: 2rem;
}
</style>

View file

@ -1,19 +1,11 @@
import type { MetaTagsProps } from 'svelte-meta-tags';
import type { PageServerLoad } from './$types';
import { PUBLIC_SITE_URL } from '$env/static/public'; import { PUBLIC_SITE_URL } from '$env/static/public';
import type { ArticlePageLoad } from '$lib/types/article'; import type { ArticlePageLoad } from '$lib/types/article';
import type { MetaTagsProps } from 'svelte-meta-tags';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ fetch, params, setHeaders, url }) => { export const load: PageServerLoad = async ({ fetch, params, setHeaders, url, parent }) => {
const { page } = params; const { page } = params;
const resp = await fetch(`/api/articles?page=${page}`); const { cacheControl } = await parent();
const {
articles,
currentPage,
totalPages,
limit,
totalArticles,
cacheControl,
}: ArticlePageLoad = await resp.json();
if (cacheControl?.includes('no-cache')) { if (cacheControl?.includes('no-cache')) {
setHeaders({ setHeaders({
@ -57,12 +49,12 @@ export const load: PageServerLoad = async ({ fetch, params, setHeaders, url }) =
url: currentPageUrl, url: currentPageUrl,
}); });
const articlesData = await fetch(`/api/articles?page=${page}`);
const { articles, currentPage } = await articlesData.json();
return { return {
articles, articles,
currentPage, currentPage,
totalPages,
limit,
totalArticles,
metaTagsChild: metaTags, metaTagsChild: metaTags,
}; };
}; };

View file

@ -1,33 +1,55 @@
<script lang="ts"> <script lang="ts">
import Pagination from '$lib/components/Pagination.svelte'; import Articles from '$lib/components/Articles.svelte';
import type { Article } from '$lib/types/article'; import Pagination from '$lib/components/Pagination.svelte';
import Articles from '$lib/components/Articles.svelte'; import ArticlesSkeleton from '$lib/components/ArticlesSkeleton.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
interface Props { interface Props {
data: PageData; data: PageData;
} }
let { data }: Props = $props(); let { data }: Props = $props();
let articles: Article[] = $state(data?.articles);
let currentPage: number = $state(data?.currentPage); // Use the data directly - it will be reactive when navigating between pages
let totalArticles: number = $state(data?.totalArticles); let articles = $derived(data?.articles || []);
let limit: number = $state(data?.limit); let currentPage: number = $derived(data?.currentPage || 1);
let totalArticles: number = $derived(data?.totalArticles || 0);
let limit: number = $derived(data?.limit || 10);
let totalPages: number = $derived(data?.totalPages || 1);
</script> </script>
<h1 style:margin-bottom={"2rem"}>Favorite Tech Articles</h1> <div class="articles-content">
<Pagination <Pagination
additionalClasses="top-pagination" additionalClasses="top-pagination"
pageSize={limit} pageSize={limit}
totalCount={totalArticles} totalCount={totalArticles}
currentPage={currentPage || 1} currentPage={currentPage || 1}
base="/articles" totalPages={totalPages || 1}
/> base="/articles"
<Articles {articles} {totalArticles} classes={['columns']} /> />
<Pagination
additionalClasses="bottom-pagination" <Articles data={{ articles, totalArticles, classes: ['columns'], compact: false }} />
pageSize={limit}
totalCount={totalArticles} <Pagination
currentPage={currentPage || 1} additionalClasses="bottom-pagination"
base="/articles" pageSize={limit}
/> totalCount={totalArticles}
currentPage={currentPage || 1}
totalPages={totalPages || 1}
base="/articles"
/>
</div>
<style>
.articles-content {
width: 100%;
}
:global(.top-pagination) {
margin-bottom: 2rem;
}
:global(.bottom-pagination) {
margin-top: 2rem;
}
</style>

View file

@ -15,6 +15,8 @@ const config = {
adapter: adapter(), adapter: adapter(),
alias: { alias: {
$: './src', $: './src',
$lib: './src/lib',
'@': './src'
} }
}, },
compilerOptions: { compilerOptions: {

View file

@ -12,10 +12,7 @@
"strict": true, "strict": true,
"moduleResolution": "bundler", "moduleResolution": "bundler",
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true
"paths": {
"@/*": ["./src/*"]
}
} }
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// //