opengraph images

Orual 89adf83c b73b1f86

+950 -5
+388 -2
Cargo.lock
··· 187 187 checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" 188 188 189 189 [[package]] 190 + name = "arrayref" 191 + version = "0.3.9" 192 + source = "registry+https://github.com/rust-lang/crates.io-index" 193 + checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" 194 + 195 + [[package]] 190 196 name = "arrayvec" 191 197 version = "0.7.6" 192 198 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 226 232 ] 227 233 228 234 [[package]] 235 + name = "askama" 236 + version = "0.12.1" 237 + source = "registry+https://github.com/rust-lang/crates.io-index" 238 + checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" 239 + dependencies = [ 240 + "askama_derive", 241 + "askama_escape 0.10.3", 242 + "humansize", 243 + "num-traits", 244 + "percent-encoding", 245 + ] 246 + 247 + [[package]] 248 + name = "askama_derive" 249 + version = "0.12.5" 250 + source = "registry+https://github.com/rust-lang/crates.io-index" 251 + checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" 252 + dependencies = [ 253 + "askama_parser", 254 + "basic-toml", 255 + "mime", 256 + "mime_guess", 257 + "proc-macro2", 258 + "quote", 259 + "serde", 260 + "syn 2.0.110", 261 + ] 262 + 263 + [[package]] 264 + name = "askama_escape" 265 + version = "0.10.3" 266 + source = "registry+https://github.com/rust-lang/crates.io-index" 267 + checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" 268 + 269 + [[package]] 229 270 name = "askama_escape" 230 271 version = "0.13.0" 231 272 source = "registry+https://github.com/rust-lang/crates.io-index" 232 273 checksum = "3df27b8d5ddb458c5fb1bbc1ce172d4a38c614a97d550b0ac89003897fb01de4" 233 274 234 275 [[package]] 276 + name = "askama_parser" 277 + version = "0.2.1" 278 + source = "registry+https://github.com/rust-lang/crates.io-index" 279 + checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" 280 + dependencies = [ 281 + "nom", 282 + ] 283 + 284 + [[package]] 235 285 name = "ast_node" 236 286 version = "3.0.4" 237 287 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 539 589 checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" 540 590 541 591 [[package]] 592 + name = "basic-toml" 593 + version = "0.1.10" 594 + source = "registry+https://github.com/rust-lang/crates.io-index" 595 + checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" 596 + dependencies = [ 597 + "serde", 598 + ] 599 + 600 + [[package]] 542 601 name = "better_scoped_tls" 543 602 version = "1.0.1" 544 603 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 734 793 ] 735 794 736 795 [[package]] 796 + name = "bytemuck" 797 + version = "1.24.0" 798 + source = "registry+https://github.com/rust-lang/crates.io-index" 799 + checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" 800 + 801 + [[package]] 737 802 name = "byteorder" 738 803 version = "1.5.0" 739 804 source = "registry+https://github.com/rust-lang/crates.io-index" 740 805 checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 806 + 807 + [[package]] 808 + name = "byteorder-lite" 809 + version = "0.1.0" 810 + source = "registry+https://github.com/rust-lang/crates.io-index" 811 + checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" 741 812 742 813 [[package]] 743 814 name = "bytes" ··· 1004 1075 ] 1005 1076 1006 1077 [[package]] 1078 + name = "color_quant" 1079 + version = "1.1.0" 1080 + source = "registry+https://github.com/rust-lang/crates.io-index" 1081 + checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 1082 + 1083 + [[package]] 1007 1084 name = "colorchoice" 1008 1085 version = "1.0.4" 1009 1086 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1276 1353 checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" 1277 1354 dependencies = [ 1278 1355 "memchr", 1356 + ] 1357 + 1358 + [[package]] 1359 + name = "core_maths" 1360 + version = "0.1.1" 1361 + source = "registry+https://github.com/rust-lang/crates.io-index" 1362 + checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" 1363 + dependencies = [ 1364 + "libm", 1279 1365 ] 1280 1366 1281 1367 [[package]] ··· 1551 1637 ] 1552 1638 1553 1639 [[package]] 1640 + name = "data-url" 1641 + version = "0.3.2" 1642 + source = "registry+https://github.com/rust-lang/crates.io-index" 1643 + checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" 1644 + 1645 + [[package]] 1554 1646 name = "deadpool" 1555 1647 version = "0.12.3" 1556 1648 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2653 2745 source = "registry+https://github.com/rust-lang/crates.io-index" 2654 2746 checksum = "592391fc30a77f94bc5a3385d1569052907e3b3cecb28099671b9d5801dee6c6" 2655 2747 dependencies = [ 2656 - "askama_escape", 2748 + "askama_escape 0.13.0", 2657 2749 "dioxus-core 0.7.1", 2658 2750 "dioxus-core-types 0.7.1", 2659 2751 "rustc-hash 2.1.1", ··· 3251 3343 ] 3252 3344 3253 3345 [[package]] 3346 + name = "float-cmp" 3347 + version = "0.9.0" 3348 + source = "registry+https://github.com/rust-lang/crates.io-index" 3349 + checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 3350 + 3351 + [[package]] 3254 3352 name = "fluent-uri" 3255 3353 version = "0.3.2" 3256 3354 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3277 3375 version = "0.2.0" 3278 3376 source = "registry+https://github.com/rust-lang/crates.io-index" 3279 3377 checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" 3378 + 3379 + [[package]] 3380 + name = "fontconfig-parser" 3381 + version = "0.5.8" 3382 + source = "registry+https://github.com/rust-lang/crates.io-index" 3383 + checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" 3384 + dependencies = [ 3385 + "roxmltree", 3386 + ] 3387 + 3388 + [[package]] 3389 + name = "fontdb" 3390 + version = "0.22.0" 3391 + source = "registry+https://github.com/rust-lang/crates.io-index" 3392 + checksum = "a3a6f9af55fb97ad673fb7a69533eb2f967648a06fa21f8c9bb2cd6d33975716" 3393 + dependencies = [ 3394 + "fontconfig-parser", 3395 + "log", 3396 + "memmap2", 3397 + "slotmap", 3398 + "tinyvec", 3399 + "ttf-parser", 3400 + ] 3280 3401 3281 3402 [[package]] 3282 3403 name = "foreign-types" ··· 3673 3794 "r-efi", 3674 3795 "wasip2", 3675 3796 "wasm-bindgen", 3797 + ] 3798 + 3799 + [[package]] 3800 + name = "gif" 3801 + version = "0.13.3" 3802 + source = "registry+https://github.com/rust-lang/crates.io-index" 3803 + checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" 3804 + dependencies = [ 3805 + "color_quant", 3806 + "weezl", 3676 3807 ] 3677 3808 3678 3809 [[package]] ··· 4502 4633 ] 4503 4634 4504 4635 [[package]] 4636 + name = "image-webp" 4637 + version = "0.1.3" 4638 + source = "registry+https://github.com/rust-lang/crates.io-index" 4639 + checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" 4640 + dependencies = [ 4641 + "byteorder-lite", 4642 + "quick-error 2.0.1", 4643 + ] 4644 + 4645 + [[package]] 4646 + name = "imagesize" 4647 + version = "0.13.0" 4648 + source = "registry+https://github.com/rust-lang/crates.io-index" 4649 + checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" 4650 + 4651 + [[package]] 4505 4652 name = "indexmap" 4506 4653 version = "1.9.3" 4507 4654 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5037 5184 ] 5038 5185 5039 5186 [[package]] 5187 + name = "kurbo" 5188 + version = "0.11.3" 5189 + source = "registry+https://github.com/rust-lang/crates.io-index" 5190 + checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" 5191 + dependencies = [ 5192 + "arrayvec", 5193 + "euclid", 5194 + "smallvec", 5195 + ] 5196 + 5197 + [[package]] 5040 5198 name = "langtag" 5041 5199 version = "0.4.0" 5042 5200 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5903 6061 "log", 5904 6062 "mime", 5905 6063 "mime_guess", 5906 - "quick-error", 6064 + "quick-error 1.2.3", 5907 6065 "rand 0.8.5", 5908 6066 "safemem", 5909 6067 "tempfile", ··· 6703 6861 ] 6704 6862 6705 6863 [[package]] 6864 + name = "pico-args" 6865 + version = "0.5.0" 6866 + source = "registry+https://github.com/rust-lang/crates.io-index" 6867 + checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" 6868 + 6869 + [[package]] 6706 6870 name = "pin-project" 6707 6871 version = "1.1.10" 6708 6872 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6978 7142 checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 6979 7143 6980 7144 [[package]] 7145 + name = "quick-error" 7146 + version = "2.0.1" 7147 + source = "registry+https://github.com/rust-lang/crates.io-index" 7148 + checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" 7149 + 7150 + [[package]] 6981 7151 name = "quick-xml" 6982 7152 version = "0.37.5" 6983 7153 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 7344 7514 checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" 7345 7515 7346 7516 [[package]] 7517 + name = "resvg" 7518 + version = "0.44.0" 7519 + source = "registry+https://github.com/rust-lang/crates.io-index" 7520 + checksum = "4a325d5e8d1cebddd070b13f44cec8071594ab67d1012797c121f27a669b7958" 7521 + dependencies = [ 7522 + "gif", 7523 + "image-webp", 7524 + "log", 7525 + "pico-args", 7526 + "rgb", 7527 + "svgtypes", 7528 + "tiny-skia", 7529 + "usvg", 7530 + "zune-jpeg", 7531 + ] 7532 + 7533 + [[package]] 7347 7534 name = "rfc6979" 7348 7535 version = "0.4.0" 7349 7536 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 7378 7565 ] 7379 7566 7380 7567 [[package]] 7568 + name = "rgb" 7569 + version = "0.8.52" 7570 + source = "registry+https://github.com/rust-lang/crates.io-index" 7571 + checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" 7572 + dependencies = [ 7573 + "bytemuck", 7574 + ] 7575 + 7576 + [[package]] 7381 7577 name = "ring" 7382 7578 version = "0.17.14" 7383 7579 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 7414 7610 "tiny_http", 7415 7611 "url", 7416 7612 ] 7613 + 7614 + [[package]] 7615 + name = "roxmltree" 7616 + version = "0.20.0" 7617 + source = "registry+https://github.com/rust-lang/crates.io-index" 7618 + checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" 7417 7619 7418 7620 [[package]] 7419 7621 name = "rsa" ··· 7527 7729 version = "1.0.22" 7528 7730 source = "registry+https://github.com/rust-lang/crates.io-index" 7529 7731 checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 7732 + 7733 + [[package]] 7734 + name = "rustybuzz" 7735 + version = "0.18.0" 7736 + source = "registry+https://github.com/rust-lang/crates.io-index" 7737 + checksum = "c85d1ccd519e61834798eb52c4e886e8c2d7d698dd3d6ce0b1b47eb8557f1181" 7738 + dependencies = [ 7739 + "bitflags 2.10.0", 7740 + "bytemuck", 7741 + "core_maths", 7742 + "log", 7743 + "smallvec", 7744 + "ttf-parser", 7745 + "unicode-bidi-mirroring", 7746 + "unicode-ccc", 7747 + "unicode-properties", 7748 + "unicode-script", 7749 + ] 7530 7750 7531 7751 [[package]] 7532 7752 name = "ryu" ··· 8112 8332 checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" 8113 8333 8114 8334 [[package]] 8335 + name = "simplecss" 8336 + version = "0.2.2" 8337 + source = "registry+https://github.com/rust-lang/crates.io-index" 8338 + checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" 8339 + dependencies = [ 8340 + "log", 8341 + ] 8342 + 8343 + [[package]] 8115 8344 name = "siphasher" 8116 8345 version = "0.3.11" 8117 8346 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 8197 8426 "static_assertions", 8198 8427 "version_check", 8199 8428 ] 8429 + 8430 + [[package]] 8431 + name = "smawk" 8432 + version = "0.3.2" 8433 + source = "registry+https://github.com/rust-lang/crates.io-index" 8434 + checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" 8200 8435 8201 8436 [[package]] 8202 8437 name = "smol_str" ··· 8340 8575 checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 8341 8576 8342 8577 [[package]] 8578 + name = "strict-num" 8579 + version = "0.1.1" 8580 + source = "registry+https://github.com/rust-lang/crates.io-index" 8581 + checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" 8582 + dependencies = [ 8583 + "float-cmp", 8584 + ] 8585 + 8586 + [[package]] 8343 8587 name = "string_cache" 8344 8588 version = "0.8.9" 8345 8589 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 8435 8679 version = "3.0.0" 8436 8680 source = "registry+https://github.com/rust-lang/crates.io-index" 8437 8681 checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" 8682 + 8683 + [[package]] 8684 + name = "svgtypes" 8685 + version = "0.15.3" 8686 + source = "registry+https://github.com/rust-lang/crates.io-index" 8687 + checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" 8688 + dependencies = [ 8689 + "kurbo", 8690 + "siphasher 1.0.1", 8691 + ] 8438 8692 8439 8693 [[package]] 8440 8694 name = "swc_allocator" ··· 8808 9062 source = "registry+https://github.com/rust-lang/crates.io-index" 8809 9063 checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" 8810 9064 dependencies = [ 9065 + "smawk", 8811 9066 "unicode-linebreak", 8812 9067 "unicode-width 0.2.2", 8813 9068 ] ··· 8905 9160 ] 8906 9161 8907 9162 [[package]] 9163 + name = "tiny-skia" 9164 + version = "0.11.4" 9165 + source = "registry+https://github.com/rust-lang/crates.io-index" 9166 + checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" 9167 + dependencies = [ 9168 + "arrayref", 9169 + "arrayvec", 9170 + "bytemuck", 9171 + "cfg-if", 9172 + "log", 9173 + "png", 9174 + "tiny-skia-path", 9175 + ] 9176 + 9177 + [[package]] 9178 + name = "tiny-skia-path" 9179 + version = "0.11.4" 9180 + source = "registry+https://github.com/rust-lang/crates.io-index" 9181 + checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" 9182 + dependencies = [ 9183 + "arrayref", 9184 + "bytemuck", 9185 + "strict-num", 9186 + ] 9187 + 9188 + [[package]] 8908 9189 name = "tiny_http" 8909 9190 version = "0.12.0" 8910 9191 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 9404 9685 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 9405 9686 9406 9687 [[package]] 9688 + name = "ttf-parser" 9689 + version = "0.24.1" 9690 + source = "registry+https://github.com/rust-lang/crates.io-index" 9691 + checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a" 9692 + dependencies = [ 9693 + "core_maths", 9694 + ] 9695 + 9696 + [[package]] 9407 9697 name = "tungstenite" 9408 9698 version = "0.23.0" 9409 9699 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 9528 9818 checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 9529 9819 9530 9820 [[package]] 9821 + name = "unicode-bidi" 9822 + version = "0.3.18" 9823 + source = "registry+https://github.com/rust-lang/crates.io-index" 9824 + checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" 9825 + 9826 + [[package]] 9827 + name = "unicode-bidi-mirroring" 9828 + version = "0.3.0" 9829 + source = "registry+https://github.com/rust-lang/crates.io-index" 9830 + checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f" 9831 + 9832 + [[package]] 9833 + name = "unicode-ccc" 9834 + version = "0.3.0" 9835 + source = "registry+https://github.com/rust-lang/crates.io-index" 9836 + checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42" 9837 + 9838 + [[package]] 9531 9839 name = "unicode-id-start" 9532 9840 version = "1.4.0" 9533 9841 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 9555 9863 ] 9556 9864 9557 9865 [[package]] 9866 + name = "unicode-properties" 9867 + version = "0.1.4" 9868 + source = "registry+https://github.com/rust-lang/crates.io-index" 9869 + checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" 9870 + 9871 + [[package]] 9872 + name = "unicode-script" 9873 + version = "0.5.7" 9874 + source = "registry+https://github.com/rust-lang/crates.io-index" 9875 + checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" 9876 + 9877 + [[package]] 9558 9878 name = "unicode-segmentation" 9559 9879 version = "1.12.0" 9560 9880 source = "registry+https://github.com/rust-lang/crates.io-index" 9561 9881 checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 9882 + 9883 + [[package]] 9884 + name = "unicode-vo" 9885 + version = "0.1.0" 9886 + source = "registry+https://github.com/rust-lang/crates.io-index" 9887 + checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" 9562 9888 9563 9889 [[package]] 9564 9890 name = "unicode-width" ··· 9609 9935 checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" 9610 9936 9611 9937 [[package]] 9938 + name = "usvg" 9939 + version = "0.44.0" 9940 + source = "registry+https://github.com/rust-lang/crates.io-index" 9941 + checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6" 9942 + dependencies = [ 9943 + "base64 0.22.1", 9944 + "data-url", 9945 + "flate2", 9946 + "fontdb", 9947 + "imagesize", 9948 + "kurbo", 9949 + "log", 9950 + "pico-args", 9951 + "roxmltree", 9952 + "rustybuzz", 9953 + "simplecss", 9954 + "siphasher 1.0.1", 9955 + "strict-num", 9956 + "svgtypes", 9957 + "tiny-skia-path", 9958 + "unicode-bidi", 9959 + "unicode-script", 9960 + "unicode-vo", 9961 + "xmlwriter", 9962 + ] 9963 + 9964 + [[package]] 9612 9965 name = "utf-8" 9613 9966 version = "0.7.6" 9614 9967 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 9929 10282 name = "weaver-app" 9930 10283 version = "0.1.0" 9931 10284 dependencies = [ 10285 + "askama", 9932 10286 "axum", 9933 10287 "base64 0.22.1", 9934 10288 "chrono", ··· 9938 10292 "dioxus-free-icons", 9939 10293 "dioxus-primitives", 9940 10294 "dotenvy", 10295 + "fontdb", 9941 10296 "gloo-storage", 9942 10297 "gloo-timers", 9943 10298 "http", ··· 9958 10313 "regex", 9959 10314 "regex-lite", 9960 10315 "reqwest", 10316 + "resvg", 9961 10317 "serde", 9962 10318 "serde_html_form", 9963 10319 "serde_ipld_dagcbor", 9964 10320 "serde_json", 9965 10321 "syntect", 10322 + "textwrap", 9966 10323 "time", 10324 + "tiny-skia", 9967 10325 "tokio", 9968 10326 "tracing", 9969 10327 "tracing-subscriber", 9970 10328 "tracing-wasm", 9971 10329 "urlencoding", 10330 + "usvg", 9972 10331 "wasm-bindgen", 9973 10332 "wasm-bindgen-futures", 9974 10333 "weaver-api", ··· 10250 10609 ] 10251 10610 10252 10611 [[package]] 10612 + name = "weezl" 10613 + version = "0.1.12" 10614 + source = "registry+https://github.com/rust-lang/crates.io-index" 10615 + checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" 10616 + 10617 + [[package]] 10253 10618 name = "widestring" 10254 10619 version = "1.2.1" 10255 10620 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 10890 11255 ] 10891 11256 10892 11257 [[package]] 11258 + name = "xmlwriter" 11259 + version = "0.1.0" 11260 + source = "registry+https://github.com/rust-lang/crates.io-index" 11261 + checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" 11262 + 11263 + [[package]] 10893 11264 name = "xxhash-rust" 10894 11265 version = "0.8.15" 10895 11266 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 11121 11492 dependencies = [ 11122 11493 "cc", 11123 11494 "pkg-config", 11495 + ] 11496 + 11497 + [[package]] 11498 + name = "zune-core" 11499 + version = "0.4.12" 11500 + source = "registry+https://github.com/rust-lang/crates.io-index" 11501 + checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" 11502 + 11503 + [[package]] 11504 + name = "zune-jpeg" 11505 + version = "0.4.21" 11506 + source = "registry+https://github.com/rust-lang/crates.io-index" 11507 + checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" 11508 + dependencies = [ 11509 + "zune-core", 11124 11510 ] 11125 11511 11126 11512 [[package]]
+9 -1
crates/weaver-app/Cargo.toml
··· 14 14 web = ["dioxus/web", "dioxus-primitives/web"] 15 15 desktop = ["dioxus/desktop"] 16 16 mobile = ["dioxus/mobile"] 17 - server = [ "dioxus/server", "dep:jacquard-axum", "dep:axum"] 17 + server = [ "dioxus/server", "dep:jacquard-axum", "dep:axum", "dep:resvg", "dep:usvg", "dep:tiny-skia", "dep:textwrap", "dep:askama", "dep:fontdb"] 18 18 19 19 20 20 [dependencies] ··· 55 55 web-time = "1.1" 56 56 urlencoding = "2.1" 57 57 tracing-subscriber = { version = "0.3", default-features = false, features = ["std", "registry"] } 58 + 59 + # OG image generation (server-only) 60 + resvg = { version = "0.44", optional = true } 61 + usvg = { version = "0.44", optional = true } 62 + tiny-skia = { version = "0.11", optional = true } 63 + textwrap = { version = "0.16", optional = true } 64 + askama = { version = "0.12", optional = true } 65 + fontdb = { version = "0.22", optional = true } 58 66 59 67 [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] 60 68 webbrowser = "1.0.6"
+43
crates/weaver-app/assets/styling/main.css
··· 40 40 color: var(--color-subtle); 41 41 opacity: 0.5; 42 42 } 43 + 44 + a {{ 45 + color: var(--color-link); 46 + text-decoration: none; 47 + }} 48 + 49 + 50 + @font-face { 51 + font-family: "Ioskeley Mono"; 52 + font-style: normal; 53 + font-weight: normal; 54 + src: url("/assets/IoskeleyMono-Regular.woff2") format("woff2"); 55 + } 56 + @font-face { 57 + font-family: "Ioskeley Mono"; 58 + font-style: normal; 59 + font-weight: lighter; 60 + src: url("/assets/IoskeleyMono-Light.woff2") format("woff2"); 61 + } 62 + @font-face { 63 + font-family: "Ioskeley Mono"; 64 + font-style: italic; 65 + font-weight: lighter; 66 + src: url("/assets/IoskeleyMono-LightItalic.woff2") format("woff2"); 67 + } 68 + @font-face { 69 + font-family: "Ioskeley Mono"; 70 + font-style: normal; 71 + font-weight: bold; 72 + src: url("/assets/IoskeleyMono-Bold.woff2") format("woff2"); 73 + } 74 + @font-face { 75 + font-family: "Ioskeley Mono"; 76 + font-style: italic; 77 + font-weight: normal; 78 + src: url("/assets/IoskeleyMono-Italic.woff2") format("woff2"); 79 + } 80 + @font-face { 81 + font-family: "Ioskeley Mono"; 82 + font-style: italic; 83 + font-weight: bold; 84 + src: url("/assets/IoskeleyMono-BoldItalic.woff2") format("woff2"); 85 + }
+98 -2
crates/weaver-app/src/components/entry.rs
··· 117 117 } 118 118 } 119 119 120 + /// Extract a plain-text preview from markdown content (first ~160 chars) 121 + fn extract_preview(content: &str, max_len: usize) -> String { 122 + // Simple extraction: skip markdown syntax, get plain text 123 + let plain: String = content 124 + .lines() 125 + .filter(|line| { 126 + let trimmed = line.trim(); 127 + // Skip headings, images, links, code blocks 128 + !trimmed.starts_with('#') 129 + && !trimmed.starts_with('!') 130 + && !trimmed.starts_with("```") 131 + && !trimmed.is_empty() 132 + }) 133 + .take(5) 134 + .collect::<Vec<_>>() 135 + .join(" "); 136 + 137 + // Clean up markdown inline syntax 138 + let cleaned = plain 139 + .replace("**", "") 140 + .replace("__", "") 141 + .replace('*', "") 142 + .replace('_', "") 143 + .replace('`', ""); 144 + 145 + if cleaned.len() <= max_len { 146 + cleaned 147 + } else { 148 + format!("{}...", &cleaned[..max_len - 3]) 149 + } 150 + } 151 + 120 152 /// Full entry page with metadata, content, and navigation 121 153 #[component] 122 154 fn EntryPageView( ··· 133 165 .map(|t| t.as_ref()) 134 166 .unwrap_or("Untitled"); 135 167 168 + // Get entry path for URLs 169 + let entry_path = entry_view 170 + .path 171 + .as_ref() 172 + .map(|p| p.as_ref().to_string()) 173 + .unwrap_or_else(|| title.to_string()); 174 + 175 + // Get author handle 176 + let author_handle = entry_view 177 + .authors 178 + .first() 179 + .map(|a| { 180 + use weaver_api::sh_weaver::actor::ProfileDataViewInner; 181 + match &a.record.inner { 182 + ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_string(), 183 + ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_string(), 184 + ProfileDataViewInner::TangledProfileView(p) => p.handle.as_ref().to_string(), 185 + _ => "unknown".to_string(), 186 + } 187 + }) 188 + .unwrap_or_else(|| "unknown".to_string()); 189 + 190 + // Build OG URLs 191 + let base = if crate::env::WEAVER_APP_ENV == "dev" { 192 + format!("http://127.0.0.1:{}", crate::env::WEAVER_PORT) 193 + } else { 194 + crate::env::WEAVER_APP_HOST.to_string() 195 + }; 196 + let canonical_url = format!( 197 + "{}/{}/{}/{}", 198 + base, 199 + ident(), 200 + book_title(), 201 + entry_path 202 + ); 203 + let og_image_url = format!( 204 + "{}/og/{}/{}/{}.png", 205 + base, 206 + ident(), 207 + book_title(), 208 + entry_path 209 + ); 210 + 211 + // Extract description preview from content 212 + let description = extract_preview(entry_record().content.as_ref(), 160); 213 + 214 + // Full page title 215 + let page_title = format!("{} | {} | Weaver", title, book_title()); 216 + 136 217 tracing::info!("Entry: {book_title} - {title}"); 137 218 138 219 rsx! { 139 - // Set page title 140 - document::Title { "{title}" } 220 + // Page title and OG meta tags 221 + document::Title { "{page_title}" } 222 + 223 + // OpenGraph tags 224 + document::Meta { property: "og:title", content: "{title}" } 225 + document::Meta { property: "og:description", content: "{description}" } 226 + document::Meta { property: "og:image", content: "{og_image_url}" } 227 + document::Meta { property: "og:type", content: "article" } 228 + document::Meta { property: "og:url", content: "{canonical_url}" } 229 + document::Meta { property: "og:site_name", content: "Weaver" } 230 + 231 + // Twitter Card tags 232 + document::Meta { name: "twitter:card", content: "summary_large_image" } 233 + document::Meta { name: "twitter:title", content: "{title}" } 234 + document::Meta { name: "twitter:description", content: "{description}" } 235 + document::Meta { name: "twitter:image", content: "{og_image_url}" } 236 + document::Meta { name: "twitter:creator", content: "@{author_handle}" } 141 237 document::Link { rel: "stylesheet", href: ENTRY_CSS } 142 238 143 239 div { class: "entry-page-layout",
+166
crates/weaver-app/src/main.rs
··· 31 31 #[cfg(feature = "server")] 32 32 mod blobcache; 33 33 mod cache_impl; 34 + #[cfg(feature = "server")] 35 + mod og; 34 36 /// Define a components module that contains all shared components for our app. 35 37 mod components; 36 38 mod config; ··· 419 421 Ok(bytes) => Ok(build_image_response(bytes)), 420 422 Err(_) => Ok(image_not_found()), 421 423 } 424 + } 425 + 426 + // Route: /og/{ident}/{book_title}/{entry_title} - OpenGraph image for entry 427 + #[cfg(all(feature = "fullstack-server", feature = "server"))] 428 + #[get("/og/{ident}/{book_title}/{entry_title}", fetcher: Extension<Arc<fetch::Fetcher>>)] 429 + pub async fn og_image( 430 + ident: SmolStr, 431 + book_title: SmolStr, 432 + entry_title: SmolStr, 433 + ) -> Result<axum::response::Response> { 434 + use axum::{ 435 + http::{header::{CACHE_CONTROL, CONTENT_TYPE}, StatusCode}, 436 + response::IntoResponse, 437 + }; 438 + use weaver_api::sh_weaver::actor::ProfileDataViewInner; 439 + use weaver_api::sh_weaver::notebook::Title; 440 + 441 + // Strip .png extension if present 442 + let entry_title = entry_title.strip_suffix(".png").unwrap_or(&entry_title); 443 + 444 + let Ok(at_ident) = AtIdentifier::new_owned(ident.clone()) else { 445 + return Ok((StatusCode::BAD_REQUEST, "Invalid identifier").into_response()); 446 + }; 447 + 448 + // Fetch entry data 449 + let entry_result = fetcher.get_entry(at_ident.clone(), book_title.clone(), entry_title.into()).await; 450 + 451 + let arc_data = match entry_result { 452 + Ok(Some(data)) => data, 453 + Ok(None) => return Ok((StatusCode::NOT_FOUND, "Entry not found").into_response()), 454 + Err(e) => { 455 + tracing::error!("Failed to fetch entry for OG image: {:?}", e); 456 + return Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to fetch entry").into_response()); 457 + } 458 + }; 459 + let (book_entry, entry) = arc_data.as_ref(); 460 + 461 + // Build cache key using entry CID 462 + let entry_cid = book_entry.entry.cid.as_ref(); 463 + let cache_key = og::cache_key(&ident, &book_title, entry_title, entry_cid); 464 + 465 + // Check cache first 466 + if let Some(cached) = og::get_cached(&cache_key) { 467 + return Ok(( 468 + [ 469 + (CONTENT_TYPE, "image/png"), 470 + (CACHE_CONTROL, "public, max-age=3600"), 471 + ], 472 + cached, 473 + ).into_response()); 474 + } 475 + 476 + // Extract metadata 477 + let title: &str = entry.title.as_ref(); 478 + 479 + // Use book_title from URL - it's the notebook slug/title 480 + // TODO: Could fetch actual notebook record to get display title 481 + let notebook_title_str: String = book_title.to_string(); 482 + 483 + let author_handle = book_entry.entry.authors.first() 484 + .map(|a| match &a.record.inner { 485 + ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_string(), 486 + ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_string(), 487 + ProfileDataViewInner::TangledProfileView(p) => p.handle.as_ref().to_string(), 488 + _ => "unknown".to_string(), 489 + }) 490 + .unwrap_or_else(|| "unknown".to_string()); 491 + 492 + // Check for hero image in embeds 493 + let hero_image_data = if let Some(ref embeds) = entry.embeds { 494 + if let Some(ref images) = embeds.images { 495 + if let Some(first_image) = images.images.first() { 496 + // Get DID from the entry URI 497 + let did = book_entry.entry.uri.authority(); 498 + 499 + let blob = first_image.image.blob(); 500 + let cid = blob.cid(); 501 + let mime = blob.mime_type.as_ref(); 502 + let format = mime.strip_prefix("image/").unwrap_or("jpeg"); 503 + 504 + // Build CDN URL 505 + let cdn_url = format!( 506 + "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@{}", 507 + did.as_str(), cid.as_ref(), format 508 + ); 509 + 510 + // Fetch the image 511 + match reqwest::get(&cdn_url).await { 512 + Ok(response) if response.status().is_success() => { 513 + match response.bytes().await { 514 + Ok(bytes) => { 515 + use base64::Engine; 516 + let base64_str = base64::engine::general_purpose::STANDARD.encode(&bytes); 517 + Some(format!("data:{};base64,{}", mime, base64_str)) 518 + } 519 + Err(_) => None 520 + } 521 + } 522 + _ => None 523 + } 524 + } else { 525 + None 526 + } 527 + } else { 528 + None 529 + } 530 + } else { 531 + None 532 + }; 533 + 534 + // Extract content snippet - render markdown to HTML then strip tags 535 + let content_snippet: String = { 536 + let parser = markdown_weaver::Parser::new(entry.content.as_ref()); 537 + let mut html = String::new(); 538 + markdown_weaver::html::push_html(&mut html, parser); 539 + // Strip HTML tags 540 + regex_lite::Regex::new(r"<[^>]+>") 541 + .unwrap() 542 + .replace_all(&html, "") 543 + .replace("&amp;", "&") 544 + .replace("&lt;", "<") 545 + .replace("&gt;", ">") 546 + .replace("&quot;", "\"") 547 + .replace("&#39;", "'") 548 + .replace('\n', " ") 549 + .split_whitespace() 550 + .collect::<Vec<_>>() 551 + .join(" ") 552 + }; 553 + 554 + // Generate image - hero or text-only based on available data 555 + let png_bytes = if let Some(ref hero_data) = hero_image_data { 556 + match og::generate_hero_image(hero_data, title, &notebook_title_str, &author_handle) { 557 + Ok(bytes) => bytes, 558 + Err(e) => { 559 + tracing::error!("Failed to generate hero OG image: {:?}, falling back to text", e); 560 + og::generate_text_only(title, &content_snippet, &notebook_title_str, &author_handle) 561 + .map_err(|e| { 562 + tracing::error!("Failed to generate text OG image: {:?}", e); 563 + }) 564 + .ok() 565 + .unwrap_or_default() 566 + } 567 + } 568 + } else { 569 + match og::generate_text_only(title, &content_snippet, &notebook_title_str, &author_handle) { 570 + Ok(bytes) => bytes, 571 + Err(e) => { 572 + tracing::error!("Failed to generate OG image: {:?}", e); 573 + return Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate image").into_response()); 574 + } 575 + } 576 + }; 577 + 578 + // Cache the generated image 579 + og::cache_image(cache_key, png_bytes.clone()); 580 + 581 + Ok(( 582 + [ 583 + (CONTENT_TYPE, "image/png"), 584 + (CACHE_CONTROL, "public, max-age=3600"), 585 + ], 586 + png_bytes, 587 + ).into_response()) 422 588 } 423 589 424 590 // #[server(endpoint = "static_routes", output = server_fn::codec::Json)]
+206
crates/weaver-app/src/og/mod.rs
··· 1 + //! OpenGraph image generation module 2 + //! 3 + //! Generates social card images for entry pages using SVG templates rendered to PNG. 4 + 5 + use askama::Template; 6 + use std::sync::OnceLock; 7 + use std::time::Duration; 8 + 9 + use crate::cache_impl::{Cache, new_cache}; 10 + 11 + /// Cache for generated OG images 12 + /// Key: "{ident}/{book}/{entry}/{cid}" - includes CID for invalidation 13 + static OG_CACHE: OnceLock<Cache<String, Vec<u8>>> = OnceLock::new(); 14 + 15 + fn get_cache() -> &'static Cache<String, Vec<u8>> { 16 + OG_CACHE.get_or_init(|| { 17 + // Cache up to 1000 images for 1 hour 18 + new_cache(1000, Duration::from_secs(3600)) 19 + }) 20 + } 21 + 22 + /// Generate cache key from entry identifiers 23 + pub fn cache_key(ident: &str, book: &str, entry: &str, cid: &str) -> String { 24 + format!("{}/{}/{}/{}", ident, book, entry, cid) 25 + } 26 + 27 + /// Try to get a cached OG image 28 + pub fn get_cached(key: &str) -> Option<Vec<u8>> { 29 + get_cache().get(&key.to_string()) 30 + } 31 + 32 + /// Store an OG image in the cache 33 + pub fn cache_image(key: String, image: Vec<u8>) { 34 + get_cache().insert(key, image); 35 + } 36 + 37 + /// Error type for OG image generation 38 + #[derive(Debug)] 39 + pub enum OgError { 40 + NotFound, 41 + FetchError(String), 42 + RenderError(String), 43 + TemplateError(String), 44 + } 45 + 46 + impl std::fmt::Display for OgError { 47 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 48 + match self { 49 + OgError::NotFound => write!(f, "Entry not found"), 50 + OgError::FetchError(e) => write!(f, "Fetch error: {}", e), 51 + OgError::RenderError(e) => write!(f, "Render error: {}", e), 52 + OgError::TemplateError(e) => write!(f, "Template error: {}", e), 53 + } 54 + } 55 + } 56 + 57 + impl std::error::Error for OgError {} 58 + 59 + /// Standard OG image dimensions 60 + pub const OG_WIDTH: u32 = 1200; 61 + pub const OG_HEIGHT: u32 = 630; 62 + 63 + /// Rose Pine theme colors 64 + mod colors { 65 + pub const BASE: &str = "#191724"; 66 + pub const TEXT: &str = "#e0def4"; 67 + pub const SUBTLE: &str = "#908caa"; 68 + pub const MUTED: &str = "#6e6a86"; 69 + pub const OVERLAY: &str = "#524f67"; 70 + } 71 + 72 + /// Text-only template (no hero image) 73 + #[derive(Template)] 74 + #[template(path = "og_text_only.svg", escape = "none")] 75 + pub struct TextOnlyTemplate { 76 + pub title_lines: Vec<String>, 77 + pub content_lines: Vec<String>, 78 + pub notebook_title: String, 79 + pub author_handle: String, 80 + } 81 + 82 + /// Hero image template (full-bleed image with overlay) 83 + #[derive(Template)] 84 + #[template(path = "og_hero_image.svg", escape = "none")] 85 + pub struct HeroImageTemplate { 86 + pub hero_image_data: String, 87 + pub title_lines: Vec<String>, 88 + pub notebook_title: String, 89 + pub author_handle: String, 90 + } 91 + 92 + /// Global font database, initialized once 93 + static FONTDB: OnceLock<fontdb::Database> = OnceLock::new(); 94 + 95 + fn get_fontdb() -> &'static fontdb::Database { 96 + FONTDB.get_or_init(|| { 97 + let mut db = fontdb::Database::new(); 98 + // Load IBM Plex Sans from embedded bytes 99 + let font_data = include_bytes!("../../assets/fonts/IBMPlexSans-VariableFont_wdth,wght.ttf"); 100 + db.load_font_data(font_data.to_vec()); 101 + let font_data = 102 + include_bytes!("../../assets/fonts/ioskeley-mono/IoskeleyMono-Regular.woff2"); 103 + db.load_font_data(font_data.to_vec()); 104 + db 105 + }) 106 + } 107 + 108 + /// Wrap title text into lines that fit the SVG width 109 + pub fn wrap_title(title: &str, max_chars: usize, max_lines: usize) -> Vec<String> { 110 + textwrap::wrap(title, max_chars) 111 + .into_iter() 112 + .take(max_lines) 113 + .map(|s| s.to_string()) 114 + .collect() 115 + } 116 + 117 + /// Render an SVG string to PNG bytes 118 + pub fn render_svg_to_png(svg: &str) -> Result<Vec<u8>, OgError> { 119 + let fontdb = get_fontdb(); 120 + 121 + let options = usvg::Options { 122 + fontdb: std::sync::Arc::new(fontdb.clone()), 123 + ..Default::default() 124 + }; 125 + 126 + let tree = usvg::Tree::from_str(svg, &options) 127 + .map_err(|e| OgError::RenderError(format!("Failed to parse SVG: {}", e)))?; 128 + 129 + let mut pixmap = tiny_skia::Pixmap::new(OG_WIDTH, OG_HEIGHT) 130 + .ok_or_else(|| OgError::RenderError("Failed to create pixmap".to_string()))?; 131 + 132 + resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut()); 133 + 134 + pixmap 135 + .encode_png() 136 + .map_err(|e| OgError::RenderError(format!("Failed to encode PNG: {}", e))) 137 + } 138 + 139 + /// Generate a text-only OG image 140 + pub fn generate_text_only( 141 + title: &str, 142 + content: &str, 143 + notebook_title: &str, 144 + author_handle: &str, 145 + ) -> Result<Vec<u8>, OgError> { 146 + let title_lines = wrap_title(title, 50, 2); 147 + let content_lines = wrap_title(content, 70, 5); 148 + 149 + let template = TextOnlyTemplate { 150 + title_lines, 151 + content_lines, 152 + notebook_title: notebook_title.to_string(), 153 + author_handle: author_handle.to_string(), 154 + }; 155 + 156 + let svg = template 157 + .render() 158 + .map_err(|e| OgError::TemplateError(e.to_string()))?; 159 + 160 + render_svg_to_png(&svg) 161 + } 162 + 163 + /// Generate a hero image OG image 164 + pub fn generate_hero_image( 165 + hero_image_data: &str, 166 + title: &str, 167 + notebook_title: &str, 168 + author_handle: &str, 169 + ) -> Result<Vec<u8>, OgError> { 170 + let title_lines = wrap_title(title, 50, 2); 171 + 172 + let template = HeroImageTemplate { 173 + hero_image_data: hero_image_data.to_string(), 174 + title_lines, 175 + notebook_title: notebook_title.to_string(), 176 + author_handle: author_handle.to_string(), 177 + }; 178 + 179 + let svg = template 180 + .render() 181 + .map_err(|e| OgError::TemplateError(e.to_string()))?; 182 + 183 + render_svg_to_png(&svg) 184 + } 185 + 186 + #[cfg(test)] 187 + mod tests { 188 + use super::*; 189 + 190 + #[test] 191 + fn test_wrap_title_short() { 192 + let lines = wrap_title("Hello World", 28, 3); 193 + assert_eq!(lines, vec!["Hello World"]); 194 + } 195 + 196 + #[test] 197 + fn test_wrap_title_long() { 198 + let lines = wrap_title( 199 + "This is a very long title that should wrap onto multiple lines", 200 + 28, 201 + 3, 202 + ); 203 + assert!(lines.len() > 1); 204 + assert!(lines.len() <= 3); 205 + } 206 + }
+18
crates/weaver-app/templates/og_hero_image.svg
··· 1 + <svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 2 + <!-- Hero image (upper portion) --> 3 + <image xlink:href="{{ hero_image_data }}" x="0" y="0" width="1200" height="420" preserveAspectRatio="xMidYMid slice"/> 4 + 5 + <!-- Bottom panel with theme colors --> 6 + <rect x="0" y="420" width="1200" height="210" fill="#191724"/> 7 + 8 + <!-- Title --> 9 + {% for line in title_lines %} 10 + <text x="60" y="{{ 480 + loop.index0 * 56 }}" fill="#c4a7e7" font-family="IBM Plex Sans, sans-serif" font-size="52" font-weight="900">{{ line }}</text> 11 + {% endfor %} 12 + 13 + <!-- Notebook + Author row --> 14 + <text x="60" y="600" fill="#ebbcba" font-family="IBM Plex Sans, sans-serif" font-size="32">{{ notebook_title }} · @{{ author_handle }}</text> 15 + 16 + <!-- Weaver branding --> 17 + <text x="1080" y="600" fill="#908caa" font-family="IBM Plex Sans, sans-serif" font-size="20">weaver.sh</text> 18 + </svg>
+22
crates/weaver-app/templates/og_text_only.svg
··· 1 + <svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg"> 2 + <!-- Background --> 3 + <rect width="1200" height="630" fill="#191724"/> 4 + 5 + <!-- Entry title (large, wrapped) --> 6 + {% for line in title_lines %} 7 + <text x="60" y="{{ 140 + loop.index0 * 56 }}" fill="#c4a7e7" font-family="IBM Plex Sans, sans-serif" font-size="64" font-weight="800">{{ line }}</text> 8 + {% endfor %} 9 + 10 + <!-- Notebook title + Author --> 11 + <text x="60" y="320" fill="#ebbcba" font-family="IBM Plex Sans, sans-serif" font-size="32">{{ notebook_title }} · @{{ author_handle }}</text> 12 + 13 + <!-- Content snippet --> 14 + {% for line in content_lines %} 15 + <text x="60" y="{{ 380 + loop.index0 * 36 }}" fill="#e0def4" font-family="IBM Plex Sans, sans-serif" font-size="28">{{ line }}</text> 16 + {% endfor %} 17 + 18 + 19 + 20 + <!-- Weaver branding --> 21 + <text x="60" y="590" fill="#908caa" font-family="IBM Plex Sans, sans-serif" font-size="24">weaver.sh</text> 22 + </svg>