My personal website, in gleam+lustre!
0
fork

Configure Feed

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

Add Atom feed(!!!!!) and give posts time


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

+259 -75
+2
.gitignore
··· 18 18 assets/styles.css 19 19 assets/sitemap.xml 20 20 assets/feed.xml 21 + assets/atom.xml 22 + assets/rss.xml
+116 -21
dev/homepage/prepare.gleam
··· 11 11 import gleam/json 12 12 import gleam/list 13 13 import gleam/option 14 + import gleam/regexp 14 15 import gleam/result 15 16 import gleam/string 16 17 import gleam/time/calendar ··· 20 21 import homepage/stuff.{type Entry, type Model, type Route, Entry, Model} 21 22 import lustre/element 22 23 import simplifile 24 + import webls/atom 23 25 import webls/rss 24 26 25 27 @external(javascript, "./nonexistent.js", "okay") ··· 69 71 Error(msg) -> panic as msg 70 72 } 71 73 case generate_sitemap() { 74 + Ok(_) -> Nil 75 + Error(msg) -> panic as msg 76 + } 77 + case generate_atom() { 72 78 Ok(_) -> Nil 73 79 Error(msg) -> panic as msg 74 80 } ··· 167 173 } 168 174 169 175 fn to_canonical(route: Route) { 170 - address <> route |> homepage.to_url 176 + address <> homepage.to_url(route) 171 177 } 172 178 173 179 pub fn generate_rss() { ··· 183 189 |> rss.with_channel_web_master("rss-webmaster@strawmelonjuice.com") 184 190 |> rss.with_channel_last_build_date(timestamp.system_time()) 185 191 |> rss.with_channel_copyright( 186 - "The rights to this content is reserved by M Bloeiman.", 192 + "The rights to this content is reserved by M Bloeiman, but can be freely shared with attribution in the form of a link to the original website. AI's and LLM's are not allowed to use any of this feed's content.", 187 193 ) 188 194 |> rss.with_channel_language("en") 189 195 |> rss.with_channel_items( 190 - list.map(homepage.posts_entries(homepage.posts()), fn(entry) { 191 - let permalink = address <> "/post/" <> int.to_string(entry.id) 192 - let Entry(route:, title:, last_updated:, parent: _, description: _) = 193 - entry.entry 194 - let title = glentities.encode(title, glentities.Hex) 195 - let summary = glentities.encode(entry.description, glentities.Hex) 196 - rss.item(title, summary) 197 - |> rss.with_item_link(to_canonical(route)) 198 - |> rss.with_item_author("rss-webmaster@strawmelonjuice.com") 199 - |> rss.with_item_pub_date({ 200 - last_updated 201 - |> timestamp.from_calendar( 202 - time: calendar.TimeOfDay(13, 00, 00, 00), 203 - offset: calendar.local_offset(), 204 - ) 205 - }) 206 - |> rss.with_item_guid(#(permalink, option.Some(True))) 207 - }), 196 + list.map( 197 + homepage.posts_entries(homepage.posts()), 198 + fn(entry: stuff.FeedEntry) -> rss.RssItem { 199 + let permalink = address <> "/post/" <> int.to_string(entry.id) 200 + let Entry( 201 + route:, 202 + title:, 203 + last_updated: _, 204 + parent: _, 205 + description: _, 206 + ) = entry.entry 207 + let title = glentities.encode(title, glentities.Hex) 208 + let summary = glentities.encode(entry.description, glentities.Hex) 209 + rss.item(title, summary) 210 + |> rss.with_item_link(to_canonical(route)) 211 + |> rss.with_item_author("rss-webmaster@strawmelonjuice.com") 212 + |> rss.with_item_pub_date({ entry.time_posted }) 213 + |> rss.with_item_guid(#(permalink, option.Some(True))) 214 + }, 215 + ), 208 216 ), 209 217 ] 210 218 |> rss.to_string() 211 219 use _ <- try_annot( 220 + simplifile.write("./assets/rss.xml", contents:), 221 + "❌\tSomething went wrong writing `./assets/rss.xml`.", 222 + ) 223 + io.println("✅\tWrote: `./assets/rss.xml`") 224 + 225 + use _ <- try_annot( 212 226 simplifile.write("./assets/feed.xml", contents:), 213 - "❌\tSomething went wrong writing `./assets/sitemap.xml`.", 227 + "❌\tSomething went wrong writing `./assets/feed.xml`.", 214 228 ) 215 229 io.println("✅\tWrote: `./assets/feed.xml`") 230 + Ok(Nil) 231 + } 232 + 233 + pub fn generate_atom() { 234 + let feed_id = address <> "/atom.xml" 235 + let feed_title = atom.plain_text("Mar's site - all posts") 236 + let last_updated = timestamp.system_time() 237 + let post_entries = homepage.posts_entries(homepage.posts()) 238 + let contents = 239 + atom.feed(feed_id, feed_title, last_updated) 240 + |> atom.with_feed_subtitle("Posts on strawmelonjuice dot com") 241 + |> atom.with_feed_link(atom.link(address)) 242 + |> atom.with_feed_rights(atom.plain_text( 243 + "The rights to this content is reserved by M Bloeiman, but can be freely shared with attribution in the form of a link to the original website. AI's and LLM's are not allowed to use any of this feed's content.", 244 + )) 245 + // Atom uses a Person type for authors/webmasters 246 + |> atom.with_feed_author( 247 + atom.person("M Bloeiman") 248 + |> atom.with_person_email("rss-webmaster@strawmelonjuice.com"), 249 + ) 250 + |> atom.with_feed_entries( 251 + list.map(post_entries, fn(entry) { 252 + let Entry(route:, title:, last_updated: _, parent: _, description: _) = 253 + entry.entry 254 + 255 + let entry_id = address <> "/post/" <> int.to_string(entry.id) 256 + let entry_title = 257 + atom.plain_text(glentities.encode(title, glentities.Hex)) 258 + let updated_at = entry.time_posted 259 + 260 + atom.entry(entry_id, entry_title, updated_at) 261 + |> atom.with_entry_link(atom.link(to_canonical(route))) 262 + // Description 263 + |> atom.with_entry_summary( 264 + atom.plain_text(glentities.encode(entry.description, glentities.Hex)), 265 + ) 266 + // Post content 267 + |> atom.with_entry_content(atom.PlainText( 268 + "Could not load post with entry id " <> int.to_string(entry.id), 269 + )) 270 + |> atom.with_entry_authors([ 271 + atom.person("M Bloeiman") 272 + |> atom.with_person_email("atom-webmaster@strawmelonjuice.com") 273 + |> atom.with_person_uri("https://strawmelonjuice.com/me"), 274 + ]) 275 + }), 276 + ) 277 + |> atom.to_string() 278 + 279 + let contents = 280 + list.map_fold( 281 + over: post_entries, 282 + from: contents, 283 + with: fn(contents_so_far, entry) { 284 + let post_contents = { 285 + let assert Ok(post) = 286 + list.find(homepage.posts(), fn(p) { p.id == entry.id }) 287 + as "Non existing post does exist in entries?" 288 + { post |> homepage.post_normalize }.body 289 + |> element.to_readable_string 290 + } 291 + 292 + let new_contents = 293 + string.replace( 294 + contents_so_far, 295 + "<content>Could not load post with entry id " 296 + <> { entry.id |> int.to_string() } 297 + <> "</content>", 298 + with: "<content type=\"html\"><![CDATA[" 299 + <> post_contents 300 + <> "]]></content>", 301 + ) 302 + #(new_contents, Nil) 303 + }, 304 + ).0 305 + 306 + use _ <- try_annot( 307 + simplifile.write("./assets/atom.xml", contents:), 308 + "❌\tSomething went wrong writing `./assets/atom.xml`.", 309 + ) 310 + io.println("✅\tWrote: `./assets/atom.xml`") 216 311 |> Ok 217 312 } 218 313
+1
gleam.toml
··· 10 10 gleam_time = ">= 1.7.0 and < 2.0.0" 11 11 webls = ">= 1.6.1 and < 2.0.0" 12 12 smalto = ">= 3.0.0 and < 4.0.0" 13 + gleam_regexp = ">= 1.1.1 and < 2.0.0" 13 14 14 15 [dev-dependencies] 15 16 glentities = ">= 6.2.1 and < 7.0.0"
+1
manifest.toml
··· 56 56 chilp = { version = ">= 1.0.0 and < 2.0.0" } 57 57 gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" } 58 58 gleam_json = { version = ">= 3.1.0 and < 4.0.0" } 59 + gleam_regexp = { version = ">= 1.1.1 and < 2.0.0" } 59 60 gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 60 61 gleam_time = { version = ">= 1.7.0 and < 2.0.0" } 61 62 glentities = { version = ">= 6.2.1 and < 7.0.0" }
+78 -42
src/homepage.gleam
··· 1 + import gleam/time/timestamp 2 + 1 3 /// Post data ------------------------------------------------------------------- 2 4 /// 3 5 /// Some special tags: ··· 21 23 category: "Devlog", 22 24 title: "I lied", 23 25 summary: "The Gleam for backend experiment is back!", 24 - published: Date(2026, April, 5), 26 + published: Date(2026, April, 5) 27 + |> stuff.date_stamp(hours: 21, minutes: 49), 25 28 revised: None, 26 29 body: File(Djot, "./written-contents/blog/devblog/lumina/3.dj"), 27 30 tags: [ ··· 35 38 category: "Devlog", 36 39 title: "Liveblog Start", 37 40 summary: "A liveblog of my longest-running and definitely most ambitious project.", 38 - published: Date(2026, March, 27), 41 + published: Date(2026, March, 27) |> stuff.no_time_timestamp, 39 42 revised: None, 40 43 body: File(Djot, "./written-contents/blog/devblog/lumina/2.dj"), 41 44 tags: [ ··· 49 52 category: "Devlog", 50 53 title: "[Liveblog] The Lumina-Peonies project", 51 54 summary: "A liveblog of my longest-running and definitely most ambitious project.", 52 - published: Date(2026, March, 27), 55 + published: Date(2026, March, 27) |> stuff.no_time_timestamp, 53 56 revised: None, 54 57 body: LiveBlog([#(3, "April 2026: 'I lied'"), #(2, "Start of a Journey")]), 55 58 tags: [ ··· 73 76 id: 900, 74 77 title: "New site, who dis?", 75 78 summary: "I rewrote this site from scratch!", 76 - published: Date(2026, March, 11), 79 + published: Date(2026, March, 11) |> stuff.no_time_timestamp, 77 80 revised: None, 78 81 body: File(Djot, "./written-contents/blog/personal/2026/new-site.dj"), 79 82 aliases: ["first"], ··· 100 103 category: "Tech", 101 104 title: "Nix, Mise and other spice.", 102 105 summary: "I'm Just making some changes!", 103 - published: Date(2026, February, 7), 106 + published: Date(2026, February, 7) |> stuff.no_time_timestamp, 104 107 revised: None, 105 108 body: File(Djot, "./written-contents/blog/personal/2026/nix.dj"), 106 109 tags: [ ··· 123 126 category: "Personal Opinion", 124 127 title: "The ideal medium", 125 128 summary: "A 22-year-old tech wizz yells at a cloud (the digital one) to tell you to buy a walkman", 126 - published: Date(2025, December, 14), 127 - revised: Some(Date(2026, January, 27)), 129 + published: Date(2025, December, 14) |> stuff.no_time_timestamp, 130 + revised: Some(Date(2026, January, 27) |> stuff.no_time_timestamp), 128 131 body: File( 129 132 Djot, 130 133 "./written-contents/blog/personal/2025/the-ideal-medium.dj", ··· 155 158 category: "Meta", 156 159 title: "strawmelonjuice logo revamp 2026!", 157 160 summary: "New logo design for strawmelonjuice in 2026! OMG!", 158 - published: Date(2026, January, 10), 161 + published: Date(2026, January, 10) |> stuff.no_time_timestamp, 159 162 revised: None, 160 163 body: File( 161 164 Djot, ··· 178 181 category: "Tech", 179 182 title: "Denouncing GitHub", 180 183 summary: "Sad news for today.", 181 - published: Date(2025, November, 9), 184 + published: Date(2025, November, 9) |> stuff.no_time_timestamp, 182 185 revised: None, 183 186 body: File( 184 187 Djot, ··· 210 213 category: "brainfart as art", 211 214 title: "A misunderstanding", 212 215 summary: "Just trying things out lol", 213 - published: Date(2023, July, 14), 214 - revised: Some(Date(2023, August, 2)), 216 + published: Date(2023, July, 14) |> stuff.no_time_timestamp, 217 + revised: Some(Date(2023, August, 2) |> stuff.no_time_timestamp), 215 218 body: File( 216 219 Djot, 217 220 "./written-contents/blog/creative/drawings/other drawings/brain-lost-misunderstanding.dj", ··· 236 239 id: 894, 237 240 title: "MouseMouse 3.2 Textured", 238 241 summary: "The mousemouse with a new papertextured mix - a follow-up to the previous MouseMouse 3.2 post!", 239 - published: Date(2023, June, 20), 242 + published: Date(2023, June, 20) |> stuff.no_time_timestamp, 240 243 revised: None, 241 244 body: File( 242 245 Djot, ··· 266 269 id: 893, 267 270 title: "MouseMouse 3.2", 268 271 summary: "Showing off the latest version of MouseMouse with much more detail than previous versions!", 269 - published: Date(2023, June, 15), 272 + published: Date(2023, June, 15) |> stuff.no_time_timestamp, 270 273 revised: None, 271 274 body: File( 272 275 Djot, ··· 294 297 id: 892, 295 298 title: "Amigo & Ferry say hi!", 296 299 summary: "Introducing Amigo The Slug and Ferry the cactus - characters created for the Blub discord server that became fan favorites!", 297 - published: Date(2023, April, 10), 300 + published: Date(2023, April, 10) |> stuff.no_time_timestamp, 298 301 revised: None, 299 302 body: File( 300 303 Djot, ··· 326 329 category: "Art", 327 330 title: "Bella Bee says hi!", 328 331 summary: "Introducing Bella Bee - inspired by a nature documentary and my first try at drawing a bee character!", 329 - published: Date(2023, March, 25), 332 + published: Date(2023, March, 25) |> stuff.no_time_timestamp, 330 333 revised: None, 331 334 body: File( 332 335 Djot, ··· 354 357 Post( 355 358 id: 890, 356 359 category: "Art", 357 - published: Date(2023, March, 15), 360 + published: Date(2023, March, 15) |> stuff.no_time_timestamp, 358 361 revised: None, 359 362 body: File( 360 363 Djot, ··· 384 387 889, 385 388 title: "Leafornia Chapter 2: First Contact", 386 389 summary: "Leafornia arrives in New York and has her first difficult encounter with humans on earth.", 387 - published: Date(2023, January, 25), 390 + published: Date(2023, January, 25) |> stuff.no_time_timestamp, 388 391 revised: None, 389 392 category: "Stories", 390 393 tags: [ ··· 415 418 888, 416 419 title: "Leafornia Chapter 1: The Devil Steps Down", 417 420 summary: "The first chapter of Leafornia's story - the devil herself comes to earth to fix a wrongdoing.", 418 - published: Date(2023, January, 20), 421 + published: Date(2023, January, 20) |> stuff.no_time_timestamp, 419 422 revised: None, 420 423 category: "Stories", 421 424 tags: [ ··· 450 453 Entry( 451 454 Index, 452 455 "Homepage", 453 - Date(2026, March, 16), 456 + Date(2026, April, 5), 454 457 NotFound(""), 455 458 "Welcome to my personal site!", 456 459 ), ··· 801 804 last_updated: case post.revised { 802 805 Some(new) -> new 803 806 None -> post.published 804 - }, 807 + } 808 + |> stuff.stamp_date, 805 809 parent: Posts, 806 810 description: "Author: Mar Bloeiman (@strawmelonjuice), Summary: `" 807 811 <> post.summary 808 812 <> "´, Category: " 809 813 <> post.category, 810 814 ), 815 + time_posted: case post.revised { 816 + Some(new) -> new 817 + None -> post.published 818 + }, 811 819 ) 812 820 }) 813 821 } ··· 864 872 use <- bool.guard(tag |> string.starts_with("."), Error(Nil)) 865 873 Ok(Entry( 866 874 title: to_title(Tagged(tag)) |> string.drop_end(13), 867 - last_updated: post.revised |> option.unwrap(post.published), 875 + last_updated: post.revised 876 + |> option.unwrap(post.published) 877 + |> stuff.stamp_date, 868 878 parent: Posts, 869 879 route: Tagged(tag), 870 880 description: "Blog posts from Mar Bloeiman (@strawmelonjuice) that are tagged with '" ··· 876 886 // One entry for the one category 877 887 list.wrap(Entry( 878 888 title: to_title(Category(category)) |> string.drop_end(13), 879 - last_updated: post.revised |> option.unwrap(post.published), 889 + last_updated: post.revised 890 + |> option.unwrap(post.published) 891 + |> stuff.stamp_date, 880 892 parent: Posts, 881 893 route: Category(category), 882 894 description: "Blog posts from Mar Bloeiman (@strawmelonjuice) that are categorised as '" ··· 1070 1082 }) 1071 1083 } 1072 1084 1073 - fn post_normalize(takes: Post) -> NormalizedPost { 1085 + pub fn post_normalize(takes: Post) -> NormalizedPost { 1074 1086 let Post( 1075 1087 id:, 1076 1088 title:, ··· 1128 1140 id:, 1129 1141 title:, 1130 1142 summary:, 1131 - revised:, 1132 - published:, 1143 + revised: revised 1144 + |> option.map(stuff.stamp_date), 1145 + published: published 1146 + |> timestamp.to_calendar(calendar.local_offset()) 1147 + |> pair.first, 1133 1148 body: post_body_normalize(takes), 1134 1149 comments:, 1135 1150 category:, ··· 1182 1197 |> list.find(fn(post) { post.id == id }) 1183 1198 |> result.map(fn(sub_item) { 1184 1199 #( 1185 - sub_item.published |> date_to_yyyy_mm_dd(), 1200 + sub_item.published 1201 + |> timestamp.to_calendar(calendar.local_offset()) 1202 + |> pair.first 1203 + |> date_to_yyyy_mm_dd, 1186 1204 sub_item |> post_body_normalize() |> list.wrap(), 1187 1205 ) 1188 1206 }) ··· 2091 2109 [ 2092 2110 html.h2([attribute.class("text-3xl text-accent-content font-light")], [ 2093 2111 html.text("All posts "), 2094 - 2095 - html.a( 2096 - [ 2097 - attribute.href("/feed.xml"), 2098 - attribute.target("_blank"), 2099 - attribute.class( 2100 - "inline-block btn btn-circle btn-accent text-accent-content", 2101 - ), 2102 - ], 2103 - [ 2104 - html.img([ 2105 - attribute.class("size-[1.2em] p-0 m-1"), 2106 - attribute.src("/svg/feed.svg"), 2107 - ]), 2108 - ], 2109 - ), 2112 + html.div([attribute.class("float-right")], [ 2113 + html.a( 2114 + [ 2115 + attribute.href("/atom.xml"), 2116 + attribute.target("_blank"), 2117 + attribute.class( 2118 + "btn btn-accent text-accent-content w-fit text-[green] p-1 m-1", 2119 + ), 2120 + ], 2121 + [ 2122 + html.img([ 2123 + attribute.class("h-[1.2em] w-[1.2em] p-0 m-0"), 2124 + attribute.src("/svg/feed.svg"), 2125 + ]), 2126 + element.text("ATOM"), 2127 + ], 2128 + ), 2129 + html.a( 2130 + [ 2131 + attribute.href("/rss.xml"), 2132 + attribute.target("_blank"), 2133 + attribute.class( 2134 + "btn btn-accent text-accent-content w-fit text-[orange] p-1", 2135 + ), 2136 + ], 2137 + [ 2138 + html.img([ 2139 + attribute.class("size-[1.2em] p-0 m-1"), 2140 + attribute.src("/svg/feed.svg"), 2141 + ]), 2142 + element.text("RSS"), 2143 + ], 2144 + ), 2145 + ]), 2110 2146 ]), 2111 2147 html.p([attribute.class("mt-6")], [ 2112 2148 element.text("Filter: "),
+33 -10
src/homepage/djotparse.gleam
··· 4 4 import gleam/io 5 5 import gleam/list 6 6 import gleam/option.{type Option, None, Some} 7 + import gleam/regexp 7 8 import gleam/string 8 9 import homepage/stuff 9 10 import homepage/stuff/prestyled_elements ··· 19 20 import smalto/languages/rust 20 21 21 22 pub fn djot_to_html(djot djot: String) -> String { 23 + let assert Ok(regex_h1) = regexp.from_string("<h1([\\s\\S]*?)>") 24 + let assert Ok(regex_h2) = regexp.from_string("<h2([\\s\\S]*?)>") 25 + let assert Ok(regex_h3) = regexp.from_string("<h3([\\s\\S]*?)>") 26 + let assert Ok(regex_h4) = regexp.from_string("<h4([\\s\\S]*?)>") 27 + 22 28 let #(h1, h2, h3, h4, a, p) = { 23 29 // keep the styling defined in one place. 24 30 #( 25 31 // H1 > H3 26 - prestyled_elements.h3("") |> element.to_string() |> string.drop_end(6), 32 + #( 33 + prestyled_elements.h3("") |> element.to_string() |> string.drop_end(6), 34 + "</h3>", 35 + ), 27 36 // H2 > H4 28 - prestyled_elements.h4("") |> element.to_string() |> string.drop_end(6), 37 + #( 38 + prestyled_elements.h4("") |> element.to_string() |> string.drop_end(6), 39 + "</h4>", 40 + ), 29 41 // H3 > H5 30 - prestyled_elements.h5("") |> element.to_string() |> string.drop_end(6), 42 + #( 43 + prestyled_elements.h5("") |> element.to_string() |> string.drop_end(6), 44 + "</h5>", 45 + ), 31 46 // H5 > H6 32 - prestyled_elements.h6("") |> element.to_string() |> string.drop_end(6), 47 + #( 48 + prestyled_elements.h6("") |> element.to_string() |> string.drop_end(6), 49 + "</h6>", 50 + ), 33 51 // links 34 52 prestyled_elements.link(stuff.NotFound(""), "", fn(_) { attribute.none() }) 35 - |> element.to_string() 36 - |> string.drop_end(5), 53 + |> element.to_string() 54 + |> string.drop_end(5) 55 + <> " ", 37 56 // paragraphs 38 57 prestyled_elements.paragraph("") 39 58 |> element.to_string() ··· 46 65 |> jot.to_html 47 66 |> string.replace("<p>", p) 48 67 |> string.replace("<li", "<li class=\"mt-2\"") 49 - |> string.replace("<h4", h4) 50 - |> string.replace("<h3", h3) 51 - |> string.replace("<h2", h2) 52 - |> string.replace("<h1", h1) 68 + |> regexp.replace(regex_h4, _, h4.0) 69 + |> string.replace("</h4>", h4.1) 70 + |> regexp.replace(regex_h3, _, h3.0) 71 + |> string.replace("</h3>", h3.1) 72 + |> regexp.replace(regex_h2, _, h2.0 <> ">") 73 + |> string.replace("</h2>", h2.1) 74 + |> regexp.replace(regex_h1, _, h1.0) 75 + |> string.replace("</h1>", h1.1) 53 76 |> string.replace("<a ", a) 54 77 |> string.replace("<strong", "<strong class=\"font-bold\"") 55 78 |> string.replace("<em", "<em class=\"italic\"")
+28 -2
src/homepage/stuff.gleam
··· 7 7 import gleam/result 8 8 import gleam/string 9 9 import gleam/time/calendar 10 + import gleam/time/timestamp 10 11 import gleam/uri 11 12 import lustre/attribute.{type Attribute} 12 13 import lustre/element.{type Element as LustreElement} ··· 16 17 pub type Element = 17 18 LustreElement(Msg) 18 19 20 + pub fn stamp_date(stamp: timestamp.Timestamp) -> calendar.Date { 21 + timestamp.to_calendar(stamp, calendar.local_offset()).0 22 + } 23 + 24 + pub fn no_time_timestamp(date: calendar.Date) { 25 + timestamp.from_calendar( 26 + date:, 27 + time: calendar.TimeOfDay(hours: 13, minutes: 00, seconds: 0, nanoseconds: 0), 28 + offset: calendar.local_offset(), 29 + ) 30 + } 31 + 32 + pub fn date_stamp( 33 + date date: calendar.Date, 34 + hours hours: Int, 35 + minutes minutes: Int, 36 + ) { 37 + timestamp.from_calendar( 38 + date:, 39 + time: calendar.TimeOfDay(hours:, minutes:, seconds: 0, nanoseconds: 0), 40 + offset: calendar.local_offset(), 41 + ) 42 + } 43 + 19 44 pub type Entry { 20 45 Entry( 21 46 /// The route for this entry ··· 44 69 /// Description 45 70 description: String, 46 71 entry: Entry, 72 + time_posted: timestamp.Timestamp, 47 73 ) 48 74 } 49 75 ··· 72 98 category: String, 73 99 title: String, 74 100 summary: String, 75 - published: calendar.Date, 76 - revised: Option(calendar.Date), 101 + published: timestamp.Timestamp, 102 + revised: Option(timestamp.Timestamp), 77 103 body: Body, 78 104 tags: List(String), 79 105 aliases: List(String),