My personal website, in gleam+lustre!
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Now generating an rss feed


Signed-off-by: MLC Bloeiman <mar@strawmelonjuice.com>

+172 -84
+1
.gitignore
··· 17 17 # Generated on prepare 18 18 assets/styles.css 19 19 assets/sitemap.xml 20 + assets/feed.xml
+63 -2
dev/homepage/prepare.gleam
··· 2 2 //// https://forge.strawmelonjuice.com/CynthiaWebsiteEngine/ByYou/src/branch/main/by-you/src/byyou/cli.gleam 3 3 4 4 import gleam/erlang/application 5 + import gleam/int 5 6 import gleam/io 6 7 import gleam/json 7 8 import gleam/list 9 + import gleam/option 8 10 import gleam/result 9 11 import gleam/string 10 - import homepage 12 + import gleam/time/calendar 13 + import gleam/time/timestamp 14 + import glentities 15 + import homepage.{Entry} 11 16 import simplifile 17 + import webls/rss 12 18 13 19 @external(javascript, "./nonexistent.js", "okay") 14 20 fn assertive_priv_file_read(file: String) -> String { ··· 43 49 Ok(_) -> Nil 44 50 Error(msg) -> panic as msg 45 51 } 52 + case generate_rss() { 53 + Ok(_) -> Nil 54 + Error(msg) -> panic as msg 55 + } 46 56 } 47 57 58 + pub fn generate_rss() { 59 + let contents = 60 + [ 61 + // Only the 'sitewide' one for now. 62 + rss.channel( 63 + "Mar's site - all posts", 64 + "Posts on strawmelonjuice dot com", 65 + "https://strawmelonjuice.com", 66 + ) 67 + |> rss.with_channel_category("Mixed") 68 + |> rss.with_channel_web_master("rss-webmaster@strawmelonjuice.com") 69 + |> rss.with_channel_last_build_date(timestamp.system_time()) 70 + |> rss.with_channel_copyright( 71 + "The rights to this content is reserved by M Bloeiman.", 72 + ) 73 + |> rss.with_channel_language("en") 74 + |> rss.with_channel_items( 75 + list.map(homepage.posts_entries(), fn(entry) { 76 + let permalink = 77 + "https://strawmelonjuice.com/post/" <> int.to_string(entry.id) 78 + let Entry(route:, title:, last_updated:, parent: _) = entry.entry 79 + let title = glentities.encode(title, glentities.Hex) 80 + let description = glentities.encode(entry.description, glentities.Hex) 81 + rss.item(title, description) 82 + |> rss.with_item_link( 83 + "https://strawmelonjuice.com" <> route |> homepage.to_url, 84 + ) 85 + |> rss.with_item_author("rss-webmaster@strawmelonjuice.com") 86 + |> rss.with_item_pub_date({ 87 + last_updated 88 + |> timestamp.from_calendar( 89 + time: calendar.TimeOfDay(13, 00, 00, 00), 90 + offset: calendar.local_offset(), 91 + ) 92 + }) 93 + |> rss.with_item_guid(#(permalink, option.Some(True))) 94 + }), 95 + ), 96 + ] 97 + |> rss.to_string() 98 + use _ <- result.try( 99 + simplifile.write("./assets/feed.xml", contents:) 100 + |> result.replace_error( 101 + "❌\tSomething went wrong writing `./assets/sitemap.xml`.", 102 + ), 103 + ) 104 + io.println("✅\tWrote: `./assets/feed.xml`") 105 + |> Ok 106 + } 107 + 108 + // Couldve done this w webls too but didn't 48 109 fn generate_sitemap() { 49 110 let contents = "<?xml version=\"1.0\" encoding=\"UTF-8\"?> 50 111 ··· 55 116 56 117 <lastmod>" <> entry.last_updated |> homepage.date_to_yyyy_mm_dd <> "</lastmod> 57 118 58 - <changefreq>monthly</changefreq> 119 + <changefreq>Weekly</changefreq> 59 120 60 121 <priority>0.8</priority> 61 122
+3
gleam.toml
··· 8 8 jot = ">= 10.1.1 and < 11.0.0" 9 9 chilp = ">= 1.0.0 and < 2.0.0" 10 10 gleam_time = ">= 1.7.0 and < 2.0.0" 11 + webls = ">= 1.6.1 and < 2.0.0" 12 + glentities = ">= 6.2.1 and < 7.0.0" 11 13 12 14 [dev-dependencies] 13 15 lustre_dev_tools = ">= 2.3.4 and < 3.0.0" ··· 26 28 links = [ 27 29 { rel = "preconnect", href = "https://fontlay.com", crossorigin = "" }, 28 30 { rel = "shortcut icon", href = "/strawmelonjuice.png", type = "image/x-icon" }, 31 + { rel = "alternate", href = "/feed.xml", type = "application/rss+xml", title = "Site-wide RSS Feed" }, 29 32 { rel = "me", href = "https://pony.social/@strawmelonjuice" }, 30 33 ] 31 34 stylesheets = [
+4
manifest.toml
··· 23 23 { name = "gleam_stdlib", version = "0.70.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86949BF5D1F0E4AC0AB5B06F235D8A5CC11A2DFC33BF22F752156ED61CA7D0FF" }, 24 24 { name = "gleam_time", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "56DB0EF9433826D3B99DB0B4AF7A2BFED13D09755EC64B1DAAB46F804A9AD47D" }, 25 25 { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, 26 + { name = "glentities", version = "6.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glentities", source = "hex", outer_checksum = "78A0B28789C1A7840468C683FC9588B0B59AA38BE8CF5DACD1AF2E60A91AE638" }, 26 27 { name = "glint", version = "1.2.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "2214C7CEFDE457CEE62140C3D4899B964E05236DA74E4243DFADF4AF29C382BB" }, 27 28 { name = "glisten", version = "8.0.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "86B838196592D9EBDE7A1D2369AE3A51E568F7DD2D168706C463C42D17B95312" }, 28 29 { name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" }, ··· 45 46 { name = "splitter", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "3DFD6B6C49E61EDAF6F7B27A42054A17CFF6CA2135FF553D0CB61C234D281DD0" }, 46 47 { name = "telemetry", version = "1.4.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "D1FF426F988AC1092F9D684D34D08E51042A70567C16BE793FBC8F399FD2E77D" }, 47 48 { name = "tom", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "90791DA4AACE637E30081FE77049B8DB850FBC8CACC31515376BCC4E59BE1DD2" }, 49 + { name = "webls", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "webls", source = "hex", outer_checksum = "40317445399BFBBDCE8D5976C84ACD92319D4A6F1500A926F19AFCC9ED6639F1" }, 48 50 { name = "wisp", version = "2.2.0", build_tools = ["gleam"], requirements = ["directories", "exception", "filepath", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "houdini", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "655163D4DE19E3DD4AC75813A991BFD5523CB4FF2FC5F9F58FD6FB39D5D1806D" }, 49 51 ] 50 52 ··· 54 56 gleam_json = { version = ">= 3.1.0 and < 4.0.0" } 55 57 gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 56 58 gleam_time = { version = ">= 1.7.0 and < 2.0.0" } 59 + glentities = { version = ">= 6.2.1 and < 7.0.0" } 57 60 jot = { version = ">= 10.1.1 and < 11.0.0" } 58 61 lustre = { version = ">= 5.6.0 and < 6.0.0" } 59 62 lustre_dev_tools = { version = ">= 2.3.4 and < 3.0.0" } 60 63 modem = { version = ">= 2.1.2 and < 3.0.0" } 61 64 simplifile = { version = ">= 2.3.2 and < 3.0.0" } 65 + webls = { version = ">= 1.6.1 and < 2.0.0" }
+101 -82
src/homepage.gleam
··· 202 202 ] 203 203 } 204 204 205 + const highlighted_posts = [900] 206 + 205 207 const pages = [ 206 208 // If index isn't fist, things break. 207 209 Entry(Index, "Homepage", Date(2026, March, 16), NotFound(uri.empty)), ··· 212 214 Entry(Sitemap, "Sitemap", Date(2026, March, 16), Index), 213 215 Entry(AllAndEverything, "/everything page", Date(2026, March, 16), Index), 214 216 ] 215 - 216 - pub fn sitemap() -> List(Entry) { 217 - let posts = 218 - posts() 219 - |> list.map(fn(post) { 220 - Entry( 221 - route: PostById(post.id), 222 - title: post.title, 223 - last_updated: case post.revised { 224 - Some(new) -> new 225 - None -> post.published 226 - }, 227 - parent: Posts, 228 - ) 229 - }) 230 - pages |> list.append(posts) 231 - } 232 - 233 - fn view_sitemap() -> List(Element(Msg)) { 234 - let assert Ok(index_entry) = list.first(pages) 235 - // To be clear: 236 - let assert Index = index_entry.route 237 - [ 238 - title("Sitemap"), 239 - html.br([attribute.class("mt-4")]), 240 - 241 - html.ul([attribute.class("list-['=>'] list-inside")], [ 242 - render_sitemap_recursively(start_entry: index_entry, level: 0), 243 - ]), 244 - ] 245 - } 246 - 247 - // For now we render a boring with-depth list, plan is to create a whole graph out of this! 248 - fn render_sitemap_recursively(start_entry entry: Entry, level level: Int) { 249 - let children = 250 - list.filter(sitemap(), fn(maybe_child) { maybe_child.parent == entry.route }) 251 - html.li([], [ 252 - html.a( 253 - [href(entry.route), attribute.class("link link-secondary-content ps-2")], 254 - [element.text(entry.title)], 255 - ), 256 - 257 - case children { 258 - [] -> element.none() 259 - _ -> { 260 - list.map(children, fn(child) { 261 - case child.route == Sitemap { 262 - False -> render_sitemap_recursively(child, level + 1) 263 - // A CHILD OF MY OWN?! 264 - True -> 265 - html.li([], [ 266 - html.span([attribute.class("text-info-content ps-2")], [ 267 - element.text(child.title), 268 - ]), 269 - element.text(" <--- You are here"), 270 - ]) 271 - } 272 - }) 273 - |> html.ul( 274 - [ 275 - attribute.class( 276 - { 277 - case level, entry.route { 278 - 0, _ -> "list-['===>']" 279 - // Index 280 - 1, Posts -> "list-['----Post:']" 281 - 1, _ -> "list-['======>']" 282 - 2, _ -> "list-['=========>']" 283 - _, _ -> "list-disc" 284 - } 285 - } 286 - <> " list-inside mb-2", 287 - ), 288 - ], 289 - _, 290 - ) 291 - } 292 - }, 293 - ]) 294 - } 295 - 296 - const highlighted_posts = [900] 297 217 298 218 // Links on my php 299 219 const personal_start_page_links: List(#(String, String)) = [ ··· 523 443 Entry(route: Route, title: String, last_updated: calendar.Date, parent: Route) 524 444 } 525 445 446 + pub type FeedEntry { 447 + FeedEntry( 448 + /// Id, used to determine permalink. 449 + id: Int, 450 + /// Description 451 + description: String, 452 + entry: Entry, 453 + ) 454 + } 455 + 526 456 type Badge { 527 457 Badge( 528 458 clickable_url: String, ··· 607 537 PersonalStartPage 608 538 } 609 539 540 + pub fn posts_entries() -> List(FeedEntry) { 541 + posts() 542 + |> list.map(fn(post) { 543 + FeedEntry( 544 + post.id, 545 + post.summary, 546 + Entry( 547 + route: PostById(post.id), 548 + title: post.title, 549 + last_updated: case post.revised { 550 + Some(new) -> new 551 + None -> post.published 552 + }, 553 + parent: Posts, 554 + ), 555 + ) 556 + }) 557 + } 558 + 610 559 fn render_badge(badge: Badge) -> Element(msg) { 611 560 let img = 612 561 html.img([ ··· 640 589 "self" -> html.a([attribute.href(badge.img_url)], [img]) 641 590 url -> html.a([attribute.href(url)], [img]) 642 591 } 592 + } 593 + 594 + pub fn sitemap() -> List(Entry) { 595 + let posts = 596 + posts_entries() 597 + |> list.map(fn(entry) { entry.entry }) 598 + pages |> list.append(posts) 599 + } 600 + 601 + fn view_sitemap() -> List(Element(Msg)) { 602 + let assert Ok(index_entry) = list.first(pages) 603 + // To be clear: 604 + let assert Index = index_entry.route 605 + [ 606 + title("Sitemap"), 607 + html.br([attribute.class("mt-4")]), 608 + 609 + html.ul([attribute.class("list-['=>'] list-inside")], [ 610 + render_sitemap_recursively(start_entry: index_entry, level: 0), 611 + ]), 612 + ] 613 + } 614 + 615 + // For now we render a boring with-depth list, plan is to create a whole graph out of this! 616 + fn render_sitemap_recursively(start_entry entry: Entry, level level: Int) { 617 + let children = 618 + list.filter(sitemap(), fn(maybe_child) { maybe_child.parent == entry.route }) 619 + html.li([], [ 620 + html.a( 621 + [href(entry.route), attribute.class("link link-secondary-content ps-2")], 622 + [element.text(entry.title)], 623 + ), 624 + 625 + case children { 626 + [] -> element.none() 627 + _ -> { 628 + list.map(children, fn(child) { 629 + case child.route == Sitemap { 630 + False -> render_sitemap_recursively(child, level + 1) 631 + // A CHILD OF MY OWN?! 632 + True -> 633 + html.li([], [ 634 + html.span([attribute.class("text-info-content ps-2")], [ 635 + element.text(child.title), 636 + ]), 637 + element.text(" <--- You are here"), 638 + ]) 639 + } 640 + }) 641 + |> html.ul( 642 + [ 643 + attribute.class( 644 + { 645 + case level, entry.route { 646 + 0, _ -> "list-['===>']" 647 + // Index 648 + 1, Posts -> "list-['----Post:']" 649 + 1, _ -> "list-['======>']" 650 + 2, _ -> "list-['=========>']" 651 + _, _ -> "list-disc" 652 + } 653 + } 654 + <> " list-inside mb-2", 655 + ), 656 + ], 657 + _, 658 + ) 659 + } 660 + }, 661 + ]) 643 662 } 644 663 645 664 fn post_normalize(takes: Post) -> NormalizedPost {