From 58745a447f4c5d1e6b68daee7adc89de5f8fe366 Mon Sep 17 00:00:00 2001
From: Stephen D <webmaster@scd31.com>
Date: Wed, 20 Sep 2023 21:19:18 -0500
Subject: [PATCH] add opengraph to posts and pages

---
 src/blog.rs    | 27 ++++++++++++++++++-------
 src/path.rs    |  2 +-
 src/post.rs    | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++
 src/wrapper.rs |  9 ---------
 4 files changed, 74 insertions(+), 17 deletions(-)
 delete mode 100644 src/wrapper.rs

diff --git a/src/blog.rs b/src/blog.rs
index 7e67d7c..a937856 100644
--- a/src/blog.rs
+++ b/src/blog.rs
@@ -58,7 +58,7 @@ impl Blog {
 
         content.push_str("</table>");
 
-        Ok(self.dress_page(None, &content))
+        Ok(self.dress_page(None, &content, None))
     }
 
     fn rss(&self) -> anyhow::Result<String> {
@@ -85,7 +85,7 @@ impl Blog {
         Ok(out)
     }
 
-    fn dress_page(&self, title: Option<&str>, content: &str) -> String {
+    fn dress_page(&self, title: Option<&str>, content: &str, head: Option<&str>) -> String {
         let title = match title {
             Some(title) => format!("{} | {}", title.trim(), self.config.name.trim()),
             None => self.config.name.trim().to_string(),
@@ -109,8 +109,10 @@ impl Blog {
             page_links.push_str(&p.link_short());
         }
 
+        let head = head.unwrap_or("");
+
         format!(
-            r#"<!DOCTYPE html><html lang="en"><head><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="/style.css" />{favicons}<title>{title}</title></head><body><div><a href="/">Home</a>{page_links}{logo}</div><hr>{content}</body></html>"#
+            r#"<!DOCTYPE html><html lang="en"><head><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="/style.css" />{favicons}<title>{title}</title>{head}</head><body><div><a href="/">Home</a>{page_links}{logo}</div><hr>{content}</body></html>"#
         )
     }
 }
@@ -145,13 +147,21 @@ impl TryFrom<Blog> for RenderedBlog {
         let mut pages = FnvHashMap::default();
 
         for p in &b.posts {
-            let body = b.dress_page(Some(&p.title), &p.html()?);
+            let body = b.dress_page(
+                Some(&p.title),
+                &p.html()?,
+                Some(p.open_graph(&b.config.root, &b.config.logo)?.as_ref()),
+            );
 
             insert_path(&mut pages, &p.url, Response::html(body))?;
         }
 
         for p in &b.pages {
-            let body = b.dress_page(Some(&p.title), &p.html()?);
+            let body = b.dress_page(
+                Some(&p.title),
+                &p.html()?,
+                Some(p.open_graph(&b.config.root, &b.config.logo)?.as_ref()),
+            );
 
             insert_path(&mut pages, &p.url, Response::html(body))?;
         }
@@ -184,8 +194,11 @@ impl TryFrom<Blog> for RenderedBlog {
             Response::svg(include_bytes!("assets/rss.svg").to_vec()),
         )?;
 
-        let not_found =
-            Response::html(b.dress_page(Some("Page not found"), include_str!("assets/404.html")));
+        let not_found = Response::html(b.dress_page(
+            Some("Page not found"),
+            include_str!("assets/404.html"),
+            None,
+        ));
 
         if let Some(fav) = b.favicon {
             for (p, r) in fav.responses {
diff --git a/src/path.rs b/src/path.rs
index 497d8fa..dac7df4 100644
--- a/src/path.rs
+++ b/src/path.rs
@@ -1,6 +1,6 @@
 use std::fmt::{Debug, Display};
 
-#[derive(Eq, PartialEq, Hash, Debug)]
+#[derive(Eq, PartialEq, Hash, Debug, Clone)]
 pub struct UrlPath {
     parts: Vec<String>,
 }
diff --git a/src/post.rs b/src/post.rs
index 1b3faec..577f58d 100644
--- a/src/post.rs
+++ b/src/post.rs
@@ -5,6 +5,7 @@ use chrono::{DateTime, NaiveDate, Utc};
 use orgize::{elements::Keyword, export::HtmlHandler, Element};
 
 use crate::{
+    config::Logo,
     date::parse_org_date,
     load::Loadable,
     parser::{parse, CustomHtmlHandler, Parser},
@@ -20,6 +21,7 @@ pub struct Post {
     pub index: u32,
     pub content: String,
     pub url: String,
+    pub primary_photo: Option<String>,
 }
 
 impl Post {
@@ -65,6 +67,45 @@ impl Post {
         ))
     }
 
+    pub fn open_graph(&self, root_url: &str, logo: &Option<Logo>) -> anyhow::Result<String> {
+        let root = UrlPath::new_from_raw(root_url.to_string());
+
+        let og_image = match self
+            .primary_photo
+            .as_ref()
+            .or_else(|| logo.as_ref().map(|l| &l.path))
+        {
+            Some(l) => format!(
+                r#"<meta property="og:image" content="{}" />"#,
+                root.clone().join(l)
+            ),
+            None => String::new(),
+        };
+
+        let date = match self.date {
+            Some(d) => {
+                format!(
+                    r#"<meta property="og:article:published_time" content="{}" />"#,
+                    DateTime::<Utc>::from_utc(
+                        d.and_hms_opt(0, 0, 0)
+                            .context("Could not convert date to datetime")?,
+                        Utc
+                    )
+                    .to_rfc3339()
+                )
+            }
+            None => "".to_string(),
+        };
+
+        Ok(format!(
+            r#"<meta property="og:title" content="{}" /><meta property="og:type" content="article" />{}<meta property="og:url" content="{}" />{}"#,
+            self.title,
+            og_image,
+            root.join(&self.url),
+            date,
+        ))
+    }
+
     pub fn link(&self) -> anyhow::Result<String> {
         let link = match self.date_f()? {
             Some(date_f) => format!(
@@ -147,6 +188,7 @@ struct PostParser {
     url: String,
     content: Vec<u8>,
     handler: CustomHtmlHandler,
+    primary_photo: Option<String>,
 }
 
 impl PostParser {
@@ -159,6 +201,7 @@ impl PostParser {
             url,
             content: vec![],
             handler: CustomHtmlHandler::new(base_path)?,
+            primary_photo: None,
         })
     }
 }
@@ -178,6 +221,14 @@ impl Parser<Post> for PostParser {
                 self.title = Some(value.to_string());
             }
 
+            Element::Keyword(Keyword { key, value, .. }) if key == "PREVIEW" => {
+                if self.primary_photo.is_some() {
+                    bail!("Post has more than one preview");
+                }
+
+                self.primary_photo = Some(format!("/thumb/{value}"));
+            }
+
             Element::Keyword(Keyword { key, value, .. }) if key == "DATE" => {
                 if self.date.is_some() {
                     bail!("Post has more than one date");
@@ -215,6 +266,7 @@ impl Parser<Post> for PostParser {
         let index = self.index.unwrap_or(u32::MAX);
         let content = String::from_utf8(self.content)?;
         let url = self.url;
+        let primary_photo = self.primary_photo;
 
         Ok(Post {
             hidden,
@@ -223,6 +275,7 @@ impl Parser<Post> for PostParser {
             index,
             content,
             url,
+            primary_photo,
         })
     }
 }
diff --git a/src/wrapper.rs b/src/wrapper.rs
deleted file mode 100644
index 1d40a3b..0000000
--- a/src/wrapper.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-pub struct Wrapper {
-    //
-}
-
-impl Wrapper {
-    fn new() {}
-    fn before() {}
-    fn after() {}
-}
-- 
GitLab