Add Atom feed

This commit is contained in:
Mikko Ahlroth 2024-04-06 00:57:35 +03:00
parent ace0b8b453
commit bc92b53483
15 changed files with 250 additions and 19 deletions

View file

@ -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"

View file

@ -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" }

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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),
)
}

View file

@ -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,

View file

@ -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")]),
]),
]),

View file

@ -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()
},
])
})
])
}
}

View file

@ -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,

View file

@ -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()

View file

@ -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))
}

86
src/lustre/ssg/atom.gleam Normal file
View file

@ -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)])
}

5
src/lustre/ssg/xml.gleam Normal file
View file

@ -0,0 +1,5 @@
import lustre/element.{advanced}
pub fn declaration() {
advanced("", "?xml version=\"1.0\" encoding=\"utf-8\"?", [], [], False, True)
}