diff --git a/src/gloss/config.gleam b/src/gloss/config.gleam index 427b2bf..26b5927 100644 --- a/src/gloss/config.gleam +++ b/src/gloss/config.gleam @@ -1,7 +1,7 @@ import gleam/option import gloss/paths.{type PathConfiguration} import gloss/rendering/views.{ - type BaseView, type FeedView, type ListPageView, type PageView, + type BaseView, type FeedView, type ListPageView, type MetaView, type PageView, type SinglePostView, } import gloss/compiler.{type CompileDatabase, type Compiler} @@ -13,6 +13,7 @@ import gloss/rendering/database.{type RenderDatabase} as _ pub type Views { Views( base: fn(Database, Configuration) -> BaseView, + meta: fn(Database, Configuration) -> MetaView, single_post_full: fn(Database, Configuration) -> SinglePostView, single_post_list: fn(Database, Configuration) -> SinglePostView, page: fn(Database, Configuration) -> PageView, diff --git a/src/gloss/defaults.gleam b/src/gloss/defaults.gleam index 2fecdab..8dda366 100644 --- a/src/gloss/defaults.gleam +++ b/src/gloss/defaults.gleam @@ -4,6 +4,7 @@ import gloss/rendering/views/base import gloss/rendering/views/list_page import gloss/rendering/views/page import gloss/rendering/views/feed +import gloss/rendering/views/meta import gloss/renderer import gloss/paths import gloss/parser @@ -12,6 +13,7 @@ import gloss/compiler const default_views = config.Views( base: base.generate, + meta: meta.generate, single_post_full: single_post.full_view, single_post_list: single_post.list_view, page: page.generate, diff --git a/src/gloss/models/post.gleam b/src/gloss/models/post.gleam index 200f217..aa3da6e 100644 --- a/src/gloss/models/post.gleam +++ b/src/gloss/models/post.gleam @@ -61,3 +61,10 @@ pub fn comparator(a: Post, b: Post) -> Order { } } } + +pub fn to_iso8601(post: Post) -> String { + case post.date { + JustDate(d) -> date.format_iso(d) + DateTime(luxon: l, ..) -> luxon.to_iso(l) + } +} diff --git a/src/gloss/renderer.gleam b/src/gloss/renderer.gleam index f3d4a04..c4c90a1 100644 --- a/src/gloss/renderer.gleam +++ b/src/gloss/renderer.gleam @@ -4,7 +4,7 @@ import gleam/result import gleam/int import lustre/element.{type Element} import gloss/rendering/views.{ - type BaseView, type FeedView, type ListPageView, type PageView, + type BaseView, type FeedView, type ListPageView, type MetaView, type PageView, type SinglePostView, ListInfo, } import gloss/rendering/database.{ @@ -22,6 +22,7 @@ import gloss/utils/date pub type Views { Views( base: BaseView, + meta: MetaView, single_post_full: SinglePostView, page: PageView, list_page: ListPageView, @@ -37,6 +38,7 @@ pub fn render( let views = Views( base: config.rendering.views.base(db, config), + meta: config.rendering.views.meta(db, config), single_post_full: config.rendering.views.single_post_full(db, config), page: config.rendering.views.page(db, config), list_page: config.rendering.views.list_page(db, config), @@ -76,7 +78,11 @@ pub fn render_posts( |> list.map(fn(post_with_id) { let assert Ok(content) = dict.get(post_contents, post_with_id.id) let rendered = - views.base(views.single_post_full(content), post_with_id.post.title) + views.base( + views.single_post_full(content), + views.meta(views.Post(post_with_id.post)), + post_with_id.post.title, + ) RenderedSinglePost(post_with_id.post, rendered) }) } @@ -87,7 +93,12 @@ pub fn render_pages( views: Views, ) { list.map(compiled_pages, fn(page) { - let rendered = views.base(views.page(page), page.orig.title) + let rendered = + views.base( + views.page(page), + views.meta(views.Page(page.orig)), + page.orig.title, + ) RenderedPage(page.orig, rendered) }) } @@ -233,7 +244,12 @@ fn pageify_posts( extra_header: extra_header, ) - let page_content = views.base(views.list_page(info), title_prefix) + let page_content = + views.base( + views.list_page(info), + views.meta(views.Other(title_prefix, "")), + title_prefix, + ) ListPage(page: page, content: page_content) }) } diff --git a/src/gloss/rendering/views.gleam b/src/gloss/rendering/views.gleam index fe38409..eb9122f 100644 --- a/src/gloss/rendering/views.gleam +++ b/src/gloss/rendering/views.gleam @@ -1,8 +1,19 @@ import lustre/element.{type Element} import gloss/compiler.{type CompiledPage, type CompiledPost} +import gloss/models/post +import gloss/models/page pub type BaseView = - fn(Element(Nil), String) -> Element(Nil) + fn(Element(Nil), List(Element(Nil)), String) -> Element(Nil) + +pub type PageType { + Post(post.Post) + Page(page.Page) + Other(title: String, description: String) +} + +pub type MetaView = + fn(PageType) -> List(Element(Nil)) pub type SinglePostView = fn(CompiledPost) -> Element(Nil) diff --git a/src/gloss/rendering/views/base.gleam b/src/gloss/rendering/views/base.gleam index 7af4b9d..55d5340 100644 --- a/src/gloss/rendering/views/base.gleam +++ b/src/gloss/rendering/views/base.gleam @@ -26,8 +26,8 @@ pub type PreRendered { pub fn generate(db: Database, config: Configuration) { let pre_rendered = pre_render(db, config) - fn(inner: Element(Nil), title_prefix: String) { - view(db, config, pre_rendered, inner, title_prefix) + fn(inner: Element(Nil), extra_meta: List(Element(Nil)), title_prefix: String) { + view(db, config, pre_rendered, inner, extra_meta, title_prefix) } } @@ -44,6 +44,7 @@ fn view( config: Configuration, pre_rendered: PreRendered, inner: Element(Nil), + extra_meta: List(Element(Nil)), title_prefix: String, ) { let title_text = case title_prefix { @@ -71,6 +72,7 @@ fn view( option.Some(url) -> link([rel("me"), value(url)]) _ -> element.none() }, + ..extra_meta ]), body([], [ header([id("title"), role("banner")], [ diff --git a/src/gloss/rendering/views/feed.gleam b/src/gloss/rendering/views/feed.gleam index a54f4d0..55ab3f0 100644 --- a/src/gloss/rendering/views/feed.gleam +++ b/src/gloss/rendering/views/feed.gleam @@ -11,7 +11,6 @@ import gloss/models/post import gloss/config.{type Configuration} import gloss/compiler.{type CompiledPost} import gloss/utils/luxon -import gloss/utils/date pub fn generate(_db: Database, config: Configuration) { fn(posts: List(CompiledPost)) { @@ -43,10 +42,7 @@ pub fn generate(_db: Database, config: Configuration) { ), 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) - } + let date_str = post.to_iso8601(post.orig) entry([ title(post.orig.title), diff --git a/src/gloss/rendering/views/meta.gleam b/src/gloss/rendering/views/meta.gleam new file mode 100644 index 0000000..11ee677 --- /dev/null +++ b/src/gloss/rendering/views/meta.gleam @@ -0,0 +1,112 @@ +import gleam/dict +import gleam/list +import gleam/option +import gleam/result +import lustre/element/html +import lustre/attribute +import gloss/rendering/views.{type PageType, Other, Page, Post} +import gloss/models/post +import gloss/models/page +import gloss/config.{type Configuration} +import gloss/models/database.{type Database} + +pub fn generate(_db: Database, config: Configuration) { + fn(page_type: PageType) { + list.flatten([ + [ + name_meta("author", config.author.name), + name_meta("description", description(page_type)), + // Schema.org + itemprop_meta("name", title(page_type)), + itemprop_meta("author", config.author.name), + itemprop_meta("headline", title(page_type)), + itemprop_meta("description", description(page_type)), + // Twitter/X card + name_meta("twitter:card", "summary_large_image"), + name_meta("twitter:site", ""), + name_meta("twitter:title", title(page_type)), + name_meta("twitter:description", description(page_type)), + name_meta("twitter:creator", ""), + // Open Graph + property_meta("og:title", title(page_type)), + property_meta("og:type", case page_type { + Post(..) | Page(..) -> "article" + _ -> "website" + }), + property_meta("og:description", description(page_type)), + property_meta("og:site_name", config.blog_name), + ], + case page_type { + Post(post) -> { + let timestamp = post.to_iso8601(post) + + let image = + post.headers + |> dict.from_list() + |> dict.get("image") + |> option.from_result() + + [ + property_meta("article:published_time", timestamp), + itemprop_meta("datePublished", timestamp), + property_meta( + "og:url", + config.blog_url <> config.paths.single_post(post), + ), + ..case image { + option.Some(image) -> [ + name_meta("twitter:image", image), + property_meta("og:image", image), + itemprop_meta("image", image), + ] + option.None -> [] + } + ] + } + Page(page) -> [ + property_meta("og:url", config.blog_url <> config.paths.page(page)), + ] + _ -> [] + }, + ]) + } +} + +fn name_meta(name: String, content: String) { + meta("name", name, content) +} + +fn itemprop_meta(itemprop: String, content: String) { + meta("itemprop", itemprop, content) +} + +fn property_meta(property: String, content: String) { + meta("property", property, content) +} + +fn meta(attr_name: String, attr_value: String, content: String) { + html.meta([ + attribute.attribute(attr_name, attr_value), + attribute.attribute("content", content), + ]) +} + +fn description(page_type: PageType) { + case page_type { + Post(post.Post(headers: headers, ..)) + | Page(page.Page(headers: headers, ..)) -> + headers + |> dict.from_list() + |> dict.get("description") + |> result.unwrap("") + Other(description: description, ..) -> description + } +} + +fn title(page_type: PageType) { + case page_type { + Post(post.Post(title: title, ..)) + | Page(page.Page(title: title, ..)) + | Other(title: title, ..) -> title + } +}