diff --git a/gleam.toml b/gleam.toml index f4e1211..df355a8 100644 --- a/gleam.toml +++ b/gleam.toml @@ -15,7 +15,7 @@ target = "javascript" [dependencies] gleam_stdlib = "~> 0.36 or ~> 1.0" -lustre = "~> 4.0" +lustre = "~> 4.1" lustre_ssg = { path = "../ssg" } gleam_javascript = "~> 0.8" ranged_int = "~> 1.0" diff --git a/manifest.toml b/manifest.toml index 26104d4..c823023 100644 --- a/manifest.toml +++ b/manifest.toml @@ -3,26 +3,27 @@ packages = [ { name = "bigi", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "bigi", source = "hex", outer_checksum = "B6F7CAF319F13F32DB4331A750534912A9AEE1C195DD8E5DA83A42A4AD390274" }, + { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, { name = "gleam_javascript", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "14D5B7E1A70681E0776BF0A0357F575B822167960C844D3D3FA114D3A75F05A8" }, { name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" }, { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, { name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" }, - { name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" }, + { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, { name = "jot", version = "0.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "jot", source = "hex", outer_checksum = "574A2DACA106E9B4826C9F3F2D3911844C7826D554C08E404696CC16F85E0392" }, - { name = "lustre", version = "4.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "1D40C1378279F7015687F8C9DB739D6880BB0B843F4428B85C61EDDA8BF21FC6" }, + { name = "lustre", version = "4.1.5", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "C90B3DC868D346C49E98C8A62F2E594AE559D5FF25A46269D60FAA5939FCE827" }, { name = "lustre_ssg", version = "0.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "jot", "lustre", "simplifile", "tom"], source = "local", path = "../ssg" }, { name = "ranged_int", version = "1.0.0", build_tools = ["gleam"], requirements = ["bigi", "gleam_stdlib"], otp_app = "ranged_int", source = "hex", outer_checksum = "8AACD49213E87BC6E7CE5F80038C1989966CF8187382760B6168E5EA9F364B09" }, - { name = "simplifile", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C44DB387524F90DC42142699C78C850003289D32C7C99C7D32873792A299CDF7" }, + { name = "simplifile", version = "1.6.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "B75D3C64E526D9D7EDEED5F3BA31DAAF5F2B4D80A4183FE17FDB02ED526E4E96" }, { name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" }, { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" }, ] [requirements] -bigi = { version = "~> 2.1"} +bigi = { version = "~> 2.1" } gleam_javascript = { version = "~> 0.8" } gleam_stdlib = { version = "~> 0.36 or ~> 1.0" } gleeunit = { version = "~> 1.0" } -lustre = { version = "~> 4.0" } +lustre = { version = "~> 4.1" } lustre_ssg = { path = "../ssg" } ranged_int = { version = "~> 1.0" } diff --git a/src/ffi_luxon.mjs b/src/ffi_luxon.mjs index 9883980..eb84c39 100644 --- a/src/ffi_luxon.mjs +++ b/src/ffi_luxon.mjs @@ -12,6 +12,10 @@ export function dateTimeInZone(dtStr, tz) { return new Ok(dt); } +export function utcNow() { + return DateTime.utc(); +} + export function toDate(dt) { return new Date( dt.year, diff --git a/src/gloss/config.gleam b/src/gloss/config.gleam index c9d7592..1a47112 100644 --- a/src/gloss/config.gleam +++ b/src/gloss/config.gleam @@ -1,7 +1,8 @@ +import gleam/option import gloss/paths.{type PathConfiguration} import gloss/rendering/templates.{ - type BaseRenderer, type ListPageRenderer, type PostContentRenderer, - type SinglePostRenderer, + type BaseRenderer, type FeedRenderer, type ListPageRenderer, + type PostContentRenderer, type SinglePostRenderer, } import gloss/parser.{type Parser} import gloss/writer.{type Writer} @@ -13,6 +14,7 @@ pub type Templates { single_post_full: fn(Database, Configuration) -> SinglePostRenderer, single_post_list: fn(Database, Configuration) -> SinglePostRenderer, list_page: fn(Database, Configuration) -> ListPageRenderer, + feed: fn(Database, Configuration) -> FeedRenderer, ) } @@ -22,12 +24,19 @@ pub type Rendering { copyright: String, content_renderer: PostContentRenderer, posts_per_page: Int, + posts_in_feed: Int, ) } +pub type Author { + Author(name: String, email: option.Option(String), url: option.Option(String)) +} + pub type Configuration { Configuration( blog_name: String, + blog_url: String, + author: Author, rendering: Rendering, paths: PathConfiguration, parser: Parser, diff --git a/src/gloss/defaults.gleam b/src/gloss/defaults.gleam index ad0bd84..82f837e 100644 --- a/src/gloss/defaults.gleam +++ b/src/gloss/defaults.gleam @@ -2,6 +2,7 @@ import gloss/config.{type Configuration, Configuration, Rendering} import gloss/rendering/templates/single_post import gloss/rendering/templates/base import gloss/rendering/templates/list_page +import gloss/rendering/templates/feed import gloss/paths import gloss/parser import gloss/writer @@ -11,16 +12,25 @@ const default_templates = config.Templates( single_post_full: single_post.full_view, single_post_list: single_post.list_view, list_page: list_page.generate, + feed: feed.generate, ) -pub fn default_config() -> Configuration { +pub fn default_config( + blog_name: String, + blog_url: String, + author: config.Author, + copyright: String, +) -> Configuration { Configuration( - blog_name: "", + blog_name: blog_name, + blog_url: blog_url, + author: author, rendering: Rendering( templates: default_templates, - copyright: "", + copyright: copyright, content_renderer: single_post.content_renderer, posts_per_page: 10, + posts_in_feed: 20, ), paths: paths.conf( paths.default_index, diff --git a/src/gloss/renderer.gleam b/src/gloss/renderer.gleam index 061a11f..583729d 100644 --- a/src/gloss/renderer.gleam +++ b/src/gloss/renderer.gleam @@ -4,8 +4,8 @@ import gleam/result import gleam/int import lustre/element.{type Element} import gloss/rendering/templates.{ - type BaseRenderer, type ListPageRenderer, type PostContentRenderer, - type SinglePostRenderer, ListInfo, + type BaseRenderer, type FeedRenderer, type ListPageRenderer, + type PostContentRenderer, type SinglePostRenderer, ListInfo, } import gloss/rendering/database.{ type Database as RenderDatabase, type RenderedPost, type RenderedSinglePost, @@ -21,6 +21,7 @@ pub type Renderers { base: BaseRenderer, single_post_full: SinglePostRenderer, list_page: ListPageRenderer, + feed: FeedRenderer, ) } @@ -30,6 +31,7 @@ pub fn render(db: Database, config: Configuration) -> RenderDatabase { base: config.rendering.templates.base(db, config), single_post_full: config.rendering.templates.single_post_full(db, config), list_page: config.rendering.templates.list_page(db, config), + feed: config.rendering.templates.feed(db, config), ) let all_posts = database.get_posts_with_ids(db, ordered_tree.Desc) @@ -41,6 +43,7 @@ pub fn render(db: Database, config: Configuration) -> RenderDatabase { let tag_pages = render_tag_pages(config, db, post_contents, renderers) let year_pages = render_year_pages(config, db, post_contents, renderers) let month_pages = render_month_pages(config, db, post_contents, renderers) + let feed = render_feed(all_posts, post_contents, renderers) render_database.Database( orig: db, @@ -51,6 +54,7 @@ pub fn render(db: Database, config: Configuration) -> RenderDatabase { tag_pages: tag_pages, year_pages: year_pages, month_pages: month_pages, + feed: feed, ) } @@ -189,6 +193,19 @@ fn render_month_pages( }) } +fn render_feed( + posts: List(database.PostWithID), + posts_with_contents: Dict(PostID, RenderedPost), + renderers: Renderers, +) { + let posts = + list.map(posts, fn(post) { + let assert Ok(rendered) = dict.get(posts_with_contents, post.id) + rendered + }) + renderers.feed(posts) +} + fn pageify_posts( posts: List(PostWithID), config: Configuration, diff --git a/src/gloss/rendering/database.gleam b/src/gloss/rendering/database.gleam index 8d6ec58..ff8fdec 100644 --- a/src/gloss/rendering/database.gleam +++ b/src/gloss/rendering/database.gleam @@ -34,5 +34,6 @@ pub type Database { tag_pages: Dict(String, List(RenderedPage)), year_pages: Dict(Int, List(RenderedPage)), month_pages: Dict(#(Int, Month), List(RenderedPage)), + feed: Element(Nil), ) } diff --git a/src/gloss/rendering/templates.gleam b/src/gloss/rendering/templates.gleam index 04f1ec2..6995018 100644 --- a/src/gloss/rendering/templates.gleam +++ b/src/gloss/rendering/templates.gleam @@ -11,6 +11,9 @@ pub type BaseRenderer = pub type SinglePostRenderer = fn(RenderedPost) -> Element(Nil) +pub type FeedRenderer = + fn(List(RenderedPost)) -> Element(Nil) + pub type ListInfo { ListInfo( root_path: String, diff --git a/src/gloss/rendering/templates/base.gleam b/src/gloss/rendering/templates/base.gleam index 3c5e3a0..e0355eb 100644 --- a/src/gloss/rendering/templates/base.gleam +++ b/src/gloss/rendering/templates/base.gleam @@ -3,12 +3,13 @@ import gleam/list import gleam/int import gleam/float import gleam/string +import gleam/option import lustre/element.{type Element, text} import lustre/element/html.{ a, body, footer, h1, head, header, html, li, link, main, meta, nav, p, section, title, ul, } -import lustre/attribute.{attribute, href, id, name, rel, role, style} +import lustre/attribute.{attribute, href, id, name, rel, role, style, value} import gloss/models/database.{type Database} import gloss/utils/ordered_tree import gloss/utils/date @@ -58,6 +59,10 @@ fn view( link([href("./css/normalize.css"), rel("stylesheet")]), link([href("./css/magick.css"), rel("stylesheet")]), link([href("./css/custom.css"), rel("stylesheet")]), + case config.author.url { + option.Some(url) -> link([rel("me"), value(url)]) + _ -> element.none() + }, ]), body([], [ header([id("title"), role("banner")], [ @@ -73,13 +78,14 @@ fn view( nav([id("archives")], [pre_rendered.archives]), ]), footer([], [ - p([], [text(config.rendering.copyright)]), p([], [ + text(config.rendering.copyright), + text(" · "), text("Powered by: "), a([href("https://gleam.run/")], [text("Gleam")]), - text(" · "), + text(", "), a([href("https://hexdocs.pm/lustre")], [text("Lustre")]), - text(" · "), + text(", "), a([href("https://gitlab.com/Nicd/gloss")], [text("Gloss")]), ]), ]), diff --git a/src/gloss/rendering/templates/feed.gleam b/src/gloss/rendering/templates/feed.gleam new file mode 100644 index 0000000..df46464 --- /dev/null +++ b/src/gloss/rendering/templates/feed.gleam @@ -0,0 +1,67 @@ +import gleam/list +import gleam/option +import lustre/element +import lustre/attribute.{attribute} +import lustre/ssg/atom.{ + author, content, email, entry, feed, generator, id, link, name, rights, + summary, title, updated, uri, +} +import gloss/models/database.{type Database} +import gloss/models/post +import gloss/config.{type Configuration} +import gloss/rendering/database.{type RenderedPost} as _ +import gloss/utils/luxon +import gloss/utils/date + +pub fn generate(_db: Database, config: Configuration) { + fn(posts: List(RenderedPost)) { + let now = luxon.utc_now() + let #(posts, _) = list.split(posts, config.rendering.posts_in_feed) + + feed([ + title(config.blog_name), + updated(luxon.to_iso(now)), + link([attribute("href", config.blog_url)]), + id(config.blog_url), + author([ + name(config.author.name), + case config.author.email { + option.Some(e) -> email(e) + option.None -> element.none() + }, + case config.author.url { + option.Some(u) -> uri(u) + option.None -> element.none() + }, + ]), + generator( + [ + attribute("uri", "https://gitlab.com/Nicd/gloss"), + attribute("version", "1.0.0"), + ], + "Gloss", + ), + rights(config.rendering.copyright), + ..list.map(posts, fn(post) { + let date_str = case post.orig.date { + post.JustDate(d) -> date.format_iso(d) + post.DateTime(luxon: l, ..) -> luxon.to_iso(l) + } + + entry([ + title(post.orig.title), + id( + config.blog_url + <> config.paths.html(config.paths.single_post(post.orig)), + ), + updated(date_str), + content(post.content.full), + case post.content.short { + option.Some(s) -> summary(s) + option.None -> element.none() + }, + ]) + }) + ]) + } +} diff --git a/src/gloss/utils/luxon.gleam b/src/gloss/utils/luxon.gleam index a35921c..290e84c 100644 --- a/src/gloss/utils/luxon.gleam +++ b/src/gloss/utils/luxon.gleam @@ -8,6 +8,9 @@ pub fn date_time_in_zone(date: Date, time: Time, tz: String) { do_date_time_in_zone(datetime_str, tz) } +@external(javascript, "../../ffi_luxon.mjs", "utcNow") +pub fn utc_now() -> DateTime + @external(javascript, "../../ffi_luxon.mjs", "dateTimeInZone") fn do_date_time_in_zone( datetime_str: String, diff --git a/src/gloss/writer.gleam b/src/gloss/writer.gleam index 546d42f..de3a7b3 100644 --- a/src/gloss/writer.gleam +++ b/src/gloss/writer.gleam @@ -2,6 +2,8 @@ import gleam/list import gleam/dict import gleam/result import lustre/ssg +import lustre/ssg/xml +import lustre/element import gloss/rendering/database.{type Database} as _ import gloss/models/post.{type Post} import gloss/paths/post.{type PostPath} as _ @@ -67,6 +69,13 @@ pub fn write(db: Database, path_conf: PathConfiguration) { }) }) + let site = + ssg.add_static_asset( + site, + "/feed.xml", + element.to_string(xml.declaration()) <> element.to_string(db.feed), + ) + site |> ssg.add_dynamic_route("/", single_posts, fn(c) { c }) |> ssg.build() diff --git a/src/gloss2.gleam b/src/gloss2.gleam index 69730f7..eb9f950 100644 --- a/src/gloss2.gleam +++ b/src/gloss2.gleam @@ -1,12 +1,22 @@ import gleam/result +import gleam/option import gleam/io import gloss/builder import gloss/config.{type Configuration, Configuration} import gloss/defaults pub fn main() { - let config = defaults.default_config() - let config = Configuration(..config, blog_name: "Random Notes") + let config = + defaults.default_config( + "Random Notes", + "http://localhost:61055", + config.Author( + "Mikko Ahlroth", + option.Some("mikko@ahlroth.fi"), + option.Some("https://social.ahlcode.fi/@nicd"), + ), + "© Mikko Ahlroth", + ) io.debug(build(config)) } diff --git a/src/lustre/ssg/atom.gleam b/src/lustre/ssg/atom.gleam new file mode 100644 index 0000000..fadfd6b --- /dev/null +++ b/src/lustre/ssg/atom.gleam @@ -0,0 +1,86 @@ +import lustre/element.{type Element, advanced, element, namespaced, text} +import lustre/attribute.{attribute} + +pub fn feed(children: List(Element(a))) { + element("feed", [attribute("xmlns", "http://www.w3.org/2005/Atom")], children) +} + +pub fn entry(children: List(Element(a))) { + element("entry", [], children) +} + +pub fn id(uri: String) { + element("id", [], [text(uri)]) +} + +pub fn title(title: String) { + element("title", [attribute("type", "html")], [text(title)]) +} + +pub fn updated(iso_timestamp: String) { + element("updated", [], [text(iso_timestamp)]) +} + +pub fn published(iso_timestamp: String) { + element("published", [], [text(iso_timestamp)]) +} + +pub fn author(children: List(Element(a))) { + element("author", [], children) +} + +pub fn contributor(children: List(Element(a))) { + element("contributor", [], children) +} + +pub fn source(children: List(Element(a))) { + namespaced("", "source", [], children) +} + +pub fn link(attributes: List(attribute.Attribute(a))) { + advanced("", "link", attributes, [], True, False) +} + +pub fn name(name: String) { + element("name", [], [text(name)]) +} + +pub fn email(email: String) { + element("email", [], [text(email)]) +} + +pub fn uri(uri: String) { + element("uri", [], [text(uri)]) +} + +pub fn category(attributes: List(attribute.Attribute(a))) { + advanced("", "category", attributes, [], True, False) +} + +pub fn generator(attributes: List(attribute.Attribute(a)), name: String) { + element("generator", attributes, [text(name)]) +} + +pub fn icon(path: String) { + element("icon", [], [text(path)]) +} + +pub fn logo(path: String) { + element("logo", [], [text(path)]) +} + +pub fn rights(rights: String) { + element("rights", [], [text(rights)]) +} + +pub fn subtitle(subtitle: String) { + element("subtitle", [attribute("type", "html")], [text(subtitle)]) +} + +pub fn summary(summary: String) { + element("summary", [attribute("type", "html")], [text(summary)]) +} + +pub fn content(content: String) { + element("content", [attribute("type", "html")], [text(content)]) +} diff --git a/src/lustre/ssg/xml.gleam b/src/lustre/ssg/xml.gleam new file mode 100644 index 0000000..c031baa --- /dev/null +++ b/src/lustre/ssg/xml.gleam @@ -0,0 +1,5 @@ +import lustre/element.{advanced} + +pub fn declaration() { + advanced("", "?xml version=\"1.0\" encoding=\"utf-8\"?", [], [], False, True) +}