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
187
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
188
188
189
189
[[package]]
190
190
+
name = "arrayref"
191
191
+
version = "0.3.9"
192
192
+
source = "registry+https://github.com/rust-lang/crates.io-index"
193
193
+
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
194
194
+
195
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
235
+
name = "askama"
236
236
+
version = "0.12.1"
237
237
+
source = "registry+https://github.com/rust-lang/crates.io-index"
238
238
+
checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
239
239
+
dependencies = [
240
240
+
"askama_derive",
241
241
+
"askama_escape 0.10.3",
242
242
+
"humansize",
243
243
+
"num-traits",
244
244
+
"percent-encoding",
245
245
+
]
246
246
+
247
247
+
[[package]]
248
248
+
name = "askama_derive"
249
249
+
version = "0.12.5"
250
250
+
source = "registry+https://github.com/rust-lang/crates.io-index"
251
251
+
checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
252
252
+
dependencies = [
253
253
+
"askama_parser",
254
254
+
"basic-toml",
255
255
+
"mime",
256
256
+
"mime_guess",
257
257
+
"proc-macro2",
258
258
+
"quote",
259
259
+
"serde",
260
260
+
"syn 2.0.110",
261
261
+
]
262
262
+
263
263
+
[[package]]
264
264
+
name = "askama_escape"
265
265
+
version = "0.10.3"
266
266
+
source = "registry+https://github.com/rust-lang/crates.io-index"
267
267
+
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
268
268
+
269
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
276
+
name = "askama_parser"
277
277
+
version = "0.2.1"
278
278
+
source = "registry+https://github.com/rust-lang/crates.io-index"
279
279
+
checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
280
280
+
dependencies = [
281
281
+
"nom",
282
282
+
]
283
283
+
284
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
592
+
name = "basic-toml"
593
593
+
version = "0.1.10"
594
594
+
source = "registry+https://github.com/rust-lang/crates.io-index"
595
595
+
checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
596
596
+
dependencies = [
597
597
+
"serde",
598
598
+
]
599
599
+
600
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
796
+
name = "bytemuck"
797
797
+
version = "1.24.0"
798
798
+
source = "registry+https://github.com/rust-lang/crates.io-index"
799
799
+
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
800
800
+
801
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
806
+
807
807
+
[[package]]
808
808
+
name = "byteorder-lite"
809
809
+
version = "0.1.0"
810
810
+
source = "registry+https://github.com/rust-lang/crates.io-index"
811
811
+
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
741
812
742
813
[[package]]
743
814
name = "bytes"
···
1004
1075
]
1005
1076
1006
1077
[[package]]
1078
1078
+
name = "color_quant"
1079
1079
+
version = "1.1.0"
1080
1080
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1081
1081
+
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
1082
1082
+
1083
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
1356
+
]
1357
1357
+
1358
1358
+
[[package]]
1359
1359
+
name = "core_maths"
1360
1360
+
version = "0.1.1"
1361
1361
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1362
1362
+
checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
1363
1363
+
dependencies = [
1364
1364
+
"libm",
1279
1365
]
1280
1366
1281
1367
[[package]]
···
1551
1637
]
1552
1638
1553
1639
[[package]]
1640
1640
+
name = "data-url"
1641
1641
+
version = "0.3.2"
1642
1642
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1643
1643
+
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
1644
1644
+
1645
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
2656
-
"askama_escape",
2748
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
3346
+
name = "float-cmp"
3347
3347
+
version = "0.9.0"
3348
3348
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3349
3349
+
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
3350
3350
+
3351
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
3378
+
3379
3379
+
[[package]]
3380
3380
+
name = "fontconfig-parser"
3381
3381
+
version = "0.5.8"
3382
3382
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3383
3383
+
checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"
3384
3384
+
dependencies = [
3385
3385
+
"roxmltree",
3386
3386
+
]
3387
3387
+
3388
3388
+
[[package]]
3389
3389
+
name = "fontdb"
3390
3390
+
version = "0.22.0"
3391
3391
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3392
3392
+
checksum = "a3a6f9af55fb97ad673fb7a69533eb2f967648a06fa21f8c9bb2cd6d33975716"
3393
3393
+
dependencies = [
3394
3394
+
"fontconfig-parser",
3395
3395
+
"log",
3396
3396
+
"memmap2",
3397
3397
+
"slotmap",
3398
3398
+
"tinyvec",
3399
3399
+
"ttf-parser",
3400
3400
+
]
3280
3401
3281
3402
[[package]]
3282
3403
name = "foreign-types"
···
3673
3794
"r-efi",
3674
3795
"wasip2",
3675
3796
"wasm-bindgen",
3797
3797
+
]
3798
3798
+
3799
3799
+
[[package]]
3800
3800
+
name = "gif"
3801
3801
+
version = "0.13.3"
3802
3802
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3803
3803
+
checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b"
3804
3804
+
dependencies = [
3805
3805
+
"color_quant",
3806
3806
+
"weezl",
3676
3807
]
3677
3808
3678
3809
[[package]]
···
4502
4633
]
4503
4634
4504
4635
[[package]]
4636
4636
+
name = "image-webp"
4637
4637
+
version = "0.1.3"
4638
4638
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4639
4639
+
checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904"
4640
4640
+
dependencies = [
4641
4641
+
"byteorder-lite",
4642
4642
+
"quick-error 2.0.1",
4643
4643
+
]
4644
4644
+
4645
4645
+
[[package]]
4646
4646
+
name = "imagesize"
4647
4647
+
version = "0.13.0"
4648
4648
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4649
4649
+
checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
4650
4650
+
4651
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
5187
+
name = "kurbo"
5188
5188
+
version = "0.11.3"
5189
5189
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5190
5190
+
checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62"
5191
5191
+
dependencies = [
5192
5192
+
"arrayvec",
5193
5193
+
"euclid",
5194
5194
+
"smallvec",
5195
5195
+
]
5196
5196
+
5197
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
5906
-
"quick-error",
6064
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
6864
+
name = "pico-args"
6865
6865
+
version = "0.5.0"
6866
6866
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6867
6867
+
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
6868
6868
+
6869
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
7145
+
name = "quick-error"
7146
7146
+
version = "2.0.1"
7147
7147
+
source = "registry+https://github.com/rust-lang/crates.io-index"
7148
7148
+
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
7149
7149
+
7150
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
7517
+
name = "resvg"
7518
7518
+
version = "0.44.0"
7519
7519
+
source = "registry+https://github.com/rust-lang/crates.io-index"
7520
7520
+
checksum = "4a325d5e8d1cebddd070b13f44cec8071594ab67d1012797c121f27a669b7958"
7521
7521
+
dependencies = [
7522
7522
+
"gif",
7523
7523
+
"image-webp",
7524
7524
+
"log",
7525
7525
+
"pico-args",
7526
7526
+
"rgb",
7527
7527
+
"svgtypes",
7528
7528
+
"tiny-skia",
7529
7529
+
"usvg",
7530
7530
+
"zune-jpeg",
7531
7531
+
]
7532
7532
+
7533
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
7568
+
name = "rgb"
7569
7569
+
version = "0.8.52"
7570
7570
+
source = "registry+https://github.com/rust-lang/crates.io-index"
7571
7571
+
checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
7572
7572
+
dependencies = [
7573
7573
+
"bytemuck",
7574
7574
+
]
7575
7575
+
7576
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
7613
+
7614
7614
+
[[package]]
7615
7615
+
name = "roxmltree"
7616
7616
+
version = "0.20.0"
7617
7617
+
source = "registry+https://github.com/rust-lang/crates.io-index"
7618
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
7732
+
7733
7733
+
[[package]]
7734
7734
+
name = "rustybuzz"
7735
7735
+
version = "0.18.0"
7736
7736
+
source = "registry+https://github.com/rust-lang/crates.io-index"
7737
7737
+
checksum = "c85d1ccd519e61834798eb52c4e886e8c2d7d698dd3d6ce0b1b47eb8557f1181"
7738
7738
+
dependencies = [
7739
7739
+
"bitflags 2.10.0",
7740
7740
+
"bytemuck",
7741
7741
+
"core_maths",
7742
7742
+
"log",
7743
7743
+
"smallvec",
7744
7744
+
"ttf-parser",
7745
7745
+
"unicode-bidi-mirroring",
7746
7746
+
"unicode-ccc",
7747
7747
+
"unicode-properties",
7748
7748
+
"unicode-script",
7749
7749
+
]
7530
7750
7531
7751
[[package]]
7532
7752
name = "ryu"
···
8112
8332
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
8113
8333
8114
8334
[[package]]
8335
8335
+
name = "simplecss"
8336
8336
+
version = "0.2.2"
8337
8337
+
source = "registry+https://github.com/rust-lang/crates.io-index"
8338
8338
+
checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c"
8339
8339
+
dependencies = [
8340
8340
+
"log",
8341
8341
+
]
8342
8342
+
8343
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
8429
+
8430
8430
+
[[package]]
8431
8431
+
name = "smawk"
8432
8432
+
version = "0.3.2"
8433
8433
+
source = "registry+https://github.com/rust-lang/crates.io-index"
8434
8434
+
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
8200
8435
8201
8436
[[package]]
8202
8437
name = "smol_str"
···
8340
8575
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
8341
8576
8342
8577
[[package]]
8578
8578
+
name = "strict-num"
8579
8579
+
version = "0.1.1"
8580
8580
+
source = "registry+https://github.com/rust-lang/crates.io-index"
8581
8581
+
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
8582
8582
+
dependencies = [
8583
8583
+
"float-cmp",
8584
8584
+
]
8585
8585
+
8586
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
8682
+
8683
8683
+
[[package]]
8684
8684
+
name = "svgtypes"
8685
8685
+
version = "0.15.3"
8686
8686
+
source = "registry+https://github.com/rust-lang/crates.io-index"
8687
8687
+
checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc"
8688
8688
+
dependencies = [
8689
8689
+
"kurbo",
8690
8690
+
"siphasher 1.0.1",
8691
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
9065
+
"smawk",
8811
9066
"unicode-linebreak",
8812
9067
"unicode-width 0.2.2",
8813
9068
]
···
8905
9160
]
8906
9161
8907
9162
[[package]]
9163
9163
+
name = "tiny-skia"
9164
9164
+
version = "0.11.4"
9165
9165
+
source = "registry+https://github.com/rust-lang/crates.io-index"
9166
9166
+
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
9167
9167
+
dependencies = [
9168
9168
+
"arrayref",
9169
9169
+
"arrayvec",
9170
9170
+
"bytemuck",
9171
9171
+
"cfg-if",
9172
9172
+
"log",
9173
9173
+
"png",
9174
9174
+
"tiny-skia-path",
9175
9175
+
]
9176
9176
+
9177
9177
+
[[package]]
9178
9178
+
name = "tiny-skia-path"
9179
9179
+
version = "0.11.4"
9180
9180
+
source = "registry+https://github.com/rust-lang/crates.io-index"
9181
9181
+
checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
9182
9182
+
dependencies = [
9183
9183
+
"arrayref",
9184
9184
+
"bytemuck",
9185
9185
+
"strict-num",
9186
9186
+
]
9187
9187
+
9188
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
9688
+
name = "ttf-parser"
9689
9689
+
version = "0.24.1"
9690
9690
+
source = "registry+https://github.com/rust-lang/crates.io-index"
9691
9691
+
checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a"
9692
9692
+
dependencies = [
9693
9693
+
"core_maths",
9694
9694
+
]
9695
9695
+
9696
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
9821
+
name = "unicode-bidi"
9822
9822
+
version = "0.3.18"
9823
9823
+
source = "registry+https://github.com/rust-lang/crates.io-index"
9824
9824
+
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
9825
9825
+
9826
9826
+
[[package]]
9827
9827
+
name = "unicode-bidi-mirroring"
9828
9828
+
version = "0.3.0"
9829
9829
+
source = "registry+https://github.com/rust-lang/crates.io-index"
9830
9830
+
checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f"
9831
9831
+
9832
9832
+
[[package]]
9833
9833
+
name = "unicode-ccc"
9834
9834
+
version = "0.3.0"
9835
9835
+
source = "registry+https://github.com/rust-lang/crates.io-index"
9836
9836
+
checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42"
9837
9837
+
9838
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
9866
+
name = "unicode-properties"
9867
9867
+
version = "0.1.4"
9868
9868
+
source = "registry+https://github.com/rust-lang/crates.io-index"
9869
9869
+
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
9870
9870
+
9871
9871
+
[[package]]
9872
9872
+
name = "unicode-script"
9873
9873
+
version = "0.5.7"
9874
9874
+
source = "registry+https://github.com/rust-lang/crates.io-index"
9875
9875
+
checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f"
9876
9876
+
9877
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
9882
+
9883
9883
+
[[package]]
9884
9884
+
name = "unicode-vo"
9885
9885
+
version = "0.1.0"
9886
9886
+
source = "registry+https://github.com/rust-lang/crates.io-index"
9887
9887
+
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
9562
9888
9563
9889
[[package]]
9564
9890
name = "unicode-width"
···
9609
9935
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
9610
9936
9611
9937
[[package]]
9938
9938
+
name = "usvg"
9939
9939
+
version = "0.44.0"
9940
9940
+
source = "registry+https://github.com/rust-lang/crates.io-index"
9941
9941
+
checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6"
9942
9942
+
dependencies = [
9943
9943
+
"base64 0.22.1",
9944
9944
+
"data-url",
9945
9945
+
"flate2",
9946
9946
+
"fontdb",
9947
9947
+
"imagesize",
9948
9948
+
"kurbo",
9949
9949
+
"log",
9950
9950
+
"pico-args",
9951
9951
+
"roxmltree",
9952
9952
+
"rustybuzz",
9953
9953
+
"simplecss",
9954
9954
+
"siphasher 1.0.1",
9955
9955
+
"strict-num",
9956
9956
+
"svgtypes",
9957
9957
+
"tiny-skia-path",
9958
9958
+
"unicode-bidi",
9959
9959
+
"unicode-script",
9960
9960
+
"unicode-vo",
9961
9961
+
"xmlwriter",
9962
9962
+
]
9963
9963
+
9964
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
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
10295
+
"fontdb",
9941
10296
"gloo-storage",
9942
10297
"gloo-timers",
9943
10298
"http",
···
9958
10313
"regex",
9959
10314
"regex-lite",
9960
10315
"reqwest",
10316
10316
+
"resvg",
9961
10317
"serde",
9962
10318
"serde_html_form",
9963
10319
"serde_ipld_dagcbor",
9964
10320
"serde_json",
9965
10321
"syntect",
10322
10322
+
"textwrap",
9966
10323
"time",
10324
10324
+
"tiny-skia",
9967
10325
"tokio",
9968
10326
"tracing",
9969
10327
"tracing-subscriber",
9970
10328
"tracing-wasm",
9971
10329
"urlencoding",
10330
10330
+
"usvg",
9972
10331
"wasm-bindgen",
9973
10332
"wasm-bindgen-futures",
9974
10333
"weaver-api",
···
10250
10609
]
10251
10610
10252
10611
[[package]]
10612
10612
+
name = "weezl"
10613
10613
+
version = "0.1.12"
10614
10614
+
source = "registry+https://github.com/rust-lang/crates.io-index"
10615
10615
+
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
10616
10616
+
10617
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
11258
+
name = "xmlwriter"
11259
11259
+
version = "0.1.0"
11260
11260
+
source = "registry+https://github.com/rust-lang/crates.io-index"
11261
11261
+
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
11262
11262
+
11263
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
11495
+
]
11496
11496
+
11497
11497
+
[[package]]
11498
11498
+
name = "zune-core"
11499
11499
+
version = "0.4.12"
11500
11500
+
source = "registry+https://github.com/rust-lang/crates.io-index"
11501
11501
+
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
11502
11502
+
11503
11503
+
[[package]]
11504
11504
+
name = "zune-jpeg"
11505
11505
+
version = "0.4.21"
11506
11506
+
source = "registry+https://github.com/rust-lang/crates.io-index"
11507
11507
+
checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
11508
11508
+
dependencies = [
11509
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
17
-
server = [ "dioxus/server", "dep:jacquard-axum", "dep:axum"]
17
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
58
+
59
59
+
# OG image generation (server-only)
60
60
+
resvg = { version = "0.44", optional = true }
61
61
+
usvg = { version = "0.44", optional = true }
62
62
+
tiny-skia = { version = "0.11", optional = true }
63
63
+
textwrap = { version = "0.16", optional = true }
64
64
+
askama = { version = "0.12", optional = true }
65
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
43
+
44
44
+
a {{
45
45
+
color: var(--color-link);
46
46
+
text-decoration: none;
47
47
+
}}
48
48
+
49
49
+
50
50
+
@font-face {
51
51
+
font-family: "Ioskeley Mono";
52
52
+
font-style: normal;
53
53
+
font-weight: normal;
54
54
+
src: url("/assets/IoskeleyMono-Regular.woff2") format("woff2");
55
55
+
}
56
56
+
@font-face {
57
57
+
font-family: "Ioskeley Mono";
58
58
+
font-style: normal;
59
59
+
font-weight: lighter;
60
60
+
src: url("/assets/IoskeleyMono-Light.woff2") format("woff2");
61
61
+
}
62
62
+
@font-face {
63
63
+
font-family: "Ioskeley Mono";
64
64
+
font-style: italic;
65
65
+
font-weight: lighter;
66
66
+
src: url("/assets/IoskeleyMono-LightItalic.woff2") format("woff2");
67
67
+
}
68
68
+
@font-face {
69
69
+
font-family: "Ioskeley Mono";
70
70
+
font-style: normal;
71
71
+
font-weight: bold;
72
72
+
src: url("/assets/IoskeleyMono-Bold.woff2") format("woff2");
73
73
+
}
74
74
+
@font-face {
75
75
+
font-family: "Ioskeley Mono";
76
76
+
font-style: italic;
77
77
+
font-weight: normal;
78
78
+
src: url("/assets/IoskeleyMono-Italic.woff2") format("woff2");
79
79
+
}
80
80
+
@font-face {
81
81
+
font-family: "Ioskeley Mono";
82
82
+
font-style: italic;
83
83
+
font-weight: bold;
84
84
+
src: url("/assets/IoskeleyMono-BoldItalic.woff2") format("woff2");
85
85
+
}
+98
-2
crates/weaver-app/src/components/entry.rs
···
117
117
}
118
118
}
119
119
120
120
+
/// Extract a plain-text preview from markdown content (first ~160 chars)
121
121
+
fn extract_preview(content: &str, max_len: usize) -> String {
122
122
+
// Simple extraction: skip markdown syntax, get plain text
123
123
+
let plain: String = content
124
124
+
.lines()
125
125
+
.filter(|line| {
126
126
+
let trimmed = line.trim();
127
127
+
// Skip headings, images, links, code blocks
128
128
+
!trimmed.starts_with('#')
129
129
+
&& !trimmed.starts_with('!')
130
130
+
&& !trimmed.starts_with("```")
131
131
+
&& !trimmed.is_empty()
132
132
+
})
133
133
+
.take(5)
134
134
+
.collect::<Vec<_>>()
135
135
+
.join(" ");
136
136
+
137
137
+
// Clean up markdown inline syntax
138
138
+
let cleaned = plain
139
139
+
.replace("**", "")
140
140
+
.replace("__", "")
141
141
+
.replace('*', "")
142
142
+
.replace('_', "")
143
143
+
.replace('`', "");
144
144
+
145
145
+
if cleaned.len() <= max_len {
146
146
+
cleaned
147
147
+
} else {
148
148
+
format!("{}...", &cleaned[..max_len - 3])
149
149
+
}
150
150
+
}
151
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
168
+
// Get entry path for URLs
169
169
+
let entry_path = entry_view
170
170
+
.path
171
171
+
.as_ref()
172
172
+
.map(|p| p.as_ref().to_string())
173
173
+
.unwrap_or_else(|| title.to_string());
174
174
+
175
175
+
// Get author handle
176
176
+
let author_handle = entry_view
177
177
+
.authors
178
178
+
.first()
179
179
+
.map(|a| {
180
180
+
use weaver_api::sh_weaver::actor::ProfileDataViewInner;
181
181
+
match &a.record.inner {
182
182
+
ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_string(),
183
183
+
ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_string(),
184
184
+
ProfileDataViewInner::TangledProfileView(p) => p.handle.as_ref().to_string(),
185
185
+
_ => "unknown".to_string(),
186
186
+
}
187
187
+
})
188
188
+
.unwrap_or_else(|| "unknown".to_string());
189
189
+
190
190
+
// Build OG URLs
191
191
+
let base = if crate::env::WEAVER_APP_ENV == "dev" {
192
192
+
format!("http://127.0.0.1:{}", crate::env::WEAVER_PORT)
193
193
+
} else {
194
194
+
crate::env::WEAVER_APP_HOST.to_string()
195
195
+
};
196
196
+
let canonical_url = format!(
197
197
+
"{}/{}/{}/{}",
198
198
+
base,
199
199
+
ident(),
200
200
+
book_title(),
201
201
+
entry_path
202
202
+
);
203
203
+
let og_image_url = format!(
204
204
+
"{}/og/{}/{}/{}.png",
205
205
+
base,
206
206
+
ident(),
207
207
+
book_title(),
208
208
+
entry_path
209
209
+
);
210
210
+
211
211
+
// Extract description preview from content
212
212
+
let description = extract_preview(entry_record().content.as_ref(), 160);
213
213
+
214
214
+
// Full page title
215
215
+
let page_title = format!("{} | {} | Weaver", title, book_title());
216
216
+
136
217
tracing::info!("Entry: {book_title} - {title}");
137
218
138
219
rsx! {
139
139
-
// Set page title
140
140
-
document::Title { "{title}" }
220
220
+
// Page title and OG meta tags
221
221
+
document::Title { "{page_title}" }
222
222
+
223
223
+
// OpenGraph tags
224
224
+
document::Meta { property: "og:title", content: "{title}" }
225
225
+
document::Meta { property: "og:description", content: "{description}" }
226
226
+
document::Meta { property: "og:image", content: "{og_image_url}" }
227
227
+
document::Meta { property: "og:type", content: "article" }
228
228
+
document::Meta { property: "og:url", content: "{canonical_url}" }
229
229
+
document::Meta { property: "og:site_name", content: "Weaver" }
230
230
+
231
231
+
// Twitter Card tags
232
232
+
document::Meta { name: "twitter:card", content: "summary_large_image" }
233
233
+
document::Meta { name: "twitter:title", content: "{title}" }
234
234
+
document::Meta { name: "twitter:description", content: "{description}" }
235
235
+
document::Meta { name: "twitter:image", content: "{og_image_url}" }
236
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
34
+
#[cfg(feature = "server")]
35
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
424
+
}
425
425
+
426
426
+
// Route: /og/{ident}/{book_title}/{entry_title} - OpenGraph image for entry
427
427
+
#[cfg(all(feature = "fullstack-server", feature = "server"))]
428
428
+
#[get("/og/{ident}/{book_title}/{entry_title}", fetcher: Extension<Arc<fetch::Fetcher>>)]
429
429
+
pub async fn og_image(
430
430
+
ident: SmolStr,
431
431
+
book_title: SmolStr,
432
432
+
entry_title: SmolStr,
433
433
+
) -> Result<axum::response::Response> {
434
434
+
use axum::{
435
435
+
http::{header::{CACHE_CONTROL, CONTENT_TYPE}, StatusCode},
436
436
+
response::IntoResponse,
437
437
+
};
438
438
+
use weaver_api::sh_weaver::actor::ProfileDataViewInner;
439
439
+
use weaver_api::sh_weaver::notebook::Title;
440
440
+
441
441
+
// Strip .png extension if present
442
442
+
let entry_title = entry_title.strip_suffix(".png").unwrap_or(&entry_title);
443
443
+
444
444
+
let Ok(at_ident) = AtIdentifier::new_owned(ident.clone()) else {
445
445
+
return Ok((StatusCode::BAD_REQUEST, "Invalid identifier").into_response());
446
446
+
};
447
447
+
448
448
+
// Fetch entry data
449
449
+
let entry_result = fetcher.get_entry(at_ident.clone(), book_title.clone(), entry_title.into()).await;
450
450
+
451
451
+
let arc_data = match entry_result {
452
452
+
Ok(Some(data)) => data,
453
453
+
Ok(None) => return Ok((StatusCode::NOT_FOUND, "Entry not found").into_response()),
454
454
+
Err(e) => {
455
455
+
tracing::error!("Failed to fetch entry for OG image: {:?}", e);
456
456
+
return Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to fetch entry").into_response());
457
457
+
}
458
458
+
};
459
459
+
let (book_entry, entry) = arc_data.as_ref();
460
460
+
461
461
+
// Build cache key using entry CID
462
462
+
let entry_cid = book_entry.entry.cid.as_ref();
463
463
+
let cache_key = og::cache_key(&ident, &book_title, entry_title, entry_cid);
464
464
+
465
465
+
// Check cache first
466
466
+
if let Some(cached) = og::get_cached(&cache_key) {
467
467
+
return Ok((
468
468
+
[
469
469
+
(CONTENT_TYPE, "image/png"),
470
470
+
(CACHE_CONTROL, "public, max-age=3600"),
471
471
+
],
472
472
+
cached,
473
473
+
).into_response());
474
474
+
}
475
475
+
476
476
+
// Extract metadata
477
477
+
let title: &str = entry.title.as_ref();
478
478
+
479
479
+
// Use book_title from URL - it's the notebook slug/title
480
480
+
// TODO: Could fetch actual notebook record to get display title
481
481
+
let notebook_title_str: String = book_title.to_string();
482
482
+
483
483
+
let author_handle = book_entry.entry.authors.first()
484
484
+
.map(|a| match &a.record.inner {
485
485
+
ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_string(),
486
486
+
ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_string(),
487
487
+
ProfileDataViewInner::TangledProfileView(p) => p.handle.as_ref().to_string(),
488
488
+
_ => "unknown".to_string(),
489
489
+
})
490
490
+
.unwrap_or_else(|| "unknown".to_string());
491
491
+
492
492
+
// Check for hero image in embeds
493
493
+
let hero_image_data = if let Some(ref embeds) = entry.embeds {
494
494
+
if let Some(ref images) = embeds.images {
495
495
+
if let Some(first_image) = images.images.first() {
496
496
+
// Get DID from the entry URI
497
497
+
let did = book_entry.entry.uri.authority();
498
498
+
499
499
+
let blob = first_image.image.blob();
500
500
+
let cid = blob.cid();
501
501
+
let mime = blob.mime_type.as_ref();
502
502
+
let format = mime.strip_prefix("image/").unwrap_or("jpeg");
503
503
+
504
504
+
// Build CDN URL
505
505
+
let cdn_url = format!(
506
506
+
"https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@{}",
507
507
+
did.as_str(), cid.as_ref(), format
508
508
+
);
509
509
+
510
510
+
// Fetch the image
511
511
+
match reqwest::get(&cdn_url).await {
512
512
+
Ok(response) if response.status().is_success() => {
513
513
+
match response.bytes().await {
514
514
+
Ok(bytes) => {
515
515
+
use base64::Engine;
516
516
+
let base64_str = base64::engine::general_purpose::STANDARD.encode(&bytes);
517
517
+
Some(format!("data:{};base64,{}", mime, base64_str))
518
518
+
}
519
519
+
Err(_) => None
520
520
+
}
521
521
+
}
522
522
+
_ => None
523
523
+
}
524
524
+
} else {
525
525
+
None
526
526
+
}
527
527
+
} else {
528
528
+
None
529
529
+
}
530
530
+
} else {
531
531
+
None
532
532
+
};
533
533
+
534
534
+
// Extract content snippet - render markdown to HTML then strip tags
535
535
+
let content_snippet: String = {
536
536
+
let parser = markdown_weaver::Parser::new(entry.content.as_ref());
537
537
+
let mut html = String::new();
538
538
+
markdown_weaver::html::push_html(&mut html, parser);
539
539
+
// Strip HTML tags
540
540
+
regex_lite::Regex::new(r"<[^>]+>")
541
541
+
.unwrap()
542
542
+
.replace_all(&html, "")
543
543
+
.replace("&", "&")
544
544
+
.replace("<", "<")
545
545
+
.replace(">", ">")
546
546
+
.replace(""", "\"")
547
547
+
.replace("'", "'")
548
548
+
.replace('\n', " ")
549
549
+
.split_whitespace()
550
550
+
.collect::<Vec<_>>()
551
551
+
.join(" ")
552
552
+
};
553
553
+
554
554
+
// Generate image - hero or text-only based on available data
555
555
+
let png_bytes = if let Some(ref hero_data) = hero_image_data {
556
556
+
match og::generate_hero_image(hero_data, title, ¬ebook_title_str, &author_handle) {
557
557
+
Ok(bytes) => bytes,
558
558
+
Err(e) => {
559
559
+
tracing::error!("Failed to generate hero OG image: {:?}, falling back to text", e);
560
560
+
og::generate_text_only(title, &content_snippet, ¬ebook_title_str, &author_handle)
561
561
+
.map_err(|e| {
562
562
+
tracing::error!("Failed to generate text OG image: {:?}", e);
563
563
+
})
564
564
+
.ok()
565
565
+
.unwrap_or_default()
566
566
+
}
567
567
+
}
568
568
+
} else {
569
569
+
match og::generate_text_only(title, &content_snippet, ¬ebook_title_str, &author_handle) {
570
570
+
Ok(bytes) => bytes,
571
571
+
Err(e) => {
572
572
+
tracing::error!("Failed to generate OG image: {:?}", e);
573
573
+
return Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate image").into_response());
574
574
+
}
575
575
+
}
576
576
+
};
577
577
+
578
578
+
// Cache the generated image
579
579
+
og::cache_image(cache_key, png_bytes.clone());
580
580
+
581
581
+
Ok((
582
582
+
[
583
583
+
(CONTENT_TYPE, "image/png"),
584
584
+
(CACHE_CONTROL, "public, max-age=3600"),
585
585
+
],
586
586
+
png_bytes,
587
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
1
+
//! OpenGraph image generation module
2
2
+
//!
3
3
+
//! Generates social card images for entry pages using SVG templates rendered to PNG.
4
4
+
5
5
+
use askama::Template;
6
6
+
use std::sync::OnceLock;
7
7
+
use std::time::Duration;
8
8
+
9
9
+
use crate::cache_impl::{Cache, new_cache};
10
10
+
11
11
+
/// Cache for generated OG images
12
12
+
/// Key: "{ident}/{book}/{entry}/{cid}" - includes CID for invalidation
13
13
+
static OG_CACHE: OnceLock<Cache<String, Vec<u8>>> = OnceLock::new();
14
14
+
15
15
+
fn get_cache() -> &'static Cache<String, Vec<u8>> {
16
16
+
OG_CACHE.get_or_init(|| {
17
17
+
// Cache up to 1000 images for 1 hour
18
18
+
new_cache(1000, Duration::from_secs(3600))
19
19
+
})
20
20
+
}
21
21
+
22
22
+
/// Generate cache key from entry identifiers
23
23
+
pub fn cache_key(ident: &str, book: &str, entry: &str, cid: &str) -> String {
24
24
+
format!("{}/{}/{}/{}", ident, book, entry, cid)
25
25
+
}
26
26
+
27
27
+
/// Try to get a cached OG image
28
28
+
pub fn get_cached(key: &str) -> Option<Vec<u8>> {
29
29
+
get_cache().get(&key.to_string())
30
30
+
}
31
31
+
32
32
+
/// Store an OG image in the cache
33
33
+
pub fn cache_image(key: String, image: Vec<u8>) {
34
34
+
get_cache().insert(key, image);
35
35
+
}
36
36
+
37
37
+
/// Error type for OG image generation
38
38
+
#[derive(Debug)]
39
39
+
pub enum OgError {
40
40
+
NotFound,
41
41
+
FetchError(String),
42
42
+
RenderError(String),
43
43
+
TemplateError(String),
44
44
+
}
45
45
+
46
46
+
impl std::fmt::Display for OgError {
47
47
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48
48
+
match self {
49
49
+
OgError::NotFound => write!(f, "Entry not found"),
50
50
+
OgError::FetchError(e) => write!(f, "Fetch error: {}", e),
51
51
+
OgError::RenderError(e) => write!(f, "Render error: {}", e),
52
52
+
OgError::TemplateError(e) => write!(f, "Template error: {}", e),
53
53
+
}
54
54
+
}
55
55
+
}
56
56
+
57
57
+
impl std::error::Error for OgError {}
58
58
+
59
59
+
/// Standard OG image dimensions
60
60
+
pub const OG_WIDTH: u32 = 1200;
61
61
+
pub const OG_HEIGHT: u32 = 630;
62
62
+
63
63
+
/// Rose Pine theme colors
64
64
+
mod colors {
65
65
+
pub const BASE: &str = "#191724";
66
66
+
pub const TEXT: &str = "#e0def4";
67
67
+
pub const SUBTLE: &str = "#908caa";
68
68
+
pub const MUTED: &str = "#6e6a86";
69
69
+
pub const OVERLAY: &str = "#524f67";
70
70
+
}
71
71
+
72
72
+
/// Text-only template (no hero image)
73
73
+
#[derive(Template)]
74
74
+
#[template(path = "og_text_only.svg", escape = "none")]
75
75
+
pub struct TextOnlyTemplate {
76
76
+
pub title_lines: Vec<String>,
77
77
+
pub content_lines: Vec<String>,
78
78
+
pub notebook_title: String,
79
79
+
pub author_handle: String,
80
80
+
}
81
81
+
82
82
+
/// Hero image template (full-bleed image with overlay)
83
83
+
#[derive(Template)]
84
84
+
#[template(path = "og_hero_image.svg", escape = "none")]
85
85
+
pub struct HeroImageTemplate {
86
86
+
pub hero_image_data: String,
87
87
+
pub title_lines: Vec<String>,
88
88
+
pub notebook_title: String,
89
89
+
pub author_handle: String,
90
90
+
}
91
91
+
92
92
+
/// Global font database, initialized once
93
93
+
static FONTDB: OnceLock<fontdb::Database> = OnceLock::new();
94
94
+
95
95
+
fn get_fontdb() -> &'static fontdb::Database {
96
96
+
FONTDB.get_or_init(|| {
97
97
+
let mut db = fontdb::Database::new();
98
98
+
// Load IBM Plex Sans from embedded bytes
99
99
+
let font_data = include_bytes!("../../assets/fonts/IBMPlexSans-VariableFont_wdth,wght.ttf");
100
100
+
db.load_font_data(font_data.to_vec());
101
101
+
let font_data =
102
102
+
include_bytes!("../../assets/fonts/ioskeley-mono/IoskeleyMono-Regular.woff2");
103
103
+
db.load_font_data(font_data.to_vec());
104
104
+
db
105
105
+
})
106
106
+
}
107
107
+
108
108
+
/// Wrap title text into lines that fit the SVG width
109
109
+
pub fn wrap_title(title: &str, max_chars: usize, max_lines: usize) -> Vec<String> {
110
110
+
textwrap::wrap(title, max_chars)
111
111
+
.into_iter()
112
112
+
.take(max_lines)
113
113
+
.map(|s| s.to_string())
114
114
+
.collect()
115
115
+
}
116
116
+
117
117
+
/// Render an SVG string to PNG bytes
118
118
+
pub fn render_svg_to_png(svg: &str) -> Result<Vec<u8>, OgError> {
119
119
+
let fontdb = get_fontdb();
120
120
+
121
121
+
let options = usvg::Options {
122
122
+
fontdb: std::sync::Arc::new(fontdb.clone()),
123
123
+
..Default::default()
124
124
+
};
125
125
+
126
126
+
let tree = usvg::Tree::from_str(svg, &options)
127
127
+
.map_err(|e| OgError::RenderError(format!("Failed to parse SVG: {}", e)))?;
128
128
+
129
129
+
let mut pixmap = tiny_skia::Pixmap::new(OG_WIDTH, OG_HEIGHT)
130
130
+
.ok_or_else(|| OgError::RenderError("Failed to create pixmap".to_string()))?;
131
131
+
132
132
+
resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut());
133
133
+
134
134
+
pixmap
135
135
+
.encode_png()
136
136
+
.map_err(|e| OgError::RenderError(format!("Failed to encode PNG: {}", e)))
137
137
+
}
138
138
+
139
139
+
/// Generate a text-only OG image
140
140
+
pub fn generate_text_only(
141
141
+
title: &str,
142
142
+
content: &str,
143
143
+
notebook_title: &str,
144
144
+
author_handle: &str,
145
145
+
) -> Result<Vec<u8>, OgError> {
146
146
+
let title_lines = wrap_title(title, 50, 2);
147
147
+
let content_lines = wrap_title(content, 70, 5);
148
148
+
149
149
+
let template = TextOnlyTemplate {
150
150
+
title_lines,
151
151
+
content_lines,
152
152
+
notebook_title: notebook_title.to_string(),
153
153
+
author_handle: author_handle.to_string(),
154
154
+
};
155
155
+
156
156
+
let svg = template
157
157
+
.render()
158
158
+
.map_err(|e| OgError::TemplateError(e.to_string()))?;
159
159
+
160
160
+
render_svg_to_png(&svg)
161
161
+
}
162
162
+
163
163
+
/// Generate a hero image OG image
164
164
+
pub fn generate_hero_image(
165
165
+
hero_image_data: &str,
166
166
+
title: &str,
167
167
+
notebook_title: &str,
168
168
+
author_handle: &str,
169
169
+
) -> Result<Vec<u8>, OgError> {
170
170
+
let title_lines = wrap_title(title, 50, 2);
171
171
+
172
172
+
let template = HeroImageTemplate {
173
173
+
hero_image_data: hero_image_data.to_string(),
174
174
+
title_lines,
175
175
+
notebook_title: notebook_title.to_string(),
176
176
+
author_handle: author_handle.to_string(),
177
177
+
};
178
178
+
179
179
+
let svg = template
180
180
+
.render()
181
181
+
.map_err(|e| OgError::TemplateError(e.to_string()))?;
182
182
+
183
183
+
render_svg_to_png(&svg)
184
184
+
}
185
185
+
186
186
+
#[cfg(test)]
187
187
+
mod tests {
188
188
+
use super::*;
189
189
+
190
190
+
#[test]
191
191
+
fn test_wrap_title_short() {
192
192
+
let lines = wrap_title("Hello World", 28, 3);
193
193
+
assert_eq!(lines, vec!["Hello World"]);
194
194
+
}
195
195
+
196
196
+
#[test]
197
197
+
fn test_wrap_title_long() {
198
198
+
let lines = wrap_title(
199
199
+
"This is a very long title that should wrap onto multiple lines",
200
200
+
28,
201
201
+
3,
202
202
+
);
203
203
+
assert!(lines.len() > 1);
204
204
+
assert!(lines.len() <= 3);
205
205
+
}
206
206
+
}
+18
crates/weaver-app/templates/og_hero_image.svg
···
1
1
+
<svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
2
2
+
<!-- Hero image (upper portion) -->
3
3
+
<image xlink:href="{{ hero_image_data }}" x="0" y="0" width="1200" height="420" preserveAspectRatio="xMidYMid slice"/>
4
4
+
5
5
+
<!-- Bottom panel with theme colors -->
6
6
+
<rect x="0" y="420" width="1200" height="210" fill="#191724"/>
7
7
+
8
8
+
<!-- Title -->
9
9
+
{% for line in title_lines %}
10
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
11
+
{% endfor %}
12
12
+
13
13
+
<!-- Notebook + Author row -->
14
14
+
<text x="60" y="600" fill="#ebbcba" font-family="IBM Plex Sans, sans-serif" font-size="32">{{ notebook_title }} · @{{ author_handle }}</text>
15
15
+
16
16
+
<!-- Weaver branding -->
17
17
+
<text x="1080" y="600" fill="#908caa" font-family="IBM Plex Sans, sans-serif" font-size="20">weaver.sh</text>
18
18
+
</svg>
+22
crates/weaver-app/templates/og_text_only.svg
···
1
1
+
<svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg">
2
2
+
<!-- Background -->
3
3
+
<rect width="1200" height="630" fill="#191724"/>
4
4
+
5
5
+
<!-- Entry title (large, wrapped) -->
6
6
+
{% for line in title_lines %}
7
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
8
+
{% endfor %}
9
9
+
10
10
+
<!-- Notebook title + Author -->
11
11
+
<text x="60" y="320" fill="#ebbcba" font-family="IBM Plex Sans, sans-serif" font-size="32">{{ notebook_title }} · @{{ author_handle }}</text>
12
12
+
13
13
+
<!-- Content snippet -->
14
14
+
{% for line in content_lines %}
15
15
+
<text x="60" y="{{ 380 + loop.index0 * 36 }}" fill="#e0def4" font-family="IBM Plex Sans, sans-serif" font-size="28">{{ line }}</text>
16
16
+
{% endfor %}
17
17
+
18
18
+
19
19
+
20
20
+
<!-- Weaver branding -->
21
21
+
<text x="60" y="590" fill="#908caa" font-family="IBM Plex Sans, sans-serif" font-size="24">weaver.sh</text>
22
22
+
</svg>