tangled
alpha
login
or
join now
nonbinary.computer
/
weaver
atproto blogging
24
fork
atom
overview
issues
2
pulls
pipelines
opengraph images
Orual
1 month ago
89adf83c
b73b1f86
+950
-5
8 changed files
expand all
collapse all
unified
split
Cargo.lock
crates
weaver-app
Cargo.toml
assets
styling
main.css
src
components
entry.rs
main.rs
og
mod.rs
templates
og_hero_image.svg
og_text_only.svg
+388
-2
Cargo.lock
···
187
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
188
189
[[package]]
0
0
0
0
0
0
190
name = "arrayvec"
191
version = "0.7.6"
192
source = "registry+https://github.com/rust-lang/crates.io-index"
···
226
]
227
228
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
229
name = "askama_escape"
230
version = "0.13.0"
231
source = "registry+https://github.com/rust-lang/crates.io-index"
232
checksum = "3df27b8d5ddb458c5fb1bbc1ce172d4a38c614a97d550b0ac89003897fb01de4"
233
234
[[package]]
0
0
0
0
0
0
0
0
0
235
name = "ast_node"
236
version = "3.0.4"
237
source = "registry+https://github.com/rust-lang/crates.io-index"
···
539
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
540
541
[[package]]
0
0
0
0
0
0
0
0
0
542
name = "better_scoped_tls"
543
version = "1.0.1"
544
source = "registry+https://github.com/rust-lang/crates.io-index"
···
734
]
735
736
[[package]]
0
0
0
0
0
0
737
name = "byteorder"
738
version = "1.5.0"
739
source = "registry+https://github.com/rust-lang/crates.io-index"
740
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
0
0
0
0
0
0
741
742
[[package]]
743
name = "bytes"
···
1004
]
1005
1006
[[package]]
0
0
0
0
0
0
1007
name = "colorchoice"
1008
version = "1.0.4"
1009
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1276
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
1277
dependencies = [
1278
"memchr",
0
0
0
0
0
0
0
0
0
1279
]
1280
1281
[[package]]
···
1551
]
1552
1553
[[package]]
0
0
0
0
0
0
1554
name = "deadpool"
1555
version = "0.12.3"
1556
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2653
source = "registry+https://github.com/rust-lang/crates.io-index"
2654
checksum = "592391fc30a77f94bc5a3385d1569052907e3b3cecb28099671b9d5801dee6c6"
2655
dependencies = [
2656
-
"askama_escape",
2657
"dioxus-core 0.7.1",
2658
"dioxus-core-types 0.7.1",
2659
"rustc-hash 2.1.1",
···
3251
]
3252
3253
[[package]]
0
0
0
0
0
0
3254
name = "fluent-uri"
3255
version = "0.3.2"
3256
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3277
version = "0.2.0"
3278
source = "registry+https://github.com/rust-lang/crates.io-index"
3279
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
3280
3281
[[package]]
3282
name = "foreign-types"
···
3673
"r-efi",
3674
"wasip2",
3675
"wasm-bindgen",
0
0
0
0
0
0
0
0
0
0
3676
]
3677
3678
[[package]]
···
4502
]
4503
4504
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
4505
name = "indexmap"
4506
version = "1.9.3"
4507
source = "registry+https://github.com/rust-lang/crates.io-index"
···
5037
]
5038
5039
[[package]]
0
0
0
0
0
0
0
0
0
0
0
5040
name = "langtag"
5041
version = "0.4.0"
5042
source = "registry+https://github.com/rust-lang/crates.io-index"
···
5903
"log",
5904
"mime",
5905
"mime_guess",
5906
-
"quick-error",
5907
"rand 0.8.5",
5908
"safemem",
5909
"tempfile",
···
6703
]
6704
6705
[[package]]
0
0
0
0
0
0
6706
name = "pin-project"
6707
version = "1.1.10"
6708
source = "registry+https://github.com/rust-lang/crates.io-index"
···
6978
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
6979
6980
[[package]]
0
0
0
0
0
0
6981
name = "quick-xml"
6982
version = "0.37.5"
6983
source = "registry+https://github.com/rust-lang/crates.io-index"
···
7344
checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"
7345
7346
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
7347
name = "rfc6979"
7348
version = "0.4.0"
7349
source = "registry+https://github.com/rust-lang/crates.io-index"
···
7378
]
7379
7380
[[package]]
0
0
0
0
0
0
0
0
0
7381
name = "ring"
7382
version = "0.17.14"
7383
source = "registry+https://github.com/rust-lang/crates.io-index"
···
7414
"tiny_http",
7415
"url",
7416
]
0
0
0
0
0
0
7417
7418
[[package]]
7419
name = "rsa"
···
7527
version = "1.0.22"
7528
source = "registry+https://github.com/rust-lang/crates.io-index"
7529
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
7530
7531
[[package]]
7532
name = "ryu"
···
8112
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
8113
8114
[[package]]
0
0
0
0
0
0
0
0
0
8115
name = "siphasher"
8116
version = "0.3.11"
8117
source = "registry+https://github.com/rust-lang/crates.io-index"
···
8197
"static_assertions",
8198
"version_check",
8199
]
0
0
0
0
0
0
8200
8201
[[package]]
8202
name = "smol_str"
···
8340
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
8341
8342
[[package]]
0
0
0
0
0
0
0
0
0
8343
name = "string_cache"
8344
version = "0.8.9"
8345
source = "registry+https://github.com/rust-lang/crates.io-index"
···
8435
version = "3.0.0"
8436
source = "registry+https://github.com/rust-lang/crates.io-index"
8437
checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
0
0
0
0
0
0
0
0
0
0
8438
8439
[[package]]
8440
name = "swc_allocator"
···
8808
source = "registry+https://github.com/rust-lang/crates.io-index"
8809
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
8810
dependencies = [
0
8811
"unicode-linebreak",
8812
"unicode-width 0.2.2",
8813
]
···
8905
]
8906
8907
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
8908
name = "tiny_http"
8909
version = "0.12.0"
8910
source = "registry+https://github.com/rust-lang/crates.io-index"
···
9404
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
9405
9406
[[package]]
0
0
0
0
0
0
0
0
0
9407
name = "tungstenite"
9408
version = "0.23.0"
9409
source = "registry+https://github.com/rust-lang/crates.io-index"
···
9528
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
9529
9530
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
9531
name = "unicode-id-start"
9532
version = "1.4.0"
9533
source = "registry+https://github.com/rust-lang/crates.io-index"
···
9555
]
9556
9557
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
9558
name = "unicode-segmentation"
9559
version = "1.12.0"
9560
source = "registry+https://github.com/rust-lang/crates.io-index"
9561
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
0
0
0
0
0
0
9562
9563
[[package]]
9564
name = "unicode-width"
···
9609
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
9610
9611
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
9612
name = "utf-8"
9613
version = "0.7.6"
9614
source = "registry+https://github.com/rust-lang/crates.io-index"
···
9929
name = "weaver-app"
9930
version = "0.1.0"
9931
dependencies = [
0
9932
"axum",
9933
"base64 0.22.1",
9934
"chrono",
···
9938
"dioxus-free-icons",
9939
"dioxus-primitives",
9940
"dotenvy",
0
9941
"gloo-storage",
9942
"gloo-timers",
9943
"http",
···
9958
"regex",
9959
"regex-lite",
9960
"reqwest",
0
9961
"serde",
9962
"serde_html_form",
9963
"serde_ipld_dagcbor",
9964
"serde_json",
9965
"syntect",
0
9966
"time",
0
9967
"tokio",
9968
"tracing",
9969
"tracing-subscriber",
9970
"tracing-wasm",
9971
"urlencoding",
0
9972
"wasm-bindgen",
9973
"wasm-bindgen-futures",
9974
"weaver-api",
···
10250
]
10251
10252
[[package]]
0
0
0
0
0
0
10253
name = "widestring"
10254
version = "1.2.1"
10255
source = "registry+https://github.com/rust-lang/crates.io-index"
···
10890
]
10891
10892
[[package]]
0
0
0
0
0
0
10893
name = "xxhash-rust"
10894
version = "0.8.15"
10895
source = "registry+https://github.com/rust-lang/crates.io-index"
···
11121
dependencies = [
11122
"cc",
11123
"pkg-config",
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
11124
]
11125
11126
[[package]]
···
187
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
188
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]]
196
name = "arrayvec"
197
version = "0.7.6"
198
source = "registry+https://github.com/rust-lang/crates.io-index"
···
232
]
233
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]]
270
name = "askama_escape"
271
version = "0.13.0"
272
source = "registry+https://github.com/rust-lang/crates.io-index"
273
checksum = "3df27b8d5ddb458c5fb1bbc1ce172d4a38c614a97d550b0ac89003897fb01de4"
274
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]]
285
name = "ast_node"
286
version = "3.0.4"
287
source = "registry+https://github.com/rust-lang/crates.io-index"
···
589
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
590
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]]
601
name = "better_scoped_tls"
602
version = "1.0.1"
603
source = "registry+https://github.com/rust-lang/crates.io-index"
···
793
]
794
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]]
802
name = "byteorder"
803
version = "1.5.0"
804
source = "registry+https://github.com/rust-lang/crates.io-index"
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"
812
813
[[package]]
814
name = "bytes"
···
1075
]
1076
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]]
1084
name = "colorchoice"
1085
version = "1.0.4"
1086
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1353
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
1354
dependencies = [
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",
1365
]
1366
1367
[[package]]
···
1637
]
1638
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]]
1646
name = "deadpool"
1647
version = "0.12.3"
1648
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2745
source = "registry+https://github.com/rust-lang/crates.io-index"
2746
checksum = "592391fc30a77f94bc5a3385d1569052907e3b3cecb28099671b9d5801dee6c6"
2747
dependencies = [
2748
+
"askama_escape 0.13.0",
2749
"dioxus-core 0.7.1",
2750
"dioxus-core-types 0.7.1",
2751
"rustc-hash 2.1.1",
···
3343
]
3344
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]]
3352
name = "fluent-uri"
3353
version = "0.3.2"
3354
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3375
version = "0.2.0"
3376
source = "registry+https://github.com/rust-lang/crates.io-index"
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
+
]
3401
3402
[[package]]
3403
name = "foreign-types"
···
3794
"r-efi",
3795
"wasip2",
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",
3807
]
3808
3809
[[package]]
···
4633
]
4634
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]]
4652
name = "indexmap"
4653
version = "1.9.3"
4654
source = "registry+https://github.com/rust-lang/crates.io-index"
···
5184
]
5185
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]]
5198
name = "langtag"
5199
version = "0.4.0"
5200
source = "registry+https://github.com/rust-lang/crates.io-index"
···
6061
"log",
6062
"mime",
6063
"mime_guess",
6064
+
"quick-error 1.2.3",
6065
"rand 0.8.5",
6066
"safemem",
6067
"tempfile",
···
6861
]
6862
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]]
6870
name = "pin-project"
6871
version = "1.1.10"
6872
source = "registry+https://github.com/rust-lang/crates.io-index"
···
7142
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
7143
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]]
7151
name = "quick-xml"
7152
version = "0.37.5"
7153
source = "registry+https://github.com/rust-lang/crates.io-index"
···
7514
checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"
7515
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]]
7534
name = "rfc6979"
7535
version = "0.4.0"
7536
source = "registry+https://github.com/rust-lang/crates.io-index"
···
7565
]
7566
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]]
7577
name = "ring"
7578
version = "0.17.14"
7579
source = "registry+https://github.com/rust-lang/crates.io-index"
···
7610
"tiny_http",
7611
"url",
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"
7619
7620
[[package]]
7621
name = "rsa"
···
7729
version = "1.0.22"
7730
source = "registry+https://github.com/rust-lang/crates.io-index"
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
+
]
7750
7751
[[package]]
7752
name = "ryu"
···
8332
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
8333
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]]
8344
name = "siphasher"
8345
version = "0.3.11"
8346
source = "registry+https://github.com/rust-lang/crates.io-index"
···
8426
"static_assertions",
8427
"version_check",
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"
8435
8436
[[package]]
8437
name = "smol_str"
···
8575
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
8576
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]]
8587
name = "string_cache"
8588
version = "0.8.9"
8589
source = "registry+https://github.com/rust-lang/crates.io-index"
···
8679
version = "3.0.0"
8680
source = "registry+https://github.com/rust-lang/crates.io-index"
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
+
]
8692
8693
[[package]]
8694
name = "swc_allocator"
···
9062
source = "registry+https://github.com/rust-lang/crates.io-index"
9063
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
9064
dependencies = [
9065
+
"smawk",
9066
"unicode-linebreak",
9067
"unicode-width 0.2.2",
9068
]
···
9160
]
9161
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]]
9189
name = "tiny_http"
9190
version = "0.12.0"
9191
source = "registry+https://github.com/rust-lang/crates.io-index"
···
9685
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
9686
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]]
9697
name = "tungstenite"
9698
version = "0.23.0"
9699
source = "registry+https://github.com/rust-lang/crates.io-index"
···
9818
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
9819
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]]
9839
name = "unicode-id-start"
9840
version = "1.4.0"
9841
source = "registry+https://github.com/rust-lang/crates.io-index"
···
9863
]
9864
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]]
9878
name = "unicode-segmentation"
9879
version = "1.12.0"
9880
source = "registry+https://github.com/rust-lang/crates.io-index"
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"
9888
9889
[[package]]
9890
name = "unicode-width"
···
9935
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
9936
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]]
9965
name = "utf-8"
9966
version = "0.7.6"
9967
source = "registry+https://github.com/rust-lang/crates.io-index"
···
10282
name = "weaver-app"
10283
version = "0.1.0"
10284
dependencies = [
10285
+
"askama",
10286
"axum",
10287
"base64 0.22.1",
10288
"chrono",
···
10292
"dioxus-free-icons",
10293
"dioxus-primitives",
10294
"dotenvy",
10295
+
"fontdb",
10296
"gloo-storage",
10297
"gloo-timers",
10298
"http",
···
10313
"regex",
10314
"regex-lite",
10315
"reqwest",
10316
+
"resvg",
10317
"serde",
10318
"serde_html_form",
10319
"serde_ipld_dagcbor",
10320
"serde_json",
10321
"syntect",
10322
+
"textwrap",
10323
"time",
10324
+
"tiny-skia",
10325
"tokio",
10326
"tracing",
10327
"tracing-subscriber",
10328
"tracing-wasm",
10329
"urlencoding",
10330
+
"usvg",
10331
"wasm-bindgen",
10332
"wasm-bindgen-futures",
10333
"weaver-api",
···
10609
]
10610
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]]
10618
name = "widestring"
10619
version = "1.2.1"
10620
source = "registry+https://github.com/rust-lang/crates.io-index"
···
11255
]
11256
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]]
11264
name = "xxhash-rust"
11265
version = "0.8.15"
11266
source = "registry+https://github.com/rust-lang/crates.io-index"
···
11492
dependencies = [
11493
"cc",
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",
11510
]
11511
11512
[[package]]
+9
-1
crates/weaver-app/Cargo.toml
···
14
web = ["dioxus/web", "dioxus-primitives/web"]
15
desktop = ["dioxus/desktop"]
16
mobile = ["dioxus/mobile"]
17
-
server = [ "dioxus/server", "dep:jacquard-axum", "dep:axum"]
18
19
20
[dependencies]
···
55
web-time = "1.1"
56
urlencoding = "2.1"
57
tracing-subscriber = { version = "0.3", default-features = false, features = ["std", "registry"] }
0
0
0
0
0
0
0
0
58
59
[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies]
60
webbrowser = "1.0.6"
···
14
web = ["dioxus/web", "dioxus-primitives/web"]
15
desktop = ["dioxus/desktop"]
16
mobile = ["dioxus/mobile"]
17
+
server = [ "dioxus/server", "dep:jacquard-axum", "dep:axum", "dep:resvg", "dep:usvg", "dep:tiny-skia", "dep:textwrap", "dep:askama", "dep:fontdb"]
18
19
20
[dependencies]
···
55
web-time = "1.1"
56
urlencoding = "2.1"
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 }
66
67
[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies]
68
webbrowser = "1.0.6"
+43
crates/weaver-app/assets/styling/main.css
···
40
color: var(--color-subtle);
41
opacity: 0.5;
42
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
40
color: var(--color-subtle);
41
opacity: 0.5;
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
}
118
}
119
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
120
/// Full entry page with metadata, content, and navigation
121
#[component]
122
fn EntryPageView(
···
133
.map(|t| t.as_ref())
134
.unwrap_or("Untitled");
135
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
136
tracing::info!("Entry: {book_title} - {title}");
137
138
rsx! {
139
-
// Set page title
140
-
document::Title { "{title}" }
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
141
document::Link { rel: "stylesheet", href: ENTRY_CSS }
142
143
div { class: "entry-page-layout",
···
117
}
118
}
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
+
152
/// Full entry page with metadata, content, and navigation
153
#[component]
154
fn EntryPageView(
···
165
.map(|t| t.as_ref())
166
.unwrap_or("Untitled");
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
+
217
tracing::info!("Entry: {book_title} - {title}");
218
219
rsx! {
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}" }
237
document::Link { rel: "stylesheet", href: ENTRY_CSS }
238
239
div { class: "entry-page-layout",
+166
crates/weaver-app/src/main.rs
···
31
#[cfg(feature = "server")]
32
mod blobcache;
33
mod cache_impl;
0
0
34
/// Define a components module that contains all shared components for our app.
35
mod components;
36
mod config;
···
419
Ok(bytes) => Ok(build_image_response(bytes)),
420
Err(_) => Ok(image_not_found()),
421
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
422
}
423
424
// #[server(endpoint = "static_routes", output = server_fn::codec::Json)]
···
31
#[cfg(feature = "server")]
32
mod blobcache;
33
mod cache_impl;
34
+
#[cfg(feature = "server")]
35
+
mod og;
36
/// Define a components module that contains all shared components for our app.
37
mod components;
38
mod config;
···
421
Ok(bytes) => Ok(build_image_response(bytes)),
422
Err(_) => Ok(image_not_found()),
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("&", "&")
544
+
.replace("<", "<")
545
+
.replace(">", ">")
546
+
.replace(""", "\"")
547
+
.replace("'", "'")
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, ¬ebook_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, ¬ebook_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, ¬ebook_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())
588
}
589
590
// #[server(endpoint = "static_routes", output = server_fn::codec::Json)]
+206
crates/weaver-app/src/og/mod.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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>