The Appview for the kipclip.com atproto bookmarking service

Add preview images to reading list (#9)

- Extract og:image and twitter:image during bookmark enrichment
- Display preview images in ReadingListCard component
- Add background re-enrichment for existing bookmarks missing images
- Add POST /api/bookmarks/:rkey/enrich endpoint
- Fix BASE_URL loading to read from env at init time (not module load)
- Fix meta tag regex to handle both attribute orderings

authored by

Tijs Teulings and committed by
GitHub
d9b6aba3 f8c2a0bd

+484 -66
+246 -30
deno.lock
··· 1 1 { 2 2 "version": "5", 3 3 "specifiers": { 4 + "jsr:@deno/esbuild-plugin@^1.2.0": "1.2.1", 5 + "jsr:@deno/loader@~0.3.10": "0.3.10", 4 6 "jsr:@fresh/build-id@1": "1.0.1", 5 7 "jsr:@fresh/core@^2.2.0": "2.2.0", 6 8 "jsr:@panva/jose@6.1.0": "6.1.0", 7 9 "jsr:@std/assert@1": "1.0.16", 10 + "jsr:@std/bytes@^1.0.6": "1.0.6", 8 11 "jsr:@std/dotenv@0.225": "0.225.6", 12 + "jsr:@std/encoding@1": "1.0.10", 9 13 "jsr:@std/encoding@^1.0.10": "1.0.10", 10 14 "jsr:@std/fmt@^1.0.8": "1.0.8", 11 15 "jsr:@std/fs@^1.0.19": "1.0.21", 12 16 "jsr:@std/html@^1.0.5": "1.0.5", 13 17 "jsr:@std/http@^1.0.21": "1.0.23", 14 18 "jsr:@std/internal@^1.0.12": "1.0.12", 19 + "jsr:@std/jsonc@^1.0.2": "1.0.2", 15 20 "jsr:@std/media-types@1": "1.1.0", 21 + "jsr:@std/media-types@^1.1.0": "1.1.0", 16 22 "jsr:@std/path@1": "1.1.4", 23 + "jsr:@std/path@^1.1.1": "1.1.4", 17 24 "jsr:@std/path@^1.1.2": "1.1.4", 18 25 "jsr:@std/path@^1.1.4": "1.1.4", 26 + "jsr:@std/semver@^1.0.6": "1.0.7", 27 + "jsr:@std/uuid@^1.0.9": "1.1.0", 19 28 "jsr:@tijs/atproto-oauth@2.5.1": "2.5.1", 20 29 "jsr:@tijs/atproto-sessions@2.1.0": "2.1.0", 21 30 "jsr:@tijs/atproto-storage@0.1.1": "0.1.1", ··· 27 36 "npm:@opentelemetry/api@^1.9.0": "1.9.0", 28 37 "npm:@preact/signals@^2.2.1": "2.5.1_preact@10.28.2", 29 38 "npm:@sentry/deno@10.34.0": "10.34.0", 39 + "npm:esbuild-wasm@~0.25.11": "0.25.12", 40 + "npm:esbuild@0.25.7": "0.25.7", 30 41 "npm:esbuild@0.27.2": "0.27.2", 31 42 "npm:html-entities@2.6.0": "2.6.0", 32 43 "npm:iron-session@8.0.4": "8.0.4", 33 44 "npm:preact-render-to-string@^6.6.3": "6.6.5_preact@10.28.2", 45 + "npm:preact@^10.27.0": "10.28.2", 34 46 "npm:preact@^10.27.2": "10.28.2", 35 47 "npm:react-dom@19.2.3": "19.2.3_react@19.2.3", 36 48 "npm:react@19.2.3": "19.2.3" 37 49 }, 38 50 "jsr": { 51 + "@deno/esbuild-plugin@1.2.1": { 52 + "integrity": "df629467913adc1f960149fdfa3a3430ba8c20381c310fba096db244e6c3c9f6", 53 + "dependencies": [ 54 + "jsr:@deno/loader", 55 + "jsr:@std/path@^1.1.1" 56 + ] 57 + }, 58 + "@deno/loader@0.3.10": { 59 + "integrity": "a9c0aa44a0499e7fecef52c29fbc206c1c8f8946388f25d9d0789a23313bfd43" 60 + }, 39 61 "@fresh/build-id@1.0.1": { 40 62 "integrity": "12a2ec25fd52ae9ec68c26848a5696cd1c9b537f7c983c7e56e4fb1e7e816c20", 41 63 "dependencies": [ 42 - "jsr:@std/encoding" 64 + "jsr:@std/encoding@^1.0.10" 43 65 ] 44 66 }, 45 67 "@fresh/core@2.2.0": { 46 68 "integrity": "b3c00f82288a2c4c8ec85e4abb67b080b366ec5971860f2f2898eb281ea1a80f", 47 69 "dependencies": [ 70 + "jsr:@deno/esbuild-plugin", 48 71 "jsr:@fresh/build-id", 72 + "jsr:@std/encoding@^1.0.10", 49 73 "jsr:@std/fmt", 50 74 "jsr:@std/fs", 51 75 "jsr:@std/html", 52 76 "jsr:@std/http", 77 + "jsr:@std/jsonc", 78 + "jsr:@std/media-types@^1.1.0", 53 79 "jsr:@std/path@^1.1.2", 80 + "jsr:@std/semver", 81 + "jsr:@std/uuid", 54 82 "npm:@opentelemetry/api", 55 83 "npm:@preact/signals", 56 - "npm:preact", 57 - "npm:preact-render-to-string" 84 + "npm:esbuild-wasm", 85 + "npm:esbuild@0.25.7", 86 + "npm:preact-render-to-string", 87 + "npm:preact@^10.27.0", 88 + "npm:preact@^10.27.2" 58 89 ] 59 90 }, 60 91 "@panva/jose@6.1.0": { ··· 65 96 "dependencies": [ 66 97 "jsr:@std/internal" 67 98 ] 99 + }, 100 + "@std/bytes@1.0.6": { 101 + "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" 68 102 }, 69 103 "@std/dotenv@0.225.6": { 70 104 "integrity": "1d6f9db72f565bd26790fa034c26e45ecb260b5245417be76c2279e5734c421b" ··· 85 119 "integrity": "4e2d693f474cae8c16a920fa5e15a3b72267b94b84667f11a50c6dd1cb18d35e" 86 120 }, 87 121 "@std/http@1.0.23": { 88 - "integrity": "6634e9e034c589bf35101c1b5ee5bbf052a5987abca20f903e58bdba85c80dee" 122 + "integrity": "6634e9e034c589bf35101c1b5ee5bbf052a5987abca20f903e58bdba85c80dee", 123 + "dependencies": [ 124 + "jsr:@std/encoding@^1.0.10" 125 + ] 89 126 }, 90 127 "@std/internal@1.0.12": { 91 128 "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" 129 + }, 130 + "@std/jsonc@1.0.2": { 131 + "integrity": "909605dae3af22bd75b1cbda8d64a32cf1fd2cf6efa3f9e224aba6d22c0f44c7" 92 132 }, 93 133 "@std/media-types@1.1.0": { 94 134 "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" ··· 99 139 "jsr:@std/internal" 100 140 ] 101 141 }, 142 + "@std/semver@1.0.7": { 143 + "integrity": "7d5f65391762dc4358abde80fc3354086ddb40101f140295e60f290c138887d0" 144 + }, 145 + "@std/uuid@1.1.0": { 146 + "integrity": "6268db2ccf172849c9be80763354ca305d49ef4af41fe995623d44fcc3f7457c", 147 + "dependencies": [ 148 + "jsr:@std/bytes" 149 + ] 150 + }, 102 151 "@tijs/atproto-oauth@2.5.1": { 103 152 "integrity": "7ecedb22b91b4e14f81bd729e749404286cd42b8d3636d93478d23033b354d75", 104 153 "dependencies": [ ··· 135 184 "@atproto/syntax@0.4.0": { 136 185 "integrity": "sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA==" 137 186 }, 187 + "@esbuild/aix-ppc64@0.25.7": { 188 + "integrity": "sha512-uD0kKFHh6ETr8TqEtaAcV+dn/2qnYbH/+8wGEdY70Qf7l1l/jmBUbrmQqwiPKAQE6cOQ7dTj6Xr0HzQDGHyceQ==", 189 + "os": ["aix"], 190 + "cpu": ["ppc64"] 191 + }, 138 192 "@esbuild/aix-ppc64@0.27.2": { 139 193 "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", 140 194 "os": ["aix"], 141 195 "cpu": ["ppc64"] 142 196 }, 197 + "@esbuild/android-arm64@0.25.7": { 198 + "integrity": "sha512-p0ohDnwyIbAtztHTNUTzN5EGD/HJLs1bwysrOPgSdlIA6NDnReoVfoCyxG6W1d85jr2X80Uq5KHftyYgaK9LPQ==", 199 + "os": ["android"], 200 + "cpu": ["arm64"] 201 + }, 143 202 "@esbuild/android-arm64@0.27.2": { 144 203 "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", 145 204 "os": ["android"], 146 205 "cpu": ["arm64"] 147 206 }, 207 + "@esbuild/android-arm@0.25.7": { 208 + "integrity": "sha512-Jhuet0g1k9rAJHrXGIh7sFknFuT4sfytYZpZpuZl7YKDhnPByVAm5oy2LEBmMbuYf3ejWVYCc2seX81Mk+madA==", 209 + "os": ["android"], 210 + "cpu": ["arm"] 211 + }, 148 212 "@esbuild/android-arm@0.27.2": { 149 213 "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", 150 214 "os": ["android"], 151 215 "cpu": ["arm"] 152 216 }, 217 + "@esbuild/android-x64@0.25.7": { 218 + "integrity": "sha512-mMxIJFlSgVK23HSsII3ZX9T2xKrBCDGyk0qiZnIW10LLFFtZLkFD6imZHu7gUo2wkNZwS9Yj3mOtZD3ZPcjCcw==", 219 + "os": ["android"], 220 + "cpu": ["x64"] 221 + }, 153 222 "@esbuild/android-x64@0.27.2": { 154 223 "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", 155 224 "os": ["android"], 156 225 "cpu": ["x64"] 157 226 }, 227 + "@esbuild/darwin-arm64@0.25.7": { 228 + "integrity": "sha512-jyOFLGP2WwRwxM8F1VpP6gcdIJc8jq2CUrURbbTouJoRO7XCkU8GdnTDFIHdcifVBT45cJlOYsZ1kSlfbKjYUQ==", 229 + "os": ["darwin"], 230 + "cpu": ["arm64"] 231 + }, 158 232 "@esbuild/darwin-arm64@0.27.2": { 159 233 "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", 160 234 "os": ["darwin"], 161 235 "cpu": ["arm64"] 236 + }, 237 + "@esbuild/darwin-x64@0.25.7": { 238 + "integrity": "sha512-m9bVWqZCwQ1BthruifvG64hG03zzz9gE2r/vYAhztBna1/+qXiHyP9WgnyZqHgGeXoimJPhAmxfbeU+nMng6ZA==", 239 + "os": ["darwin"], 240 + "cpu": ["x64"] 162 241 }, 163 242 "@esbuild/darwin-x64@0.27.2": { 164 243 "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", 165 244 "os": ["darwin"], 166 245 "cpu": ["x64"] 167 246 }, 247 + "@esbuild/freebsd-arm64@0.25.7": { 248 + "integrity": "sha512-Bss7P4r6uhr3kDzRjPNEnTm/oIBdTPRNQuwaEFWT/uvt6A1YzK/yn5kcx5ZxZ9swOga7LqeYlu7bDIpDoS01bA==", 249 + "os": ["freebsd"], 250 + "cpu": ["arm64"] 251 + }, 168 252 "@esbuild/freebsd-arm64@0.27.2": { 169 253 "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", 170 254 "os": ["freebsd"], 171 255 "cpu": ["arm64"] 172 256 }, 257 + "@esbuild/freebsd-x64@0.25.7": { 258 + "integrity": "sha512-S3BFyjW81LXG7Vqmr37ddbThrm3A84yE7ey/ERBlK9dIiaWgrjRlre3pbG7txh1Uaxz8N7wGGQXmC9zV+LIpBQ==", 259 + "os": ["freebsd"], 260 + "cpu": ["x64"] 261 + }, 173 262 "@esbuild/freebsd-x64@0.27.2": { 174 263 "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", 175 264 "os": ["freebsd"], 176 265 "cpu": ["x64"] 177 266 }, 267 + "@esbuild/linux-arm64@0.25.7": { 268 + "integrity": "sha512-HfQZQqrNOfS1Okn7PcsGUqHymL1cWGBslf78dGvtrj8q7cN3FkapFgNA4l/a5lXDwr7BqP2BSO6mz9UremNPbg==", 269 + "os": ["linux"], 270 + "cpu": ["arm64"] 271 + }, 178 272 "@esbuild/linux-arm64@0.27.2": { 179 273 "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", 180 274 "os": ["linux"], 181 275 "cpu": ["arm64"] 182 276 }, 277 + "@esbuild/linux-arm@0.25.7": { 278 + "integrity": "sha512-JZMIci/1m5vfQuhKoFXogCKVYVfYQmoZJg8vSIMR4TUXbF+0aNlfXH3DGFEFMElT8hOTUF5hisdZhnrZO/bkDw==", 279 + "os": ["linux"], 280 + "cpu": ["arm"] 281 + }, 183 282 "@esbuild/linux-arm@0.27.2": { 184 283 "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", 185 284 "os": ["linux"], 186 285 "cpu": ["arm"] 187 286 }, 287 + "@esbuild/linux-ia32@0.25.7": { 288 + "integrity": "sha512-9Jex4uVpdeofiDxnwHRgen+j6398JlX4/6SCbbEFEXN7oMO2p0ueLN+e+9DdsdPLUdqns607HmzEFnxwr7+5wQ==", 289 + "os": ["linux"], 290 + "cpu": ["ia32"] 291 + }, 188 292 "@esbuild/linux-ia32@0.27.2": { 189 293 "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", 190 294 "os": ["linux"], 191 295 "cpu": ["ia32"] 192 296 }, 297 + "@esbuild/linux-loong64@0.25.7": { 298 + "integrity": "sha512-TG1KJqjBlN9IHQjKVUYDB0/mUGgokfhhatlay8aZ/MSORMubEvj/J1CL8YGY4EBcln4z7rKFbsH+HeAv0d471w==", 299 + "os": ["linux"], 300 + "cpu": ["loong64"] 301 + }, 193 302 "@esbuild/linux-loong64@0.27.2": { 194 303 "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", 195 304 "os": ["linux"], 196 305 "cpu": ["loong64"] 197 306 }, 307 + "@esbuild/linux-mips64el@0.25.7": { 308 + "integrity": "sha512-Ty9Hj/lx7ikTnhOfaP7ipEm/ICcBv94i/6/WDg0OZ3BPBHhChsUbQancoWYSO0WNkEiSW5Do4febTTy4x1qYQQ==", 309 + "os": ["linux"], 310 + "cpu": ["mips64el"] 311 + }, 198 312 "@esbuild/linux-mips64el@0.27.2": { 199 313 "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", 200 314 "os": ["linux"], 201 315 "cpu": ["mips64el"] 202 316 }, 317 + "@esbuild/linux-ppc64@0.25.7": { 318 + "integrity": "sha512-MrOjirGQWGReJl3BNQ58BLhUBPpWABnKrnq8Q/vZWWwAB1wuLXOIxS2JQ1LT3+5T+3jfPh0tyf5CpbyQHqnWIQ==", 319 + "os": ["linux"], 320 + "cpu": ["ppc64"] 321 + }, 203 322 "@esbuild/linux-ppc64@0.27.2": { 204 323 "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", 205 324 "os": ["linux"], 206 325 "cpu": ["ppc64"] 207 326 }, 327 + "@esbuild/linux-riscv64@0.25.7": { 328 + "integrity": "sha512-9pr23/pqzyqIZEZmQXnFyqp3vpa+KBk5TotfkzGMqpw089PGm0AIowkUppHB9derQzqniGn3wVXgck19+oqiOw==", 329 + "os": ["linux"], 330 + "cpu": ["riscv64"] 331 + }, 208 332 "@esbuild/linux-riscv64@0.27.2": { 209 333 "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", 210 334 "os": ["linux"], 211 335 "cpu": ["riscv64"] 212 336 }, 337 + "@esbuild/linux-s390x@0.25.7": { 338 + "integrity": "sha512-4dP11UVGh9O6Y47m8YvW8eoA3r8qL2toVZUbBKyGta8j6zdw1cn9F/Rt59/Mhv0OgY68pHIMjGXWOUaykCnx+w==", 339 + "os": ["linux"], 340 + "cpu": ["s390x"] 341 + }, 213 342 "@esbuild/linux-s390x@0.27.2": { 214 343 "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", 215 344 "os": ["linux"], 216 345 "cpu": ["s390x"] 217 346 }, 347 + "@esbuild/linux-x64@0.25.7": { 348 + "integrity": "sha512-ghJMAJTdw/0uhz7e7YnpdX1xVn7VqA0GrWrAO2qKMuqbvgHT2VZiBv1BQ//VcHsPir4wsL3P2oPggfKPzTKoCA==", 349 + "os": ["linux"], 350 + "cpu": ["x64"] 351 + }, 218 352 "@esbuild/linux-x64@0.27.2": { 219 353 "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", 220 354 "os": ["linux"], 221 355 "cpu": ["x64"] 222 356 }, 357 + "@esbuild/netbsd-arm64@0.25.7": { 358 + "integrity": "sha512-bwXGEU4ua45+u5Ci/a55B85KWaDSRS8NPOHtxy2e3etDjbz23wlry37Ffzapz69JAGGc4089TBo+dGzydQmydg==", 359 + "os": ["netbsd"], 360 + "cpu": ["arm64"] 361 + }, 223 362 "@esbuild/netbsd-arm64@0.27.2": { 224 363 "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", 225 364 "os": ["netbsd"], 226 365 "cpu": ["arm64"] 227 366 }, 367 + "@esbuild/netbsd-x64@0.25.7": { 368 + "integrity": "sha512-tUZRvLtgLE5OyN46sPSYlgmHoBS5bx2URSrgZdW1L1teWPYVmXh+QN/sKDqkzBo/IHGcKcHLKDhBeVVkO7teEA==", 369 + "os": ["netbsd"], 370 + "cpu": ["x64"] 371 + }, 228 372 "@esbuild/netbsd-x64@0.27.2": { 229 373 "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", 230 374 "os": ["netbsd"], 231 375 "cpu": ["x64"] 376 + }, 377 + "@esbuild/openbsd-arm64@0.25.7": { 378 + "integrity": "sha512-bTJ50aoC+WDlDGBReWYiObpYvQfMjBNlKztqoNUL0iUkYtwLkBQQeEsTq/I1KyjsKA5tyov6VZaPb8UdD6ci6Q==", 379 + "os": ["openbsd"], 380 + "cpu": ["arm64"] 232 381 }, 233 382 "@esbuild/openbsd-arm64@0.27.2": { 234 383 "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", 235 384 "os": ["openbsd"], 236 385 "cpu": ["arm64"] 237 386 }, 387 + "@esbuild/openbsd-x64@0.25.7": { 388 + "integrity": "sha512-TA9XfJrgzAipFUU895jd9j2SyDh9bbNkK2I0gHcvqb/o84UeQkBpi/XmYX3cO1q/9hZokdcDqQxIi6uLVrikxg==", 389 + "os": ["openbsd"], 390 + "cpu": ["x64"] 391 + }, 238 392 "@esbuild/openbsd-x64@0.27.2": { 239 393 "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", 240 394 "os": ["openbsd"], 241 395 "cpu": ["x64"] 242 396 }, 397 + "@esbuild/openharmony-arm64@0.25.7": { 398 + "integrity": "sha512-5VTtExUrWwHHEUZ/N+rPlHDwVFQ5aME7vRJES8+iQ0xC/bMYckfJ0l2n3yGIfRoXcK/wq4oXSItZAz5wslTKGw==", 399 + "os": ["openharmony"], 400 + "cpu": ["arm64"] 401 + }, 243 402 "@esbuild/openharmony-arm64@0.27.2": { 244 403 "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", 245 404 "os": ["openharmony"], 246 405 "cpu": ["arm64"] 247 406 }, 407 + "@esbuild/sunos-x64@0.25.7": { 408 + "integrity": "sha512-umkbn7KTxsexhv2vuuJmj9kggd4AEtL32KodkJgfhNOHMPtQ55RexsaSrMb+0+jp9XL4I4o2y91PZauVN4cH3A==", 409 + "os": ["sunos"], 410 + "cpu": ["x64"] 411 + }, 248 412 "@esbuild/sunos-x64@0.27.2": { 249 413 "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", 250 414 "os": ["sunos"], 251 415 "cpu": ["x64"] 252 416 }, 417 + "@esbuild/win32-arm64@0.25.7": { 418 + "integrity": "sha512-j20JQGP/gz8QDgzl5No5Gr4F6hurAZvtkFxAKhiv2X49yi/ih8ECK4Y35YnjlMogSKJk931iNMcd35BtZ4ghfw==", 419 + "os": ["win32"], 420 + "cpu": ["arm64"] 421 + }, 253 422 "@esbuild/win32-arm64@0.27.2": { 254 423 "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", 255 424 "os": ["win32"], 256 425 "cpu": ["arm64"] 257 426 }, 427 + "@esbuild/win32-ia32@0.25.7": { 428 + "integrity": "sha512-4qZ6NUfoiiKZfLAXRsvFkA0hoWVM+1y2bSHXHkpdLAs/+r0LgwqYohmfZCi985c6JWHhiXP30mgZawn/XrqAkQ==", 429 + "os": ["win32"], 430 + "cpu": ["ia32"] 431 + }, 258 432 "@esbuild/win32-ia32@0.27.2": { 259 433 "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", 260 434 "os": ["win32"], 261 435 "cpu": ["ia32"] 436 + }, 437 + "@esbuild/win32-x64@0.25.7": { 438 + "integrity": "sha512-FaPsAHTwm+1Gfvn37Eg3E5HIpfR3i6x1AIcla/MkqAIupD4BW3MrSeUqfoTzwwJhk3WE2/KqUn4/eenEJC76VA==", 439 + "os": ["win32"], 440 + "cpu": ["x64"] 262 441 }, 263 442 "@esbuild/win32-x64@0.27.2": { 264 443 "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", ··· 395 574 "detect-libc@2.0.2": { 396 575 "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==" 397 576 }, 577 + "esbuild-wasm@0.25.12": { 578 + "integrity": "sha512-rZqkjL3Y6FwLpSHzLnaEy8Ps6veCNo1kZa9EOfJvmWtBq5dJH4iVjfmOO6Mlkv9B0tt9WFPFmb/VxlgJOnueNg==", 579 + "bin": true 580 + }, 581 + "esbuild@0.25.7": { 582 + "integrity": "sha512-daJB0q2dmTzo90L9NjRaohhRWrCzYxWNFTjEi72/h+p5DcY3yn4MacWfDakHmaBaDzDiuLJsCh0+6LK/iX+c+Q==", 583 + "optionalDependencies": [ 584 + "@esbuild/aix-ppc64@0.25.7", 585 + "@esbuild/android-arm@0.25.7", 586 + "@esbuild/android-arm64@0.25.7", 587 + "@esbuild/android-x64@0.25.7", 588 + "@esbuild/darwin-arm64@0.25.7", 589 + "@esbuild/darwin-x64@0.25.7", 590 + "@esbuild/freebsd-arm64@0.25.7", 591 + "@esbuild/freebsd-x64@0.25.7", 592 + "@esbuild/linux-arm@0.25.7", 593 + "@esbuild/linux-arm64@0.25.7", 594 + "@esbuild/linux-ia32@0.25.7", 595 + "@esbuild/linux-loong64@0.25.7", 596 + "@esbuild/linux-mips64el@0.25.7", 597 + "@esbuild/linux-ppc64@0.25.7", 598 + "@esbuild/linux-riscv64@0.25.7", 599 + "@esbuild/linux-s390x@0.25.7", 600 + "@esbuild/linux-x64@0.25.7", 601 + "@esbuild/netbsd-arm64@0.25.7", 602 + "@esbuild/netbsd-x64@0.25.7", 603 + "@esbuild/openbsd-arm64@0.25.7", 604 + "@esbuild/openbsd-x64@0.25.7", 605 + "@esbuild/openharmony-arm64@0.25.7", 606 + "@esbuild/sunos-x64@0.25.7", 607 + "@esbuild/win32-arm64@0.25.7", 608 + "@esbuild/win32-ia32@0.25.7", 609 + "@esbuild/win32-x64@0.25.7" 610 + ], 611 + "scripts": true, 612 + "bin": true 613 + }, 398 614 "esbuild@0.27.2": { 399 615 "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", 400 616 "optionalDependencies": [ 401 - "@esbuild/aix-ppc64", 402 - "@esbuild/android-arm", 403 - "@esbuild/android-arm64", 404 - "@esbuild/android-x64", 405 - "@esbuild/darwin-arm64", 406 - "@esbuild/darwin-x64", 407 - "@esbuild/freebsd-arm64", 408 - "@esbuild/freebsd-x64", 409 - "@esbuild/linux-arm", 410 - "@esbuild/linux-arm64", 411 - "@esbuild/linux-ia32", 412 - "@esbuild/linux-loong64", 413 - "@esbuild/linux-mips64el", 414 - "@esbuild/linux-ppc64", 415 - "@esbuild/linux-riscv64", 416 - "@esbuild/linux-s390x", 417 - "@esbuild/linux-x64", 418 - "@esbuild/netbsd-arm64", 419 - "@esbuild/netbsd-x64", 420 - "@esbuild/openbsd-arm64", 421 - "@esbuild/openbsd-x64", 422 - "@esbuild/openharmony-arm64", 423 - "@esbuild/sunos-x64", 424 - "@esbuild/win32-arm64", 425 - "@esbuild/win32-ia32", 426 - "@esbuild/win32-x64" 617 + "@esbuild/aix-ppc64@0.27.2", 618 + "@esbuild/android-arm@0.27.2", 619 + "@esbuild/android-arm64@0.27.2", 620 + "@esbuild/android-x64@0.27.2", 621 + "@esbuild/darwin-arm64@0.27.2", 622 + "@esbuild/darwin-x64@0.27.2", 623 + "@esbuild/freebsd-arm64@0.27.2", 624 + "@esbuild/freebsd-x64@0.27.2", 625 + "@esbuild/linux-arm@0.27.2", 626 + "@esbuild/linux-arm64@0.27.2", 627 + "@esbuild/linux-ia32@0.27.2", 628 + "@esbuild/linux-loong64@0.27.2", 629 + "@esbuild/linux-mips64el@0.27.2", 630 + "@esbuild/linux-ppc64@0.27.2", 631 + "@esbuild/linux-riscv64@0.27.2", 632 + "@esbuild/linux-s390x@0.27.2", 633 + "@esbuild/linux-x64@0.27.2", 634 + "@esbuild/netbsd-arm64@0.27.2", 635 + "@esbuild/netbsd-x64@0.27.2", 636 + "@esbuild/openbsd-arm64@0.27.2", 637 + "@esbuild/openbsd-x64@0.27.2", 638 + "@esbuild/openharmony-arm64@0.27.2", 639 + "@esbuild/sunos-x64@0.27.2", 640 + "@esbuild/win32-arm64@0.27.2", 641 + "@esbuild/win32-ia32@0.27.2", 642 + "@esbuild/win32-x64@0.27.2" 427 643 ], 428 644 "scripts": true, 429 645 "bin": true
+32 -17
frontend/components/ReadingList.tsx
··· 31 31 return ( 32 32 <article 33 33 onClick={handleClick} 34 - className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 cursor-pointer transition-shadow hover:shadow-md" 34 + className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden cursor-pointer transition-shadow hover:shadow-md" 35 35 > 36 - <h3 className="text-lg font-semibold text-gray-900 mb-2 line-clamp-2"> 37 - {bookmark.title || domain} 38 - </h3> 39 - {bookmark.description && ( 40 - <p className="text-gray-600 text-sm mb-4 line-clamp-3"> 41 - {bookmark.description} 42 - </p> 43 - )} 44 - <div className="flex items-center gap-2 text-sm text-gray-500"> 45 - {bookmark.favicon && ( 36 + {bookmark.image && ( 37 + <div className="aspect-[2/1] bg-gray-100 overflow-hidden"> 46 38 <img 47 - src={bookmark.favicon} 39 + src={bookmark.image} 48 40 alt="" 49 - className="w-4 h-4" 41 + className="w-full h-full object-cover" 50 42 onError={(e) => { 51 - (e.target as HTMLImageElement).style.display = "none"; 43 + (e.target as HTMLImageElement).parentElement!.style.display = 44 + "none"; 52 45 }} 53 46 /> 47 + </div> 48 + )} 49 + <div className="p-6"> 50 + <h3 className="text-lg font-semibold text-gray-900 mb-2 line-clamp-2"> 51 + {bookmark.title || domain} 52 + </h3> 53 + {bookmark.description && ( 54 + <p className="text-gray-600 text-sm mb-4 line-clamp-3"> 55 + {bookmark.description} 56 + </p> 54 57 )} 55 - <span>{domain}</span> 56 - <span className="text-gray-300">|</span> 57 - <span>{formattedDate}</span> 58 + <div className="flex items-center gap-2 text-sm text-gray-500"> 59 + {bookmark.favicon && ( 60 + <img 61 + src={bookmark.favicon} 62 + alt="" 63 + className="w-4 h-4" 64 + onError={(e) => { 65 + (e.target as HTMLImageElement).style.display = "none"; 66 + }} 67 + /> 68 + )} 69 + <span>{domain}</span> 70 + <span className="text-gray-300">|</span> 71 + <span>{formattedDate}</span> 72 + </div> 58 73 </div> 59 74 </article> 60 75 );
+44 -1
frontend/context/AppContext.tsx
··· 2 2 createContext, 3 3 type ReactNode, 4 4 useContext, 5 + useEffect, 5 6 useMemo, 7 + useRef, 6 8 useState, 7 9 } from "react"; 8 10 import type { ··· 12 14 SessionInfo, 13 15 UserSettings, 14 16 } from "../../shared/types.ts"; 15 - import { apiGet, apiPatch } from "../utils/api.ts"; 17 + import { apiGet, apiPatch, apiPost } from "../utils/api.ts"; 16 18 17 19 const DEFAULT_SETTINGS: UserSettings = { 18 20 readingListTag: "toread", ··· 239 241 [...readingListSelectedTags].every((tag) => b.tags?.includes(tag)) 240 242 ); 241 243 }, [readingListBookmarks, readingListSelectedTags]); 244 + 245 + // Track which bookmarks are being enriched to avoid duplicate requests 246 + const enrichingRef = useRef<Set<string>>(new Set()); 247 + 248 + // Background re-enrichment for reading list bookmarks missing images 249 + useEffect(() => { 250 + const bookmarksNeedingEnrichment = readingListBookmarks.filter( 251 + (b) => !b.image && !enrichingRef.current.has(b.uri), 252 + ); 253 + 254 + if (bookmarksNeedingEnrichment.length === 0) return; 255 + 256 + // Rate-limit: enrich up to 3 bookmarks at a time with delay between batches 257 + const enrichBatch = async () => { 258 + const batch = bookmarksNeedingEnrichment.slice(0, 3); 259 + 260 + for (const bookmark of batch) { 261 + enrichingRef.current.add(bookmark.uri); 262 + 263 + try { 264 + // Extract rkey from URI: at://did/collection/rkey 265 + const rkey = bookmark.uri.split("/").pop(); 266 + if (!rkey) continue; 267 + 268 + const response = await apiPost(`/api/bookmarks/${rkey}/enrich`); 269 + if (response.ok) { 270 + const data = await response.json(); 271 + if (data.bookmark) { 272 + updateBookmark(data.bookmark); 273 + } 274 + } 275 + } catch (err) { 276 + console.error("Failed to enrich bookmark:", err); 277 + } 278 + } 279 + }; 280 + 281 + // Delay slightly to not block initial render 282 + const timeoutId = setTimeout(enrichBatch, 1000); 283 + return () => clearTimeout(timeoutId); 284 + }, [readingListBookmarks]); 242 285 243 286 const value: AppContextValue = { 244 287 // State
+51 -8
lib/enrichment.ts
··· 4 4 /** Maximum lengths for metadata fields */ 5 5 const MAX_TITLE_LENGTH = 200; 6 6 const MAX_DESCRIPTION_LENGTH = 500; 7 + const MAX_IMAGE_URL_LENGTH = 2000; 7 8 8 9 /** 9 10 * Check if a hostname points to a private/internal IP range. ··· 59 60 } 60 61 61 62 /** 62 - * Validate favicon URL. 63 + * Validate and sanitize a URL (for favicon, image, etc). 63 64 * Only allow http/https URLs, block javascript:, data:, etc. 64 65 */ 65 - function sanitizeFaviconUrl( 66 - faviconUrl: string, 66 + function sanitizeUrl( 67 + urlString: string, 67 68 baseUrl: URL, 69 + maxLength: number = MAX_IMAGE_URL_LENGTH, 68 70 ): string | undefined { 69 71 try { 70 - const resolved = new URL(faviconUrl, baseUrl); 72 + const resolved = new URL(urlString, baseUrl); 71 73 // Only allow http/https protocols 72 74 if (resolved.protocol === "http:" || resolved.protocol === "https:") { 73 - return resolved.href; 75 + const href = resolved.href; 76 + return href.length <= maxLength ? href : undefined; 74 77 } 75 78 } catch { 76 79 // Invalid URL ··· 79 82 } 80 83 81 84 /** 85 + * Validate favicon URL. 86 + * Only allow http/https URLs, block javascript:, data:, etc. 87 + */ 88 + function sanitizeFaviconUrl( 89 + faviconUrl: string, 90 + baseUrl: URL, 91 + ): string | undefined { 92 + return sanitizeUrl(faviconUrl, baseUrl); 93 + } 94 + 95 + /** 82 96 * Extracts metadata from a URL by fetching and parsing the HTML. 83 97 */ 84 98 export function extractUrlMetadata(url: string): Promise<UrlMetadata> { ··· 154 168 metadata.title = sanitizeText(decode(titleMatch[1]), MAX_TITLE_LENGTH); 155 169 } 156 170 157 - // Try og:title as fallback 171 + // Try og:title as fallback (handle both attribute orderings) 158 172 if (!metadata.title) { 159 173 const ogTitleMatch = html.match( 160 174 /<meta[^>]+property=["']og:title["'][^>]+content=["']([^"']+)["']/i, 175 + ) || html.match( 176 + /<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:title["']/i, 161 177 ); 162 178 if (ogTitleMatch) { 163 179 metadata.title = sanitizeText(decode(ogTitleMatch[1]), MAX_TITLE_LENGTH); 164 180 } 165 181 } 166 182 167 - // Extract description from meta tags 183 + // Extract description from meta tags (handle both attribute orderings) 168 184 const descMatch = html.match( 169 185 /<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["']/i, 186 + ) || html.match( 187 + /<meta[^>]+content=["']([^"']+)["'][^>]+name=["']description["']/i, 170 188 ); 171 189 if (descMatch) { 172 190 metadata.description = sanitizeText( ··· 175 193 ); 176 194 } 177 195 178 - // Try og:description as fallback 196 + // Try og:description as fallback (handle both attribute orderings) 179 197 if (!metadata.description) { 180 198 const ogDescMatch = html.match( 181 199 /<meta[^>]+property=["']og:description["'][^>]+content=["']([^"']+)["']/i, 200 + ) || html.match( 201 + /<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:description["']/i, 182 202 ); 183 203 if (ogDescMatch) { 184 204 metadata.description = sanitizeText( ··· 199 219 // Default to hostname/favicon.ico if no valid favicon found 200 220 if (!metadata.favicon) { 201 221 metadata.favicon = new URL("/favicon.ico", url.origin).href; 222 + } 223 + 224 + // Extract preview image - try og:image first 225 + // Handle both attribute orderings: property before content, or content before property 226 + const ogImageMatch = html.match( 227 + /<meta[^>]+property=["']og:image["'][^>]+content=["']([^"']+)["']/i, 228 + ) || html.match( 229 + /<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:image["']/i, 230 + ); 231 + if (ogImageMatch) { 232 + metadata.image = sanitizeUrl(ogImageMatch[1], url); 233 + } 234 + 235 + // Try twitter:image as fallback 236 + if (!metadata.image) { 237 + const twitterImageMatch = html.match( 238 + /<meta[^>]+name=["']twitter:image["'][^>]+content=["']([^"']+)["']/i, 239 + ) || html.match( 240 + /<meta[^>]+content=["']([^"']+)["'][^>]+name=["']twitter:image["']/i, 241 + ); 242 + if (twitterImageMatch) { 243 + metadata.image = sanitizeUrl(twitterImageMatch[1], url); 244 + } 202 245 } 203 246 204 247 // Use hostname as fallback title
+14 -8
lib/oauth-config.ts
··· 10 10 11 11 // OAuth instance and base URL - initialized lazily 12 12 let oauth: ReturnType<typeof createATProtoOAuth> | null = null; 13 - let baseUrl: string | null = Deno.env.get("BASE_URL") || null; 13 + let baseUrl: string | null = null; 14 14 15 15 /** 16 16 * Get the base URL. Must be called after initOAuth(). ··· 38 38 throw new Error("COOKIE_SECRET environment variable is required"); 39 39 } 40 40 41 - // Derive base URL from request if not set in environment 41 + // Use BASE_URL from environment, or derive from request if not set 42 42 if (!baseUrl) { 43 - const url = new URL(request.url); 44 - // Check for X-Forwarded-Proto header (set by ngrok and other proxies) 45 - const forwardedProto = request.headers.get("X-Forwarded-Proto"); 46 - const protocol = forwardedProto || url.protocol.replace(":", ""); 47 - baseUrl = `${protocol}://${url.host}`; 48 - console.log(`Derived BASE_URL from request: ${baseUrl}`); 43 + const envBaseUrl = Deno.env.get("BASE_URL"); 44 + if (envBaseUrl) { 45 + baseUrl = envBaseUrl; 46 + console.log(`Using BASE_URL from environment: ${baseUrl}`); 47 + } else { 48 + const url = new URL(request.url); 49 + // Check for X-Forwarded-Proto header (set by ngrok and other proxies) 50 + const forwardedProto = request.headers.get("X-Forwarded-Proto"); 51 + const protocol = forwardedProto || url.protocol.replace(":", ""); 52 + baseUrl = `${protocol}://${url.host}`; 53 + console.log(`Derived BASE_URL from request: ${baseUrl}`); 54 + } 49 55 } 50 56 51 57 // Create OAuth integration with SQLiteStorage
+90
routes/api/bookmarks.ts
··· 59 59 title: record.value.$enriched?.title || record.value.title, 60 60 description: record.value.$enriched?.description, 61 61 favicon: record.value.$enriched?.favicon, 62 + image: record.value.$enriched?.image, 62 63 })); 63 64 64 65 const result: ListBookmarksResponse = { bookmarks }; ··· 109 110 title: metadata.title, 110 111 description: metadata.description, 111 112 favicon: metadata.favicon, 113 + image: metadata.image, 112 114 }, 113 115 }; 114 116 ··· 140 142 title: metadata.title, 141 143 description: metadata.description, 142 144 favicon: metadata.favicon, 145 + image: metadata.image, 143 146 }; 144 147 145 148 const result: AddBookmarkResponse = { success: true, bookmark }; ··· 246 249 title: record.$enriched?.title, 247 250 description: record.$enriched?.description, 248 251 favicon: record.$enriched?.favicon, 252 + image: record.$enriched?.image, 249 253 }; 250 254 251 255 // Check if should send to Instapaper ··· 269 273 return setSessionCookie(Response.json(result), setCookieHeader); 270 274 } catch (error: any) { 271 275 console.error("Error updating bookmark tags:", error); 276 + return Response.json({ error: error.message }, { status: 500 }); 277 + } 278 + }); 279 + 280 + // Re-enrich bookmark (refresh metadata from URL) 281 + app = app.post("/api/bookmarks/:rkey/enrich", async (ctx) => { 282 + try { 283 + const { session: oauthSession, setCookieHeader, error } = 284 + await getSessionFromRequest(ctx.req); 285 + if (!oauthSession) { 286 + return createAuthErrorResponse(error); 287 + } 288 + 289 + const rkey = ctx.params.rkey; 290 + 291 + // Get current record 292 + const getParams = new URLSearchParams({ 293 + repo: oauthSession.did, 294 + collection: BOOKMARK_COLLECTION, 295 + rkey, 296 + }); 297 + const getResponse = await oauthSession.makeRequest( 298 + "GET", 299 + `${oauthSession.pdsUrl}/xrpc/com.atproto.repo.getRecord?${getParams}`, 300 + ); 301 + 302 + if (!getResponse.ok) { 303 + const errorText = await getResponse.text(); 304 + throw new Error(`Failed to get record: ${errorText}`); 305 + } 306 + 307 + const currentRecord = await getResponse.json(); 308 + const url = currentRecord.value.subject; 309 + 310 + // Re-fetch metadata 311 + const metadata = await extractUrlMetadata(url); 312 + 313 + // Update record with new enrichment data 314 + const record = { 315 + ...currentRecord.value, 316 + $enriched: { 317 + title: metadata.title, 318 + description: metadata.description, 319 + favicon: metadata.favicon, 320 + image: metadata.image, 321 + }, 322 + }; 323 + 324 + const response = await oauthSession.makeRequest( 325 + "POST", 326 + `${oauthSession.pdsUrl}/xrpc/com.atproto.repo.putRecord`, 327 + { 328 + headers: { "Content-Type": "application/json" }, 329 + body: JSON.stringify({ 330 + repo: oauthSession.did, 331 + collection: BOOKMARK_COLLECTION, 332 + rkey, 333 + record, 334 + }), 335 + }, 336 + ); 337 + 338 + if (!response.ok) { 339 + const errorText = await response.text(); 340 + throw new Error(`Failed to update record: ${errorText}`); 341 + } 342 + 343 + const data = await response.json(); 344 + const bookmark: EnrichedBookmark = { 345 + uri: data.uri, 346 + cid: data.cid, 347 + subject: record.subject, 348 + createdAt: record.createdAt, 349 + tags: record.tags || [], 350 + title: record.$enriched?.title, 351 + description: record.$enriched?.description, 352 + favicon: record.$enriched?.favicon, 353 + image: record.$enriched?.image, 354 + }; 355 + 356 + return setSessionCookie( 357 + Response.json({ success: true, bookmark }), 358 + setCookieHeader, 359 + ); 360 + } catch (error: any) { 361 + console.error("Error re-enriching bookmark:", error); 272 362 return Response.json({ error: error.message }, { status: 500 }); 273 363 } 274 364 });
+1
routes/api/initial-data.ts
··· 64 64 title: record.value.$enriched?.title || record.value.title, 65 65 description: record.value.$enriched?.description, 66 66 favicon: record.value.$enriched?.favicon, 67 + image: record.value.$enriched?.image, 67 68 })); 68 69 } 69 70
+1
routes/api/share.ts
··· 75 75 title: record.value.$enriched?.title || record.value.title, 76 76 description: record.value.$enriched?.description, 77 77 favicon: record.value.$enriched?.favicon, 78 + image: record.value.$enriched?.image, 78 79 })); 79 80 80 81 const filteredBookmarks = allBookmarks.filter((bookmark) =>
+1
routes/share/rss.ts
··· 68 68 title: record.value.$enriched?.title, 69 69 description: record.value.$enriched?.description, 70 70 favicon: record.value.$enriched?.favicon, 71 + image: record.value.$enriched?.image, 71 72 })) 72 73 .sort( 73 74 (a: any, b: any) =>
+4 -2
shared/types.ts
··· 12 12 uri: string; // AT Protocol URI for this record 13 13 cid: string; // Content ID 14 14 title?: string; // Extracted page title 15 - description?: string; // Extracted meta description (future) 16 - favicon?: string; // Extracted favicon URL (future) 15 + description?: string; // Extracted meta description 16 + favicon?: string; // Extracted favicon URL 17 + image?: string; // Preview image (og:image) 17 18 } 18 19 19 20 // API request/response types ··· 36 37 title?: string; 37 38 description?: string; 38 39 favicon?: string; 40 + image?: string; // Preview image (og:image) 39 41 } 40 42 41 43 // Session info